Pull diagnostics fixes (#32242)

Kirill Bulatov created

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

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

Release Notes:

- N/A

Change summary

assets/settings/default.json                | 11 +
crates/collab/src/rpc.rs                    |  2 
crates/diagnostics/src/diagnostics_tests.rs | 18 +++
crates/editor/src/editor.rs                 | 89 +++++++++++++-------
crates/editor/src/editor_tests.rs           | 29 +++++-
crates/language/src/buffer.rs               | 11 ++
crates/project/src/lsp_command.rs           |  4 
crates/project/src/lsp_store.rs             | 98 +++++++++++++++-------
crates/project/src/lsp_store/clangd_ext.rs  |  1 
crates/project/src/project.rs               |  8 
crates/project/src/project_settings.rs      | 92 +++++++++++++++------
crates/project/src/project_tests.rs         |  8 +
crates/proto/proto/lsp.proto                |  2 
crates/proto/proto/zed.proto                |  2 
crates/proto/src/proto.rs                   |  6 
15 files changed, 272 insertions(+), 109 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -1033,9 +1033,14 @@
     "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 using LSP pull diagnostics mechanism in Zed.
+    "lsp_pull_diagnostics": {
+      // Whether to pull for diagnostics or not.
+      "enabled": true,
+      // Minimum time to wait before pulling diagnostics from the language server(s).
+      // 0 turns the debounce off.
+      "debounce_ms": 50
+    },
     // Settings for inline diagnostics
     "inline": {
       // Whether to show diagnostics inline or not

crates/collab/src/rpc.rs 🔗

@@ -356,7 +356,7 @@ impl Server {
             .add_message_handler(broadcast_project_message_from_host::<proto::BufferSaved>)
             .add_message_handler(broadcast_project_message_from_host::<proto::UpdateDiffBases>)
             .add_message_handler(
-                broadcast_project_message_from_host::<proto::RefreshDocumentsDiagnostics>,
+                broadcast_project_message_from_host::<proto::PullWorkspaceDiagnostics>,
             )
             .add_request_handler(get_users)
             .add_request_handler(fuzzy_search_users)

crates/diagnostics/src/diagnostics_tests.rs 🔗

@@ -105,7 +105,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
             }
             ],
             version: None
-        }, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
+        }, None, 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,
                 },
+                None,
                 DiagnosticSourceKind::Pushed,
                 &[],
                 cx,
@@ -262,6 +263,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
                     ],
                     version: None,
                 },
+                None,
                 DiagnosticSourceKind::Pushed,
                 &[],
                 cx,
@@ -370,6 +372,7 @@ async fn test_diagnostics_with_folds(cx: &mut TestAppContext) {
                     }],
                     version: None,
                 },
+                None,
                 DiagnosticSourceKind::Pushed,
                 &[],
                 cx,
@@ -468,6 +471,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
                     }],
                     version: None,
                 },
+                None,
                 DiagnosticSourceKind::Pushed,
                 &[],
                 cx,
@@ -511,6 +515,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
                     }],
                     version: None,
                 },
+                None,
                 DiagnosticSourceKind::Pushed,
                 &[],
                 cx,
@@ -553,6 +558,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
                     }],
                     version: None,
                 },
+                None,
                 DiagnosticSourceKind::Pushed,
                 &[],
                 cx,
@@ -566,6 +572,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
                     diagnostics: vec![],
                     version: None,
                 },
+                None,
                 DiagnosticSourceKind::Pushed,
                 &[],
                 cx,
@@ -607,6 +614,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
                     }],
                     version: None,
                 },
+                None,
                 DiagnosticSourceKind::Pushed,
                 &[],
                 cx,
@@ -740,6 +748,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
                                 diagnostics: diagnostics.clone(),
                                 version: None,
                             },
+                            None,
                             DiagnosticSourceKind::Pushed,
                             &[],
                             cx,
@@ -928,6 +937,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
                                 diagnostics: diagnostics.clone(),
                                 version: None,
                             },
+                            None,
                             DiagnosticSourceKind::Pushed,
                             &[],
                             cx,
@@ -984,6 +994,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext)
                             ..Default::default()
                         }],
                     },
+                    None,
                     DiagnosticSourceKind::Pushed,
                     &[],
                     cx,
@@ -1018,6 +1029,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext)
                         version: None,
                         diagnostics: Vec::new(),
                     },
+                    None,
                     DiagnosticSourceKind::Pushed,
                     &[],
                     cx,
@@ -1100,6 +1112,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) {
                             },
                         ],
                     },
+                    None,
                     DiagnosticSourceKind::Pushed,
                     &[],
                     cx,
@@ -1239,6 +1252,7 @@ async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
                         ..Default::default()
                     }],
                 },
+                None,
                 DiagnosticSourceKind::Pushed,
                 &[],
                 cx,
@@ -1291,6 +1305,7 @@ async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext)
                         ..Default::default()
                     }],
                 },
+                None,
                 DiagnosticSourceKind::Pushed,
                 &[],
                 cx,
@@ -1393,6 +1408,7 @@ async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
                     ],
                     version: None,
                 },
