Use rust-analyzer's flycheck as source of cargo diagnostics (#29779)

Kirill Bulatov created

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

Change summary

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 ++-
crates/project/src/lsp_store/lsp_ext_command.rs   |  36 
crates/project/src/lsp_store/rust_analyzer_ext.rs | 166 ++++
crates/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(+), 1,071 deletions(-)

Detailed changes

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",

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

crates/collab/src/rpc.rs 🔗

@@ -328,6 +328,9 @@ impl Server {
                 forward_read_only_project_request::<proto::LspExtSwitchSourceHeader>,
             )
             .add_request_handler(forward_read_only_project_request::<proto::LspExtGoToParentModule>)
+            .add_request_handler(forward_read_only_project_request::<proto::LspExtCancelFlycheck>)
+            .add_request_handler(forward_read_only_project_request::<proto::LspExtRunFlycheck>)
+            .add_request_handler(forward_read_only_project_request::<proto::LspExtClearFlycheck>)
             .add_request_handler(
                 forward_read_only_project_request::<proto::LanguageServerIdForName>,
             )

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::<LspExpandMacro, _, _>(|params, _| async move {
+    let mut expand_request_a = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
+        |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::<LspExpandMacro, _, _>(|params, _| async move {
+    let mut expand_request_b = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
+        |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)

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

crates/diagnostics/src/cargo.rs 🔗

@@ -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<Entity<Worktree>> {
-    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<FetchUpdate>)> {
-    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::<CargoFetchDiagnosticData>(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::<Vec<_>>();
-                        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<lsp::CodeDescription> {
-    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<lsp::CodeDescription> {
-    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<lsp::DiagnosticSeverity> {
-    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<lsp::Url, Vec<lsp::TextEdit>> = 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)
-}

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<Task<()>>,
-    rust_analyzer: Option<LanguageServerId>,
+    fetch_task: Option<Task<()>>,
+    cancel_task: Option<Task<()>>,
+    diagnostic_sources: Arc<Vec<ProjectPath>>,
 }
 
 impl EventEmitter<EditorEvent> 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<Self>) {
-        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<Vec<Entity<Worktree>>>,
-        window: &mut Window,
+        diagnostics_sources: Arc<Vec<ProjectPath>>,
         cx: &mut Context<Self>,
     ) {
-        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::<lsp::Diagnostic>::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<ProjectPath> {
+        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()
-}

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 {

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,

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<Editor>, 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<Editor>,
+) {
+    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<Editor>,
+) {
+    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<Editor>,
+) {
+    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);
+}

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,
             });

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<String, LanguageServerProgress>,
     pub has_pending_diagnostic_updates: bool,
-    pub progress_tokens: HashSet<String>,
+    progress_tokens: HashSet<String>,
 }
 
 #[derive(Clone, Debug)]
@@ -3449,8 +3450,14 @@ impl LspStore {
         client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>);
         client.add_entity_request_handler(Self::handle_lsp_command::<LinkedEditingRange>);
 
+        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::<lsp_ext_command::ExpandMacro>);
         client.add_entity_request_handler(Self::handle_lsp_command::<lsp_ext_command::OpenDocs>);
+        client.add_entity_request_handler(
+            Self::handle_lsp_command::<lsp_ext_command::GoToParentModule>,
+        );
         client.add_entity_request_handler(
             Self::handle_lsp_command::<lsp_ext_command::GetLspRunnables>,
         );
@@ -6236,13 +6243,6 @@ impl LspStore {
         })
     }
 
-    pub fn language_server_with_name(&self, name: &str, cx: &App) -> Option<LanguageServerId> {
-        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<proto::LanguageServerIdForNameResponse> {
         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<Self>,
+        envelope: TypedEnvelope<proto::LspExtCancelFlycheck>,
+        mut cx: AsyncApp,
+    ) -> Result<proto::Ack> {
+        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::<lsp_store::lsp_ext_command::LspExtCancelFlycheck>(&())
+                    .context("handling lsp ext cancel flycheck")
+            } else {
+                anyhow::Ok(())
+            }
+        })??;
+
+        Ok(proto::Ack {})
+    }
+
+    async fn handle_lsp_ext_run_flycheck(
+        lsp_store: Entity<Self>,
+        envelope: TypedEnvelope<proto::LspExtRunFlycheck>,
+        mut cx: AsyncApp,
+    ) -> Result<proto::Ack> {
+        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::LspExtRunFlycheck>(
+                        &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<Self>,
+        envelope: TypedEnvelope<proto::LspExtClearFlycheck>,
+        mut cx: AsyncApp,
+    ) -> Result<proto::Ack> {
+        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::<lsp_store::lsp_ext_command::LspExtClearFlycheck>(&())
+                    .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,

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<ExpandedMacro>;
     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<lsp::TextDocumentIdentifier>,
+}
+
+impl lsp::notification::Notification for LspExtClearFlycheck {
+    type Params = ();
+    const METHOD: &'static str = "rust-analyzer/clearFlycheck";
+}

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<LspStore>, language_server:
         })
         .detach();
 }
+
+pub fn cancel_flycheck(
+    project: Entity<Project>,
+    buffer_path: ProjectPath,
+    cx: &mut App,
+) -> Task<anyhow::Result<()>> {
+    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::<lsp_store::lsp_ext_command::LspExtCancelFlycheck>(&())?;
+                    }
+                    anyhow::Ok(())
+                })?
+                .context("lsp ext cancel flycheck")?;
+        };
+        anyhow::Ok(())
+    })
+}
+
+pub fn run_flycheck(
+    project: Entity<Project>,
+    buffer_path: ProjectPath,
+    cx: &mut App,
+) -> Task<anyhow::Result<()>> {
+    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::LspExtRunFlycheck>(
+                            &lsp_store::lsp_ext_command::RunFlycheckParams {
+                                text_document: None,
+                            },
+                        )?;
+                    }
+                    anyhow::Ok(())
+                })?
+                .context("lsp ext run flycheck")?;
+        };
+        anyhow::Ok(())
+    })
+}
+
+pub fn clear_flycheck(
+    project: Entity<Project>,
+    buffer_path: ProjectPath,
+    cx: &mut App,
+) -> Task<anyhow::Result<()>> {
+    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::<lsp_store::lsp_ext_command::LspExtClearFlycheck>(&())?;
+                    }
+                    anyhow::Ok(())
+                })?
+                .context("lsp ext clear flycheck")?;
+        };
+        anyhow::Ok(())
+    })
+}

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<LanguageServerId> {
-        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,

crates/project/src/project.rs 🔗

@@ -4748,42 +4748,6 @@ impl Project {
         })
     }
 
-    pub fn language_server_with_name(
-        &self,
-        name: &str,
-        cx: &App,
-    ) -> Task<Option<LanguageServerId>> {
-        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 {

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<String>,
-
-    /// Extra environment variables to pass to the diagnostics fetch command.
-    ///
-    /// Default: {}
-    #[serde(default)]
-    pub env: HashMap<String, String>,
-}
-
-fn default_diagnostics_fetch_command() -> Vec<String> {
-    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)]

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;
+}

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;

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,