Pull diagnostics fixes (#32242)

Follow-up of https://github.com/zed-industries/zed/pull/19230

* starts to send `result_id` in pull requests to allow servers to reply
with non-full results
* fixes a bug where disk-based diagnostics were offset after pulling the
diagnostics
* fixes a bug due to which pull diagnostics could not be disabled
* uses better names and comments for the workspace pull diagnostics part

Release Notes:

- N/A
This commit is contained in:
Kirill Bulatov 2025-06-06 16:18:05 +03:00 committed by GitHub
parent 508b604b67
commit 380d8c5662
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 272 additions and 109 deletions

View file

@ -3832,7 +3832,7 @@ impl LspCommand for GetDocumentDiagnostics {
fn to_lsp(
&self,
path: &Path,
_: &Buffer,
buffer: &Buffer,
language_server: &Arc<LanguageServer>,
_: &App,
) -> Result<lsp::DocumentDiagnosticParams> {
@ -3849,7 +3849,7 @@ impl LspCommand for GetDocumentDiagnostics {
uri: file_path_to_lsp_url(path)?,
},
identifier,
previous_result_id: None,
previous_result_id: buffer.result_id(),
partial_result_params: Default::default(),
work_done_progress_params: Default::default(),
})

View file

@ -255,8 +255,8 @@ impl LocalLspStore {
let fs = self.fs.clone();
let pull_diagnostics = ProjectSettings::get_global(cx)
.diagnostics
.lsp_pull_diagnostics_debounce_ms
.is_some();
.lsp_pull_diagnostics
.enabled;
cx.spawn(async move |cx| {
let result = async {
let toolchains = this.update(cx, |this, cx| this.toolchain_store(cx))?;
@ -480,6 +480,7 @@ impl LocalLspStore {
this.merge_diagnostics(
server_id,
params,
None,
DiagnosticSourceKind::Pushed,
&adapter.disk_based_diagnostic_sources,
|diagnostic, cx| match diagnostic.source_kind {
@ -871,9 +872,9 @@ impl LocalLspStore {
let mut cx = cx.clone();
async move {
this.update(&mut cx, |this, cx| {
cx.emit(LspStoreEvent::RefreshDocumentsDiagnostics);
cx.emit(LspStoreEvent::PullWorkspaceDiagnostics);
this.downstream_client.as_ref().map(|(client, project_id)| {
client.send(proto::RefreshDocumentsDiagnostics {
client.send(proto::PullWorkspaceDiagnostics {
project_id: *project_id,
})
})
@ -2138,8 +2139,16 @@ impl LocalLspStore {
for (server_id, diagnostics) in
diagnostics.get(file.path()).cloned().unwrap_or_default()
{
self.update_buffer_diagnostics(buffer_handle, server_id, None, diagnostics, cx)
.log_err();
self.update_buffer_diagnostics(
buffer_handle,
server_id,
None,
None,
diagnostics,
Vec::new(),
cx,
)
.log_err();
}
}
let Some(language) = language else {
@ -2208,8 +2217,10 @@ impl LocalLspStore {
&mut self,
buffer: &Entity<Buffer>,
server_id: LanguageServerId,
result_id: Option<String>,
version: Option<i32>,
mut diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
new_diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
reused_diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
cx: &mut Context<LspStore>,
) -> Result<()> {
fn compare_diagnostics(a: &Diagnostic, b: &Diagnostic) -> Ordering {
@ -2220,7 +2231,11 @@ impl LocalLspStore {
.then_with(|| a.message.cmp(&b.message))
}
diagnostics.sort_unstable_by(|a, b| {
let mut diagnostics = Vec::with_capacity(new_diagnostics.len() + reused_diagnostics.len());
diagnostics.extend(new_diagnostics.into_iter().map(|d| (true, d)));
diagnostics.extend(reused_diagnostics.into_iter().map(|d| (false, d)));
diagnostics.sort_unstable_by(|(_, a), (_, b)| {
Ordering::Equal
.then_with(|| a.range.start.cmp(&b.range.start))
.then_with(|| b.range.end.cmp(&a.range.end))
@ -2236,13 +2251,15 @@ impl LocalLspStore {
let mut sanitized_diagnostics = Vec::with_capacity(diagnostics.len());
for entry in diagnostics {
for (new_diagnostic, entry) in diagnostics {
let start;
let end;
if entry.diagnostic.is_disk_based {
if new_diagnostic && entry.diagnostic.is_disk_based {
// Some diagnostics are based on files on disk instead of buffers'
// current contents. Adjust these diagnostics' ranges to reflect
// any unsaved edits.
// Do not alter the reused ones though, as their coordinates were stored as anchors
// and were properly adjusted on reuse.
start = Unclipped((*edits_since_save).old_to_new(entry.range.start.0));
end = Unclipped((*edits_since_save).old_to_new(entry.range.end.0));
} else {
@ -2273,6 +2290,7 @@ impl LocalLspStore {
let set = DiagnosticSet::new(sanitized_diagnostics, &snapshot);
buffer.update(cx, |buffer, cx| {
buffer.set_result_id(result_id);
buffer.update_diagnostics(server_id, set, cx)
});
Ok(())
@ -3479,7 +3497,7 @@ pub enum LspStoreEvent {
edits: Vec<(lsp::Range, Snippet)>,
most_recent_edit: clock::Lamport,
},
RefreshDocumentsDiagnostics,
PullWorkspaceDiagnostics,
}
#[derive(Clone, Debug, Serialize)]
@ -3527,7 +3545,7 @@ impl LspStore {
client.add_entity_request_handler(Self::handle_register_buffer_with_language_servers);
client.add_entity_request_handler(Self::handle_rename_project_entry);
client.add_entity_request_handler(Self::handle_language_server_id_for_name);
client.add_entity_request_handler(Self::handle_refresh_documents_diagnostics);
client.add_entity_request_handler(Self::handle_pull_workspace_diagnostics);
client.add_entity_request_handler(Self::handle_lsp_command::<GetCodeActions>);
client.add_entity_request_handler(Self::handle_lsp_command::<GetCompletions>);
client.add_entity_request_handler(Self::handle_lsp_command::<GetHover>);
@ -6594,21 +6612,32 @@ impl LspStore {
.insert(language_server_id);
}
#[cfg(test)]
pub fn update_diagnostic_entries(
&mut self,
server_id: LanguageServerId,
abs_path: PathBuf,
result_id: Option<String>,
version: Option<i32>,
diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
cx: &mut Context<Self>,
) -> anyhow::Result<()> {
self.merge_diagnostic_entries(server_id, abs_path, version, diagnostics, |_, _| false, cx)
self.merge_diagnostic_entries(
server_id,
abs_path,
result_id,
version,
diagnostics,
|_, _| false,
cx,
)
}
pub fn merge_diagnostic_entries<F: Fn(&Diagnostic, &App) -> bool + Clone>(
&mut self,
server_id: LanguageServerId,
abs_path: PathBuf,
result_id: Option<String>,
version: Option<i32>,
mut diagnostics: Vec<DiagnosticEntry<Unclipped<PointUtf16>>>,
filter: F,
@ -6633,29 +6662,32 @@ impl LspStore {
.buffer_snapshot_for_lsp_version(&buffer_handle, server_id, version, cx)?;
let buffer = buffer_handle.read(cx);
diagnostics.extend(
buffer
.get_diagnostics(server_id)
.into_iter()
.flat_map(|diag| {
diag.iter().filter(|v| filter(&v.diagnostic, cx)).map(|v| {
let start = Unclipped(v.range.start.to_point_utf16(&snapshot));
let end = Unclipped(v.range.end.to_point_utf16(&snapshot));
DiagnosticEntry {
range: start..end,
diagnostic: v.diagnostic.clone(),
}
})
}),
);
let reused_diagnostics = buffer
.get_diagnostics(server_id)
.into_iter()
.flat_map(|diag| {
diag.iter().filter(|v| filter(&v.diagnostic, cx)).map(|v| {
let start = Unclipped(v.range.start.to_point_utf16(&snapshot));
let end = Unclipped(v.range.end.to_point_utf16(&snapshot));
DiagnosticEntry {
range: start..end,
diagnostic: v.diagnostic.clone(),
}
})
})
.collect::<Vec<_>>();
self.as_local_mut().unwrap().update_buffer_diagnostics(
&buffer_handle,
server_id,
result_id,
version,
diagnostics.clone(),
reused_diagnostics.clone(),
cx,
)?;
diagnostics.extend(reused_diagnostics);
}
let updated = worktree.update(cx, |worktree, cx| {
@ -8139,13 +8171,13 @@ impl LspStore {
Ok(proto::Ack {})
}
async fn handle_refresh_documents_diagnostics(
async fn handle_pull_workspace_diagnostics(
this: Entity<Self>,
_: TypedEnvelope<proto::RefreshDocumentsDiagnostics>,
_: TypedEnvelope<proto::PullWorkspaceDiagnostics>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
this.update(&mut cx, |_, cx| {
cx.emit(LspStoreEvent::RefreshDocumentsDiagnostics);
cx.emit(LspStoreEvent::PullWorkspaceDiagnostics);
})?;
Ok(proto::Ack {})
}
@ -8872,6 +8904,7 @@ impl LspStore {
&mut self,
language_server_id: LanguageServerId,
params: lsp::PublishDiagnosticsParams,
result_id: Option<String>,
source_kind: DiagnosticSourceKind,
disk_based_sources: &[String],
cx: &mut Context<Self>,
@ -8879,6 +8912,7 @@ impl LspStore {
self.merge_diagnostics(
language_server_id,
params,
result_id,
source_kind,
disk_based_sources,
|_, _| false,
@ -8890,6 +8924,7 @@ impl LspStore {
&mut self,
language_server_id: LanguageServerId,
mut params: lsp::PublishDiagnosticsParams,
result_id: Option<String>,
source_kind: DiagnosticSourceKind,
disk_based_sources: &[String],
filter: F,
@ -9027,6 +9062,7 @@ impl LspStore {
self.merge_diagnostic_entries(
language_server_id,
abs_path,
result_id,
params.version,
diagnostics,
filter,

View file

@ -84,6 +84,7 @@ pub fn register_notifications(
this.merge_diagnostics(
server_id,
mapped_diagnostics,
None,
DiagnosticSourceKind::Pushed,
&adapter.disk_based_diagnostic_sources,
|diag, _| !is_inactive_region(diag),

View file

@ -317,7 +317,7 @@ pub enum Event {
SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
ExpandedAllForEntry(WorktreeId, ProjectEntryId),
AgentLocationChanged,
RefreshDocumentsDiagnostics,
PullWorkspaceDiagnostics,
}
pub struct AgentLocationChanged;
@ -2814,9 +2814,7 @@ impl Project {
}
LspStoreEvent::RefreshInlayHints => cx.emit(Event::RefreshInlayHints),
LspStoreEvent::RefreshCodeLens => cx.emit(Event::RefreshCodeLens),
LspStoreEvent::RefreshDocumentsDiagnostics => {
cx.emit(Event::RefreshDocumentsDiagnostics)
}
LspStoreEvent::PullWorkspaceDiagnostics => cx.emit(Event::PullWorkspaceDiagnostics),
LspStoreEvent::LanguageServerPrompt(prompt) => {
cx.emit(Event::LanguageServerPrompt(prompt.clone()))
}
@ -3732,6 +3730,7 @@ impl Project {
&mut self,
language_server_id: LanguageServerId,
source_kind: DiagnosticSourceKind,
result_id: Option<String>,
params: lsp::PublishDiagnosticsParams,
disk_based_sources: &[String],
cx: &mut Context<Self>,
@ -3740,6 +3739,7 @@ impl Project {
lsp_store.update_diagnostics(
language_server_id,
params,
result_id,
source_kind,
disk_based_sources,
cx,

View file

@ -127,9 +127,8 @@ pub struct DiagnosticsSettings {
/// Whether or not to include warning diagnostics.
pub include_warnings: bool,
/// Minimum time to wait before pulling diagnostics from the language server(s).
/// 0 turns the debounce off, None disables the feature.
pub lsp_pull_diagnostics_debounce_ms: Option<u64>,
/// Settings for using LSP pull diagnostics mechanism in Zed.
pub lsp_pull_diagnostics: LspPullDiagnosticsSettings,
/// Settings for showing inline diagnostics.
pub inline: InlineDiagnosticsSettings,
@ -146,6 +145,26 @@ impl DiagnosticsSettings {
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(default)]
pub struct LspPullDiagnosticsSettings {
/// Whether to pull for diagnostics or not.
///
/// Default: true
#[serde(default = "default_true")]
pub enabled: bool,
/// Minimum time to wait before pulling diagnostics from the language server(s).
/// 0 turns the debounce off.
///
/// Default: 50
#[serde(default = "default_lsp_diagnostics_pull_debounce_ms")]
pub debounce_ms: u64,
}
fn default_lsp_diagnostics_pull_debounce_ms() -> u64 {
50
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(default)]
pub struct InlineDiagnosticsSettings {
@ -157,11 +176,13 @@ pub struct InlineDiagnosticsSettings {
/// last editor event.
///
/// Default: 150
#[serde(default = "default_inline_diagnostics_update_debounce_ms")]
pub update_debounce_ms: u64,
/// The amount of padding between the end of the source line and the start
/// of the inline diagnostic in units of columns.
///
/// Default: 4
#[serde(default = "default_inline_diagnostics_padding")]
pub padding: u32,
/// The minimum column to display inline diagnostics. This setting can be
/// used to horizontally align inline diagnostics at some position. Lines
@ -173,6 +194,47 @@ pub struct InlineDiagnosticsSettings {
pub max_severity: Option<DiagnosticSeverity>,
}
fn default_inline_diagnostics_update_debounce_ms() -> u64 {
150
}
fn default_inline_diagnostics_padding() -> u32 {
4
}
impl Default for DiagnosticsSettings {
fn default() -> Self {
Self {
button: true,
include_warnings: true,
lsp_pull_diagnostics: LspPullDiagnosticsSettings::default(),
inline: InlineDiagnosticsSettings::default(),
cargo: None,
}
}
}
impl Default for LspPullDiagnosticsSettings {
fn default() -> Self {
Self {
enabled: true,
debounce_ms: default_lsp_diagnostics_pull_debounce_ms(),
}
}
}
impl Default for InlineDiagnosticsSettings {
fn default() -> Self {
Self {
enabled: false,
update_debounce_ms: default_inline_diagnostics_update_debounce_ms(),
padding: default_inline_diagnostics_padding(),
min_column: 0,
max_severity: None,
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct CargoDiagnosticsSettings {
/// When enabled, Zed disables rust-analyzer's check on save and starts to query
@ -208,30 +270,6 @@ impl DiagnosticSeverity {
}
}
impl Default for DiagnosticsSettings {
fn default() -> Self {
Self {
button: true,
include_warnings: true,
lsp_pull_diagnostics_debounce_ms: Some(30),
inline: InlineDiagnosticsSettings::default(),
cargo: None,
}
}
}
impl Default for InlineDiagnosticsSettings {
fn default() -> Self {
Self {
enabled: false,
update_debounce_ms: 150,
padding: 4,
min_column: 0,
max_severity: None,
}
}
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct GitSettings {
/// Whether or not to show the git gutter.

View file

@ -1332,6 +1332,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
..Default::default()
}],
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
@ -1350,6 +1351,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
..Default::default()
}],
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
@ -1441,6 +1443,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
..Default::default()
}],
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
@ -1459,6 +1462,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
..Default::default()
}],
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
@ -2376,6 +2380,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
LanguageServerId(0),
PathBuf::from("/dir/a.rs"),
None,
None,
vec![
DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 10))
@ -2442,6 +2447,7 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
LanguageServerId(0),
Path::new("/dir/a.rs").to_owned(),
None,
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
diagnostic: Diagnostic {
@ -2460,6 +2466,7 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC
LanguageServerId(1),
Path::new("/dir/a.rs").to_owned(),
None,
None,
vec![DiagnosticEntry {
range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
diagnostic: Diagnostic {
@ -4596,6 +4603,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
lsp_store.update_diagnostics(
LanguageServerId(0),
message,
None,
DiagnosticSourceKind::Pushed,
&[],
cx,