From ba59305510d14d8a02cdcd7835793eadaaa818d3 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 2 May 2025 10:07:51 +0300 Subject: [PATCH] Use rust-analyzer's flycheck as source of cargo diagnostics (#29779) Follow-up of https://github.com/zed-industries/zed/pull/29706 Instead of doing `cargo check` manually, use rust-analyzer's flycheck: at the cost of more sophisticated check command configuration, we keep much less code in Zed, and get a proper progress report. User-facing UI does not change except `diagnostics_fetch_command` and `env` settings removed from the diagnostics settings. Release Notes: - N/A --- Cargo.lock | 3 - assets/settings/default.json | 19 +- crates/collab/src/rpc.rs | 3 + crates/collab/src/tests/editor_tests.rs | 16 +- crates/diagnostics/Cargo.toml | 3 - crates/diagnostics/src/cargo.rs | 603 ------------------ crates/diagnostics/src/diagnostics.rs | 362 +++-------- crates/diagnostics/src/toolbar_controls.rs | 12 +- crates/editor/src/actions.rs | 3 + crates/editor/src/rust_analyzer_ext.rs | 94 ++- crates/languages/src/rust.rs | 13 +- crates/project/src/lsp_store.rs | 140 ++-- .../project/src/lsp_store/lsp_ext_command.rs | 36 +- .../src/lsp_store/rust_analyzer_ext.rs | 166 ++++- .../project/src/manifest_tree/server_tree.rs | 14 - crates/project/src/project.rs | 40 +- crates/project/src/project_settings.rs | 29 +- crates/proto/proto/lsp.proto | 21 +- crates/proto/proto/zed.proto | 5 +- crates/proto/src/proto.rs | 9 + 20 files changed, 520 insertions(+), 1071 deletions(-) delete mode 100644 crates/diagnostics/src/cargo.rs diff --git a/Cargo.lock b/Cargo.lock index c499cd2f2c..70d7a433ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4399,7 +4399,6 @@ name = "diagnostics" version = "0.1.0" dependencies = [ "anyhow", - "cargo_metadata", "client", "collections", "component", @@ -4409,7 +4408,6 @@ dependencies = [ "futures 0.3.31", "gpui", "indoc", - "itertools 0.14.0", "language", "linkme", "log", @@ -4421,7 +4419,6 @@ dependencies = [ "serde", "serde_json", "settings", - "smol", "text", "theme", "ui", diff --git a/assets/settings/default.json b/assets/settings/default.json index 6313b6e45d..432d76100a 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -935,22 +935,9 @@ "max_severity": null }, "rust": { - // When enabled, Zed runs `cargo check --message-format=json`-based commands and - // collect cargo diagnostics instead of rust-analyzer. - "fetch_cargo_diagnostics": false, - // A command override for fetching the cargo diagnostics. - // First argument is the command, followed by the arguments. - "diagnostics_fetch_command": [ - "cargo", - "check", - "--quiet", - "--workspace", - "--message-format=json", - "--all-targets", - "--keep-going" - ], - // Extra environment variables to pass to the diagnostics fetch command. - "env": {} + // When enabled, Zed disables rust-analyzer's check on save and starts to query + // Cargo diagnostics separately. + "fetch_cargo_diagnostics": false } }, // Files or globs of files that will be excluded by Zed entirely. They will be skipped during file diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 3874500d25..f1fd537666 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -328,6 +328,9 @@ impl Server { forward_read_only_project_request::, ) .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) .add_request_handler( forward_read_only_project_request::, ) diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index a6ebfcb41d..316937733e 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -25,7 +25,7 @@ use language::{ use project::{ ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT, lsp_store::{ - lsp_ext_command::{ExpandedMacro, LspExpandMacro}, + lsp_ext_command::{ExpandedMacro, LspExtExpandMacro}, rust_analyzer_ext::RUST_ANALYZER_NAME, }, project_settings::{InlineBlameSettings, ProjectSettings}, @@ -2704,8 +2704,8 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes let fake_language_server = fake_language_servers.next().await.unwrap(); // host - let mut expand_request_a = - fake_language_server.set_request_handler::(|params, _| async move { + let mut expand_request_a = fake_language_server.set_request_handler::( + |params, _| async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), @@ -2715,7 +2715,8 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes name: "test_macro_name".to_string(), expansion: "test_macro_expansion on the host".to_string(), })) - }); + }, + ); editor_a.update_in(cx_a, |editor, window, cx| { expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx) @@ -2738,8 +2739,8 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes }); // client - let mut expand_request_b = - fake_language_server.set_request_handler::(|params, _| async move { + let mut expand_request_b = fake_language_server.set_request_handler::( + |params, _| async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), @@ -2749,7 +2750,8 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes name: "test_macro_name".to_string(), expansion: "test_macro_expansion on the client".to_string(), })) - }); + }, + ); editor_b.update_in(cx_b, |editor, window, cx| { expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx) diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index dba7071304..5d781a5c18 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -14,7 +14,6 @@ doctest = false [dependencies] anyhow.workspace = true -cargo_metadata.workspace = true collections.workspace = true component.workspace = true ctor.workspace = true @@ -23,7 +22,6 @@ env_logger.workspace = true futures.workspace = true gpui.workspace = true indoc.workspace = true -itertools.workspace = true language.workspace = true linkme.workspace = true log.workspace = true @@ -34,7 +32,6 @@ rand.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true -smol.workspace = true text.workspace = true theme.workspace = true ui.workspace = true diff --git a/crates/diagnostics/src/cargo.rs b/crates/diagnostics/src/cargo.rs deleted file mode 100644 index f537df8374..0000000000 --- a/crates/diagnostics/src/cargo.rs +++ /dev/null @@ -1,603 +0,0 @@ -use std::{ - path::{Component, Path, Prefix}, - process::Stdio, - sync::atomic::{self, AtomicUsize}, -}; - -use cargo_metadata::{ - Message, - diagnostic::{Applicability, Diagnostic as CargoDiagnostic, DiagnosticLevel, DiagnosticSpan}, -}; -use collections::HashMap; -use gpui::{AppContext, Entity, Task}; -use itertools::Itertools as _; -use language::Diagnostic; -use project::{ - Worktree, lsp_store::rust_analyzer_ext::CARGO_DIAGNOSTICS_SOURCE_NAME, - project_settings::ProjectSettings, -}; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use smol::{ - channel::Receiver, - io::{AsyncBufReadExt, BufReader}, - process::Command, -}; -use ui::App; -use util::ResultExt; - -use crate::ProjectDiagnosticsEditor; - -#[derive(Debug, serde::Deserialize)] -#[serde(untagged)] -enum CargoMessage { - Cargo(Message), - Rustc(CargoDiagnostic), -} - -/// Appends formatted string to a `String`. -macro_rules! format_to { - ($buf:expr) => (); - ($buf:expr, $lit:literal $($arg:tt)*) => { - { - use ::std::fmt::Write as _; - // We can't do ::std::fmt::Write::write_fmt($buf, format_args!($lit $($arg)*)) - // unfortunately, as that loses out on autoref behavior. - _ = $buf.write_fmt(format_args!($lit $($arg)*)) - } - }; -} - -pub fn cargo_diagnostics_sources( - editor: &ProjectDiagnosticsEditor, - cx: &App, -) -> Vec> { - let fetch_cargo_diagnostics = ProjectSettings::get_global(cx) - .diagnostics - .fetch_cargo_diagnostics(); - if !fetch_cargo_diagnostics { - return Vec::new(); - } - editor - .project - .read(cx) - .worktrees(cx) - .filter(|worktree| worktree.read(cx).entry_for_path("Cargo.toml").is_some()) - .collect() -} - -#[derive(Debug)] -pub enum FetchUpdate { - Diagnostic(CargoDiagnostic), - Progress(String), -} - -#[derive(Debug)] -pub enum FetchStatus { - Started, - Progress { message: String }, - Finished, -} - -pub fn fetch_worktree_diagnostics( - worktree_root: &Path, - cx: &App, -) -> Option<(Task<()>, Receiver)> { - let diagnostics_settings = ProjectSettings::get_global(cx) - .diagnostics - .cargo - .as_ref() - .filter(|cargo_diagnostics| cargo_diagnostics.fetch_cargo_diagnostics)?; - let command_string = diagnostics_settings - .diagnostics_fetch_command - .iter() - .join(" "); - let mut command_parts = diagnostics_settings.diagnostics_fetch_command.iter(); - let mut command = Command::new(command_parts.next()?) - .args(command_parts) - .envs(diagnostics_settings.env.clone()) - .current_dir(worktree_root) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .kill_on_drop(true) - .spawn() - .log_err()?; - - let stdout = command.stdout.take()?; - let mut reader = BufReader::new(stdout); - let (tx, rx) = smol::channel::unbounded(); - let error_threshold = 10; - - let cargo_diagnostics_fetch_task = cx.background_spawn(async move { - let _command = command; - let mut errors = 0; - loop { - let mut line = String::new(); - match reader.read_line(&mut line).await { - Ok(0) => { - return; - }, - Ok(_) => { - errors = 0; - let mut deserializer = serde_json::Deserializer::from_str(&line); - deserializer.disable_recursion_limit(); - let send_result = match CargoMessage::deserialize(&mut deserializer) { - Ok(CargoMessage::Cargo(Message::CompilerMessage(message))) => tx.send(FetchUpdate::Diagnostic(message.message)).await, - Ok(CargoMessage::Cargo(Message::CompilerArtifact(artifact))) => tx.send(FetchUpdate::Progress(format!("Compiled {:?}", artifact.manifest_path.parent().unwrap_or(&artifact.manifest_path)))).await, - Ok(CargoMessage::Cargo(_)) => Ok(()), - Ok(CargoMessage::Rustc(rustc_message)) => tx.send(FetchUpdate::Diagnostic(rustc_message)).await, - Err(_) => { - log::debug!("Failed to parse cargo diagnostics from line '{line}'"); - Ok(()) - }, - }; - if send_result.is_err() { - return; - } - }, - Err(e) => { - log::error!("Failed to read line from {command_string} command output when fetching cargo diagnostics: {e}"); - errors += 1; - if errors >= error_threshold { - log::error!("Failed {error_threshold} times, aborting the diagnostics fetch"); - return; - } - }, - } - } - }); - - Some((cargo_diagnostics_fetch_task, rx)) -} - -static CARGO_DIAGNOSTICS_FETCH_GENERATION: AtomicUsize = AtomicUsize::new(0); - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -struct CargoFetchDiagnosticData { - generation: usize, -} - -pub fn next_cargo_fetch_generation() { - CARGO_DIAGNOSTICS_FETCH_GENERATION.fetch_add(1, atomic::Ordering::Release); -} - -pub fn is_outdated_cargo_fetch_diagnostic(diagnostic: &Diagnostic) -> bool { - if let Some(data) = diagnostic - .data - .clone() - .and_then(|data| serde_json::from_value::(data).ok()) - { - let current_generation = CARGO_DIAGNOSTICS_FETCH_GENERATION.load(atomic::Ordering::Acquire); - data.generation < current_generation - } else { - false - } -} - -/// Converts a Rust root diagnostic to LSP form -/// -/// This flattens the Rust diagnostic by: -/// -/// 1. Creating a LSP diagnostic with the root message and primary span. -/// 2. Adding any labelled secondary spans to `relatedInformation` -/// 3. Categorising child diagnostics as either `SuggestedFix`es, -/// `relatedInformation` or additional message lines. -/// -/// If the diagnostic has no primary span this will return `None` -/// -/// Taken from https://github.com/rust-lang/rust-analyzer/blob/fe7b4f2ad96f7c13cc571f45edc2c578b35dddb4/crates/rust-analyzer/src/diagnostics/to_proto.rs#L275-L285 -pub(crate) fn map_rust_diagnostic_to_lsp( - worktree_root: &Path, - cargo_diagnostic: &CargoDiagnostic, -) -> Vec<(lsp::Url, lsp::Diagnostic)> { - let primary_spans: Vec<&DiagnosticSpan> = cargo_diagnostic - .spans - .iter() - .filter(|s| s.is_primary) - .collect(); - if primary_spans.is_empty() { - return Vec::new(); - } - - let severity = diagnostic_severity(cargo_diagnostic.level); - - let mut source = String::from(CARGO_DIAGNOSTICS_SOURCE_NAME); - let mut code = cargo_diagnostic.code.as_ref().map(|c| c.code.clone()); - - if let Some(code_val) = &code { - // See if this is an RFC #2103 scoped lint (e.g. from Clippy) - let scoped_code: Vec<&str> = code_val.split("::").collect(); - if scoped_code.len() == 2 { - source = String::from(scoped_code[0]); - code = Some(String::from(scoped_code[1])); - } - } - - let mut needs_primary_span_label = true; - let mut subdiagnostics = Vec::new(); - let mut tags = Vec::new(); - - for secondary_span in cargo_diagnostic.spans.iter().filter(|s| !s.is_primary) { - if let Some(label) = secondary_span.label.clone() { - subdiagnostics.push(lsp::DiagnosticRelatedInformation { - location: location(worktree_root, secondary_span), - message: label, - }); - } - } - - let mut message = cargo_diagnostic.message.clone(); - for child in &cargo_diagnostic.children { - let child = map_rust_child_diagnostic(worktree_root, child); - match child { - MappedRustChildDiagnostic::SubDiagnostic(sub) => { - subdiagnostics.push(sub); - } - MappedRustChildDiagnostic::MessageLine(message_line) => { - format_to!(message, "\n{message_line}"); - - // These secondary messages usually duplicate the content of the - // primary span label. - needs_primary_span_label = false; - } - } - } - - if let Some(code) = &cargo_diagnostic.code { - let code = code.code.as_str(); - if matches!( - code, - "dead_code" - | "unknown_lints" - | "unreachable_code" - | "unused_attributes" - | "unused_imports" - | "unused_macros" - | "unused_variables" - ) { - tags.push(lsp::DiagnosticTag::UNNECESSARY); - } - - if matches!(code, "deprecated") { - tags.push(lsp::DiagnosticTag::DEPRECATED); - } - } - - let code_description = match source.as_str() { - "rustc" => rustc_code_description(code.as_deref()), - "clippy" => clippy_code_description(code.as_deref()), - _ => None, - }; - - let generation = CARGO_DIAGNOSTICS_FETCH_GENERATION.load(atomic::Ordering::Acquire); - let data = Some( - serde_json::to_value(CargoFetchDiagnosticData { generation }) - .expect("Serializing a regular Rust struct"), - ); - - primary_spans - .iter() - .flat_map(|primary_span| { - let primary_location = primary_location(worktree_root, primary_span); - let message = { - let mut message = message.clone(); - if needs_primary_span_label { - if let Some(primary_span_label) = &primary_span.label { - format_to!(message, "\n{primary_span_label}"); - } - } - message - }; - // Each primary diagnostic span may result in multiple LSP diagnostics. - let mut diagnostics = Vec::new(); - - let mut related_info_macro_calls = vec![]; - - // If error occurs from macro expansion, add related info pointing to - // where the error originated - // Also, we would generate an additional diagnostic, so that exact place of macro - // will be highlighted in the error origin place. - let span_stack = std::iter::successors(Some(*primary_span), |span| { - Some(&span.expansion.as_ref()?.span) - }); - for (i, span) in span_stack.enumerate() { - if is_dummy_macro_file(&span.file_name) { - continue; - } - - // First span is the original diagnostic, others are macro call locations that - // generated that code. - let is_in_macro_call = i != 0; - - let secondary_location = location(worktree_root, span); - if secondary_location == primary_location { - continue; - } - related_info_macro_calls.push(lsp::DiagnosticRelatedInformation { - location: secondary_location.clone(), - message: if is_in_macro_call { - "Error originated from macro call here".to_owned() - } else { - "Actual error occurred here".to_owned() - }, - }); - // For the additional in-macro diagnostic we add the inverse message pointing to the error location in code. - let information_for_additional_diagnostic = - vec![lsp::DiagnosticRelatedInformation { - location: primary_location.clone(), - message: "Exact error occurred here".to_owned(), - }]; - - let diagnostic = lsp::Diagnostic { - range: secondary_location.range, - // downgrade to hint if we're pointing at the macro - severity: Some(lsp::DiagnosticSeverity::HINT), - code: code.clone().map(lsp::NumberOrString::String), - code_description: code_description.clone(), - source: Some(source.clone()), - message: message.clone(), - related_information: Some(information_for_additional_diagnostic), - tags: if tags.is_empty() { - None - } else { - Some(tags.clone()) - }, - data: data.clone(), - }; - diagnostics.push((secondary_location.uri, diagnostic)); - } - - // Emit the primary diagnostic. - diagnostics.push(( - primary_location.uri.clone(), - lsp::Diagnostic { - range: primary_location.range, - severity, - code: code.clone().map(lsp::NumberOrString::String), - code_description: code_description.clone(), - source: Some(source.clone()), - message, - related_information: { - let info = related_info_macro_calls - .iter() - .cloned() - .chain(subdiagnostics.iter().cloned()) - .collect::>(); - if info.is_empty() { None } else { Some(info) } - }, - tags: if tags.is_empty() { - None - } else { - Some(tags.clone()) - }, - data: data.clone(), - }, - )); - - // Emit hint-level diagnostics for all `related_information` entries such as "help"s. - // This is useful because they will show up in the user's editor, unlike - // `related_information`, which just produces hard-to-read links, at least in VS Code. - let back_ref = lsp::DiagnosticRelatedInformation { - location: primary_location, - message: "original diagnostic".to_owned(), - }; - for sub in &subdiagnostics { - diagnostics.push(( - sub.location.uri.clone(), - lsp::Diagnostic { - range: sub.location.range, - severity: Some(lsp::DiagnosticSeverity::HINT), - code: code.clone().map(lsp::NumberOrString::String), - code_description: code_description.clone(), - source: Some(source.clone()), - message: sub.message.clone(), - related_information: Some(vec![back_ref.clone()]), - tags: None, // don't apply modifiers again - data: data.clone(), - }, - )); - } - - diagnostics - }) - .collect() -} - -fn rustc_code_description(code: Option<&str>) -> Option { - code.filter(|code| { - let mut chars = code.chars(); - chars.next() == Some('E') - && chars.by_ref().take(4).all(|c| c.is_ascii_digit()) - && chars.next().is_none() - }) - .and_then(|code| { - lsp::Url::parse(&format!( - "https://doc.rust-lang.org/error-index.html#{code}" - )) - .ok() - .map(|href| lsp::CodeDescription { href }) - }) -} - -fn clippy_code_description(code: Option<&str>) -> Option { - code.and_then(|code| { - lsp::Url::parse(&format!( - "https://rust-lang.github.io/rust-clippy/master/index.html#{code}" - )) - .ok() - .map(|href| lsp::CodeDescription { href }) - }) -} - -/// Determines the LSP severity from a diagnostic -fn diagnostic_severity(level: DiagnosticLevel) -> Option { - let res = match level { - DiagnosticLevel::Ice => lsp::DiagnosticSeverity::ERROR, - DiagnosticLevel::Error => lsp::DiagnosticSeverity::ERROR, - DiagnosticLevel::Warning => lsp::DiagnosticSeverity::WARNING, - DiagnosticLevel::Note => lsp::DiagnosticSeverity::INFORMATION, - DiagnosticLevel::Help => lsp::DiagnosticSeverity::HINT, - _ => return None, - }; - Some(res) -} - -enum MappedRustChildDiagnostic { - SubDiagnostic(lsp::DiagnosticRelatedInformation), - MessageLine(String), -} - -fn map_rust_child_diagnostic( - worktree_root: &Path, - cargo_diagnostic: &CargoDiagnostic, -) -> MappedRustChildDiagnostic { - let spans: Vec<&DiagnosticSpan> = cargo_diagnostic - .spans - .iter() - .filter(|s| s.is_primary) - .collect(); - if spans.is_empty() { - // `rustc` uses these spanless children as a way to print multi-line - // messages - return MappedRustChildDiagnostic::MessageLine(cargo_diagnostic.message.clone()); - } - - let mut edit_map: HashMap> = HashMap::default(); - let mut suggested_replacements = Vec::new(); - for &span in &spans { - if let Some(suggested_replacement) = &span.suggested_replacement { - if !suggested_replacement.is_empty() { - suggested_replacements.push(suggested_replacement); - } - let location = location(worktree_root, span); - let edit = lsp::TextEdit::new(location.range, suggested_replacement.clone()); - - // Only actually emit a quickfix if the suggestion is "valid enough". - // We accept both "MaybeIncorrect" and "MachineApplicable". "MaybeIncorrect" means that - // the suggestion is *complete* (contains no placeholders where code needs to be - // inserted), but might not be what the user wants, or might need minor adjustments. - if matches!( - span.suggestion_applicability, - None | Some(Applicability::MaybeIncorrect | Applicability::MachineApplicable) - ) { - edit_map.entry(location.uri).or_default().push(edit); - } - } - } - - // rustc renders suggestion diagnostics by appending the suggested replacement, so do the same - // here, otherwise the diagnostic text is missing useful information. - let mut message = cargo_diagnostic.message.clone(); - if !suggested_replacements.is_empty() { - message.push_str(": "); - let suggestions = suggested_replacements - .iter() - .map(|suggestion| format!("`{suggestion}`")) - .join(", "); - message.push_str(&suggestions); - } - - MappedRustChildDiagnostic::SubDiagnostic(lsp::DiagnosticRelatedInformation { - location: location(worktree_root, spans[0]), - message, - }) -} - -/// Converts a Rust span to a LSP location -fn location(worktree_root: &Path, span: &DiagnosticSpan) -> lsp::Location { - let file_name = worktree_root.join(&span.file_name); - let uri = url_from_abs_path(&file_name); - - let range = { - lsp::Range::new( - position(span, span.line_start, span.column_start.saturating_sub(1)), - position(span, span.line_end, span.column_end.saturating_sub(1)), - ) - }; - lsp::Location::new(uri, range) -} - -/// Returns a `Url` object from a given path, will lowercase drive letters if present. -/// This will only happen when processing windows paths. -/// -/// When processing non-windows path, this is essentially the same as `Url::from_file_path`. -pub(crate) fn url_from_abs_path(path: &Path) -> lsp::Url { - let url = lsp::Url::from_file_path(path).unwrap(); - match path.components().next() { - Some(Component::Prefix(prefix)) - if matches!(prefix.kind(), Prefix::Disk(_) | Prefix::VerbatimDisk(_)) => - { - // Need to lowercase driver letter - } - _ => return url, - } - - let driver_letter_range = { - let (scheme, drive_letter, _rest) = match url.as_str().splitn(3, ':').collect_tuple() { - Some(it) => it, - None => return url, - }; - let start = scheme.len() + ':'.len_utf8(); - start..(start + drive_letter.len()) - }; - - // Note: lowercasing the `path` itself doesn't help, the `Url::parse` - // machinery *also* canonicalizes the drive letter. So, just massage the - // string in place. - let mut url: String = url.into(); - url[driver_letter_range].make_ascii_lowercase(); - lsp::Url::parse(&url).unwrap() -} - -fn position( - span: &DiagnosticSpan, - line_number: usize, - column_offset_utf32: usize, -) -> lsp::Position { - let line_index = line_number - span.line_start; - - let column_offset_encoded = match span.text.get(line_index) { - // Fast path. - Some(line) if line.text.is_ascii() => column_offset_utf32, - Some(line) => { - let line_prefix_len = line - .text - .char_indices() - .take(column_offset_utf32) - .last() - .map(|(pos, c)| pos + c.len_utf8()) - .unwrap_or(0); - let line_prefix = &line.text[..line_prefix_len]; - line_prefix.len() - } - None => column_offset_utf32, - }; - - lsp::Position { - line: (line_number as u32).saturating_sub(1), - character: column_offset_encoded as u32, - } -} - -/// Checks whether a file name is from macro invocation and does not refer to an actual file. -fn is_dummy_macro_file(file_name: &str) -> bool { - file_name.starts_with('<') && file_name.ends_with('>') -} - -/// Extracts a suitable "primary" location from a rustc diagnostic. -/// -/// This takes locations pointing into the standard library, or generally outside the current -/// workspace into account and tries to avoid those, in case macros are involved. -fn primary_location(worktree_root: &Path, span: &DiagnosticSpan) -> lsp::Location { - let span_stack = std::iter::successors(Some(span), |span| Some(&span.expansion.as_ref()?.span)); - for span in span_stack.clone() { - let abs_path = worktree_root.join(&span.file_name); - if !is_dummy_macro_file(&span.file_name) && abs_path.starts_with(worktree_root) { - return location(worktree_root, span); - } - } - - // Fall back to the outermost macro invocation if no suitable span comes up. - let last_span = span_stack.last().unwrap(); - location(worktree_root, last_span) -} diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 507164281a..d9a257e882 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -1,4 +1,3 @@ -mod cargo; pub mod items; mod toolbar_controls; @@ -8,18 +7,14 @@ mod diagnostic_renderer; mod diagnostics_tests; use anyhow::Result; -use cargo::{ - FetchStatus, FetchUpdate, cargo_diagnostics_sources, fetch_worktree_diagnostics, - is_outdated_cargo_fetch_diagnostic, map_rust_diagnostic_to_lsp, next_cargo_fetch_generation, - url_from_abs_path, -}; -use collections::{BTreeSet, HashMap, HashSet}; +use collections::{BTreeSet, HashMap}; use diagnostic_renderer::DiagnosticBlock; use editor::{ DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey, display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId}, scroll::Autoscroll, }; +use futures::future::join_all; use gpui::{ AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, @@ -28,10 +23,10 @@ use gpui::{ use language::{ Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, Point, ToTreeSitterPoint, }; -use lsp::{DiagnosticSeverity, LanguageServerId}; +use lsp::DiagnosticSeverity; use project::{ - DiagnosticSummary, Project, ProjectPath, Worktree, - lsp_store::rust_analyzer_ext::{CARGO_DIAGNOSTICS_SOURCE_NAME, RUST_ANALYZER_NAME}, + DiagnosticSummary, Project, ProjectPath, + lsp_store::rust_analyzer_ext::{cancel_flycheck, run_flycheck}, project_settings::ProjectSettings, }; use settings::Settings; @@ -84,8 +79,9 @@ pub(crate) struct ProjectDiagnosticsEditor { } struct CargoDiagnosticsFetchState { - task: Option>, - rust_analyzer: Option, + fetch_task: Option>, + cancel_task: Option>, + diagnostic_sources: Arc>, } impl EventEmitter for ProjectDiagnosticsEditor {} @@ -252,8 +248,9 @@ impl ProjectDiagnosticsEditor { paths_to_update: Default::default(), update_excerpts_task: None, cargo_diagnostics_fetch: CargoDiagnosticsFetchState { - task: None, - rust_analyzer: None, + fetch_task: None, + cancel_task: None, + diagnostic_sources: Arc::new(Vec::new()), }, _subscription: project_event_subscription, }; @@ -346,7 +343,7 @@ impl ProjectDiagnosticsEditor { .fetch_cargo_diagnostics(); if fetch_cargo_diagnostics { - if self.cargo_diagnostics_fetch.task.is_some() { + if self.cargo_diagnostics_fetch.fetch_task.is_some() { self.stop_cargo_diagnostics_fetch(cx); } else { self.update_all_diagnostics(window, cx); @@ -375,300 +372,63 @@ impl ProjectDiagnosticsEditor { } fn update_all_diagnostics(&mut self, window: &mut Window, cx: &mut Context) { - let cargo_diagnostics_sources = cargo_diagnostics_sources(self, cx); + let cargo_diagnostics_sources = self.cargo_diagnostics_sources(cx); if cargo_diagnostics_sources.is_empty() { self.update_all_excerpts(window, cx); } else { - self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), window, cx); + self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), cx); } } fn fetch_cargo_diagnostics( &mut self, - diagnostics_sources: Arc>>, - window: &mut Window, + diagnostics_sources: Arc>, cx: &mut Context, ) { - self.cargo_diagnostics_fetch.task = Some(cx.spawn_in(window, async move |editor, cx| { - let rust_analyzer_server = editor - .update(cx, |editor, cx| { - editor - .project - .read(cx) - .language_server_with_name(RUST_ANALYZER_NAME, cx) - }) - .ok(); - let rust_analyzer_server = match rust_analyzer_server { - Some(rust_analyzer_server) => rust_analyzer_server.await, - None => None, - }; + let project = self.project.clone(); + self.cargo_diagnostics_fetch.cancel_task = None; + self.cargo_diagnostics_fetch.fetch_task = None; + self.cargo_diagnostics_fetch.diagnostic_sources = diagnostics_sources.clone(); + if self.cargo_diagnostics_fetch.diagnostic_sources.is_empty() { + return; + } - let mut worktree_diagnostics_tasks = Vec::new(); - let mut paths_with_reported_cargo_diagnostics = HashSet::default(); - if let Some(rust_analyzer_server) = rust_analyzer_server { - let can_continue = editor - .update(cx, |editor, cx| { - editor.cargo_diagnostics_fetch.rust_analyzer = Some(rust_analyzer_server); - let status_inserted = - editor - .project - .read(cx) - .lsp_store() - .update(cx, |lsp_store, cx| { - if let Some(rust_analyzer_status) = lsp_store - .language_server_statuses - .get_mut(&rust_analyzer_server) - { - rust_analyzer_status - .progress_tokens - .insert(fetch_cargo_diagnostics_token()); - paths_with_reported_cargo_diagnostics.extend(editor.diagnostics.iter().filter_map(|(buffer_id, diagnostics)| { - if diagnostics.iter().any(|d| d.diagnostic.source.as_deref() == Some(CARGO_DIAGNOSTICS_SOURCE_NAME)) { - Some(*buffer_id) - } else { - None - } - }).filter_map(|buffer_id| { - let buffer = lsp_store.buffer_store().read(cx).get(buffer_id)?; - let path = buffer.read(cx).file()?.as_local()?.abs_path(cx); - Some(url_from_abs_path(&path)) - })); - true - } else { - false - } - }); - if status_inserted { - editor.update_cargo_fetch_status(FetchStatus::Started, cx); - next_cargo_fetch_generation(); - true - } else { - false - } + self.cargo_diagnostics_fetch.fetch_task = Some(cx.spawn(async move |editor, cx| { + let mut fetch_tasks = Vec::new(); + for buffer_path in diagnostics_sources.iter().cloned() { + if cx + .update(|cx| { + fetch_tasks.push(run_flycheck(project.clone(), buffer_path, cx)); }) - .unwrap_or(false); - - if can_continue { - for worktree in diagnostics_sources.iter() { - if let Some(((_task, worktree_diagnostics), worktree_root)) = cx - .update(|_, cx| { - let worktree_root = worktree.read(cx).abs_path(); - log::info!("Fetching cargo diagnostics for {worktree_root:?}"); - fetch_worktree_diagnostics(&worktree_root, cx) - .zip(Some(worktree_root)) - }) - .ok() - .flatten() - { - let editor = editor.clone(); - worktree_diagnostics_tasks.push(cx.spawn(async move |cx| { - let _task = _task; - let mut file_diagnostics = HashMap::default(); - let mut diagnostics_total = 0; - let mut updated_urls = HashSet::default(); - while let Ok(fetch_update) = worktree_diagnostics.recv().await { - match fetch_update { - FetchUpdate::Diagnostic(diagnostic) => { - for (url, diagnostic) in map_rust_diagnostic_to_lsp( - &worktree_root, - &diagnostic, - ) { - let file_diagnostics = file_diagnostics - .entry(url) - .or_insert_with(Vec::::new); - let i = file_diagnostics - .binary_search_by(|probe| { - probe.range.start.cmp(&diagnostic.range.start) - .then(probe.range.end.cmp(&diagnostic.range.end)) - .then(Ordering::Greater) - }) - .unwrap_or_else(|i| i); - file_diagnostics.insert(i, diagnostic); - } - - let file_changed = file_diagnostics.len() > 1; - if file_changed { - if editor - .update_in(cx, |editor, window, cx| { - editor - .project - .read(cx) - .lsp_store() - .update(cx, |lsp_store, cx| { - for (uri, mut diagnostics) in - file_diagnostics.drain() - { - diagnostics.dedup(); - diagnostics_total += diagnostics.len(); - updated_urls.insert(uri.clone()); - - lsp_store.merge_diagnostics( - rust_analyzer_server, - lsp::PublishDiagnosticsParams { - uri, - diagnostics, - version: None, - }, - &[], - |diagnostic, _| { - !is_outdated_cargo_fetch_diagnostic(diagnostic) - }, - cx, - )?; - } - anyhow::Ok(()) - })?; - editor.update_all_excerpts(window, cx); - anyhow::Ok(()) - }) - .ok() - .transpose() - .ok() - .flatten() - .is_none() - { - break; - } - } - } - FetchUpdate::Progress(message) => { - if editor - .update(cx, |editor, cx| { - editor.update_cargo_fetch_status( - FetchStatus::Progress { message }, - cx, - ); - }) - .is_err() - { - return updated_urls; - } - } - } - } - - editor - .update_in(cx, |editor, window, cx| { - editor - .project - .read(cx) - .lsp_store() - .update(cx, |lsp_store, cx| { - for (uri, mut diagnostics) in - file_diagnostics.drain() - { - diagnostics.dedup(); - diagnostics_total += diagnostics.len(); - updated_urls.insert(uri.clone()); - - lsp_store.merge_diagnostics( - rust_analyzer_server, - lsp::PublishDiagnosticsParams { - uri, - diagnostics, - version: None, - }, - &[], - |diagnostic, _| { - !is_outdated_cargo_fetch_diagnostic(diagnostic) - }, - cx, - )?; - } - anyhow::Ok(()) - })?; - editor.update_all_excerpts(window, cx); - anyhow::Ok(()) - }) - .ok(); - log::info!("Fetched {diagnostics_total} cargo diagnostics for worktree {worktree_root:?}"); - updated_urls - })); - } - } - } else { - log::info!( - "No rust-analyzer language server found, skipping diagnostics fetch" - ); + .is_err() + { + break; } } - - let updated_urls = futures::future::join_all(worktree_diagnostics_tasks).await.into_iter().flatten().collect(); - if let Some(rust_analyzer_server) = rust_analyzer_server { + let _ = join_all(fetch_tasks).await; editor - .update_in(cx, |editor, window, cx| { - editor - .project - .read(cx) - .lsp_store() - .update(cx, |lsp_store, cx| { - for uri_to_cleanup in paths_with_reported_cargo_diagnostics.difference(&updated_urls).cloned() { - lsp_store.merge_diagnostics( - rust_analyzer_server, - lsp::PublishDiagnosticsParams { - uri: uri_to_cleanup, - diagnostics: Vec::new(), - version: None, - }, - &[], - |diagnostic, _| { - !is_outdated_cargo_fetch_diagnostic(diagnostic) - }, - cx, - ).ok(); - } - }); - editor.update_all_excerpts(window, cx); - - editor.stop_cargo_diagnostics_fetch(cx); - cx.notify(); + .update(cx, |editor, _| { + editor.cargo_diagnostics_fetch.fetch_task = None; }) .ok(); - } })); } - fn update_cargo_fetch_status(&self, status: FetchStatus, cx: &mut App) { - let Some(rust_analyzer) = self.cargo_diagnostics_fetch.rust_analyzer else { - return; - }; - - let work_done = match status { - FetchStatus::Started => lsp::WorkDoneProgress::Begin(lsp::WorkDoneProgressBegin { - title: "cargo".to_string(), - cancellable: None, - message: Some("Fetching cargo diagnostics".to_string()), - percentage: None, - }), - FetchStatus::Progress { message } => { - lsp::WorkDoneProgress::Report(lsp::WorkDoneProgressReport { - message: Some(message), - cancellable: None, - percentage: None, - }) - } - FetchStatus::Finished => { - lsp::WorkDoneProgress::End(lsp::WorkDoneProgressEnd { message: None }) - } - }; - let progress = lsp::ProgressParams { - token: lsp::NumberOrString::String(fetch_cargo_diagnostics_token()), - value: lsp::ProgressParamsValue::WorkDone(work_done), - }; - - self.project - .read(cx) - .lsp_store() - .update(cx, |lsp_store, cx| { - lsp_store.on_lsp_progress(progress, rust_analyzer, None, cx) - }); - } - fn stop_cargo_diagnostics_fetch(&mut self, cx: &mut App) { - self.update_cargo_fetch_status(FetchStatus::Finished, cx); - self.cargo_diagnostics_fetch.task = None; - log::info!("Finished fetching cargo diagnostics"); + self.cargo_diagnostics_fetch.fetch_task = None; + let mut cancel_gasks = Vec::new(); + for buffer_path in std::mem::take(&mut self.cargo_diagnostics_fetch.diagnostic_sources) + .iter() + .cloned() + { + cancel_gasks.push(cancel_flycheck(self.project.clone(), buffer_path, cx)); + } + + self.cargo_diagnostics_fetch.cancel_task = Some(cx.background_spawn(async move { + let _ = join_all(cancel_gasks).await; + log::info!("Finished fetching cargo diagnostics"); + })); } /// Enqueue an update of all excerpts. Updates all paths that either @@ -897,6 +657,30 @@ impl ProjectDiagnosticsEditor { }) }) } + + pub fn cargo_diagnostics_sources(&self, cx: &App) -> Vec { + let fetch_cargo_diagnostics = ProjectSettings::get_global(cx) + .diagnostics + .fetch_cargo_diagnostics(); + if !fetch_cargo_diagnostics { + return Vec::new(); + } + self.project + .read(cx) + .worktrees(cx) + .filter_map(|worktree| { + let _cargo_toml_entry = worktree.read(cx).entry_for_path("Cargo.toml")?; + let rust_file_entry = worktree.read(cx).entries(false, 0).find(|entry| { + entry + .path + .extension() + .and_then(|extension| extension.to_str()) + == Some("rs") + })?; + self.project.read(cx).path_for_entry(rust_file_entry.id, cx) + }) + .collect() + } } impl Focusable for ProjectDiagnosticsEditor { @@ -1286,7 +1070,3 @@ fn is_line_blank_or_indented_less( let line_indent = snapshot.line_indent_for_row(row); line_indent.is_line_blank() || line_indent.len(tab_size) < indent_level } - -fn fetch_cargo_diagnostics_token() -> String { - "fetch_cargo_diagnostics".to_string() -} diff --git a/crates/diagnostics/src/toolbar_controls.rs b/crates/diagnostics/src/toolbar_controls.rs index 7a0d35f856..9a7dcbe62f 100644 --- a/crates/diagnostics/src/toolbar_controls.rs +++ b/crates/diagnostics/src/toolbar_controls.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use crate::cargo::cargo_diagnostics_sources; use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh}; use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window}; use ui::prelude::*; @@ -16,11 +15,9 @@ impl Render for ToolbarControls { let mut include_warnings = false; let mut has_stale_excerpts = false; let mut is_updating = false; - let cargo_diagnostics_sources = Arc::new( - self.diagnostics() - .map(|editor| cargo_diagnostics_sources(editor.read(cx), cx)) - .unwrap_or_default(), - ); + let cargo_diagnostics_sources = Arc::new(self.diagnostics().map_or(Vec::new(), |editor| { + editor.read(cx).cargo_diagnostics_sources(cx) + })); let fetch_cargo_diagnostics = !cargo_diagnostics_sources.is_empty(); if let Some(editor) = self.diagnostics() { @@ -28,7 +25,7 @@ impl Render for ToolbarControls { include_warnings = diagnostics.include_warnings; has_stale_excerpts = !diagnostics.paths_to_update.is_empty(); is_updating = if fetch_cargo_diagnostics { - diagnostics.cargo_diagnostics_fetch.task.is_some() + diagnostics.cargo_diagnostics_fetch.fetch_task.is_some() } else { diagnostics.update_excerpts_task.is_some() || diagnostics @@ -93,7 +90,6 @@ impl Render for ToolbarControls { if fetch_cargo_diagnostics { diagnostics.fetch_cargo_diagnostics( cargo_diagnostics_sources, - window, cx, ); } else { diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 4258d0020c..3c3033c491 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -249,7 +249,9 @@ actions!( ApplyDiffHunk, Backspace, Cancel, + CancelFlycheck, CancelLanguageServerWork, + ClearFlycheck, ConfirmRename, ConfirmCompletionInsert, ConfirmCompletionReplace, @@ -372,6 +374,7 @@ actions!( RevertFile, ReloadFile, Rewrap, + RunFlycheck, ScrollCursorBottom, ScrollCursorCenter, ScrollCursorCenterTopBottom, diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index 80d811f8a3..ea2e7f4127 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -5,18 +5,19 @@ use gpui::{App, AppContext as _, Context, Entity, Window}; use language::{Capability, Language, proto::serialize_anchor}; use multi_buffer::MultiBuffer; use project::{ + ProjectItem, lsp_command::location_link_from_proto, lsp_store::{ lsp_ext_command::{DocsUrls, ExpandMacro, ExpandedMacro}, - rust_analyzer_ext::RUST_ANALYZER_NAME, + rust_analyzer_ext::{RUST_ANALYZER_NAME, cancel_flycheck, clear_flycheck, run_flycheck}, }, }; use rpc::proto; use text::ToPointUtf16; use crate::{ - Editor, ExpandMacroRecursively, GoToParentModule, GotoDefinitionKind, OpenDocs, - element::register_action, hover_links::HoverLink, + CancelFlycheck, ClearFlycheck, Editor, ExpandMacroRecursively, GoToParentModule, + GotoDefinitionKind, OpenDocs, RunFlycheck, element::register_action, hover_links::HoverLink, lsp_ext::find_specific_language_server_in_selection, }; @@ -37,6 +38,9 @@ pub fn apply_related_actions(editor: &Entity, window: &mut Window, cx: & register_action(&editor, window, go_to_parent_module); register_action(&editor, window, expand_macro_recursively); register_action(&editor, window, open_docs); + register_action(&editor, window, cancel_flycheck_action); + register_action(&editor, window, run_flycheck_action); + register_action(&editor, window, clear_flycheck_action); } } @@ -300,3 +304,87 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu }) .detach_and_log_err(cx); } + +fn cancel_flycheck_action( + editor: &mut Editor, + _: &CancelFlycheck, + _: &mut Window, + cx: &mut Context, +) { + let Some(project) = &editor.project else { + return; + }; + let Some(buffer_id) = editor + .selections + .disjoint_anchors() + .iter() + .find_map(|selection| { + let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?; + let project = project.read(cx); + let entry_id = project + .buffer_for_id(buffer_id, cx)? + .read(cx) + .entry_id(cx)?; + project.path_for_entry(entry_id, cx) + }) + else { + return; + }; + cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); +} + +fn run_flycheck_action( + editor: &mut Editor, + _: &RunFlycheck, + _: &mut Window, + cx: &mut Context, +) { + let Some(project) = &editor.project else { + return; + }; + let Some(buffer_id) = editor + .selections + .disjoint_anchors() + .iter() + .find_map(|selection| { + let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?; + let project = project.read(cx); + let entry_id = project + .buffer_for_id(buffer_id, cx)? + .read(cx) + .entry_id(cx)?; + project.path_for_entry(entry_id, cx) + }) + else { + return; + }; + run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); +} + +fn clear_flycheck_action( + editor: &mut Editor, + _: &ClearFlycheck, + _: &mut Window, + cx: &mut Context, +) { + let Some(project) = &editor.project else { + return; + }; + let Some(buffer_id) = editor + .selections + .disjoint_anchors() + .iter() + .find_map(|selection| { + let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?; + let project = project.read(cx); + let entry_id = project + .buffer_for_id(buffer_id, cx)? + .read(cx) + .entry_id(cx)?; + project.path_for_entry(entry_id, cx) + }) + else { + return; + }; + clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); +} diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 9741c8f99d..0e28d69c55 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -260,15 +260,6 @@ impl LspAdapter for RustLspAdapter { Some("rust-analyzer/flycheck".into()) } - fn retain_old_diagnostic(&self, previous_diagnostic: &Diagnostic, cx: &App) -> bool { - let zed_provides_cargo_diagnostics = ProjectSettings::get_global(cx) - .diagnostics - .fetch_cargo_diagnostics(); - // Zed manages the lifecycle of cargo diagnostics when configured so. - zed_provides_cargo_diagnostics - && previous_diagnostic.source.as_deref() == Some(CARGO_DIAGNOSTICS_SOURCE_NAME) - } - fn process_diagnostics( &self, params: &mut lsp::PublishDiagnosticsParams, @@ -516,10 +507,10 @@ impl LspAdapter for RustLspAdapter { } } - let zed_provides_cargo_diagnostics = ProjectSettings::get_global(cx) + let cargo_diagnostics_fetched_separately = ProjectSettings::get_global(cx) .diagnostics .fetch_cargo_diagnostics(); - if zed_provides_cargo_diagnostics { + if cargo_diagnostics_fetched_separately { let disable_check_on_save = json!({ "checkOnSave": false, }); diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index c588e410c7..d46c60654f 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -8,6 +8,7 @@ use crate::{ buffer_store::{BufferStore, BufferStoreEvent}, environment::ProjectEnvironment, lsp_command::{self, *}, + lsp_store, manifest_tree::{AdapterQuery, LanguageServerTree, LaunchDisposition, ManifestTree}, prettier_store::{self, PrettierStore, PrettierStoreEvent}, project_settings::{LspSettings, ProjectSettings}, @@ -3396,7 +3397,7 @@ pub struct LanguageServerStatus { pub name: String, pub pending_work: BTreeMap, pub has_pending_diagnostic_updates: bool, - pub progress_tokens: HashSet, + progress_tokens: HashSet, } #[derive(Clone, Debug)] @@ -3449,8 +3450,14 @@ impl LspStore { client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); + client.add_entity_request_handler(Self::handle_lsp_ext_cancel_flycheck); + client.add_entity_request_handler(Self::handle_lsp_ext_run_flycheck); + client.add_entity_request_handler(Self::handle_lsp_ext_clear_flycheck); client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); + client.add_entity_request_handler( + Self::handle_lsp_command::, + ); client.add_entity_request_handler( Self::handle_lsp_command::, ); @@ -6236,13 +6243,6 @@ impl LspStore { }) } - pub fn language_server_with_name(&self, name: &str, cx: &App) -> Option { - self.as_local()? - .lsp_tree - .read(cx) - .server_id_for_name(&LanguageServerName::from(name)) - } - pub fn language_servers_for_local_buffer<'a>( &'a self, buffer: &Buffer, @@ -7028,37 +7028,26 @@ impl LspStore { mut cx: AsyncApp, ) -> Result { let name = &envelope.payload.name; - match envelope.payload.buffer_id { - Some(buffer_id) => { - let buffer_id = BufferId::new(buffer_id)?; - lsp_store - .update(&mut cx, |lsp_store, cx| { - let buffer = lsp_store.buffer_store.read(cx).get_existing(buffer_id)?; - let server_id = buffer.update(cx, |buffer, cx| { - lsp_store - .language_servers_for_local_buffer(buffer, cx) - .find_map(|(adapter, server)| { - if adapter.name.0.as_ref() == name { - Some(server.server_id()) - } else { - None - } - }) - }); - Ok(server_id) - })? - .map(|server_id| proto::LanguageServerIdForNameResponse { - server_id: server_id.map(|id| id.to_proto()), - }) - } - None => lsp_store.update(&mut cx, |lsp_store, cx| { - proto::LanguageServerIdForNameResponse { - server_id: lsp_store - .language_server_with_name(name, cx) - .map(|id| id.to_proto()), - } - }), - } + let buffer_id = BufferId::new(envelope.payload.buffer_id)?; + lsp_store + .update(&mut cx, |lsp_store, cx| { + let buffer = lsp_store.buffer_store.read(cx).get_existing(buffer_id)?; + let server_id = buffer.update(cx, |buffer, cx| { + lsp_store + .language_servers_for_local_buffer(buffer, cx) + .find_map(|(adapter, server)| { + if adapter.name.0.as_ref() == name { + Some(server.server_id()) + } else { + None + } + }) + }); + Ok(server_id) + })? + .map(|server_id| proto::LanguageServerIdForNameResponse { + server_id: server_id.map(|id| id.to_proto()), + }) } async fn handle_rename_project_entry( @@ -7282,6 +7271,77 @@ impl LspStore { }) } + async fn handle_lsp_ext_cancel_flycheck( + lsp_store: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let server_id = LanguageServerId(envelope.payload.language_server_id as usize); + lsp_store.update(&mut cx, |lsp_store, _| { + if let Some(server) = lsp_store.language_server_for_id(server_id) { + server + .notify::(&()) + .context("handling lsp ext cancel flycheck") + } else { + anyhow::Ok(()) + } + })??; + + Ok(proto::Ack {}) + } + + async fn handle_lsp_ext_run_flycheck( + lsp_store: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let server_id = LanguageServerId(envelope.payload.language_server_id as usize); + lsp_store.update(&mut cx, |lsp_store, cx| { + if let Some(server) = lsp_store.language_server_for_id(server_id) { + let text_document = if envelope.payload.current_file_only { + let buffer_id = BufferId::new(envelope.payload.buffer_id)?; + lsp_store + .buffer_store() + .read(cx) + .get(buffer_id) + .and_then(|buffer| Some(buffer.read(cx).file()?.as_local()?.abs_path(cx))) + .map(|path| make_text_document_identifier(&path)) + .transpose()? + } else { + None + }; + server + .notify::( + &lsp_store::lsp_ext_command::RunFlycheckParams { text_document }, + ) + .context("handling lsp ext run flycheck") + } else { + anyhow::Ok(()) + } + })??; + + Ok(proto::Ack {}) + } + + async fn handle_lsp_ext_clear_flycheck( + lsp_store: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let server_id = LanguageServerId(envelope.payload.language_server_id as usize); + lsp_store.update(&mut cx, |lsp_store, _| { + if let Some(server) = lsp_store.language_server_for_id(server_id) { + server + .notify::(&()) + .context("handling lsp ext clear flycheck") + } else { + anyhow::Ok(()) + } + })??; + + Ok(proto::Ack {}) + } + pub fn disk_based_diagnostics_started( &mut self, language_server_id: LanguageServerId, @@ -7534,7 +7594,7 @@ impl LspStore { } } - pub fn on_lsp_progress( + fn on_lsp_progress( &mut self, progress: lsp::ProgressParams, language_server_id: LanguageServerId, diff --git a/crates/project/src/lsp_store/lsp_ext_command.rs b/crates/project/src/lsp_store/lsp_ext_command.rs index a7e6eafc74..77d88dc3a0 100644 --- a/crates/project/src/lsp_store/lsp_ext_command.rs +++ b/crates/project/src/lsp_store/lsp_ext_command.rs @@ -25,9 +25,9 @@ use std::{ use task::TaskTemplate; use text::{BufferId, PointUtf16, ToPointUtf16}; -pub enum LspExpandMacro {} +pub enum LspExtExpandMacro {} -impl lsp::request::Request for LspExpandMacro { +impl lsp::request::Request for LspExtExpandMacro { type Params = ExpandMacroParams; type Result = Option; const METHOD: &'static str = "rust-analyzer/expandMacro"; @@ -60,7 +60,7 @@ pub struct ExpandMacro { #[async_trait(?Send)] impl LspCommand for ExpandMacro { type Response = ExpandedMacro; - type LspRequest = LspExpandMacro; + type LspRequest = LspExtExpandMacro; type ProtoRequest = proto::LspExtExpandMacro; fn display_name(&self) -> &str { @@ -753,3 +753,33 @@ impl LspCommand for GetLspRunnables { BufferId::new(message.buffer_id) } } + +#[derive(Debug)] +pub struct LspExtCancelFlycheck {} + +#[derive(Debug)] +pub struct LspExtRunFlycheck {} + +#[derive(Debug)] +pub struct LspExtClearFlycheck {} + +impl lsp::notification::Notification for LspExtCancelFlycheck { + type Params = (); + const METHOD: &'static str = "rust-analyzer/cancelFlycheck"; +} + +impl lsp::notification::Notification for LspExtRunFlycheck { + type Params = RunFlycheckParams; + const METHOD: &'static str = "rust-analyzer/runFlycheck"; +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RunFlycheckParams { + pub text_document: Option, +} + +impl lsp::notification::Notification for LspExtClearFlycheck { + type Params = (); + const METHOD: &'static str = "rust-analyzer/clearFlycheck"; +} diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs index be21092075..901eeeee6e 100644 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ b/crates/project/src/lsp_store/rust_analyzer_ext.rs @@ -1,8 +1,12 @@ use ::serde::{Deserialize, Serialize}; -use gpui::{PromptLevel, WeakEntity}; +use anyhow::Context as _; +use gpui::{App, Entity, PromptLevel, Task, WeakEntity}; use lsp::LanguageServer; +use rpc::proto; -use crate::{LanguageServerPromptRequest, LspStore, LspStoreEvent}; +use crate::{ + LanguageServerPromptRequest, LspStore, LspStoreEvent, Project, ProjectPath, lsp_store, +}; pub const RUST_ANALYZER_NAME: &str = "rust-analyzer"; pub const CARGO_DIAGNOSTICS_SOURCE_NAME: &str = "rustc"; @@ -79,3 +83,161 @@ pub fn register_notifications(lsp_store: WeakEntity, language_server: }) .detach(); } + +pub fn cancel_flycheck( + project: Entity, + buffer_path: ProjectPath, + cx: &mut App, +) -> Task> { + let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); + let lsp_store = project.read(cx).lsp_store(); + let buffer = project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) + }) + }); + + cx.spawn(async move |cx| { + let buffer = buffer.await?; + let Some(rust_analyzer_server) = project + .update(cx, |project, cx| { + buffer.update(cx, |buffer, cx| { + project.language_server_id_for_name(buffer, RUST_ANALYZER_NAME, cx) + }) + })? + .await + else { + return Ok(()); + }; + let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id().to_proto())?; + + if let Some((client, project_id)) = upstream_client { + let request = proto::LspExtCancelFlycheck { + project_id, + buffer_id, + language_server_id: rust_analyzer_server.to_proto(), + }; + client + .request(request) + .await + .context("lsp ext cancel flycheck proto request")?; + } else { + lsp_store + .update(cx, |lsp_store, _| { + if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) { + server.notify::(&())?; + } + anyhow::Ok(()) + })? + .context("lsp ext cancel flycheck")?; + }; + anyhow::Ok(()) + }) +} + +pub fn run_flycheck( + project: Entity, + buffer_path: ProjectPath, + cx: &mut App, +) -> Task> { + let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); + let lsp_store = project.read(cx).lsp_store(); + let buffer = project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) + }) + }); + + cx.spawn(async move |cx| { + let buffer = buffer.await?; + let Some(rust_analyzer_server) = project + .update(cx, |project, cx| { + buffer.update(cx, |buffer, cx| { + project.language_server_id_for_name(buffer, RUST_ANALYZER_NAME, cx) + }) + })? + .await + else { + return Ok(()); + }; + let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id().to_proto())?; + + if let Some((client, project_id)) = upstream_client { + let request = proto::LspExtRunFlycheck { + project_id, + buffer_id, + language_server_id: rust_analyzer_server.to_proto(), + current_file_only: false, + }; + client + .request(request) + .await + .context("lsp ext run flycheck proto request")?; + } else { + lsp_store + .update(cx, |lsp_store, _| { + if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) { + server.notify::( + &lsp_store::lsp_ext_command::RunFlycheckParams { + text_document: None, + }, + )?; + } + anyhow::Ok(()) + })? + .context("lsp ext run flycheck")?; + }; + anyhow::Ok(()) + }) +} + +pub fn clear_flycheck( + project: Entity, + buffer_path: ProjectPath, + cx: &mut App, +) -> Task> { + let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); + let lsp_store = project.read(cx).lsp_store(); + let buffer = project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) + }) + }); + + cx.spawn(async move |cx| { + let buffer = buffer.await?; + let Some(rust_analyzer_server) = project + .update(cx, |project, cx| { + buffer.update(cx, |buffer, cx| { + project.language_server_id_for_name(buffer, RUST_ANALYZER_NAME, cx) + }) + })? + .await + else { + return Ok(()); + }; + let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id().to_proto())?; + + if let Some((client, project_id)) = upstream_client { + let request = proto::LspExtClearFlycheck { + project_id, + buffer_id, + language_server_id: rust_analyzer_server.to_proto(), + }; + client + .request(request) + .await + .context("lsp ext clear flycheck proto request")?; + } else { + lsp_store + .update(cx, |lsp_store, _| { + if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) { + server.notify::(&())?; + } + anyhow::Ok(()) + })? + .context("lsp ext clear flycheck")?; + }; + anyhow::Ok(()) + }) +} diff --git a/crates/project/src/manifest_tree/server_tree.rs b/crates/project/src/manifest_tree/server_tree.rs index 98e3e77744..0a8cbbedb4 100644 --- a/crates/project/src/manifest_tree/server_tree.rs +++ b/crates/project/src/manifest_tree/server_tree.rs @@ -247,20 +247,6 @@ impl LanguageServerTree { self.languages.adapter_for_name(name) } - pub fn server_id_for_name(&self, name: &LanguageServerName) -> Option { - self.instances - .values() - .flat_map(|instance| instance.roots.values()) - .flatten() - .find_map(|(server_name, (data, _))| { - if server_name == name { - data.id.get().copied() - } else { - None - } - }) - } - fn adapters_for_language( &self, settings_location: SettingsLocation, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fb87870a00..314634c63a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4748,42 +4748,6 @@ impl Project { }) } - pub fn language_server_with_name( - &self, - name: &str, - cx: &App, - ) -> Task> { - if self.is_local() { - Task::ready(self.lsp_store.read(cx).language_server_with_name(name, cx)) - } else if let Some(project_id) = self.remote_id() { - let request = self.client.request(proto::LanguageServerIdForName { - project_id, - buffer_id: None, - name: name.to_string(), - }); - cx.background_spawn(async move { - let response = request.await.log_err()?; - response.server_id.map(LanguageServerId::from_proto) - }) - } else if let Some(ssh_client) = self.ssh_client.as_ref() { - let request = - ssh_client - .read(cx) - .proto_client() - .request(proto::LanguageServerIdForName { - project_id: SSH_PROJECT_ID, - buffer_id: None, - name: name.to_string(), - }); - cx.background_spawn(async move { - let response = request.await.log_err()?; - response.server_id.map(LanguageServerId::from_proto) - }) - } else { - Task::ready(None) - } - } - pub fn language_server_id_for_name( &self, buffer: &Buffer, @@ -4805,7 +4769,7 @@ impl Project { } else if let Some(project_id) = self.remote_id() { let request = self.client.request(proto::LanguageServerIdForName { project_id, - buffer_id: Some(buffer.remote_id().to_proto()), + buffer_id: buffer.remote_id().to_proto(), name: name.to_string(), }); cx.background_spawn(async move { @@ -4819,7 +4783,7 @@ impl Project { .proto_client() .request(proto::LanguageServerIdForName { project_id: SSH_PROJECT_ID, - buffer_id: Some(buffer.remote_id().to_proto()), + buffer_id: buffer.remote_id().to_proto(), name: name.to_string(), }); cx.background_spawn(async move { diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 0d5fb6ab7e..21cdb25e7e 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -155,37 +155,12 @@ pub struct InlineDiagnosticsSettings { #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct CargoDiagnosticsSettings { - /// When enabled, Zed runs `cargo check --message-format=json`-based commands and - /// collect cargo diagnostics instead of rust-analyzer. + /// When enabled, Zed disables rust-analyzer's check on save and starts to query + /// Cargo diagnostics separately. /// /// Default: false #[serde(default)] pub fetch_cargo_diagnostics: bool, - - /// A command override for fetching the cargo diagnostics. - /// First argument is the command, followed by the arguments. - /// - /// Default: ["cargo", "check", "--quiet", "--workspace", "--message-format=json", "--all-targets", "--keep-going"] - #[serde(default = "default_diagnostics_fetch_command")] - pub diagnostics_fetch_command: Vec, - - /// Extra environment variables to pass to the diagnostics fetch command. - /// - /// Default: {} - #[serde(default)] - pub env: HashMap, -} - -fn default_diagnostics_fetch_command() -> Vec { - vec![ - "cargo".to_string(), - "check".to_string(), - "--quiet".to_string(), - "--workspace".to_string(), - "--message-format=json".to_string(), - "--all-targets".to_string(), - "--keep-going".to_string(), - ] } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index 906402b089..9bc17978dd 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -706,7 +706,7 @@ message LspResponse { message LanguageServerIdForName { uint64 project_id = 1; - optional uint64 buffer_id = 2; + uint64 buffer_id = 2; string name = 3; } @@ -728,3 +728,22 @@ message LspRunnable { bytes task_template = 1; optional LocationLink location = 2; } + +message LspExtCancelFlycheck { + uint64 project_id = 1; + uint64 buffer_id = 2; + uint64 language_server_id = 3; +} + +message LspExtRunFlycheck { + uint64 project_id = 1; + uint64 buffer_id = 2; + uint64 language_server_id = 3; + bool current_file_only = 4; +} + +message LspExtClearFlycheck { + uint64 project_id = 1; + uint64 buffer_id = 2; + uint64 language_server_id = 3; +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 101db00969..822f1cd9e5 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -381,7 +381,10 @@ message Envelope { DebugRequest debug_request = 342; LspExtGoToParentModule lsp_ext_go_to_parent_module = 343; - LspExtGoToParentModuleResponse lsp_ext_go_to_parent_module_response = 344;// current max + LspExtGoToParentModuleResponse lsp_ext_go_to_parent_module_response = 344; + LspExtCancelFlycheck lsp_ext_cancel_flycheck = 345; + LspExtRunFlycheck lsp_ext_run_flycheck = 346; + LspExtClearFlycheck lsp_ext_clear_flycheck = 347; // current max } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 24963fb97b..34ee21d7dd 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -171,6 +171,9 @@ messages!( (LspExtSwitchSourceHeaderResponse, Background), (LspExtGoToParentModule, Background), (LspExtGoToParentModuleResponse, Background), + (LspExtCancelFlycheck, Background), + (LspExtRunFlycheck, Background), + (LspExtClearFlycheck, Background), (MarkNotificationRead, Foreground), (MoveChannel, Foreground), (MultiLspQuery, Background), @@ -425,6 +428,9 @@ request_messages!( (SynchronizeContexts, SynchronizeContextsResponse), (LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse), (LspExtGoToParentModule, LspExtGoToParentModuleResponse), + (LspExtCancelFlycheck, Ack), + (LspExtRunFlycheck, Ack), + (LspExtClearFlycheck, Ack), (AddWorktree, AddWorktreeResponse), (ShutdownRemoteServer, Ack), (RemoveWorktree, Ack), @@ -548,6 +554,9 @@ entity_messages!( SynchronizeContexts, LspExtSwitchSourceHeader, LspExtGoToParentModule, + LspExtCancelFlycheck, + LspExtRunFlycheck, + LspExtClearFlycheck, LanguageServerLog, Toast, HideToast,