Fix diagnostics batches stopping at paths without a worktree (#64017)

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

Based on the amazing find in
https://github.com/zed-industries/zed/issues/62303#issuecomment-5553387960

Release Notes:

- Fixed diagnostics batches stopping at paths without a worktree
This commit is contained in:
Kirill Bulatov authored and GitHub committed 2026-09-11 09:16:11 +00:00
1 parent ba66199475
commit 4e693c3b6b
2 files changed
+129 -5

No files matched your search

+1 -1
View File
@@ -10146,7 +10146,7 @@ impl LspStore {
self.worktree_store.read(cx).find_worktree(abs_path, cx)
else {
log::warn!("skipping diagnostics update, no worktree found for path {abs_path:?}");
return Ok(());
continue;
};
let worktree_id = worktree.read(cx).id();
+128 -4
View File
@@ -1,4 +1,5 @@
use std::{
borrow::Cow,
path::{Path, PathBuf},
sync::Arc,
time::{Duration, Instant},
@@ -6,13 +7,15 @@ use std::{
use collections::HashMap;
use fs::{FakeFs, Fs};
use futures::StreamExt;
use futures::{FutureExt, StreamExt};
use gpui::{Entity, TestAppContext};
use language::{Buffer, CodeLabel, FakeLspAdapter, HighlightId, LocalFile, rust_lang};
use language::{
Buffer, CodeLabel, DiagnosticSourceKind, FakeLspAdapter, HighlightId, LocalFile, rust_lang,
};
use lsp::{LanguageServerId, LanguageServerName, Uri};
use parking_lot::Mutex;
use project::{
Project,
DiagnosticSummary, Event, Project,
lsp_store::{
log_store::{TestRpcLogHeaderState, TestRpcRequestTracker},
*,
@@ -20,10 +23,131 @@ use project::{
};
use serde_json::json;
use unindent::Unindent;
use util::path;
use util::{path, rel_path::rel_path};
use crate::init_test;
#[gpui::test]
async fn test_diagnostic_batches_skip_paths_without_worktrees(cx: &mut TestAppContext) {
init_test(cx);
for skipped_index in 0..=2 {
let fs = FakeFs::new(cx.executor());
fs.insert_tree(path!("/dir"), json!({ "a.rs": "one", "b.rs": "two" }))
.await;
let project = Project::test(fs, [Path::new(path!("/dir"))], cx).await;
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
let buffer_a = project
.update(cx, |project, cx| {
project.open_local_buffer(path!("/dir/a.rs"), cx)
})
.await
.unwrap();
let worktree_id =
buffer_a.read_with(cx, |buffer, cx| buffer.file().unwrap().worktree_id(cx));
let server_id = LanguageServerId(0);
for message in [Some("error"), None] {
cx.run_until_parked();
project.read_with(cx, |project, cx| {
assert_eq!(
project.get_open_buffer(&(worktree_id, rel_path("b.rs")).into(), cx),
None
);
});
let mut events = cx.events(&project);
let mut paths = vec![path!("/dir/a.rs"), path!("/dir/b.rs")];
paths.insert(skipped_index, path!("/outside.rs"));
let updates = paths
.into_iter()
.map(|path| DocumentDiagnosticsUpdate {
diagnostics: lsp::PublishDiagnosticsParams {
uri: Uri::from_file_path(path).unwrap(),
version: None,
diagnostics: message
.into_iter()
.map(|message| lsp::Diagnostic {
range: lsp::Range::new(
lsp::Position::new(0, 0),
lsp::Position::new(0, 3),
),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: lsp::DiagnosticMessage::from(message),
..lsp::Diagnostic::default()
})
.collect(),
},
result_id: None,
registration_id: None,
server_id,
disk_based_sources: Cow::Borrowed(&[]),
})
.collect();
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.merge_lsp_diagnostics(
DiagnosticSourceKind::Pushed,
updates,
|_, _, _| false,
cx,
)
.unwrap();
});
cx.run_until_parked();
project.read_with(cx, |project, cx| {
assert_eq!(
project.diagnostic_summary(false, cx),
DiagnosticSummary {
error_count: if message.is_some() { 2 } else { 0 },
warning_count: 0,
},
"skipped update at index {skipped_index}, message {message:?}"
);
});
let diagnostic_events = std::iter::from_fn(|| events.next().now_or_never().flatten())
.filter_map(|event| match event {
Event::DiagnosticsUpdated {
language_server_id,
paths,
} => Some((language_server_id, paths)),
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(
diagnostic_events,
vec![(
server_id,
vec![
(worktree_id, rel_path("a.rs")).into(),
(worktree_id, rel_path("b.rs")).into(),
],
)],
"skipped update at index {skipped_index}, message {message:?}"
);
let buffer_b = project
.update(cx, |project, cx| {
project.open_local_buffer(path!("/dir/b.rs"), cx)
})
.await
.unwrap();
for buffer in [&buffer_a, &buffer_b] {
buffer.read_with(cx, |buffer, _| {
assert_eq!(
buffer
.buffer_diagnostics(Some(server_id))
.iter()
.map(|entry| entry.diagnostic.message.to_string())
.collect::<Vec<_>>(),
message.into_iter().collect::<Vec<_>>()
);
});
}
}
}
}
#[gpui::test]
async fn test_removing_invisible_worktree_cleans_reused_lsp_bookkeeping(cx: &mut TestAppContext) {
init_test(cx);