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
This commit is contained in:
Kirill Bulatov 2025-05-02 10:07:51 +03:00 committed by GitHub
parent 672a1dd553
commit ba59305510
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 520 additions and 1071 deletions

View file

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

View file

@ -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";
}

View file

@ -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(())
})
}

View file

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

View file

@ -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 {

View file

@ -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)]