lsp: Implement support for the textDocument/diagnostic
command (#19230)
Closes [#13107](https://github.com/zed-industries/zed/issues/13107) Enabled pull diagnostics by default, for the language servers that declare support in the corresponding capabilities. ``` "diagnostics": { "lsp_pull_diagnostics_debounce_ms": null } ``` settings can be used to disable the pulling. Release Notes: - Added support for the LSP `textDocument/diagnostic` command. # Brief This is draft PR that implements the LSP `textDocument/diagnostic` command. The goal is to receive your feedback and establish further steps towards fully implementing this command. I tried to re-use existing method and structures to ensure: 1. The existing functionality works as before 2. There is no interference between the diagnostics sent by a server and the diagnostics requested by a client. The current implementation is done via a new LSP command `GetDocumentDiagnostics` that is sent when a buffer is saved and when a buffer is edited. There is a new method called `pull_diagnostic` that is called for such events. It has debounce to ensure we don't spam a server with commands every time the buffer is edited. Probably, we don't need the debounce when the buffer is saved. All in all, the goal is basically to get your feedback and ensure I am on the right track. Thanks! ## References 1. https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics ## In action You can clone any Ruby repo since the `ruby-lsp` supports the pull diagnostics only. Steps to reproduce: 1. Clone this repo https://github.com/vitallium/stimulus-lsp-error-zed 2. Install Ruby (via `asdf` or `mise). 4. Install Ruby gems via `bundle install` 5. Install Ruby LSP with `gem install ruby-lsp` 6. Check out this PR and build Zed 7. Open any file and start editing to see diagnostics in realtime. https://github.com/user-attachments/assets/0ef6ec41-e4fa-4539-8f2c-6be0d8be4129 --------- Co-authored-by: Kirill Bulatov <mail4score@gmail.com> Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
parent
04cd3fcd23
commit
7aa70a4858
24 changed files with 1408 additions and 124 deletions
|
@ -4,7 +4,8 @@ pub mod rust_analyzer_ext;
|
|||
|
||||
use crate::{
|
||||
CodeAction, Completion, CompletionResponse, CompletionSource, CoreCompletion, Hover, InlayHint,
|
||||
LspAction, ProjectItem, ProjectPath, ProjectTransaction, ResolveState, Symbol, ToolchainStore,
|
||||
LspAction, LspPullDiagnostics, ProjectItem, ProjectPath, ProjectTransaction, ResolveState,
|
||||
Symbol, ToolchainStore,
|
||||
buffer_store::{BufferStore, BufferStoreEvent},
|
||||
environment::ProjectEnvironment,
|
||||
lsp_command::{self, *},
|
||||
|
@ -39,9 +40,9 @@ use http_client::HttpClient;
|
|||
use itertools::Itertools as _;
|
||||
use language::{
|
||||
Bias, BinaryStatus, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic,
|
||||
DiagnosticEntry, DiagnosticSet, Diff, File as _, Language, LanguageName, LanguageRegistry,
|
||||
LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch, PointUtf16,
|
||||
TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||
DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName,
|
||||
LanguageRegistry, LanguageToolchainStore, LocalFile, LspAdapter, LspAdapterDelegate, Patch,
|
||||
PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
|
||||
language_settings::{
|
||||
FormatOnSave, Formatter, LanguageSettings, SelectedFormatter, language_settings,
|
||||
},
|
||||
|
@ -252,6 +253,10 @@ impl LocalLspStore {
|
|||
let this = self.weak.clone();
|
||||
let pending_workspace_folders = pending_workspace_folders.clone();
|
||||
let fs = self.fs.clone();
|
||||
let pull_diagnostics = ProjectSettings::get_global(cx)
|
||||
.diagnostics
|
||||
.lsp_pull_diagnostics_debounce_ms
|
||||
.is_some();
|
||||
cx.spawn(async move |cx| {
|
||||
let result = async {
|
||||
let toolchains = this.update(cx, |this, cx| this.toolchain_store(cx))?;
|
||||
|
@ -282,7 +287,8 @@ impl LocalLspStore {
|
|||
}
|
||||
|
||||
let initialization_params = cx.update(|cx| {
|
||||
let mut params = language_server.default_initialize_params(cx);
|
||||
let mut params =
|
||||
language_server.default_initialize_params(pull_diagnostics, cx);
|
||||
params.initialization_options = initialization_options;
|
||||
adapter.adapter.prepare_initialize_params(params, cx)
|
||||
})??;
|
||||
|
@ -474,8 +480,14 @@ impl LocalLspStore {
|
|||
this.merge_diagnostics(
|
||||
server_id,
|
||||
params,
|
||||
DiagnosticSourceKind::Pushed,
|
||||
&adapter.disk_based_diagnostic_sources,
|
||||
|diagnostic, cx| adapter.retain_old_diagnostic(diagnostic, cx),
|
||||
|diagnostic, cx| match diagnostic.source_kind {
|
||||
DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => {
|
||||
adapter.retain_old_diagnostic(diagnostic, cx)
|
||||
}
|
||||
DiagnosticSourceKind::Pulled => true,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
|
@ -851,6 +863,28 @@ impl LocalLspStore {
|
|||
})
|
||||
.detach();
|
||||
|
||||
language_server
|
||||
.on_request::<lsp::request::WorkspaceDiagnosticRefresh, _, _>({
|
||||
let this = this.clone();
|
||||
move |(), cx| {
|
||||
let this = this.clone();
|
||||
let mut cx = cx.clone();
|
||||
async move {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
cx.emit(LspStoreEvent::RefreshDocumentsDiagnostics);
|
||||
this.downstream_client.as_ref().map(|(client, project_id)| {
|
||||
client.send(proto::RefreshDocumentsDiagnostics {
|
||||
project_id: *project_id,
|
||||
})
|
||||
})
|
||||
})?
|
||||
.transpose()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
language_server
|
||||
.on_request::<lsp::request::ShowMessageRequest, _, _>({
|
||||
let this = this.clone();
|
||||
|
@ -1869,8 +1903,7 @@ impl LocalLspStore {
|
|||
);
|
||||
}
|
||||
|
||||
let uri = lsp::Url::from_file_path(abs_path)
|
||||
.map_err(|()| anyhow!("failed to convert abs path to uri"))?;
|
||||
let uri = file_path_to_lsp_url(abs_path)?;
|
||||
let text_document = lsp::TextDocumentIdentifier::new(uri);
|
||||
|
||||
let lsp_edits = {
|
||||
|
@ -1934,8 +1967,7 @@ impl LocalLspStore {
|
|||
let logger = zlog::scoped!("lsp_format");
|
||||
zlog::info!(logger => "Formatting via LSP");
|
||||
|
||||
let uri = lsp::Url::from_file_path(abs_path)
|
||||
.map_err(|()| anyhow!("failed to convert abs path to uri"))?;
|
||||
let uri = file_path_to_lsp_url(abs_path)?;
|
||||
let text_document = lsp::TextDocumentIdentifier::new(uri);
|
||||
let capabilities = &language_server.capabilities();
|
||||
|
||||
|
@ -2262,7 +2294,7 @@ impl LocalLspStore {
|
|||
}
|
||||
|
||||
let abs_path = file.abs_path(cx);
|
||||
let Some(uri) = lsp::Url::from_file_path(&abs_path).log_err() else {
|
||||
let Some(uri) = file_path_to_lsp_url(&abs_path).log_err() else {
|
||||
return;
|
||||
};
|
||||
let initial_snapshot = buffer.text_snapshot();
|
||||
|
@ -3447,6 +3479,7 @@ pub enum LspStoreEvent {
|
|||
edits: Vec<(lsp::Range, Snippet)>,
|
||||
most_recent_edit: clock::Lamport,
|
||||
},
|
||||
RefreshDocumentsDiagnostics,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
|
@ -3494,6 +3527,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_lsp_command::<GetCodeActions>);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<GetCompletions>);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<GetHover>);
|
||||
|
@ -3521,6 +3555,7 @@ impl LspStore {
|
|||
client.add_entity_request_handler(
|
||||
Self::handle_lsp_command::<lsp_ext_command::SwitchSourceHeader>,
|
||||
);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentDiagnostics>);
|
||||
}
|
||||
|
||||
pub fn as_remote(&self) -> Option<&RemoteLspStore> {
|
||||
|
@ -4043,8 +4078,7 @@ impl LspStore {
|
|||
.contains_key(&buffer.read(cx).remote_id())
|
||||
{
|
||||
if let Some(file_url) =
|
||||
lsp::Url::from_file_path(&f.abs_path(cx))
|
||||
.log_err()
|
||||
file_path_to_lsp_url(&f.abs_path(cx)).log_err()
|
||||
{
|
||||
local.unregister_buffer_from_language_servers(
|
||||
&buffer, &file_url, cx,
|
||||
|
@ -4148,7 +4182,7 @@ impl LspStore {
|
|||
if let Some(abs_path) =
|
||||
File::from_dyn(buffer_file.as_ref()).map(|file| file.abs_path(cx))
|
||||
{
|
||||
if let Some(file_url) = lsp::Url::from_file_path(&abs_path).log_err() {
|
||||
if let Some(file_url) = file_path_to_lsp_url(&abs_path).log_err() {
|
||||
local_store.unregister_buffer_from_language_servers(
|
||||
buffer_entity,
|
||||
&file_url,
|
||||
|
@ -5674,6 +5708,73 @@ impl LspStore {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn pull_diagnostics(
|
||||
&mut self,
|
||||
buffer_handle: Entity<Buffer>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<LspPullDiagnostics>>> {
|
||||
let buffer = buffer_handle.read(cx);
|
||||
let buffer_id = buffer.remote_id();
|
||||
|
||||
if let Some((client, upstream_project_id)) = self.upstream_client() {
|
||||
let request_task = client.request(proto::MultiLspQuery {
|
||||
buffer_id: buffer_id.into(),
|
||||
version: serialize_version(&buffer_handle.read(cx).version()),
|
||||
project_id: upstream_project_id,
|
||||
strategy: Some(proto::multi_lsp_query::Strategy::All(
|
||||
proto::AllLanguageServers {},
|
||||
)),
|
||||
request: Some(proto::multi_lsp_query::Request::GetDocumentDiagnostics(
|
||||
GetDocumentDiagnostics {}.to_proto(upstream_project_id, buffer_handle.read(cx)),
|
||||
)),
|
||||
});
|
||||
let buffer = buffer_handle.clone();
|
||||
cx.spawn(async move |weak_project, cx| {
|
||||
let Some(project) = weak_project.upgrade() else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let responses = request_task.await?.responses;
|
||||
let diagnostics = join_all(
|
||||
responses
|
||||
.into_iter()
|
||||
.filter_map(|lsp_response| match lsp_response.response? {
|
||||
proto::lsp_response::Response::GetDocumentDiagnosticsResponse(
|
||||
response,
|
||||
) => Some(response),
|
||||
unexpected => {
|
||||
debug_panic!("Unexpected response: {unexpected:?}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.map(|diagnostics_response| {
|
||||
GetDocumentDiagnostics {}.response_from_proto(
|
||||
diagnostics_response,
|
||||
project.clone(),
|
||||
buffer.clone(),
|
||||
cx.clone(),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(diagnostics
|
||||
.into_iter()
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect())
|
||||
})
|
||||
} else {
|
||||
let all_actions_task = self.request_multiple_lsp_locally(
|
||||
&buffer_handle,
|
||||
None::<PointUtf16>,
|
||||
GetDocumentDiagnostics {},
|
||||
cx,
|
||||
);
|
||||
cx.spawn(async move |_, _| Ok(all_actions_task.await.into_iter().flatten().collect()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inlay_hints(
|
||||
&mut self,
|
||||
buffer_handle: Entity<Buffer>,
|
||||
|
@ -6218,7 +6319,7 @@ impl LspStore {
|
|||
let worktree_id = file.worktree_id(cx);
|
||||
let abs_path = file.as_local()?.abs_path(cx);
|
||||
let text_document = lsp::TextDocumentIdentifier {
|
||||
uri: lsp::Url::from_file_path(abs_path).log_err()?,
|
||||
uri: file_path_to_lsp_url(&abs_path).log_err()?,
|
||||
};
|
||||
let local = self.as_local()?;
|
||||
|
||||
|
@ -6525,15 +6626,15 @@ impl LspStore {
|
|||
path: relative_path.into(),
|
||||
};
|
||||
|
||||
if let Some(buffer) = self.buffer_store.read(cx).get_by_path(&project_path, cx) {
|
||||
if let Some(buffer_handle) = self.buffer_store.read(cx).get_by_path(&project_path, cx) {
|
||||
let snapshot = self
|
||||
.as_local_mut()
|
||||
.unwrap()
|
||||
.buffer_snapshot_for_lsp_version(&buffer, server_id, version, cx)?;
|
||||
.buffer_snapshot_for_lsp_version(&buffer_handle, server_id, version, cx)?;
|
||||
|
||||
let buffer = buffer_handle.read(cx);
|
||||
diagnostics.extend(
|
||||
buffer
|
||||
.read(cx)
|
||||
.get_diagnostics(server_id)
|
||||
.into_iter()
|
||||
.flat_map(|diag| {
|
||||
|
@ -6549,7 +6650,7 @@ impl LspStore {
|
|||
);
|
||||
|
||||
self.as_local_mut().unwrap().update_buffer_diagnostics(
|
||||
&buffer,
|
||||
&buffer_handle,
|
||||
server_id,
|
||||
version,
|
||||
diagnostics.clone(),
|
||||
|
@ -7071,6 +7172,47 @@ impl LspStore {
|
|||
.collect(),
|
||||
})
|
||||
}
|
||||
Some(proto::multi_lsp_query::Request::GetDocumentDiagnostics(
|
||||
get_document_diagnostics,
|
||||
)) => {
|
||||
let get_document_diagnostics = GetDocumentDiagnostics::from_proto(
|
||||
get_document_diagnostics,
|
||||
this.clone(),
|
||||
buffer.clone(),
|
||||
cx.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let all_diagnostics = this
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.request_multiple_lsp_locally(
|
||||
&buffer,
|
||||
None::<PointUtf16>,
|
||||
get_document_diagnostics,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await
|
||||
.into_iter();
|
||||
|
||||
this.update(&mut cx, |project, cx| proto::MultiLspQueryResponse {
|
||||
responses: all_diagnostics
|
||||
.map(|lsp_diagnostic| proto::LspResponse {
|
||||
response: Some(
|
||||
proto::lsp_response::Response::GetDocumentDiagnosticsResponse(
|
||||
GetDocumentDiagnostics::response_to_proto(
|
||||
lsp_diagnostic,
|
||||
project,
|
||||
sender_id,
|
||||
&buffer_version,
|
||||
cx,
|
||||
),
|
||||
),
|
||||
),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
None => anyhow::bail!("empty multi lsp query request"),
|
||||
}
|
||||
}
|
||||
|
@ -7671,7 +7813,7 @@ impl LspStore {
|
|||
PathEventKind::Changed => lsp::FileChangeType::CHANGED,
|
||||
};
|
||||
Some(lsp::FileEvent {
|
||||
uri: lsp::Url::from_file_path(&event.path).ok()?,
|
||||
uri: file_path_to_lsp_url(&event.path).log_err()?,
|
||||
typ,
|
||||
})
|
||||
})
|
||||
|
@ -7997,6 +8139,17 @@ impl LspStore {
|
|||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_refresh_documents_diagnostics(
|
||||
this: Entity<Self>,
|
||||
_: TypedEnvelope<proto::RefreshDocumentsDiagnostics>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::Ack> {
|
||||
this.update(&mut cx, |_, cx| {
|
||||
cx.emit(LspStoreEvent::RefreshDocumentsDiagnostics);
|
||||
})?;
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
async fn handle_inlay_hints(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::InlayHints>,
|
||||
|
@ -8719,12 +8872,14 @@ impl LspStore {
|
|||
&mut self,
|
||||
language_server_id: LanguageServerId,
|
||||
params: lsp::PublishDiagnosticsParams,
|
||||
source_kind: DiagnosticSourceKind,
|
||||
disk_based_sources: &[String],
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
self.merge_diagnostics(
|
||||
language_server_id,
|
||||
params,
|
||||
source_kind,
|
||||
disk_based_sources,
|
||||
|_, _| false,
|
||||
cx,
|
||||
|
@ -8735,6 +8890,7 @@ impl LspStore {
|
|||
&mut self,
|
||||
language_server_id: LanguageServerId,
|
||||
mut params: lsp::PublishDiagnosticsParams,
|
||||
source_kind: DiagnosticSourceKind,
|
||||
disk_based_sources: &[String],
|
||||
filter: F,
|
||||
cx: &mut Context<Self>,
|
||||
|
@ -8799,6 +8955,7 @@ impl LspStore {
|
|||
range,
|
||||
diagnostic: Diagnostic {
|
||||
source: diagnostic.source.clone(),
|
||||
source_kind,
|
||||
code: diagnostic.code.clone(),
|
||||
code_description: diagnostic
|
||||
.code_description
|
||||
|
@ -8825,6 +8982,7 @@ impl LspStore {
|
|||
range,
|
||||
diagnostic: Diagnostic {
|
||||
source: diagnostic.source.clone(),
|
||||
source_kind,
|
||||
code: diagnostic.code.clone(),
|
||||
code_description: diagnostic
|
||||
.code_description
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue