From 50326ddc359e612e9f91cffa64565f87bc814d0a Mon Sep 17 00:00:00 2001 From: Nils Koch Date: Wed, 17 Sep 2025 17:58:46 +0200 Subject: [PATCH] project_panel: Collapse top-level entries in `Collapse all entries` command (#38310) Closes #11760 The command `project panel: collapse all entries` currently does not collapse top-level entries (the workspaces themselves). I think this should be expected behaviour if you only have a single workspace in your project. However, if you have multiple workspaces, we should collapse their top-level folders as well. This is the expected behaviour in the screenshots in #11760. For more context: Atm the `.retain` function empties the `self.expanded_dir_ids` Hash Map, because the `expanded_entries` Vec is (almost) never empty - it contains the id of the `root_entry` of the workspace. https://github.com/zed-industries/zed/blob/d48d6a745409a8998998ed59c28493a1aa733ebb/crates/project_panel/src/project_panel.rs#L1148-L1152 We then update the `self.expanded_dir_ids` in the `update_visible_entries` function, and since the Hash Map is empty, we execute the `hash_map::Entry::Vacant` arm of the following match statement. https://github.com/zed-industries/zed/blob/d48d6a745409a8998998ed59c28493a1aa733ebb/crates/project_panel/src/project_panel.rs#L3062-L3073 This change makes sure that we do not clear the `expanded_dir_ids` HashMap and always keep the keys for all visible workspaces and therefore we run the `hash_map::Entry::Occupied` arm, which does not override the `expanded_dir_ids` anymore. https://github.com/user-attachments/assets/b607523b-2ea2-4159-8edf-aed7bca05e3a cc @MrSubidubi Release Notes: - N/A *or* Added/Fixed/Improved ... --------- Co-authored-by: Finn Evers --- crates/project_panel/src/project_panel.rs | 26 ++++- .../project_panel/src/project_panel_tests.rs | 105 ++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index aed3b44515554299b3d50fbcf5e2b58123495908..ec63c5ca60ee2bdca9ba699c2af300116e5cdb3a 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1147,8 +1147,32 @@ impl ProjectPanel { ) { // By keeping entries for fully collapsed worktrees, we avoid expanding them within update_visible_entries // (which is it's default behavior when there's no entry for a worktree in expanded_dir_ids). + let multiple_worktrees = self.project.read(cx).worktrees(cx).count() > 1; + let project = self.project.read(cx); + self.expanded_dir_ids - .retain(|_, expanded_entries| expanded_entries.is_empty()); + .iter_mut() + .for_each(|(worktree_id, expanded_entries)| { + if multiple_worktrees { + *expanded_entries = Default::default(); + return; + } + + let root_entry_id = project + .worktree_for_id(*worktree_id, cx) + .map(|worktree| worktree.read(cx).snapshot()) + .and_then(|worktree_snapshot| { + worktree_snapshot.root_entry().map(|entry| entry.id) + }); + + match root_entry_id { + Some(id) => { + expanded_entries.retain(|entry_id| entry_id == &id); + } + None => *expanded_entries = Default::default(), + }; + }); + self.update_visible_entries(None, cx); cx.notify(); } diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index ad2a7d12ecce31cf1aa4458b3fd59e23f63ab08b..73c33b057807d66687c2dec13962fed5ed0412d3 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -2747,6 +2747,111 @@ async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_collapse_all_entries_multiple_worktrees(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor()); + let worktree_content = json!({ + "dir_1": { + "file_1.py": "# File contents", + }, + "dir_2": { + "file_1.py": "# File contents", + } + }); + + fs.insert_tree("/project_root_1", worktree_content.clone()) + .await; + fs.insert_tree("/project_root_2", worktree_content).await; + + let project = Project::test( + fs.clone(), + ["/project_root_1".as_ref(), "/project_root_2".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 panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + panel.update_in(cx, |panel, window, cx| { + panel.collapse_all_entries(&CollapseAllEntries, window, cx) + }); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["> project_root_1", "> project_root_2",] + ); +} + +#[gpui::test] +async fn test_collapse_all_entries_with_collapsed_root(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/project_root", + json!({ + "dir_1": { + "nested_dir": { + "file_a.py": "# File contents", + "file_b.py": "# File contents", + "file_c.py": "# File contents", + }, + "file_1.py": "# File contents", + "file_2.py": "# File contents", + "file_3.py": "# File contents", + }, + "dir_2": { + "file_1.py": "# File contents", + "file_2.py": "# File contents", + "file_3.py": "# File contents", + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/project_root".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 panel = workspace.update(cx, ProjectPanel::new).unwrap(); + + // Open project_root/dir_1 to ensure that a nested directory is expanded + toggle_expand_dir(&panel, "project_root/dir_1", cx); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v project_root", + " v dir_1 <== selected", + " > nested_dir", + " file_1.py", + " file_2.py", + " file_3.py", + " > dir_2", + ] + ); + + // Close root directory + toggle_expand_dir(&panel, "project_root", cx); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["> project_root <== selected"] + ); + + // Run collapse_all_entries and make sure root is not expanded + panel.update_in(cx, |panel, window, cx| { + panel.collapse_all_entries(&CollapseAllEntries, window, cx) + }); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["> project_root <== selected"] + ); +} + #[gpui::test] async fn test_new_file_move(cx: &mut gpui::TestAppContext) { init_test(cx);