+                None,
                 DiagnosticSourceKind::Pushed,
                 &[],
                 cx,

crates/editor/src/editor.rs 🔗

@@ -125,7 +125,7 @@ use markdown::Markdown;
 use mouse_context_menu::MouseContextMenu;
 use persistence::DB;
 use project::{
-    BreakpointWithPosition, CompletionResponse, LspPullDiagnostics, ProjectPath,
+    BreakpointWithPosition, CompletionResponse, LspPullDiagnostics, ProjectPath, PulledDiagnostics,
     debugger::{
         breakpoint_store::{
             BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
@@ -1700,7 +1700,7 @@ impl Editor {
                             }
                             editor.pull_diagnostics(window, cx);
                         }
-                        project::Event::RefreshDocumentsDiagnostics => {
+                        project::Event::PullWorkspaceDiagnostics => {
                             editor.pull_diagnostics(window, cx);
                         }
                         project::Event::SnippetEdit(id, snippet_edits) => {
@@ -15966,11 +15966,13 @@ impl Editor {
 
     fn pull_diagnostics(&mut self, window: &Window, cx: &mut Context<Self>) -> Option<()> {
         let project = self.project.as_ref()?.downgrade();
-        let debounce = Duration::from_millis(
-            ProjectSettings::get_global(cx)
-                .diagnostics
-                .lsp_pull_diagnostics_debounce_ms?,
-        );
+        let pull_diagnostics_settings = ProjectSettings::get_global(cx)
+            .diagnostics
+            .lsp_pull_diagnostics;
+        if !pull_diagnostics_settings.enabled {
+            return None;
+        }
+        let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms);
         let buffers = self.buffer.read(cx).all_buffers();
 
         self.pull_diagnostics_task = cx.spawn_in(window, async move |editor, cx| {
@@ -18733,13 +18735,16 @@ impl Editor {
                 }
                 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);
+                            // 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.
+                            // TODO: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic_refresh explains the flow how
+                            // diagnostics should be pulled: instead of pulling every open editor's buffer's diagnostics (which happens effectively due to emitting this event),
+                            // we should only pull for the current buffer's diagnostics and get the rest via the workspace diagnostics LSP request — this is not implemented yet.
+                            cx.emit(project::Event::PullWorkspaceDiagnostics);
                         }
 
                         if let Some(buffer) = edited_buffer {
@@ -20990,7 +20995,7 @@ impl SemanticsProvider for Entity<Project> {
                         let LspPullDiagnostics::Response {
                             server_id,
                             uri,
-                            diagnostics: project::PulledDiagnostics::Changed { diagnostics, .. },
+                            diagnostics,
                         } = diagnostics_set
                         else {
                             continue;
@@ -21001,25 +21006,49 @@ impl SemanticsProvider for Entity<Project> {
                             .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();
+                        match diagnostics {
+                            PulledDiagnostics::Unchanged { result_id } => {
+                                lsp_store
+                                    .merge_diagnostics(
+                                        server_id,
+                                        lsp::PublishDiagnosticsParams {
+                                            uri: uri.clone(),
+                                            diagnostics: Vec::new(),
+                                            version: None,
+                                        },
+                                        Some(result_id),
+                                        DiagnosticSourceKind::Pulled,
+                                        disk_based_sources,
+                                        |_, _| true,
+                                        cx,
+                                    )
+                                    .log_err();
+                            }
+                            PulledDiagnostics::Changed {
+                                diagnostics,
+                                result_id,
+                            } => {
+                                lsp_store
+                                    .merge_diagnostics(
+                                        server_id,
+                                        lsp::PublishDiagnosticsParams {
+                                            uri: uri.clone(),
+                                            diagnostics,
+                                            version: None,
+                                        },
+                                        result_id,
+                                        DiagnosticSourceKind::Pulled,
+                                        disk_based_sources,
+                                        |old_diagnostic, _| match old_diagnostic.source_kind {
+                                            DiagnosticSourceKind::Pulled => false,
+                                            DiagnosticSourceKind::Other
+                                            | DiagnosticSourceKind::Pushed => true,
+                                        },
+                                        cx,
+                                    )
+                                    .log_err();
+                            }
+                        }
                     }
                 })
             })

crates/editor/src/editor_tests.rs 🔗

@@ -13940,6 +13940,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
                             },
                         ],
                     },
+                    None,
                     DiagnosticSourceKind::Pushed,
                     &[],
                     cx,
@@ -21854,7 +21855,7 @@ fn assert_hunk_revert(
     assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
 }
 
-#[gpui::test]
+#[gpui::test(iterations = 10)]
 async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
     init_test(cx, |_| {});
 
@@ -21912,7 +21913,8 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
     let fake_server = fake_servers.next().await.unwrap();
     let mut first_request = fake_server
         .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
-            counter.fetch_add(1, atomic::Ordering::Release);
+            let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
+            let result_id = Some(new_result_id.to_string());
             assert_eq!(
                 params.text_document.uri,
                 lsp::Url::from_file_path(path!("/a/first.rs")).unwrap()
@@ -21923,13 +21925,27 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
                         related_documents: None,
                         full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
                             items: Vec::new(),
-                            result_id: None,
+                            result_id,
                         },
                     }),
                 ))
             }
         });
 
