Improve support for multiple registrations of textDocument/diagnostic (#43703)

Closes https://github.com/zed-industries/zed/issues/41935

The registration ID responsible for generating each diagnostic is now
tracked. This allows us to replace only the diagnostics from the same
registration ID when a pull diagnostics report is applied.

Additionally, various deficiencies in our support for pull diagnostics
have been fixed:
- Document pulls are issued for all open buffers, not just the edited
one. A shorter debounce is used for the edited buffer. Workspace
diagnostics are also now ignored for open buffers.
- Tracking of `lastResultId` is improved.
- Stored pull diagnostics are discarded when the corresponding buffer is
closed.

Release Notes:

- Improved compatibility with language servers that use the "pull
diagnostics" feature of Language Server Protocol.

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
John Tur
2025-12-03 20:47:43 +02:00
committed by GitHub
co-authored by Kirill Bulatov Kirill Bulatov
parent 493cfadb42
commit a51e975b81
13 changed files with 524 additions and 263 deletions
+53 -23
View File
@@ -14,7 +14,7 @@ use client::proto::{self, PeerId};
use clock::Global;
use collections::{HashMap, HashSet};
use futures::future;
use gpui::{App, AsyncApp, Entity, Task};
use gpui::{App, AsyncApp, Entity, SharedString, Task};
use language::{
Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, CharScopeContext,
OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped,
@@ -26,8 +26,8 @@ use language::{
use lsp::{
AdapterServerCapabilities, CodeActionKind, CodeActionOptions, CodeDescription,
CompletionContext, CompletionListItemDefaultsEditRange, CompletionTriggerKind,
DiagnosticServerCapabilities, DocumentHighlightKind, LanguageServer, LanguageServerId,
LinkedEditingRangeServerCapabilities, OneOf, RenameOptions, ServerCapabilities,
DocumentHighlightKind, LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities,
OneOf, RenameOptions, ServerCapabilities,
};
use serde_json::Value;
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
@@ -265,8 +265,9 @@ pub(crate) struct LinkedEditingRange {
pub(crate) struct GetDocumentDiagnostics {
/// We cannot blindly rely on server's capabilities.diagnostic_provider, as they're a singular field, whereas
/// a server can register multiple diagnostic providers post-mortem.
pub dynamic_caps: DiagnosticServerCapabilities,
pub previous_result_id: Option<String>,
pub registration_id: Option<SharedString>,
pub identifier: Option<String>,
pub previous_result_id: Option<SharedString>,
}
#[async_trait(?Send)]
@@ -3755,15 +3756,16 @@ impl GetDocumentDiagnostics {
.into_iter()
.filter_map(|diagnostics| {
Some(LspPullDiagnostics::Response {
registration_id: diagnostics.registration_id.map(SharedString::from),
server_id: LanguageServerId::from_proto(diagnostics.server_id),
uri: lsp::Uri::from_str(diagnostics.uri.as_str()).log_err()?,
diagnostics: if diagnostics.changed {
PulledDiagnostics::Unchanged {
result_id: diagnostics.result_id?,
result_id: SharedString::new(diagnostics.result_id?),
}
} else {
PulledDiagnostics::Changed {
result_id: diagnostics.result_id,
result_id: diagnostics.result_id.map(SharedString::new),
diagnostics: diagnostics
.diagnostics
.into_iter()
@@ -3927,6 +3929,7 @@ impl GetDocumentDiagnostics {
pub fn deserialize_workspace_diagnostics_report(
report: lsp::WorkspaceDiagnosticReportResult,
server_id: LanguageServerId,
registration_id: Option<SharedString>,
) -> Vec<WorkspaceLspPullDiagnostics> {
let mut pulled_diagnostics = HashMap::default();
match report {
@@ -3938,6 +3941,7 @@ impl GetDocumentDiagnostics {
&mut pulled_diagnostics,
server_id,
report,
registration_id.clone(),
)
}
lsp::WorkspaceDocumentDiagnosticReport::Unchanged(report) => {
@@ -3945,6 +3949,7 @@ impl GetDocumentDiagnostics {
&mut pulled_diagnostics,
server_id,
report,
registration_id.clone(),
)
}
}
@@ -3960,6 +3965,7 @@ impl GetDocumentDiagnostics {
&mut pulled_diagnostics,
server_id,
report,
registration_id.clone(),
)
}
lsp::WorkspaceDocumentDiagnosticReport::Unchanged(report) => {
@@ -3967,6 +3973,7 @@ impl GetDocumentDiagnostics {
&mut pulled_diagnostics,
server_id,
report,
registration_id.clone(),
)
}
}
@@ -3987,6 +3994,7 @@ fn process_full_workspace_diagnostics_report(
diagnostics: &mut HashMap<lsp::Uri, WorkspaceLspPullDiagnostics>,
server_id: LanguageServerId,
report: lsp::WorkspaceFullDocumentDiagnosticReport,
registration_id: Option<SharedString>,
) {
let mut new_diagnostics = HashMap::default();
process_full_diagnostics_report(
@@ -3994,6 +4002,7 @@ fn process_full_workspace_diagnostics_report(
server_id,
report.uri,
report.full_document_diagnostic_report,
registration_id,
);
diagnostics.extend(new_diagnostics.into_iter().map(|(uri, diagnostics)| {
(
@@ -4010,6 +4019,7 @@ fn process_unchanged_workspace_diagnostics_report(
diagnostics: &mut HashMap<lsp::Uri, WorkspaceLspPullDiagnostics>,
server_id: LanguageServerId,
report: lsp::WorkspaceUnchangedDocumentDiagnosticReport,
registration_id: Option<SharedString>,
) {
let mut new_diagnostics = HashMap::default();
process_unchanged_diagnostics_report(
@@ -4017,6 +4027,7 @@ fn process_unchanged_workspace_diagnostics_report(
server_id,
report.uri,
report.unchanged_document_diagnostic_report,
registration_id,
);
diagnostics.extend(new_diagnostics.into_iter().map(|(uri, diagnostics)| {
(
@@ -4050,19 +4061,12 @@ impl LspCommand for GetDocumentDiagnostics {
_: &Arc<LanguageServer>,
_: &App,
) -> Result<lsp::DocumentDiagnosticParams> {
let identifier = match &self.dynamic_caps {
lsp::DiagnosticServerCapabilities::Options(options) => options.identifier.clone(),
lsp::DiagnosticServerCapabilities::RegistrationOptions(options) => {
options.diagnostic_options.identifier.clone()
}
};
Ok(lsp::DocumentDiagnosticParams {
text_document: lsp::TextDocumentIdentifier {
uri: file_path_to_lsp_url(path)?,
},
identifier,
previous_result_id: self.previous_result_id.clone(),
identifier: self.identifier.clone(),
previous_result_id: self.previous_result_id.clone().map(|id| id.to_string()),
partial_result_params: Default::default(),
work_done_progress_params: Default::default(),
})
@@ -4097,6 +4101,7 @@ impl LspCommand for GetDocumentDiagnostics {
&mut pulled_diagnostics,
server_id,
related_documents,
self.registration_id.clone(),
);
}
process_full_diagnostics_report(
@@ -4104,6 +4109,7 @@ impl LspCommand for GetDocumentDiagnostics {
server_id,
url,
report.full_document_diagnostic_report,
self.registration_id,
);
}
lsp::DocumentDiagnosticReport::Unchanged(report) => {
@@ -4112,6 +4118,7 @@ impl LspCommand for GetDocumentDiagnostics {
&mut pulled_diagnostics,
server_id,
related_documents,
self.registration_id.clone(),
);
}
process_unchanged_diagnostics_report(
@@ -4119,6 +4126,7 @@ impl LspCommand for GetDocumentDiagnostics {
server_id,
url,
report.unchanged_document_diagnostic_report,
self.registration_id,
);
}
},
@@ -4128,6 +4136,7 @@ impl LspCommand for GetDocumentDiagnostics {
&mut pulled_diagnostics,
server_id,
related_documents,
self.registration_id,
);
}
}
@@ -4170,6 +4179,7 @@ impl LspCommand for GetDocumentDiagnostics {
server_id,
uri,
diagnostics,
registration_id,
} => {
let mut changed = false;
let (diagnostics, result_id) = match diagnostics {
@@ -4184,7 +4194,7 @@ impl LspCommand for GetDocumentDiagnostics {
};
Some(proto::PulledDiagnostics {
changed,
result_id,
result_id: result_id.map(|id| id.to_string()),
uri: uri.to_string(),
server_id: server_id.to_proto(),
diagnostics: diagnostics
@@ -4195,6 +4205,7 @@ impl LspCommand for GetDocumentDiagnostics {
.log_err()
})
.collect(),
registration_id: registration_id.as_ref().map(ToString::to_string),
})
}
})
@@ -4365,14 +4376,25 @@ fn process_related_documents(
diagnostics: &mut HashMap<lsp::Uri, LspPullDiagnostics>,
server_id: LanguageServerId,
documents: impl IntoIterator<Item = (lsp::Uri, lsp::DocumentDiagnosticReportKind)>,
registration_id: Option<SharedString>,
) {
for (url, report_kind) in documents {
match report_kind {
lsp::DocumentDiagnosticReportKind::Full(report) => {
process_full_diagnostics_report(diagnostics, server_id, url, report)
}
lsp::DocumentDiagnosticReportKind::Full(report) => process_full_diagnostics_report(
diagnostics,
server_id,
url,
report,
registration_id.clone(),
),
lsp::DocumentDiagnosticReportKind::Unchanged(report) => {
process_unchanged_diagnostics_report(diagnostics, server_id, url, report)
process_unchanged_diagnostics_report(
diagnostics,
server_id,
url,
report,
registration_id.clone(),
)
}
}
}
@@ -4383,8 +4405,9 @@ fn process_unchanged_diagnostics_report(
server_id: LanguageServerId,
uri: lsp::Uri,
report: lsp::UnchangedDocumentDiagnosticReport,
registration_id: Option<SharedString>,
) {
let result_id = report.result_id;
let result_id = SharedString::new(report.result_id);
match diagnostics.entry(uri.clone()) {
hash_map::Entry::Occupied(mut o) => match o.get_mut() {
LspPullDiagnostics::Default => {
@@ -4392,12 +4415,14 @@ fn process_unchanged_diagnostics_report(
server_id,
uri,
diagnostics: PulledDiagnostics::Unchanged { result_id },
registration_id,
});
}
LspPullDiagnostics::Response {
server_id: existing_server_id,
uri: existing_uri,
diagnostics: existing_diagnostics,
..
} => {
if server_id != *existing_server_id || &uri != existing_uri {
debug_panic!(
@@ -4417,6 +4442,7 @@ fn process_unchanged_diagnostics_report(
server_id,
uri,
diagnostics: PulledDiagnostics::Unchanged { result_id },
registration_id,
});
}
}
@@ -4427,8 +4453,9 @@ fn process_full_diagnostics_report(
server_id: LanguageServerId,
uri: lsp::Uri,
report: lsp::FullDocumentDiagnosticReport,
registration_id: Option<SharedString>,
) {
let result_id = report.result_id;
let result_id = report.result_id.map(SharedString::new);
match diagnostics.entry(uri.clone()) {
hash_map::Entry::Occupied(mut o) => match o.get_mut() {
LspPullDiagnostics::Default => {
@@ -4439,12 +4466,14 @@ fn process_full_diagnostics_report(
result_id,
diagnostics: report.items,
},
registration_id,
});
}
LspPullDiagnostics::Response {
server_id: existing_server_id,
uri: existing_uri,
diagnostics: existing_diagnostics,
..
} => {
if server_id != *existing_server_id || &uri != existing_uri {
debug_panic!(
@@ -4478,6 +4507,7 @@ fn process_full_diagnostics_report(
result_id,
diagnostics: report.items,
},
registration_id,
});
}
}