From 7aa70a4858c0af79d20432adf98f3a973b54977b Mon Sep 17 00:00:00 2001 From: Vitaly Slobodin Date: Thu, 5 Jun 2025 21:42:52 +0200 Subject: [PATCH] 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 Co-authored-by: Kirill Bulatov --- assets/settings/default.json | 3 + crates/collab/src/rpc.rs | 4 + crates/collab/src/tests/integration_tests.rs | 12 +- crates/copilot/src/copilot.rs | 2 +- crates/diagnostics/src/diagnostics_tests.rs | 20 +- crates/editor/src/editor.rs | 211 ++++-- crates/editor/src/editor_tests.rs | 132 ++++ crates/editor/src/proposed_changes_editor.rs | 8 + crates/language/src/buffer.rs | 10 + crates/language/src/proto.rs | 14 +- crates/lsp/src/lsp.rs | 14 +- crates/prettier/src/prettier.rs | 2 +- crates/project/src/lsp_command.rs | 648 +++++++++++++++++- crates/project/src/lsp_store.rs | 198 +++++- crates/project/src/lsp_store/clangd_ext.rs | 3 +- .../project/src/lsp_store/lsp_ext_command.rs | 10 +- crates/project/src/project.rs | 67 +- crates/project/src/project_settings.rs | 9 +- crates/project/src/project_tests.rs | 75 +- crates/proto/proto/buffer.proto | 8 + crates/proto/proto/lsp.proto | 58 ++ crates/proto/proto/zed.proto | 7 +- crates/proto/src/proto.rs | 7 + crates/workspace/src/workspace.rs | 10 +- 24 files changed, 1408 insertions(+), 124 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 6e0bd4d34b90636fe300b2ff9b56db84d94f1ffd..8d8c65884cdc7e593b49d22895da8c8a1b3e0e47 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1034,6 +1034,9 @@ "button": true, // Whether to show warnings or not by default. "include_warnings": true, + // Minimum time to wait before pulling diagnostics from the language server(s). + // 0 turns the debounce off, `null` disables the feature. + "lsp_pull_diagnostics_debounce_ms": 50, // Settings for inline diagnostics "inline": { // Whether to show diagnostics inline or not diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 4f371b813566d05f4efb2958751eb7c0bc97bef2..e768e4c3d01b35d6d4c3cd198220ac44a6a7ff81 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -312,6 +312,7 @@ impl Server { .add_request_handler( forward_read_only_project_request::, ) + .add_request_handler(forward_read_only_project_request::) .add_request_handler( forward_mutating_project_request::, ) @@ -354,6 +355,9 @@ impl Server { .add_message_handler(broadcast_project_message_from_host::) .add_message_handler(broadcast_project_message_from_host::) .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler( + broadcast_project_message_from_host::, + ) .add_request_handler(get_users) .add_request_handler(fuzzy_search_users) .add_request_handler(request_contact) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 202200ef58e2efa42f13666b7bf1513ad847e3ca..d00ff0babace7286883c488ad0e2782738ba3101 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -20,8 +20,8 @@ use gpui::{ UpdateGlobal, px, size, }; use language::{ - Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, - LineEnding, OffsetRangeExt, Point, Rope, + Diagnostic, DiagnosticEntry, DiagnosticSourceKind, FakeLspAdapter, Language, LanguageConfig, + LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope, language_settings::{ AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter, }, @@ -4237,7 +4237,8 @@ async fn test_collaborating_with_diagnostics( message: "message 1".to_string(), severity: lsp::DiagnosticSeverity::ERROR, is_primary: true, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() } }, DiagnosticEntry { @@ -4247,7 +4248,8 @@ async fn test_collaborating_with_diagnostics( severity: lsp::DiagnosticSeverity::WARNING, message: "message 2".to_string(), is_primary: true, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() } } ] @@ -4259,7 +4261,7 @@ async fn test_collaborating_with_diagnostics( &lsp::PublishDiagnosticsParams { uri: lsp::Url::from_file_path(path!("/a/a.rs")).unwrap(), version: None, - diagnostics: vec![], + diagnostics: Vec::new(), }, ); executor.run_until_parked(); diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index c561ec386532cce7139d71ac966e82c0166b008f..66472a78dc950fb23c8fab44530b9672e8d406ec 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -520,7 +520,7 @@ impl Copilot { let server = cx .update(|cx| { - let mut params = server.default_initialize_params(cx); + let mut params = server.default_initialize_params(false, cx); params.initialization_options = Some(editor_info_json); server.initialize(params, configuration.into(), cx) })? diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 22776d525fefe62eb51ba7ff9a4634701fed8954..1050c0ecf9ff034c9c4ea8ea56f26347371df931 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -11,7 +11,7 @@ use editor::{ }; use gpui::{TestAppContext, VisualTestContext}; use indoc::indoc; -use language::Rope; +use language::{DiagnosticSourceKind, Rope}; use lsp::LanguageServerId; use pretty_assertions::assert_eq; use project::FakeFs; @@ -105,7 +105,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) { } ], version: None - }, &[], cx).unwrap(); + }, DiagnosticSourceKind::Pushed, &[], cx).unwrap(); }); // Open the project diagnostics view while there are already diagnostics. @@ -176,6 +176,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) { }], version: None, }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -261,6 +262,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) { ], version: None, }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -368,6 +370,7 @@ async fn test_diagnostics_with_folds(cx: &mut TestAppContext) { }], version: None, }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -465,6 +468,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { }], version: None, }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -507,6 +511,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { }], version: None, }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -548,6 +553,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { }], version: None, }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -560,6 +566,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { diagnostics: vec![], version: None, }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -600,6 +607,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { }], version: None, }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -732,6 +740,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng diagnostics: diagnostics.clone(), version: None, }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -919,6 +928,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S diagnostics: diagnostics.clone(), version: None, }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -974,6 +984,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) ..Default::default() }], }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -1007,6 +1018,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) version: None, diagnostics: Vec::new(), }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -1088,6 +1100,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) { }, ], }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -1226,6 +1239,7 @@ async fn test_diagnostics_with_links(cx: &mut TestAppContext) { ..Default::default() }], }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -1277,6 +1291,7 @@ async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) ..Default::default() }], }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -1378,6 +1393,7 @@ async fn test_diagnostics_with_code(cx: &mut TestAppContext) { ], version: None, }, + DiagnosticSourceKind::Pushed, &[], cx, ) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4c386701cbca56478817be6cf63106f6bb7fa9ca..cd97ff50b2d892d63a8627cb56c20a028bbc9932 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -74,8 +74,9 @@ pub use element::{ }; use feature_flags::{DebuggerFeatureFlag, FeatureFlagAppExt}; use futures::{ - FutureExt, + FutureExt, StreamExt as _, future::{self, Shared, join}, + stream::FuturesUnordered, }; use fuzzy::{StringMatch, StringMatchCandidate}; @@ -108,9 +109,10 @@ pub use items::MAX_TAB_TITLE_LEN; use itertools::Itertools; use language::{ AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel, - CursorShape, DiagnosticEntry, DiffOptions, DocumentationConfig, EditPredictionsMode, - EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point, - Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery, + CursorShape, DiagnosticEntry, DiagnosticSourceKind, DiffOptions, DocumentationConfig, + EditPredictionsMode, EditPreview, HighlightedText, IndentKind, IndentSize, Language, + OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, + WordsQuery, language_settings::{ self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings, language_settings, @@ -123,7 +125,7 @@ use markdown::Markdown; use mouse_context_menu::MouseContextMenu; use persistence::DB; use project::{ - BreakpointWithPosition, CompletionResponse, ProjectPath, + BreakpointWithPosition, CompletionResponse, LspPullDiagnostics, ProjectPath, debugger::{ breakpoint_store::{ BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore, @@ -1072,6 +1074,7 @@ pub struct Editor { tasks_update_task: Option>, breakpoint_store: Option>, gutter_breakpoint_indicator: (Option, Option>), + pull_diagnostics_task: Task<()>, in_project_search: bool, previous_search_ranges: Option]>>, breadcrumb_header: Option, @@ -1690,6 +1693,10 @@ impl Editor { editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); } + editor.pull_diagnostics(window, cx); + } + project::Event::RefreshDocumentsDiagnostics => { + editor.pull_diagnostics(window, cx); } project::Event::SnippetEdit(id, snippet_edits) => { if let Some(buffer) = editor.buffer.read(cx).buffer(*id) { @@ -1792,7 +1799,7 @@ impl Editor { code_action_providers.push(Rc::new(project) as Rc<_>); } - let mut this = Self { + let mut editor = Self { focus_handle, show_cursor_when_unfocused: false, last_focused_descendant: None, @@ -1954,6 +1961,7 @@ impl Editor { }), ], tasks_update_task: None, + pull_diagnostics_task: Task::ready(()), linked_edit_ranges: Default::default(), in_project_search: false, previous_search_ranges: None, @@ -1978,16 +1986,17 @@ impl Editor { change_list: ChangeList::new(), mode, }; - if let Some(breakpoints) = this.breakpoint_store.as_ref() { - this._subscriptions + if let Some(breakpoints) = editor.breakpoint_store.as_ref() { + editor + ._subscriptions .push(cx.observe(breakpoints, |_, _, cx| { cx.notify(); })); } - this.tasks_update_task = Some(this.refresh_runnables(window, cx)); - this._subscriptions.extend(project_subscriptions); + editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); + editor._subscriptions.extend(project_subscriptions); - this._subscriptions.push(cx.subscribe_in( + editor._subscriptions.push(cx.subscribe_in( &cx.entity(), window, |editor, _, e: &EditorEvent, window, cx| match e { @@ -2032,14 +2041,15 @@ impl Editor { }, )); - if let Some(dap_store) = this + if let Some(dap_store) = editor .project .as_ref() .map(|project| project.read(cx).dap_store()) { let weak_editor = cx.weak_entity(); - this._subscriptions + editor + ._subscriptions .push( cx.observe_new::(move |_, _, cx| { let session_entity = cx.entity(); @@ -2054,40 +2064,44 @@ impl Editor { ); for session in dap_store.read(cx).sessions().cloned().collect::>() { - this._subscriptions + editor + ._subscriptions .push(cx.subscribe(&session, Self::on_debug_session_event)); } } - this.end_selection(window, cx); - this.scroll_manager.show_scrollbars(window, cx); - jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut this, &buffer, cx); + editor.end_selection(window, cx); + editor.scroll_manager.show_scrollbars(window, cx); + jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut editor, &buffer, cx); if full_mode { let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars(); cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); - if this.git_blame_inline_enabled { - this.start_git_blame_inline(false, window, cx); + if editor.git_blame_inline_enabled { + editor.start_git_blame_inline(false, window, cx); } - this.go_to_active_debug_line(window, cx); + editor.go_to_active_debug_line(window, cx); if let Some(buffer) = buffer.read(cx).as_singleton() { - if let Some(project) = this.project.as_ref() { + if let Some(project) = editor.project.as_ref() { let handle = project.update(cx, |project, cx| { project.register_buffer_with_language_servers(&buffer, cx) }); - this.registered_buffers + editor + .registered_buffers .insert(buffer.read(cx).remote_id(), handle); } } - this.minimap = this.create_minimap(EditorSettings::get_global(cx).minimap, window, cx); + editor.minimap = + editor.create_minimap(EditorSettings::get_global(cx).minimap, window, cx); + editor.pull_diagnostics(window, cx); } - this.report_editor_event("Editor Opened", None, cx); - this + editor.report_editor_event("Editor Opened", None, cx); + editor } pub fn deploy_mouse_context_menu( @@ -15890,6 +15904,49 @@ impl Editor { }); } + fn pull_diagnostics(&mut self, window: &Window, cx: &mut Context) -> Option<()> { + let project = self.project.as_ref()?.downgrade(); + let debounce = Duration::from_millis( + ProjectSettings::get_global(cx) + .diagnostics + .lsp_pull_diagnostics_debounce_ms?, + ); + let buffers = self.buffer.read(cx).all_buffers(); + + self.pull_diagnostics_task = cx.spawn_in(window, async move |editor, cx| { + cx.background_executor().timer(debounce).await; + + let Ok(mut pull_diagnostics_tasks) = cx.update(|_, cx| { + buffers + .into_iter() + .flat_map(|buffer| { + Some(project.upgrade()?.pull_diagnostics_for_buffer(buffer, cx)) + }) + .collect::>() + }) else { + return; + }; + + while let Some(pull_task) = pull_diagnostics_tasks.next().await { + match pull_task { + Ok(()) => { + if editor + .update_in(cx, |editor, window, cx| { + editor.update_diagnostics_state(window, cx); + }) + .is_err() + { + return; + } + } + Err(e) => log::error!("Failed to update project diagnostics: {e:#}"), + } + } + }); + + Some(()) + } + pub fn set_selections_from_remote( &mut self, selections: Vec>, @@ -18603,7 +18660,7 @@ impl Editor { match event { multi_buffer::Event::Edited { singleton_buffer_edited, - edited_buffer: buffer_edited, + edited_buffer, } => { self.scrollbar_marker_state.dirty = true; self.active_indent_guides_state.dirty = true; @@ -18614,18 +18671,25 @@ impl Editor { if self.has_active_inline_completion() { self.update_visible_inline_completion(window, cx); } - if let Some(buffer) = buffer_edited { - let buffer_id = buffer.read(cx).remote_id(); - if !self.registered_buffers.contains_key(&buffer_id) { - if let Some(project) = self.project.as_ref() { - project.update(cx, |project, cx| { - self.registered_buffers.insert( - buffer_id, - project.register_buffer_with_language_servers(&buffer, cx), - ); - }) + if let Some(project) = self.project.as_ref() { + project.update(cx, |project, cx| { + // Diagnostics are not local: an edit within one file (`pub mod foo()` -> `pub mod bar()`), may cause errors in another files with `foo()`. + // Hence, emit a project-wide event to pull for every buffer's diagnostics that has an open editor. + if edited_buffer + .as_ref() + .is_some_and(|buffer| buffer.read(cx).file().is_some()) + { + cx.emit(project::Event::RefreshDocumentsDiagnostics); } - } + + if let Some(buffer) = edited_buffer { + self.registered_buffers + .entry(buffer.read(cx).remote_id()) + .or_insert_with(|| { + project.register_buffer_with_language_servers(&buffer, cx) + }); + } + }); } cx.emit(EditorEvent::BufferEdited); cx.emit(SearchEvent::MatchesInvalidated); @@ -18744,15 +18808,19 @@ impl Editor { | multi_buffer::Event::BufferDiffChanged => cx.emit(EditorEvent::TitleChanged), multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed), multi_buffer::Event::DiagnosticsUpdated => { - self.refresh_active_diagnostics(cx); - self.refresh_inline_diagnostics(true, window, cx); - self.scrollbar_marker_state.dirty = true; - cx.notify(); + self.update_diagnostics_state(window, cx); } _ => {} }; } + fn update_diagnostics_state(&mut self, window: &mut Window, cx: &mut Context<'_, Editor>) { + self.refresh_active_diagnostics(cx); + self.refresh_inline_diagnostics(true, window, cx); + self.scrollbar_marker_state.dirty = true; + cx.notify(); + } + pub fn start_temporary_diff_override(&mut self) { self.load_diff_task.take(); self.temporary_diff_override = true; @@ -20319,6 +20387,12 @@ pub trait SemanticsProvider { new_name: String, cx: &mut App, ) -> Option>>; + + fn pull_diagnostics_for_buffer( + &self, + buffer: Entity, + cx: &mut App, + ) -> Task>; } pub trait CompletionProvider { @@ -20836,6 +20910,61 @@ impl SemanticsProvider for Entity { project.perform_rename(buffer.clone(), position, new_name, cx) })) } + + fn pull_diagnostics_for_buffer( + &self, + buffer: Entity, + cx: &mut App, + ) -> Task> { + let diagnostics = self.update(cx, |project, cx| { + project + .lsp_store() + .update(cx, |lsp_store, cx| lsp_store.pull_diagnostics(buffer, cx)) + }); + let project = self.clone(); + cx.spawn(async move |cx| { + let diagnostics = diagnostics.await.context("pulling diagnostics")?; + project.update(cx, |project, cx| { + project.lsp_store().update(cx, |lsp_store, cx| { + for diagnostics_set in diagnostics { + let LspPullDiagnostics::Response { + server_id, + uri, + diagnostics: project::PulledDiagnostics::Changed { diagnostics, .. }, + } = diagnostics_set + else { + continue; + }; + + let adapter = lsp_store.language_server_adapter_for_id(server_id); + let disk_based_sources = adapter + .as_ref() + .map(|adapter| adapter.disk_based_diagnostic_sources.as_slice()) + .unwrap_or(&[]); + lsp_store + .merge_diagnostics( + server_id, + lsp::PublishDiagnosticsParams { + uri: uri.clone(), + diagnostics, + version: None, + }, + DiagnosticSourceKind::Pulled, + disk_based_sources, + |old_diagnostic, _| match old_diagnostic.source_kind { + DiagnosticSourceKind::Pulled => false, + DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => { + true + } + }, + cx, + ) + .log_err(); + } + }) + }) + }) + } } fn inlay_hint_settings( diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index e4bd79f6e87bc6b01b4c7715f9ee85c9bdc8ca59..b500a2f3b630ddaacff2dd2d36c92a1b15d56841 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -13650,6 +13650,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu }, ], }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -21562,3 +21563,134 @@ fn assert_hunk_revert( cx.assert_editor_state(expected_reverted_text_with_selections); assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before); } + +#[gpui::test] +async fn test_pulling_diagnostics(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let diagnostic_requests = Arc::new(AtomicUsize::new(0)); + let counter = diagnostic_requests.clone(); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/a"), + json!({ + "first.rs": "fn main() { let a = 5; }", + "second.rs": "// Test file", + }), + ) + .await; + + let project = Project::test(fs, [path!("/a").as_ref()], cx).await; + let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang()); + let mut fake_servers = language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options( + lsp::DiagnosticOptions { + identifier: None, + inter_file_dependencies: true, + workspace_diagnostics: true, + work_done_progress_options: Default::default(), + }, + )), + ..Default::default() + }, + ..Default::default() + }, + ); + + let editor = workspace + .update(cx, |workspace, window, cx| { + workspace.open_abs_path( + PathBuf::from(path!("/a/first.rs")), + OpenOptions::default(), + window, + cx, + ) + }) + .unwrap() + .await + .unwrap() + .downcast::() + .unwrap(); + let fake_server = fake_servers.next().await.unwrap(); + let mut first_request = fake_server + .set_request_handler::(move |params, _| { + counter.fetch_add(1, atomic::Ordering::Release); + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(path!("/a/first.rs")).unwrap() + ); + async move { + Ok(lsp::DocumentDiagnosticReportResult::Report( + lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport { + related_documents: None, + full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport { + items: Vec::new(), + result_id: None, + }, + }), + )) + } + }); + + cx.executor().advance_clock(Duration::from_millis(60)); + cx.executor().run_until_parked(); + assert_eq!( + diagnostic_requests.load(atomic::Ordering::Acquire), + 1, + "Opening file should trigger diagnostic request" + ); + first_request + .next() + .await + .expect("should have sent the first diagnostics pull request"); + + // Editing should trigger diagnostics + editor.update_in(cx, |editor, window, cx| { + editor.handle_input("2", window, cx) + }); + cx.executor().advance_clock(Duration::from_millis(60)); + cx.executor().run_until_parked(); + assert_eq!( + diagnostic_requests.load(atomic::Ordering::Acquire), + 2, + "Editing should trigger diagnostic request" + ); + + // Moving cursor should not trigger diagnostic request + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(None, window, cx, |s| { + s.select_ranges([Point::new(0, 0)..Point::new(0, 0)]) + }); + }); + cx.executor().advance_clock(Duration::from_millis(60)); + cx.executor().run_until_parked(); + assert_eq!( + diagnostic_requests.load(atomic::Ordering::Acquire), + 2, + "Cursor movement should not trigger diagnostic request" + ); + + // Multiple rapid edits should be debounced + for _ in 0..5 { + editor.update_in(cx, |editor, window, cx| { + editor.handle_input("x", window, cx) + }); + } + cx.executor().advance_clock(Duration::from_millis(60)); + cx.executor().run_until_parked(); + + let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire); + assert!( + final_requests <= 4, + "Multiple rapid edits should be debounced (got {} requests)", + final_requests + ); +} diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index d6e253271b6914379b26abd6696ad2e2e45ab03d..d5ae65d9227d7d4c5c1ecbe95eca61415ee9f5a3 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -522,4 +522,12 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { ) -> Option>> { None } + + fn pull_diagnostics_for_buffer( + &self, + _: Entity, + _: &mut App, + ) -> Task> { + Task::ready(Ok(())) + } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 8c02eb5b4453bb9afef77e0731fc923a90df269e..ae82f3aa6303c7d28a945ddf7643e70805beb297 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -229,12 +229,21 @@ pub struct Diagnostic { pub is_disk_based: bool, /// Whether this diagnostic marks unnecessary code. pub is_unnecessary: bool, + /// Quick separation of diagnostics groups based by their source. + pub source_kind: DiagnosticSourceKind, /// Data from language server that produced this diagnostic. Passed back to the LS when we request code actions for this diagnostic. pub data: Option, /// Whether to underline the corresponding text range in the editor. pub underline: bool, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum DiagnosticSourceKind { + Pulled, + Pushed, + Other, +} + /// An operation used to synchronize this buffer with its other replicas. #[derive(Clone, Debug, PartialEq)] pub enum Operation { @@ -4636,6 +4645,7 @@ impl Default for Diagnostic { fn default() -> Self { Self { source: Default::default(), + source_kind: DiagnosticSourceKind::Other, code: None, code_description: None, severity: DiagnosticSeverity::ERROR, diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 831b7d627b1094806366304139e8715ffa0a4edb..c3b91bae317298a93e02fc3707e26425a36da959 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -1,6 +1,6 @@ //! Handles conversions of `language` items to and from the [`rpc`] protocol. -use crate::{CursorShape, Diagnostic, diagnostic_set::DiagnosticEntry}; +use crate::{CursorShape, Diagnostic, DiagnosticSourceKind, diagnostic_set::DiagnosticEntry}; use anyhow::{Context as _, Result}; use clock::ReplicaId; use lsp::{DiagnosticSeverity, LanguageServerId}; @@ -200,6 +200,11 @@ pub fn serialize_diagnostics<'a>( .into_iter() .map(|entry| proto::Diagnostic { source: entry.diagnostic.source.clone(), + source_kind: match entry.diagnostic.source_kind { + DiagnosticSourceKind::Pulled => proto::diagnostic::SourceKind::Pulled, + DiagnosticSourceKind::Pushed => proto::diagnostic::SourceKind::Pushed, + DiagnosticSourceKind::Other => proto::diagnostic::SourceKind::Other, + } as i32, start: Some(serialize_anchor(&entry.range.start)), end: Some(serialize_anchor(&entry.range.end)), message: entry.diagnostic.message.clone(), @@ -431,6 +436,13 @@ pub fn deserialize_diagnostics( is_disk_based: diagnostic.is_disk_based, is_unnecessary: diagnostic.is_unnecessary, underline: diagnostic.underline, + source_kind: match proto::diagnostic::SourceKind::from_i32( + diagnostic.source_kind, + )? { + proto::diagnostic::SourceKind::Pulled => DiagnosticSourceKind::Pulled, + proto::diagnostic::SourceKind::Pushed => DiagnosticSourceKind::Pushed, + proto::diagnostic::SourceKind::Other => DiagnosticSourceKind::Other, + }, data, }, }) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 626a238604e04da3996fe1d7024df4f27e35ec10..c68ce1e33e12c44ea8e4801a24997dde88bad9be 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -603,7 +603,7 @@ impl LanguageServer { Ok(()) } - pub fn default_initialize_params(&self, cx: &App) -> InitializeParams { + pub fn default_initialize_params(&self, pull_diagnostics: bool, cx: &App) -> InitializeParams { let workspace_folders = self .workspace_folders .lock() @@ -643,8 +643,9 @@ impl LanguageServer { refresh_support: Some(true), }), diagnostic: Some(DiagnosticWorkspaceClientCapabilities { - refresh_support: None, - }), + refresh_support: Some(true), + }) + .filter(|_| pull_diagnostics), code_lens: Some(CodeLensWorkspaceClientCapabilities { refresh_support: Some(true), }), @@ -793,6 +794,11 @@ impl LanguageServer { hierarchical_document_symbol_support: Some(true), ..DocumentSymbolClientCapabilities::default() }), + diagnostic: Some(DiagnosticClientCapabilities { + dynamic_registration: Some(false), + related_document_support: Some(true), + }) + .filter(|_| pull_diagnostics), ..TextDocumentClientCapabilities::default() }), experimental: Some(json!({ @@ -1703,7 +1709,7 @@ mod tests { let server = cx .update(|cx| { - let params = server.default_initialize_params(cx); + let params = server.default_initialize_params(false, cx); let configuration = DidChangeConfigurationParams { settings: Default::default(), }; diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 983ef5458df06d5c65939eb1035c42a3345e1e65..3c8cee63205bad94def04b48838b81491961fdac 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -292,7 +292,7 @@ impl Prettier { let server = cx .update(|cx| { - let params = server.default_initialize_params(cx); + let params = server.default_initialize_params(false, cx); let configuration = lsp::DidChangeConfigurationParams { settings: Default::default(), }; diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 1bbabe172419830d6f2984d1fce245b67ddf968d..97cc35c209c3db074778cb9656ba606f89d4ddf7 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -4,14 +4,15 @@ use crate::{ CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, DocumentHighlight, DocumentSymbol, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, - LspAction, MarkupContent, PrepareRenameResponse, ProjectTransaction, ResolveState, + LspAction, LspPullDiagnostics, MarkupContent, PrepareRenameResponse, ProjectTransaction, + PulledDiagnostics, ResolveState, lsp_store::{LocalLspStore, LspStore}, }; use anyhow::{Context as _, Result}; use async_trait::async_trait; use client::proto::{self, PeerId}; use clock::Global; -use collections::HashSet; +use collections::{HashMap, HashSet}; use futures::future; use gpui::{App, AsyncApp, Entity, Task}; use language::{ @@ -23,14 +24,18 @@ use language::{ range_from_lsp, range_to_lsp, }; use lsp::{ - AdapterServerCapabilities, CodeActionKind, CodeActionOptions, CompletionContext, - CompletionListItemDefaultsEditRange, CompletionTriggerKind, DocumentHighlightKind, - LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities, OneOf, RenameOptions, - ServerCapabilities, + AdapterServerCapabilities, CodeActionKind, CodeActionOptions, CodeDescription, + CompletionContext, CompletionListItemDefaultsEditRange, CompletionTriggerKind, + DocumentHighlightKind, LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities, + OneOf, RenameOptions, ServerCapabilities, }; +use serde_json::Value; use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature}; -use std::{cmp::Reverse, mem, ops::Range, path::Path, sync::Arc}; +use std::{ + cmp::Reverse, collections::hash_map, mem, ops::Range, path::Path, str::FromStr, sync::Arc, +}; use text::{BufferId, LineEnding}; +use util::{ResultExt as _, debug_panic}; pub use signature_help::SignatureHelp; @@ -45,7 +50,7 @@ pub fn lsp_formatting_options(settings: &LanguageSettings) -> lsp::FormattingOpt } } -pub(crate) fn file_path_to_lsp_url(path: &Path) -> Result { +pub fn file_path_to_lsp_url(path: &Path) -> Result { match lsp::Url::from_file_path(path) { Ok(url) => Ok(url), Err(()) => anyhow::bail!("Invalid file path provided to LSP request: {path:?}"), @@ -254,6 +259,9 @@ pub(crate) struct LinkedEditingRange { pub position: Anchor, } +#[derive(Clone, Debug)] +pub(crate) struct GetDocumentDiagnostics {} + #[async_trait(?Send)] impl LspCommand for PrepareRename { type Response = PrepareRenameResponse; @@ -3656,3 +3664,627 @@ impl LspCommand for LinkedEditingRange { BufferId::new(message.buffer_id) } } + +impl GetDocumentDiagnostics { + fn deserialize_lsp_diagnostic(diagnostic: proto::LspDiagnostic) -> Result { + let start = diagnostic.start.context("invalid start range")?; + let end = diagnostic.end.context("invalid end range")?; + + let range = Range:: { + start: PointUtf16 { + row: start.row, + column: start.column, + }, + end: PointUtf16 { + row: end.row, + column: end.column, + }, + }; + + let data = diagnostic.data.and_then(|data| Value::from_str(&data).ok()); + let code = diagnostic.code.map(lsp::NumberOrString::String); + + let related_information = diagnostic + .related_information + .into_iter() + .map(|info| { + let start = info.location_range_start.unwrap(); + let end = info.location_range_end.unwrap(); + + lsp::DiagnosticRelatedInformation { + location: lsp::Location { + range: lsp::Range { + start: point_to_lsp(PointUtf16::new(start.row, start.column)), + end: point_to_lsp(PointUtf16::new(end.row, end.column)), + }, + uri: lsp::Url::parse(&info.location_url.unwrap()).unwrap(), + }, + message: info.message.clone(), + } + }) + .collect::>(); + + let tags = diagnostic + .tags + .into_iter() + .filter_map(|tag| match proto::LspDiagnosticTag::from_i32(tag) { + Some(proto::LspDiagnosticTag::Unnecessary) => Some(lsp::DiagnosticTag::UNNECESSARY), + Some(proto::LspDiagnosticTag::Deprecated) => Some(lsp::DiagnosticTag::DEPRECATED), + _ => None, + }) + .collect::>(); + + Ok(lsp::Diagnostic { + range: language::range_to_lsp(range)?, + severity: match proto::lsp_diagnostic::Severity::from_i32(diagnostic.severity).unwrap() + { + proto::lsp_diagnostic::Severity::Error => Some(lsp::DiagnosticSeverity::ERROR), + proto::lsp_diagnostic::Severity::Warning => Some(lsp::DiagnosticSeverity::WARNING), + proto::lsp_diagnostic::Severity::Information => { + Some(lsp::DiagnosticSeverity::INFORMATION) + } + proto::lsp_diagnostic::Severity::Hint => Some(lsp::DiagnosticSeverity::HINT), + _ => None, + }, + code, + code_description: match diagnostic.code_description { + Some(code_description) => Some(CodeDescription { + href: lsp::Url::parse(&code_description).unwrap(), + }), + None => None, + }, + related_information: Some(related_information), + tags: Some(tags), + source: diagnostic.source.clone(), + message: diagnostic.message, + data, + }) + } + + fn serialize_lsp_diagnostic(diagnostic: lsp::Diagnostic) -> Result { + let range = language::range_from_lsp(diagnostic.range); + let related_information = diagnostic + .related_information + .unwrap_or_default() + .into_iter() + .map(|related_information| { + let location_range_start = + point_from_lsp(related_information.location.range.start).0; + let location_range_end = point_from_lsp(related_information.location.range.end).0; + + Ok(proto::LspDiagnosticRelatedInformation { + location_url: Some(related_information.location.uri.to_string()), + location_range_start: Some(proto::PointUtf16 { + row: location_range_start.row, + column: location_range_start.column, + }), + location_range_end: Some(proto::PointUtf16 { + row: location_range_end.row, + column: location_range_end.column, + }), + message: related_information.message, + }) + }) + .collect::>>()?; + + let tags = diagnostic + .tags + .unwrap_or_default() + .into_iter() + .map(|tag| match tag { + lsp::DiagnosticTag::UNNECESSARY => proto::LspDiagnosticTag::Unnecessary, + lsp::DiagnosticTag::DEPRECATED => proto::LspDiagnosticTag::Deprecated, + _ => proto::LspDiagnosticTag::None, + } as i32) + .collect(); + + Ok(proto::LspDiagnostic { + start: Some(proto::PointUtf16 { + row: range.start.0.row, + column: range.start.0.column, + }), + end: Some(proto::PointUtf16 { + row: range.end.0.row, + column: range.end.0.column, + }), + severity: match diagnostic.severity { + Some(lsp::DiagnosticSeverity::ERROR) => proto::lsp_diagnostic::Severity::Error, + Some(lsp::DiagnosticSeverity::WARNING) => proto::lsp_diagnostic::Severity::Warning, + Some(lsp::DiagnosticSeverity::INFORMATION) => { + proto::lsp_diagnostic::Severity::Information + } + Some(lsp::DiagnosticSeverity::HINT) => proto::lsp_diagnostic::Severity::Hint, + _ => proto::lsp_diagnostic::Severity::None, + } as i32, + code: diagnostic.code.as_ref().map(|code| match code { + lsp::NumberOrString::Number(code) => code.to_string(), + lsp::NumberOrString::String(code) => code.clone(), + }), + source: diagnostic.source.clone(), + related_information, + tags, + code_description: diagnostic + .code_description + .map(|desc| desc.href.to_string()), + message: diagnostic.message, + data: diagnostic.data.as_ref().map(|data| data.to_string()), + }) + } +} + +#[async_trait(?Send)] +impl LspCommand for GetDocumentDiagnostics { + type Response = Vec; + type LspRequest = lsp::request::DocumentDiagnosticRequest; + type ProtoRequest = proto::GetDocumentDiagnostics; + + fn display_name(&self) -> &str { + "Get diagnostics" + } + + fn check_capabilities(&self, server_capabilities: AdapterServerCapabilities) -> bool { + server_capabilities + .server_capabilities + .diagnostic_provider + .is_some() + } + + fn to_lsp( + &self, + path: &Path, + _: &Buffer, + language_server: &Arc, + _: &App, + ) -> Result { + let identifier = match language_server.capabilities().diagnostic_provider { + Some(lsp::DiagnosticServerCapabilities::Options(options)) => options.identifier, + Some(lsp::DiagnosticServerCapabilities::RegistrationOptions(options)) => { + options.diagnostic_options.identifier + } + None => None, + }; + + Ok(lsp::DocumentDiagnosticParams { + text_document: lsp::TextDocumentIdentifier { + uri: file_path_to_lsp_url(path)?, + }, + identifier, + previous_result_id: None, + partial_result_params: Default::default(), + work_done_progress_params: Default::default(), + }) + } + + async fn response_from_lsp( + self, + message: lsp::DocumentDiagnosticReportResult, + _: Entity, + buffer: Entity, + server_id: LanguageServerId, + cx: AsyncApp, + ) -> Result { + let url = buffer.read_with(&cx, |buffer, cx| { + buffer + .file() + .and_then(|file| file.as_local()) + .map(|file| { + let abs_path = file.abs_path(cx); + file_path_to_lsp_url(&abs_path) + }) + .transpose()? + .with_context(|| format!("missing url on buffer {}", buffer.remote_id())) + })??; + + let mut pulled_diagnostics = HashMap::default(); + match message { + lsp::DocumentDiagnosticReportResult::Report(report) => match report { + lsp::DocumentDiagnosticReport::Full(report) => { + if let Some(related_documents) = report.related_documents { + process_related_documents( + &mut pulled_diagnostics, + server_id, + related_documents, + ); + } + process_full_diagnostics_report( + &mut pulled_diagnostics, + server_id, + url, + report.full_document_diagnostic_report, + ); + } + lsp::DocumentDiagnosticReport::Unchanged(report) => { + if let Some(related_documents) = report.related_documents { + process_related_documents( + &mut pulled_diagnostics, + server_id, + related_documents, + ); + } + process_unchanged_diagnostics_report( + &mut pulled_diagnostics, + server_id, + url, + report.unchanged_document_diagnostic_report, + ); + } + }, + lsp::DocumentDiagnosticReportResult::Partial(report) => { + if let Some(related_documents) = report.related_documents { + process_related_documents( + &mut pulled_diagnostics, + server_id, + related_documents, + ); + } + } + } + + Ok(pulled_diagnostics.into_values().collect()) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentDiagnostics { + proto::GetDocumentDiagnostics { + project_id, + buffer_id: buffer.remote_id().into(), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::GetDocumentDiagnostics, + _: Entity, + buffer: Entity, + mut cx: AsyncApp, + ) -> Result { + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + })? + .await?; + Ok(Self {}) + } + + fn response_to_proto( + response: Self::Response, + _: &mut LspStore, + _: PeerId, + _: &clock::Global, + _: &mut App, + ) -> proto::GetDocumentDiagnosticsResponse { + let pulled_diagnostics = response + .into_iter() + .filter_map(|diagnostics| match diagnostics { + LspPullDiagnostics::Default => None, + LspPullDiagnostics::Response { + server_id, + uri, + diagnostics, + } => { + let mut changed = false; + let (diagnostics, result_id) = match diagnostics { + PulledDiagnostics::Unchanged { result_id } => (Vec::new(), Some(result_id)), + PulledDiagnostics::Changed { + result_id, + diagnostics, + } => { + changed = true; + (diagnostics, result_id) + } + }; + Some(proto::PulledDiagnostics { + changed, + result_id, + uri: uri.to_string(), + server_id: server_id.to_proto(), + diagnostics: diagnostics + .into_iter() + .filter_map(|diagnostic| { + GetDocumentDiagnostics::serialize_lsp_diagnostic(diagnostic) + .context("serializing diagnostics") + .log_err() + }) + .collect(), + }) + } + }) + .collect(); + + proto::GetDocumentDiagnosticsResponse { pulled_diagnostics } + } + + async fn response_from_proto( + self, + response: proto::GetDocumentDiagnosticsResponse, + _: Entity, + _: Entity, + _: AsyncApp, + ) -> Result { + let pulled_diagnostics = response + .pulled_diagnostics + .into_iter() + .filter_map(|diagnostics| { + Some(LspPullDiagnostics::Response { + server_id: LanguageServerId::from_proto(diagnostics.server_id), + uri: lsp::Url::from_str(diagnostics.uri.as_str()).log_err()?, + diagnostics: if diagnostics.changed { + PulledDiagnostics::Unchanged { + result_id: diagnostics.result_id?, + } + } else { + PulledDiagnostics::Changed { + result_id: diagnostics.result_id, + diagnostics: diagnostics + .diagnostics + .into_iter() + .filter_map(|diagnostic| { + GetDocumentDiagnostics::deserialize_lsp_diagnostic(diagnostic) + .context("deserializing diagnostics") + .log_err() + }) + .collect(), + } + }, + }) + }) + .collect(); + + Ok(pulled_diagnostics) + } + + fn buffer_id_from_proto(message: &proto::GetDocumentDiagnostics) -> Result { + BufferId::new(message.buffer_id) + } +} + +fn process_related_documents( + diagnostics: &mut HashMap, + server_id: LanguageServerId, + documents: impl IntoIterator, +) { + for (url, report_kind) in documents { + match report_kind { + lsp::DocumentDiagnosticReportKind::Full(report) => { + process_full_diagnostics_report(diagnostics, server_id, url, report) + } + lsp::DocumentDiagnosticReportKind::Unchanged(report) => { + process_unchanged_diagnostics_report(diagnostics, server_id, url, report) + } + } + } +} + +fn process_unchanged_diagnostics_report( + diagnostics: &mut HashMap, + server_id: LanguageServerId, + uri: lsp::Url, + report: lsp::UnchangedDocumentDiagnosticReport, +) { + let result_id = report.result_id; + match diagnostics.entry(uri.clone()) { + hash_map::Entry::Occupied(mut o) => match o.get_mut() { + LspPullDiagnostics::Default => { + o.insert(LspPullDiagnostics::Response { + server_id, + uri, + diagnostics: PulledDiagnostics::Unchanged { result_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!( + "Unexpected state: file {uri} has two different sets of diagnostics reported" + ); + } + match existing_diagnostics { + PulledDiagnostics::Unchanged { .. } => { + *existing_diagnostics = PulledDiagnostics::Unchanged { result_id }; + } + PulledDiagnostics::Changed { .. } => {} + } + } + }, + hash_map::Entry::Vacant(v) => { + v.insert(LspPullDiagnostics::Response { + server_id, + uri, + diagnostics: PulledDiagnostics::Unchanged { result_id }, + }); + } + } +} + +fn process_full_diagnostics_report( + diagnostics: &mut HashMap, + server_id: LanguageServerId, + uri: lsp::Url, + report: lsp::FullDocumentDiagnosticReport, +) { + let result_id = report.result_id; + match diagnostics.entry(uri.clone()) { + hash_map::Entry::Occupied(mut o) => match o.get_mut() { + LspPullDiagnostics::Default => { + o.insert(LspPullDiagnostics::Response { + server_id, + uri, + diagnostics: PulledDiagnostics::Changed { + result_id, + diagnostics: report.items, + }, + }); + } + LspPullDiagnostics::Response { + server_id: existing_server_id, + uri: existing_uri, + diagnostics: existing_diagnostics, + } => { + if server_id != *existing_server_id || &uri != existing_uri { + debug_panic!( + "Unexpected state: file {uri} has two different sets of diagnostics reported" + ); + } + match existing_diagnostics { + PulledDiagnostics::Unchanged { .. } => { + *existing_diagnostics = PulledDiagnostics::Changed { + result_id, + diagnostics: report.items, + }; + } + PulledDiagnostics::Changed { + result_id: existing_result_id, + diagnostics: existing_diagnostics, + } => { + if result_id.is_some() { + *existing_result_id = result_id; + } + existing_diagnostics.extend(report.items); + } + } + } + }, + hash_map::Entry::Vacant(v) => { + v.insert(LspPullDiagnostics::Response { + server_id, + uri, + diagnostics: PulledDiagnostics::Changed { + result_id, + diagnostics: report.items, + }, + }); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use lsp::{DiagnosticSeverity, DiagnosticTag}; + use serde_json::json; + + #[test] + fn test_serialize_lsp_diagnostic() { + let lsp_diagnostic = lsp::Diagnostic { + range: lsp::Range { + start: lsp::Position::new(0, 1), + end: lsp::Position::new(2, 3), + }, + severity: Some(DiagnosticSeverity::ERROR), + code: Some(lsp::NumberOrString::String("E001".to_string())), + source: Some("test-source".to_string()), + message: "Test error message".to_string(), + related_information: None, + tags: Some(vec![DiagnosticTag::DEPRECATED]), + code_description: None, + data: Some(json!({"detail": "test detail"})), + }; + + let proto_diagnostic = + GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic.clone()) + .expect("Failed to serialize diagnostic"); + + let start = proto_diagnostic.start.unwrap(); + let end = proto_diagnostic.end.unwrap(); + assert_eq!(start.row, 0); + assert_eq!(start.column, 1); + assert_eq!(end.row, 2); + assert_eq!(end.column, 3); + assert_eq!( + proto_diagnostic.severity, + proto::lsp_diagnostic::Severity::Error as i32 + ); + assert_eq!(proto_diagnostic.code, Some("E001".to_string())); + assert_eq!(proto_diagnostic.source, Some("test-source".to_string())); + assert_eq!(proto_diagnostic.message, "Test error message"); + } + + #[test] + fn test_deserialize_lsp_diagnostic() { + let proto_diagnostic = proto::LspDiagnostic { + start: Some(proto::PointUtf16 { row: 0, column: 1 }), + end: Some(proto::PointUtf16 { row: 2, column: 3 }), + severity: proto::lsp_diagnostic::Severity::Warning as i32, + code: Some("ERR".to_string()), + source: Some("Prism".to_string()), + message: "assigned but unused variable - a".to_string(), + related_information: vec![], + tags: vec![], + code_description: None, + data: None, + }; + + let lsp_diagnostic = GetDocumentDiagnostics::deserialize_lsp_diagnostic(proto_diagnostic) + .expect("Failed to deserialize diagnostic"); + + assert_eq!(lsp_diagnostic.range.start.line, 0); + assert_eq!(lsp_diagnostic.range.start.character, 1); + assert_eq!(lsp_diagnostic.range.end.line, 2); + assert_eq!(lsp_diagnostic.range.end.character, 3); + assert_eq!(lsp_diagnostic.severity, Some(DiagnosticSeverity::WARNING)); + assert_eq!( + lsp_diagnostic.code, + Some(lsp::NumberOrString::String("ERR".to_string())) + ); + assert_eq!(lsp_diagnostic.source, Some("Prism".to_string())); + assert_eq!(lsp_diagnostic.message, "assigned but unused variable - a"); + } + + #[test] + fn test_related_information() { + let related_info = lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: lsp::Url::parse("file:///test.rs").unwrap(), + range: lsp::Range { + start: lsp::Position::new(1, 1), + end: lsp::Position::new(1, 5), + }, + }, + message: "Related info message".to_string(), + }; + + let lsp_diagnostic = lsp::Diagnostic { + range: lsp::Range { + start: lsp::Position::new(0, 0), + end: lsp::Position::new(0, 1), + }, + severity: Some(DiagnosticSeverity::INFORMATION), + code: None, + source: Some("Prism".to_string()), + message: "assigned but unused variable - a".to_string(), + related_information: Some(vec![related_info]), + tags: None, + code_description: None, + data: None, + }; + + let proto_diagnostic = GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic) + .expect("Failed to serialize diagnostic"); + + assert_eq!(proto_diagnostic.related_information.len(), 1); + let related = &proto_diagnostic.related_information[0]; + assert_eq!(related.location_url, Some("file:///test.rs".to_string())); + assert_eq!(related.message, "Related info message"); + } + + #[test] + fn test_invalid_ranges() { + let proto_diagnostic = proto::LspDiagnostic { + start: None, + end: Some(proto::PointUtf16 { row: 2, column: 3 }), + severity: proto::lsp_diagnostic::Severity::Error as i32, + code: None, + source: None, + message: "Test message".to_string(), + related_information: vec![], + tags: vec![], + code_description: None, + data: None, + }; + + let result = GetDocumentDiagnostics::deserialize_lsp_diagnostic(proto_diagnostic); + assert!(result.is_err()); + } +} diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index dd0ed856d3e1462689203d2b99b86521d72975de..15cf954ef4f6285aa970565f4446c980f0d6dabe 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -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::({ + 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::({ 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::); client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); @@ -3521,6 +3555,7 @@ impl LspStore { client.add_entity_request_handler( Self::handle_lsp_command::, ); + client.add_entity_request_handler(Self::handle_lsp_command::); } 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, + cx: &mut Context, + ) -> Task>> { + 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::>>()? + .into_iter() + .flatten() + .collect()) + }) + } else { + let all_actions_task = self.request_multiple_lsp_locally( + &buffer_handle, + None::, + GetDocumentDiagnostics {}, + cx, + ); + cx.spawn(async move |_, _| Ok(all_actions_task.await.into_iter().flatten().collect())) + } + } + pub fn inlay_hints( &mut self, buffer_handle: Entity, @@ -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::, + 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, + _: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + this.update(&mut cx, |_, cx| { + cx.emit(LspStoreEvent::RefreshDocumentsDiagnostics); + })?; + Ok(proto::Ack {}) + } + async fn handle_inlay_hints( this: Entity, envelope: TypedEnvelope, @@ -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, ) -> 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, @@ -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 diff --git a/crates/project/src/lsp_store/clangd_ext.rs b/crates/project/src/lsp_store/clangd_ext.rs index d12015ec3131ce68427e102086aeea6ac15183a6..9f2b044ed165586a0e69f32c9f5a77323fabc03f 100644 --- a/crates/project/src/lsp_store/clangd_ext.rs +++ b/crates/project/src/lsp_store/clangd_ext.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use ::serde::{Deserialize, Serialize}; use gpui::WeakEntity; -use language::{CachedLspAdapter, Diagnostic}; +use language::{CachedLspAdapter, Diagnostic, DiagnosticSourceKind}; use lsp::LanguageServer; use util::ResultExt as _; @@ -84,6 +84,7 @@ pub fn register_notifications( this.merge_diagnostics( server_id, mapped_diagnostics, + DiagnosticSourceKind::Pushed, &adapter.disk_based_diagnostic_sources, |diag, _| !is_inactive_region(diag), cx, diff --git a/crates/project/src/lsp_store/lsp_ext_command.rs b/crates/project/src/lsp_store/lsp_ext_command.rs index 4b7616d4d1e11f11bb20e25402c8ea3053c16efb..2b6d11ceb92aee19240f10b2c140e3d48f3b9586 100644 --- a/crates/project/src/lsp_store/lsp_ext_command.rs +++ b/crates/project/src/lsp_store/lsp_ext_command.rs @@ -1,8 +1,9 @@ use crate::{ LocationLink, lsp_command::{ - LspCommand, location_link_from_lsp, location_link_from_proto, location_link_to_proto, - location_links_from_lsp, location_links_from_proto, location_links_to_proto, + LspCommand, file_path_to_lsp_url, location_link_from_lsp, location_link_from_proto, + location_link_to_proto, location_links_from_lsp, location_links_from_proto, + location_links_to_proto, }, lsp_store::LspStore, make_lsp_text_document_position, make_text_document_identifier, @@ -584,10 +585,7 @@ impl LspCommand for GetLspRunnables { _: &Arc, _: &App, ) -> Result { - let url = match lsp::Url::from_file_path(path) { - Ok(url) => url, - Err(()) => anyhow::bail!("Failed to parse path {path:?} as lsp::Url"), - }; + let url = file_path_to_lsp_url(path)?; Ok(RunnablesParams { text_document: lsp::TextDocumentIdentifier::new(url), position: self diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fe9167dfaa985924e04802443c13ab3c5732c979..41be6014563ec504620ccd6b4c5db3ac1007f4db 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -72,9 +72,9 @@ use gpui::{ }; use itertools::Itertools; use language::{ - Buffer, BufferEvent, Capability, CodeLabel, CursorShape, Language, LanguageName, - LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, Transaction, - Unclipped, language_settings::InlayHintKind, proto::split_operations, + Buffer, BufferEvent, Capability, CodeLabel, CursorShape, DiagnosticSourceKind, Language, + LanguageName, LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, + Transaction, Unclipped, language_settings::InlayHintKind, proto::split_operations, }; use lsp::{ CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, InsertTextMode, @@ -317,6 +317,7 @@ pub enum Event { SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>), ExpandedAllForEntry(WorktreeId, ProjectEntryId), AgentLocationChanged, + RefreshDocumentsDiagnostics, } pub struct AgentLocationChanged; @@ -861,6 +862,34 @@ pub const DEFAULT_COMPLETION_CONTEXT: CompletionContext = CompletionContext { trigger_character: None, }; +/// An LSP diagnostics associated with a certain language server. +#[derive(Clone, Debug, Default)] +pub enum LspPullDiagnostics { + #[default] + Default, + Response { + /// The id of the language server that produced diagnostics. + server_id: LanguageServerId, + /// URI of the resource, + uri: lsp::Url, + /// The diagnostics produced by this language server. + diagnostics: PulledDiagnostics, + }, +} + +#[derive(Clone, Debug)] +pub enum PulledDiagnostics { + Unchanged { + /// An ID the current pulled batch for this file. + /// If given, can be used to query workspace diagnostics partially. + result_id: String, + }, + Changed { + result_id: Option, + diagnostics: Vec, + }, +} + impl Project { pub fn init_settings(cx: &mut App) { WorktreeSettings::register(cx); @@ -2785,6 +2814,9 @@ impl Project { } LspStoreEvent::RefreshInlayHints => cx.emit(Event::RefreshInlayHints), LspStoreEvent::RefreshCodeLens => cx.emit(Event::RefreshCodeLens), + LspStoreEvent::RefreshDocumentsDiagnostics => { + cx.emit(Event::RefreshDocumentsDiagnostics) + } LspStoreEvent::LanguageServerPrompt(prompt) => { cx.emit(Event::LanguageServerPrompt(prompt.clone())) } @@ -3686,6 +3718,35 @@ impl Project { }) } + pub fn document_diagnostics( + &mut self, + buffer_handle: Entity, + cx: &mut Context, + ) -> Task>> { + self.lsp_store.update(cx, |lsp_store, cx| { + lsp_store.pull_diagnostics(buffer_handle, cx) + }) + } + + pub fn update_diagnostics( + &mut self, + language_server_id: LanguageServerId, + source_kind: DiagnosticSourceKind, + params: lsp::PublishDiagnosticsParams, + disk_based_sources: &[String], + cx: &mut Context, + ) -> Result<(), anyhow::Error> { + self.lsp_store.update(cx, |lsp_store, cx| { + lsp_store.update_diagnostics( + language_server_id, + params, + source_kind, + disk_based_sources, + cx, + ) + }) + } + pub fn search(&mut self, query: SearchQuery, cx: &mut Context) -> Receiver { let (result_tx, result_rx) = smol::channel::unbounded(); diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 4477b431a5b3fd5795378f8c263f63193812daa2..f32ce0c5462b4fc6407f9de5eddfafc2b6967900 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -127,6 +127,10 @@ 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, + /// Settings for showing inline diagnostics. pub inline: InlineDiagnosticsSettings, @@ -209,8 +213,9 @@ impl Default for DiagnosticsSettings { Self { button: true, include_warnings: true, - inline: Default::default(), - cargo: Default::default(), + lsp_pull_diagnostics_debounce_ms: Some(30), + inline: InlineDiagnosticsSettings::default(), + cargo: None, } } } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 2da5908b94607b69860af52a795b25cfb6948d53..d4a10d79e6ff8ea07a975b9f20fbd06152de2fc9 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1332,6 +1332,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { ..Default::default() }], }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -1349,6 +1350,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { ..Default::default() }], }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -1439,6 +1441,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) { ..Default::default() }], }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -1456,6 +1459,7 @@ async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) { ..Default::default() }], }, + DiagnosticSourceKind::Pushed, &[], cx, ) @@ -1633,7 +1637,8 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { message: "undefined variable 'A'".to_string(), group_id: 0, is_primary: true, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() } }] ) @@ -2149,7 +2154,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { is_disk_based: true, group_id: 1, is_primary: true, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() }, }, DiagnosticEntry { @@ -2161,7 +2167,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { is_disk_based: true, group_id: 2, is_primary: true, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() } } ] @@ -2227,7 +2234,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { is_disk_based: true, group_id: 4, is_primary: true, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() } }, DiagnosticEntry { @@ -2239,7 +2247,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { is_disk_based: true, group_id: 3, is_primary: true, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() }, } ] @@ -2319,7 +2328,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { is_disk_based: true, group_id: 6, is_primary: true, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() } }, DiagnosticEntry { @@ -2331,7 +2341,8 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { is_disk_based: true, group_id: 5, is_primary: true, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() }, } ] @@ -2372,7 +2383,8 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { diagnostic: Diagnostic { severity: DiagnosticSeverity::ERROR, message: "syntax error 1".to_string(), - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() }, }, DiagnosticEntry { @@ -2381,7 +2393,8 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { diagnostic: Diagnostic { severity: DiagnosticSeverity::ERROR, message: "syntax error 2".to_string(), - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() }, }, ], @@ -2435,7 +2448,8 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC severity: DiagnosticSeverity::ERROR, is_primary: true, message: "syntax error a1".to_string(), - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() }, }], cx, @@ -2452,7 +2466,8 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC severity: DiagnosticSeverity::ERROR, is_primary: true, message: "syntax error b1".to_string(), - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() }, }], cx, @@ -4578,7 +4593,13 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { lsp_store .update(cx, |lsp_store, cx| { - lsp_store.update_diagnostics(LanguageServerId(0), message, &[], cx) + lsp_store.update_diagnostics( + LanguageServerId(0), + message, + DiagnosticSourceKind::Pushed, + &[], + cx, + ) }) .unwrap(); let buffer = buffer.update(cx, |buffer, _| buffer.snapshot()); @@ -4595,7 +4616,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { message: "error 1".to_string(), group_id: 1, is_primary: true, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() } }, DiagnosticEntry { @@ -4605,7 +4627,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { message: "error 1 hint 1".to_string(), group_id: 1, is_primary: false, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() } }, DiagnosticEntry { @@ -4615,7 +4638,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { message: "error 2 hint 1".to_string(), group_id: 0, is_primary: false, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() } }, DiagnosticEntry { @@ -4625,7 +4649,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { message: "error 2 hint 2".to_string(), group_id: 0, is_primary: false, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() } }, DiagnosticEntry { @@ -4635,7 +4660,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { message: "error 2".to_string(), group_id: 0, is_primary: true, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() } } ] @@ -4651,7 +4677,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { message: "error 2 hint 1".to_string(), group_id: 0, is_primary: false, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() } }, DiagnosticEntry { @@ -4661,7 +4688,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { message: "error 2 hint 2".to_string(), group_id: 0, is_primary: false, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() } }, DiagnosticEntry { @@ -4671,7 +4699,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { message: "error 2".to_string(), group_id: 0, is_primary: true, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() } } ] @@ -4687,7 +4716,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { message: "error 1".to_string(), group_id: 1, is_primary: true, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() } }, DiagnosticEntry { @@ -4697,7 +4727,8 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { message: "error 1 hint 1".to_string(), group_id: 1, is_primary: false, - ..Default::default() + source_kind: DiagnosticSourceKind::Pushed, + ..Diagnostic::default() } }, ] diff --git a/crates/proto/proto/buffer.proto b/crates/proto/proto/buffer.proto index e7692da481c333568466e51fda57adb1f5cd3572..09a05a50cd84381c4aaccd17a846e2eb38822392 100644 --- a/crates/proto/proto/buffer.proto +++ b/crates/proto/proto/buffer.proto @@ -251,6 +251,14 @@ message Diagnostic { Anchor start = 1; Anchor end = 2; optional string source = 3; + + enum SourceKind { + Pulled = 0; + Pushed = 1; + Other = 2; + } + + SourceKind source_kind = 16; Severity severity = 4; string message = 5; optional string code = 6; diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index 47eb6fa3d328b5df06af420d8aeb845310cf3f87..b04009d622c75b708025376a9fc491045cc4395e 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -678,6 +678,7 @@ message MultiLspQuery { GetCodeActions get_code_actions = 6; GetSignatureHelp get_signature_help = 7; GetCodeLens get_code_lens = 8; + GetDocumentDiagnostics get_document_diagnostics = 9; } } @@ -703,6 +704,7 @@ message LspResponse { GetCodeActionsResponse get_code_actions_response = 2; GetSignatureHelpResponse get_signature_help_response = 3; GetCodeLensResponse get_code_lens_response = 4; + GetDocumentDiagnosticsResponse get_document_diagnostics_response = 5; } } @@ -749,3 +751,59 @@ message LspExtClearFlycheck { uint64 buffer_id = 2; uint64 language_server_id = 3; } + +message LspDiagnosticRelatedInformation { + optional string location_url = 1; + PointUtf16 location_range_start = 2; + PointUtf16 location_range_end = 3; + string message = 4; +} + +enum LspDiagnosticTag { + None = 0; + Unnecessary = 1; + Deprecated = 2; +} + +message LspDiagnostic { + PointUtf16 start = 1; + PointUtf16 end = 2; + Severity severity = 3; + optional string code = 4; + optional string code_description = 5; + optional string source = 6; + string message = 7; + repeated LspDiagnosticRelatedInformation related_information = 8; + repeated LspDiagnosticTag tags = 9; + optional string data = 10; + + enum Severity { + None = 0; + Error = 1; + Warning = 2; + Information = 3; + Hint = 4; + } +} + +message GetDocumentDiagnostics { + uint64 project_id = 1; + uint64 buffer_id = 2; + repeated VectorClockEntry version = 3; +} + +message GetDocumentDiagnosticsResponse { + repeated PulledDiagnostics pulled_diagnostics = 1; +} + +message PulledDiagnostics { + uint64 server_id = 1; + string uri = 2; + optional string result_id = 3; + bool changed = 4; + repeated LspDiagnostic diagnostics = 5; +} + +message RefreshDocumentsDiagnostics { + uint64 project_id = 1; +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 71daa99a7efaed1118720a8679f76bd72f5fb3c2..0b5be48308b6f6e1fd5f2cf0408b39da3be19170 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -387,7 +387,12 @@ message Envelope { LspExtRunFlycheck lsp_ext_run_flycheck = 346; LspExtClearFlycheck lsp_ext_clear_flycheck = 347; - LogToDebugConsole log_to_debug_console = 348; // current max + LogToDebugConsole log_to_debug_console = 348; + + GetDocumentDiagnostics get_document_diagnostics = 350; + GetDocumentDiagnosticsResponse get_document_diagnostics_response = 351; + RefreshDocumentsDiagnostics refresh_documents_diagnostics = 352; // current max + } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 32ad407a19a4df70c6e7995cd9163d6bfda5b614..e166685f101bc8510b1364e662db077068ef069c 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -307,6 +307,9 @@ messages!( (RunDebugLocators, Background), (DebugRequest, Background), (LogToDebugConsole, Background), + (GetDocumentDiagnostics, Background), + (GetDocumentDiagnosticsResponse, Background), + (RefreshDocumentsDiagnostics, Background) ); request_messages!( @@ -469,6 +472,8 @@ request_messages!( (ToggleBreakpoint, Ack), (GetDebugAdapterBinary, DebugAdapterBinary), (RunDebugLocators, DebugRequest), + (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse), + (RefreshDocumentsDiagnostics, Ack) ); entity_messages!( @@ -595,6 +600,8 @@ entity_messages!( RunDebugLocators, GetDebugAdapterBinary, LogToDebugConsole, + GetDocumentDiagnostics, + RefreshDocumentsDiagnostics ); entity_messages!( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ee815ac20f0e51d9622fc88b7d9d906026641f1a..cb816afe8a2cda1c315b052055accadba60e74ad 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2401,7 +2401,7 @@ impl Workspace { }) } }) - .log_err()?; + .ok()?; None } else { Some( @@ -2414,7 +2414,7 @@ impl Workspace { cx, ) }) - .log_err()? + .ok()? .await, ) } @@ -3111,7 +3111,7 @@ impl Workspace { window.spawn(cx, async move |cx| { let (project_entry_id, build_item) = task.await?; let result = pane.update_in(cx, |pane, window, cx| { - let result = pane.open_item( + pane.open_item( project_entry_id, project_path, focus_item, @@ -3121,9 +3121,7 @@ impl Workspace { window, cx, build_item, - ); - - result + ) }); result })