+    let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
+        editor.update(cx, |editor, cx| {
+            let buffer_result_id = editor
+                .buffer()
+                .read(cx)
+                .as_singleton()
+                .expect("created a singleton buffer")
+                .read(cx)
+                .result_id();
+            assert_eq!(expected, buffer_result_id);
+        });
+    };
+
+    ensure_result_id(None, cx);
     cx.executor().advance_clock(Duration::from_millis(60));
     cx.executor().run_until_parked();
     assert_eq!(
@@ -21941,6 +21957,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
         .next()
         .await
         .expect("should have sent the first diagnostics pull request");
+    ensure_result_id(Some("1".to_string()), cx);
 
     // Editing should trigger diagnostics
     editor.update_in(cx, |editor, window, cx| {
@@ -21953,6 +21970,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
         2,
         "Editing should trigger diagnostic request"
     );
+    ensure_result_id(Some("2".to_string()), cx);
 
     // Moving cursor should not trigger diagnostic request
     editor.update_in(cx, |editor, window, cx| {
@@ -21967,6 +21985,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
         2,
         "Cursor movement should not trigger diagnostic request"
     );
+    ensure_result_id(Some("2".to_string()), cx);
 
     // Multiple rapid edits should be debounced
     for _ in 0..5 {
@@ -21980,7 +21999,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
     let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
     assert!(
         final_requests <= 4,
-        "Multiple rapid edits should be debounced (got {} requests)",
-        final_requests
+        "Multiple rapid edits should be debounced (got {final_requests} requests)",
     );
+    ensure_result_id(Some(final_requests.to_string()), cx);
 }

crates/language/src/buffer.rs 🔗

@@ -127,6 +127,8 @@ pub struct Buffer {
     has_unsaved_edits: Cell<(clock::Global, bool)>,
     change_bits: Vec<rc::Weak<Cell<bool>>>,
     _subscriptions: Vec<gpui::Subscription>,
+    /// The result id received last time when pulling diagnostics for this buffer.
+    pull_diagnostics_result_id: Option<String>,
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -955,6 +957,7 @@ impl Buffer {
             completion_triggers_timestamp: Default::default(),
             deferred_ops: OperationQueue::new(),
             has_conflict: false,
+            pull_diagnostics_result_id: None,
             change_bits: Default::default(),
             _subscriptions: Vec::new(),
         }
@@ -2740,6 +2743,14 @@ impl Buffer {
     pub fn preserve_preview(&self) -> bool {
         !self.has_edits_since(&self.preview_version)
     }
+
+    pub fn result_id(&self) -> Option<String> {
+        self.pull_diagnostics_result_id.clone()
+    }
+
+    pub fn set_result_id(&mut self, result_id: Option<String>) {
+        self.pull_diagnostics_result_id = result_id;
+    }
 }
 
 #[doc(hidden)]

crates/project/src/lsp_command.rs 🔗

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

crates/project/src/lsp_store.rs 🔗

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

crates/project/src/lsp_store/clangd_ext.rs 🔗

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

crates/project/src/project.rs 🔗

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

crates/project/src/project_settings.rs 🔗

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

crates/project/src/project_tests.rs 🔗

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

crates/proto/proto/lsp.proto 🔗

@@ -804,6 +804,6 @@ message PulledDiagnostics {
     repeated LspDiagnostic diagnostics = 5;
 }
 
-message RefreshDocumentsDiagnostics {
+message PullWorkspaceDiagnostics {
     uint64 project_id = 1;
 }

crates/proto/proto/zed.proto 🔗

@@ -391,7 +391,7 @@ message Envelope {
 
         GetDocumentDiagnostics get_document_diagnostics = 350;
         GetDocumentDiagnosticsResponse get_document_diagnostics_response = 351;
-        RefreshDocumentsDiagnostics refresh_documents_diagnostics = 352; // current max
+        PullWorkspaceDiagnostics pull_workspace_diagnostics = 352; // current max
 
     }
 

crates/proto/src/proto.rs 🔗

@@ -309,7 +309,7 @@ messages!(
     (LogToDebugConsole, Background),
     (GetDocumentDiagnostics, Background),
     (GetDocumentDiagnosticsResponse, Background),
-    (RefreshDocumentsDiagnostics, Background)
+    (PullWorkspaceDiagnostics, Background)
 );
 
 request_messages!(
@@ -473,7 +473,7 @@ request_messages!(
     (GetDebugAdapterBinary, DebugAdapterBinary),
     (RunDebugLocators, DebugRequest),
     (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse),
-    (RefreshDocumentsDiagnostics, Ack)
+    (PullWorkspaceDiagnostics, Ack)
 );
 
 entity_messages!(
@@ -601,7 +601,7 @@ entity_messages!(
     GetDebugAdapterBinary,
     LogToDebugConsole,
     GetDocumentDiagnostics,
-    RefreshDocumentsDiagnostics
+    PullWorkspaceDiagnostics
 );
 
 entity_messages!(