From 4035186cc2a03a88bc094711b44309df7d3ed5fe Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 13 Apr 2026 02:31:23 -0700 Subject: [PATCH] Fix a reachability bug in the sidebar (#53785) 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 --- crates/sidebar/src/sidebar_tests.rs | 113 +++++++++++++++++++++++- crates/workspace/src/multi_workspace.rs | 11 +++ 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/crates/sidebar/src/sidebar_tests.rs b/crates/sidebar/src/sidebar_tests.rs index e5fef45f4cf3805bb5e42504dd78ca60930b43d6..a9251fad75cf95892be0aa88e6d056d86f1aba67 100644 --- a/crates/sidebar/src/sidebar_tests.rs +++ b/crates/sidebar/src/sidebar_tests.rs @@ -8777,6 +8777,115 @@ async fn test_worktree_add_only_migrates_threads_for_same_folder_paths(cx: &mut }); } +#[gpui::test] +async fn test_linked_worktree_workspace_reachable_after_adding_worktree_to_project( + cx: &mut TestAppContext, +) { + // When a linked worktree is opened as its own workspace and then a new + // folder is added to the main project group, the linked worktree + // workspace must still be reachable from some sidebar entry. + let (_fs, project) = init_multi_project_test(&["/my-project"], cx).await; + let fs = _fs.clone(); + + // Set up git worktree infrastructure. + fs.insert_tree( + "/my-project/.git/worktrees/wt-0", + serde_json::json!({ + "commondir": "../../", + "HEAD": "ref: refs/heads/wt-0", + }), + ) + .await; + fs.insert_tree( + "/worktrees/wt-0", + serde_json::json!({ + ".git": "gitdir: /my-project/.git/worktrees/wt-0", + "src": {}, + }), + ) + .await; + fs.add_linked_worktree_for_repo( + Path::new("/my-project/.git"), + false, + git::repository::Worktree { + path: PathBuf::from("/worktrees/wt-0"), + ref_name: Some("refs/heads/wt-0".into()), + sha: "aaa".into(), + is_main: false, + }, + ) + .await; + + // Re-scan so the main project discovers the linked worktree. + project.update(cx, |p, cx| p.git_scans_complete(cx)).await; + + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let sidebar = setup_sidebar(&multi_workspace, cx); + + // Open the linked worktree as its own workspace. + let worktree_project = project::Project::test( + fs.clone() as Arc, + ["/worktrees/wt-0".as_ref()], + cx, + ) + .await; + worktree_project + .update(cx, |p, cx| p.git_scans_complete(cx)) + .await; + multi_workspace.update_in(cx, |mw, window, cx| { + mw.test_add_workspace(worktree_project.clone(), window, cx); + }); + cx.run_until_parked(); + + // Both workspaces should be reachable. + let workspace_count = multi_workspace.read_with(cx, |mw, _| mw.workspaces().count()); + assert_eq!(workspace_count, 2, "should have 2 workspaces"); + + // Add a new folder to the main project, changing the project group key. + fs.insert_tree( + "/other-project", + serde_json::json!({ ".git": {}, "src": {} }), + ) + .await; + project + .update(cx, |project, cx| { + project.find_or_create_worktree("/other-project", true, cx) + }) + .await + .expect("should add worktree"); + cx.run_until_parked(); + + sidebar.update_in(cx, |sidebar, _window, cx| sidebar.update_entries(cx)); + cx.run_until_parked(); + + // The linked worktree workspace must still be reachable. + let entries = visible_entries_as_strings(&sidebar, cx); + let mw_workspaces: Vec<_> = multi_workspace.read_with(cx, |mw, _| { + mw.workspaces().map(|ws| ws.entity_id()).collect() + }); + sidebar.read_with(cx, |sidebar, cx| { + let multi_workspace = multi_workspace.read(cx); + let reachable: std::collections::HashSet = sidebar + .contents + .entries + .iter() + .flat_map(|entry| entry.reachable_workspaces(multi_workspace, cx)) + .map(|ws| ws.entity_id()) + .collect(); + let all: std::collections::HashSet = + mw_workspaces.iter().copied().collect(); + let unreachable = &all - &reachable; + assert!( + unreachable.is_empty(), + "all workspaces should be reachable after adding folder; \ + unreachable: {:?}, entries: {:?}", + unreachable, + entries, + ); + }); +} + mod property_test { use super::*; use gpui::proptest::prelude::*; @@ -9582,11 +9691,11 @@ mod property_test { } #[gpui::property_test(config = ProptestConfig { - cases: 50, + cases: 20, ..Default::default() })] async fn test_sidebar_invariants( - #[strategy = gpui::proptest::collection::vec(0u32..DISTRIBUTION_SLOTS * 10, 1..5)] + #[strategy = gpui::proptest::collection::vec(0u32..DISTRIBUTION_SLOTS * 10, 1..10)] raw_operations: Vec, cx: &mut TestAppContext, ) { diff --git a/crates/workspace/src/multi_workspace.rs b/crates/workspace/src/multi_workspace.rs index 7e38e57d312a6dedb333b59da7aa535ec332f284..e3d4f50a482869414f361da870a64aebf0ee9300 100644 --- a/crates/workspace/src/multi_workspace.rs +++ b/crates/workspace/src/multi_workspace.rs @@ -666,6 +666,17 @@ impl MultiWorkspace { group.key = new_key.clone(); } } + + // If another retained workspace still has the old key (e.g. a + // linked worktree workspace), re-create the old group so it + // remains reachable in the sidebar. + let other_workspace_needs_old_key = self + .retained_workspaces + .iter() + .any(|ws| ws.read(cx).project_group_key(cx) == *old_key); + if other_workspace_needs_old_key { + self.ensure_project_group_state(old_key.clone()); + } } /// Re-keys a project group and emits `ProjectGroupKeyUpdated` so