From b423ee84478715b64e81547c6bf71ddcafaf7479 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 21 May 2026 09:21:00 -0400 Subject: [PATCH] Fix bookkeeping error in tracking of language server IDs when dropping single-file worktree (#57298) Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #ISSUE Release Notes: - N/A or Added/Fixed/Improved ... --- crates/project/src/lsp_store.rs | 16 +++- crates/project/tests/integration/lsp_store.rs | 90 ++++++++++++++++++- 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index d288f0021f..4561ace3b3 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -3594,8 +3594,10 @@ impl LocalLspStore { } } servers_to_remove.retain(|server_id| !servers_to_preserve.contains(server_id)); - self.language_server_ids - .retain(|_, state| !servers_to_remove.contains(&state.id)); + self.language_server_ids.retain(|seed, state| { + seed.worktree_id != id_to_remove && !servers_to_remove.contains(&state.id) + }); + self.lsp_tree.instances.remove(&id_to_remove); for server_id_to_remove in &servers_to_remove { self.language_server_watched_paths .remove(server_id_to_remove); @@ -10101,6 +10103,16 @@ impl LspStore { .map(|(key, value)| (*key, value)) } + #[cfg(feature = "test-support")] + pub fn has_language_server_seed_for_worktree(&self, worktree_id: WorktreeId) -> bool { + self.as_local().is_some_and(|local| { + local + .language_server_ids + .keys() + .any(|seed| seed.worktree_id == worktree_id) + }) + } + pub(super) fn did_rename_entry( &self, worktree_id: WorktreeId, diff --git a/crates/project/tests/integration/lsp_store.rs b/crates/project/tests/integration/lsp_store.rs index 7d266ff136..100042f5d9 100644 --- a/crates/project/tests/integration/lsp_store.rs +++ b/crates/project/tests/integration/lsp_store.rs @@ -1,8 +1,94 @@ use std::path::Path; -use language::{CodeLabel, HighlightId}; +use fs::FakeFs; +use futures::StreamExt; +use gpui::TestAppContext; +use language::{CodeLabel, FakeLspAdapter, HighlightId, rust_lang}; +use lsp::Uri; +use project::{Project, lsp_store::*}; +use serde_json::json; +use util::path; -use project::lsp_store::*; +use crate::init_test; + +#[gpui::test] +async fn test_removing_invisible_worktree_cleans_reused_lsp_bookkeeping(cx: &mut TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree(path!("/the-root"), json!({ "main.rs": "fn main() {}" })) + .await; + fs.insert_tree( + path!("/the-registry"), + json!({ "dep": { "src": { "dep.rs": "pub fn dep() {}" } } }), + ) + .await; + + let project = Project::test(fs, [path!("/the-root").as_ref()], cx).await; + 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::default()); + + let (_visible_buffer, _visible_handle) = project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp(path!("/the-root/main.rs"), cx) + }) + .await + .unwrap(); + fake_servers.next().await.unwrap(); + cx.run_until_parked(); + + let server_id = project.read_with(cx, |project, cx| { + project + .lsp_store() + .read(cx) + .language_server_statuses() + .next() + .unwrap() + .0 + }); + let external_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer_via_lsp( + Uri::from_file_path(path!("/the-registry/dep/src/dep.rs")).unwrap(), + server_id, + cx, + ) + }) + .await + .unwrap(); + cx.run_until_parked(); + + let invisible_worktree_id = + external_buffer.read_with(cx, |buffer, cx| buffer.file().unwrap().worktree_id(cx)); + project.read_with(cx, |project, cx| { + let worktree = project.worktree_for_id(invisible_worktree_id, cx).unwrap(); + assert!(!worktree.read(cx).is_visible()); + assert!( + project + .lsp_store() + .read(cx) + .has_language_server_seed_for_worktree(invisible_worktree_id) + ); + }); + + project.update(cx, |project, cx| { + project.remove_worktree(invisible_worktree_id, cx); + }); + cx.run_until_parked(); + + project.read_with(cx, |project, cx| { + let lsp_store = project.lsp_store(); + let lsp_store = lsp_store.read(cx); + assert!( + lsp_store + .language_server_statuses() + .any(|(status_server_id, _)| status_server_id == server_id) + ); + assert!(!lsp_store.has_language_server_seed_for_worktree(invisible_worktree_id)); + }); +} #[test] fn test_glob_literal_prefix() {