diff --git a/assets/settings/default.json b/assets/settings/default.json index 6e0bd4d34b..8d8c65884c 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 4f371b8135..e768e4c3d0 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 202200ef58..d00ff0baba 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 c561ec3865..66472a78dc 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 22776d525f..1050c0ecf9 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 4c386701cb..cd97ff50b2 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 e4bd79f6e8..b500a2f3b6 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 d6e253271b..d5ae65d922 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 8c02eb5b44..ae82f3aa63 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 831b7d627b..c3b91bae31 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 626a238604..c68ce1e33e 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 983ef5458d..3c8cee6320 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 1bbabe1724..97cc35c209 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 dd0ed856d3..15cf954ef4 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 d12015ec31..9f2b044ed1 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 4b7616d4d1..2b6d11ceb9 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 fe9167dfaa..41be601456 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 4477b431a5..f32ce0c546 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 2da5908b94..d4a10d79e6 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 e7692da481..09a05a50cd 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 47eb6fa3d3..b04009d622 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 71daa99a7e..0b5be48308 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 32ad407a19..e166685f10 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 ee815ac20f..cb816afe8a 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 })