project_panel: Add expand/collapse all to context menu (#59567)

Add `ExpandAllEntries` action, keybinding, and right-click context menu
entries for expand/collapse all in the project panel.

- Add `ExpandAllEntries` action + handler + `expand_all_entries()`
method
- Add `cmd-right` / `ctrl-right` keybinding for `ExpandAllEntries`,
mirroring
  the existing `cmd-left` for `CollapseAllEntries`
- Add "Expand All" to root-entry and folder right-click context menus
- Add keybinding hint to root-entry "Collapse All" context menu entry
- Per-worktree behavior: context menu actions affect only the selected
worktree,
  subfolder "Expand All" expands from that folder down

# Objective

The project panel had no discoverable way to expand or collapse all
entries. The only ways were keyboard shortcuts (`cmd-left` for collapse
all)
or the right-click context menu. The global `ExpandAllEntries` shortcut
(`cmd-right`) was also missing. This PR adds that shortcut and exposes
both actions in the context menu so users learn the keybindings while
keeping the UI minimal.

## Solution

- Add `ExpandAllEntries` action + handler + `expand_all_entries()`
method
- Add `cmd-right` / `ctrl-right` keybinding for `ExpandAllEntries`
(global),
  mirroring the existing `cmd-left` for `CollapseAllEntries`
- Add "Expand All" to the right-click context menu for both root entries
and
  subfolder entries
- Update root-entry "Collapse All" to show its keybinding hint via
  `.action(Box::new(CollapseAllEntries))`
- **Per-worktree behavior**: each root entry's context menu actions
affect
only that worktree, subfolder "Expand All" expands from that folder down

## Testing

- Added 6 GPUI tests in `project_panel_tests.rs`:
  - `test_expand_all_entries`: single worktree
- `test_expand_all_entries_multiple_worktrees`: global expand across
worktrees
  - `test_expand_all_entries_via_window_dispatch`: action dispatch path
  - `test_per_worktree_expand`: expand only the clicked worktree
- `test_per_worktree_collapse`: collapse only the clicked worktree, keep
root visible
  - `test_expand_all_entries_with_auto_fold`: expand with auto_fold_dirs
- Ran `cargo test -p project_panel`: 106 pass

**Manual testing for reviewers:**

1. Open a project with nested directories and open the Project Panel
2. Right-click a root entry, confirm "Expand All" and "Collapse All"
appear
   with their keybinding hints (`⌘→` / `⌘←`)
3. Right-click a subfolder, confirm "Expand All" and "Collapse All"
appear
4. Click "Expand All" on a subfolder, confirm only that folder's subtree
expands
5. Click "Collapse All", confirm children collapse but the root stays
visible
6. Open a multi-folder workspace. Right-click the second root and click
   "Expand All", confirm only that folder expands
7. Press `cmd-right` (or `ctrl-right`), confirm all worktrees expand
globally

Tested on macOS only. Linux and Windows should behave the same since
there
is no platform-specific code.

## 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 adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

## Showcase



https://github.com/user-attachments/assets/adc501d7-8e3a-4687-aef0-dc00426d232d

---

Release Notes:

- Added `Expand All` and `Collapse All` to the project panel right-click
  context menu with keybinding hints.
- Added `cmd-right` / `ctrl-right` keybinding to expand all entries.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-06-26 17:44:41 +00:00
1 parent d412fb10d7
commit 9fd3e0ca3b
6 files changed
+421 -238

No files matched your search

+1
View File
@@ -955,6 +955,7 @@
"left": "project_panel::CollapseSelectedEntry",
"ctrl-left": "project_panel::CollapseAllEntries",
"right": "project_panel::ExpandSelectedEntry",
"ctrl-right": "project_panel::ExpandAllEntries",
"new": "project_panel::NewFile",
"ctrl-n": "project_panel::NewFile",
"alt-new": "project_panel::NewDirectory",
+1
View File
@@ -1019,6 +1019,7 @@
"left": "project_panel::CollapseSelectedEntry",
"cmd-left": "project_panel::CollapseAllEntries",
"right": "project_panel::ExpandSelectedEntry",
"cmd-right": "project_panel::ExpandAllEntries",
"cmd-n": "project_panel::NewFile",
"cmd-d": "project_panel::Duplicate",
"alt-cmd-n": "project_panel::NewDirectory",
+1
View File
@@ -956,6 +956,7 @@
"left": "project_panel::CollapseSelectedEntry",
"ctrl-left": "project_panel::CollapseAllEntries",
"right": "project_panel::ExpandSelectedEntry",
"ctrl-right": "project_panel::ExpandAllEntries",
"ctrl-n": "project_panel::NewFile",
"alt-n": "project_panel::NewDirectory",
"ctrl-x": "project_panel::Cut",
+164 -96
View File
@@ -61,10 +61,9 @@ use std::{
};
use theme_settings::ThemeSettings;
use ui::{
Color, ContextMenu, ContextMenuEntry, DecoratedIcon, Icon, IconDecoration, IconDecorationKind,
IndentGuideColors, IndentGuideLayout, Indicator, KeyBinding, Label, LabelSize, ListItem,
ListItemSpacing, ProjectEmptyState, ScrollAxes, ScrollableHandle, Scrollbars, StickyCandidate,
Tooltip, WithScrollbar, prelude::*, v_flex,
ContextMenu, DecoratedIcon, IconDecoration, IconDecorationKind, IndentGuideColors,
IndentGuideLayout, Indicator, KeyBinding, ListItem, ListItemSpacing, ProjectEmptyState,
ScrollAxes, ScrollableHandle, Scrollbars, StickyCandidate, Tooltip, WithScrollbar, prelude::*,
};
use util::{
ResultExt, TakeUntilExt, TryFutureExt,
@@ -340,8 +339,12 @@ actions!(
CollapseSelectedEntry,
/// Collapses the selected entry and its children in the project tree.
CollapseSelectedEntryAndChildren,
/// Expands the selected entry and its children in the project tree.
ExpandSelectedEntryAndChildren,
/// Collapses all entries in the project tree.
CollapseAllEntries,
/// Expands all entries in the project tree.
ExpandAllEntries,
/// Creates a new directory.
NewDirectory,
/// Creates a new file.
@@ -502,6 +505,14 @@ pub fn init(cx: &mut App) {
}
});
workspace.register_action(|workspace, action: &ExpandAllEntries, window, cx| {
if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
panel.update(cx, |panel, cx| {
panel.expand_all_entries(action, window, cx);
});
}
});
workspace.register_action(|workspace, action: &Rename, window, cx| {
workspace.open_panel::<ProjectPanel>(window, cx);
if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
@@ -720,40 +731,12 @@ impl ProjectPanel {
cx.notify();
}
project::Event::ExpandedAllForEntry(worktree_id, entry_id) => {
if let Some((worktree, expanded_dir_ids)) = project
.read(cx)
.worktree_for_id(*worktree_id, cx)
.zip(this.state.expanded_dir_ids.get_mut(worktree_id))
{
let worktree = worktree.read(cx);
let Some(entry) = worktree.entry_for_id(*entry_id) else {
return;
};
let include_ignored_dirs = !entry.is_ignored;
let mut dirs_to_expand = vec![*entry_id];
while let Some(current_id) = dirs_to_expand.pop() {
let Some(current_entry) = worktree.entry_for_id(current_id) else {
continue;
};
for child in worktree.child_entries(&current_entry.path) {
if !child.is_dir() || (include_ignored_dirs && child.is_ignored)
{
continue;
}
dirs_to_expand.push(child.id);
if let Err(ix) = expanded_dir_ids.binary_search(&child.id) {
expanded_dir_ids.insert(ix, child.id);
}
this.state.unfolded_dir_ids.insert(child.id);
}
}
this.update_visible_entries(None, false, false, window, cx);
cx.notify();
}
this.synchronously_expand_all_directories(
*worktree_id,
*entry_id,
window,
cx,
);
}
_ => {}
},
@@ -1120,7 +1103,6 @@ impl ProjectPanel {
};
let has_pasteable_content = self.has_pasteable_content(cx);
let entity = cx.entity();
let context_menu = ContextMenu::build(window, cx, |menu, _, cx| {
menu.context(self.focus_handle.clone()).map(|menu| {
if is_read_only {
@@ -1223,22 +1205,17 @@ impl ProjectPanel {
.action("Remove from Project", Box::new(RemoveFromProject))
})
.when(is_dir && !is_root, |menu| {
menu.separator().action(
"Collapse All",
Box::new(CollapseSelectedEntryAndChildren),
)
menu.separator()
.action("Expand All", Box::new(ExpandSelectedEntryAndChildren))
.action(
"Collapse All",
Box::new(CollapseSelectedEntryAndChildren),
)
})
.when(is_dir && is_root, |menu| {
let entity = entity.clone();
menu.separator().item(
ContextMenuEntry::new("Collapse All").handler(
move |window, cx| {
entity.update(cx, |this, cx| {
this.collapse_all_for_root(window, cx);
});
},
),
)
menu.separator()
.action("Expand All", Box::new(ExpandAllEntries))
.action("Collapse All", Box::new(CollapseAllEntries))
})
}
})
@@ -1421,31 +1398,59 @@ impl ProjectPanel {
}
}
/// Handles "Collapse All" from the context menu when a root directory is selected.
/// With a single visible worktree, keeps the root expanded (matching CollapseAllEntries behavior).
/// With multiple visible worktrees, collapses the root and all its children.
fn collapse_all_for_root(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some((worktree, entry)) = self.selected_entry(cx) else {
return;
};
let is_root = worktree.root_entry().map(|e| e.id) == Some(entry.id);
if !is_root {
return;
}
let worktree_id = worktree.id();
let root_id = entry.id;
fn collapse_worktree_expanded_dirs(
&mut self,
worktree_id: WorktreeId,
root_id: ProjectEntryId,
cx: &App,
) {
let single_worktree = self.project.read(cx).visible_worktrees(cx).count() == 1;
if let Some(expanded_dir_ids) = self.state.expanded_dir_ids.get_mut(&worktree_id) {
if self.project.read(cx).visible_worktrees(cx).count() == 1 {
if single_worktree {
expanded_dir_ids.retain(|id| id == &root_id);
} else {
expanded_dir_ids.clear();
}
}
}
self.update_visible_entries(Some((worktree_id, root_id)), false, false, window, cx);
fn all_worktree_roots(&self, cx: &App) -> Vec<(WorktreeId, ProjectEntryId)> {
self.project
.read(cx)
.visible_worktrees(cx)
.filter_map(|worktree| {
let worktree = worktree.read(cx);
Some((worktree.id(), worktree.root_entry()?.id))
})
.collect()
}
fn expand_worktree_roots(
&mut self,
roots: Vec<(WorktreeId, ProjectEntryId)>,
window: &mut Window,
cx: &mut Context<Self>,
) {
for (worktree_id, root_id) in roots {
self.expand_all_for_entry(worktree_id, root_id, cx);
self.synchronously_expand_all_directories_internal(worktree_id, root_id, cx);
}
self.update_visible_entries(None, false, false, window, cx);
cx.notify();
}
fn collapse_worktree_roots(
&mut self,
roots: Vec<(WorktreeId, ProjectEntryId)>,
window: &mut Window,
cx: &mut Context<Self>,
) {
for (worktree_id, root_id) in roots {
self.collapse_worktree_expanded_dirs(worktree_id, root_id, cx);
}
self.update_visible_entries(None, false, false, window, cx);
cx.notify();
}
@@ -1455,39 +1460,100 @@ impl ProjectPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
// 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).visible_worktrees(cx).count() > 1;
let project = self.project.read(cx);
let roots = self.all_worktree_roots(cx);
self.collapse_worktree_roots(roots, window, cx);
}
self.state
.expanded_dir_ids
.iter_mut()
.for_each(|(worktree_id, expanded_entries)| {
if multiple_worktrees {
*expanded_entries = Default::default();
return;
}
fn expand_all_entries(
&mut self,
_: &ExpandAllEntries,
window: &mut Window,
cx: &mut Context<Self>,
) {
let roots = self.all_worktree_roots(cx);
self.expand_worktree_roots(roots, window, cx);
}
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)
});
fn expand_all_for_entry_and_refresh(
&mut self,
worktree_id: WorktreeId,
entry_id: ProjectEntryId,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.expand_all_for_entry(worktree_id, entry_id, cx);
self.synchronously_expand_all_directories(worktree_id, entry_id, window, cx);
}
match root_entry_id {
Some(id) => {
expanded_entries.retain(|entry_id| entry_id == &id);
}
None => *expanded_entries = Default::default(),
};
});
fn expand_selected_entry_and_children(
&mut self,
_: &ExpandSelectedEntryAndChildren,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some((worktree, entry)) = self.selected_entry(cx) {
let worktree_id = worktree.id();
let entry_id = entry.id;
self.expand_all_for_entry_and_refresh(worktree_id, entry_id, window, cx);
}
}
fn synchronously_expand_all_directories(
&mut self,
worktree_id: WorktreeId,
entry_id: ProjectEntryId,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.synchronously_expand_all_directories_internal(worktree_id, entry_id, cx);
self.update_visible_entries(None, false, false, window, cx);
cx.notify();
}
fn synchronously_expand_all_directories_internal(
&mut self,
worktree_id: WorktreeId,
entry_id: ProjectEntryId,
cx: &mut Context<Self>,
) {
let project = self.project.read(cx);
let Some((worktree, expanded_dir_ids)) = project
.worktree_for_id(worktree_id, cx)
.zip(self.state.expanded_dir_ids.get_mut(&worktree_id))
else {
return;
};
let worktree = worktree.read(cx);
let Some(entry) = worktree.entry_for_id(entry_id) else {
return;
};
let include_ignored_dirs = !entry.is_ignored;
if let Err(ix) = expanded_dir_ids.binary_search(&entry_id) {
expanded_dir_ids.insert(ix, entry_id);
}
let mut dirs_to_expand = vec![entry_id];
while let Some(current_id) = dirs_to_expand.pop() {
let Some(current_entry) = worktree.entry_for_id(current_id) else {
continue;
};
for child in worktree.child_entries(&current_entry.path) {
if !child.is_dir() || (include_ignored_dirs && child.is_ignored) {
continue;
}
dirs_to_expand.push(child.id);
if let Err(ix) = expanded_dir_ids.binary_search(&child.id) {
expanded_dir_ids.insert(ix, child.id);
}
self.state.unfolded_dir_ids.insert(child.id);
}
}
}
fn toggle_expanded(
&mut self,
entry_id: ProjectEntryId,
@@ -6797,7 +6863,9 @@ impl Render for ProjectPanel {
.on_action(cx.listener(Self::expand_selected_entry))
.on_action(cx.listener(Self::collapse_selected_entry))
.on_action(cx.listener(Self::collapse_all_entries))
.on_action(cx.listener(Self::expand_all_entries))
.on_action(cx.listener(Self::collapse_selected_entry_and_children))
.on_action(cx.listener(Self::expand_selected_entry_and_children))
.on_action(cx.listener(Self::open))
.on_action(cx.listener(Self::open_permanent))
.on_action(cx.listener(Self::open_split_vertical))
+252 -141
View File
@@ -8163,115 +8163,99 @@ async fn test_collapse_non_root_multi_worktree(cx: &mut gpui::TestAppContext) {
}
#[gpui::test]
async fn test_collapse_all_for_root_single_worktree(cx: &mut gpui::TestAppContext) {
init_test(cx);
async fn test_expand_all_entries(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root"),
"/project_root",
json!({
"dir1": {
"subdir1": {
"file1.txt": ""
"dir_1": {
"nested_dir": {
"file_a.py": "# File contents",
"file_b.py": "# File contents",
"file_c.py": "# File contents",
},
"file2.txt": ""
"file_1.py": "# File contents",
"file_2.py": "# File contents",
"file_3.py": "# File contents",
},
"dir2": {
"file3.txt": ""
"dir_2": {
"file_1.py": "# File contents",
"file_2.py": "# File contents",
"file_3.py": "# File contents",
}
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let panel = workspace.update_in(cx, ProjectPanel::new);
cx.run_until_parked();
toggle_expand_dir(&panel, "root/dir1", cx);
toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
toggle_expand_dir(&panel, "root/dir2", cx);
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..20, cx),
&[
"v root",
" v dir1",
" v subdir1",
" file1.txt",
" file2.txt",
" v dir2 <== selected",
" file3.txt",
],
"Initial state with directories expanded"
visible_entries_as_strings(&panel, 0..10, cx),
&["v project_root", " > dir_1", " > dir_2",]
);
select_path(&panel, "root", cx);
cx.run_until_parked();
panel.update_in(cx, |panel, window, cx| {
panel.collapse_all_for_root(window, cx);
panel.expand_all_entries(&ExpandAllEntries, window, cx)
});
cx.run_until_parked();
cx.executor().run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..20, cx),
&["v root <== selected", " > dir1", " > dir2"],
"Root should remain expanded but all children should be collapsed"
let entries = visible_entries_as_strings(&panel, 0..20, cx);
assert_eq!(entries.len(), 13, "should show all 13 entries");
assert!(entries[0].starts_with("v project_root"), "root expanded");
assert!(entries[1].contains("v dir_1"), "dir_1 expanded");
assert!(entries[2].contains("v nested_dir"), "nested_dir expanded");
assert!(
entries.iter().any(|e| e.contains("file_a.py")),
"file_a visible"
);
toggle_expand_dir(&panel, "root/dir1", cx);
cx.run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..20, cx),
&[
"v root",
" v dir1 <== selected",
" > subdir1",
" file2.txt",
" > dir2",
],
"After re-expanding dir1, subdir1 should still be collapsed"
assert!(
entries.iter().any(|e| e.contains("file_c.py")),
"file_c visible"
);
assert!(
entries.iter().any(|e| e.contains("v dir_2")),
"dir_2 expanded"
);
assert!(
!entries.iter().any(|e| e.contains("> ")),
"no collapsed dirs"
);
}
#[gpui::test]
async fn test_collapse_all_for_root_multi_worktree(cx: &mut gpui::TestAppContext) {
init_test(cx);
async fn test_expand_all_entries_multiple_worktrees(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root1"),
json!({
"dir1": {
"subdir1": {
"file1.txt": ""
},
"file2.txt": ""
}
}),
)
.await;
fs.insert_tree(
path!("/root2"),
json!({
"dir2": {
"file3.txt": ""
},
"file4.txt": ""
}),
)
.await;
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(),
[path!("/root1").as_ref(), path!("/root2").as_ref()],
["/project_root_1".as_ref(), "/project_root_2".as_ref()],
cx,
)
.await;
@@ -8280,66 +8264,197 @@ async fn test_collapse_all_for_root_multi_worktree(cx: &mut gpui::TestAppContext
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let panel = workspace.update_in(cx, ProjectPanel::new);
cx.run_until_parked();
toggle_expand_dir(&panel, "root1/dir1", cx);
toggle_expand_dir(&panel, "root1/dir1/subdir1", cx);
toggle_expand_dir(&panel, "root2/dir2", cx);
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..20, cx),
&[
"v root1",
" v dir1",
" v subdir1",
" file1.txt",
" file2.txt",
"v root2",
" v dir2 <== selected",
" file3.txt",
" file4.txt",
],
"Initial state with directories expanded across worktrees"
visible_entries_as_strings(&panel, 0..10, cx),
&["> project_root_1", "> project_root_2",]
);
select_path(&panel, "root1", cx);
cx.run_until_parked();
panel.update_in(cx, |panel, window, cx| {
panel.collapse_all_for_root(window, cx);
panel.expand_all_entries(&ExpandAllEntries, window, cx)
});
cx.run_until_parked();
cx.executor().run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..20, cx),
&[
"> root1 <== selected",
"v root2",
" v dir2",
" file3.txt",
" file4.txt",
],
"With multiple worktrees, root1 should collapse completely (including itself)"
"v project_root_1",
" v dir_1",
" file_1.py",
" v dir_2",
" file_1.py",
"v project_root_2",
" v dir_1",
" file_1.py",
" v dir_2",
" file_1.py",
]
);
}
#[gpui::test]
async fn test_collapse_all_for_root_noop_on_non_root(cx: &mut gpui::TestAppContext) {
init_test(cx);
async fn test_expand_all_entries_via_window_dispatch(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 window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
cx.update(|_, cx| {
let settings = *ProjectPanelSettings::get_global(cx);
ProjectPanelSettings::override_global(
ProjectPanelSettings {
auto_reveal_entries: false,
..settings
},
cx,
);
});
let panel = workspace.update_in(cx, |workspace, window, cx| {
let panel = ProjectPanel::new(workspace, window, cx);
workspace.add_panel(panel.clone(), window, cx);
panel
});
cx.run_until_parked();
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",]
);
panel.update_in(cx, |panel, window, cx| {
panel.focus_handle(cx).focus(window, cx);
});
cx.dispatch_action(ExpandAllEntries);
cx.executor().run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..20, cx),
&[
"v project_root_1",
" v dir_1",
" file_1.py",
" v dir_2",
" file_1.py",
"v project_root_2",
" v dir_1",
" file_1.py",
" v dir_2",
" file_1.py",
]
);
}
#[gpui::test]
async fn test_expand_all_for_entry_single_worktree(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 window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = window
.read_with(cx, |mw, _| mw.workspace().clone())
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
let panel = workspace.update_in(cx, ProjectPanel::new);
cx.run_until_parked();
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",]
);
let root2_entry = find_project_entry(&panel, "project_root_2", cx).unwrap();
panel.update_in(cx, |panel, window, cx| {
let worktree_id = panel
.project
.read(cx)
.worktree_id_for_entry(root2_entry, cx)
.unwrap();
panel.expand_all_for_entry(worktree_id, root2_entry, cx);
panel.update_visible_entries(None, false, false, window, cx);
});
cx.executor().run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..20, cx),
&[
"> project_root_1",
"v project_root_2",
" v dir_1",
" file_1.py",
" v dir_2",
" file_1.py",
]
);
}
#[gpui::test]
async fn test_expand_all_entries_with_auto_fold(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/root"),
json!({
"dir1": {
"subdir1": {
"file1.txt": ""
"empty1": {
"empty2": {
"empty3": {
"file.txt": ""
}
}
},
},
"dir2": {
"file2.txt": ""
}
}),
)
@@ -8352,45 +8467,41 @@ async fn test_collapse_all_for_root_noop_on_non_root(cx: &mut gpui::TestAppConte
.unwrap();
let cx = &mut VisualTestContext::from_window(window.into(), cx);
cx.update(|_, cx| {
let settings = *ProjectPanelSettings::get_global(cx);
ProjectPanelSettings::override_global(
ProjectPanelSettings {
auto_fold_dirs: true,
..settings
},
cx,
);
});
let panel = workspace.update_in(cx, ProjectPanel::new);
cx.run_until_parked();
toggle_expand_dir(&panel, "root/dir1", cx);
toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
toggle_expand_dir(&panel, "root/dir2", cx);
panel.update_in(cx, |panel, window, cx| {
panel.collapse_all_entries(&CollapseAllEntries, window, cx)
});
cx.executor().run_until_parked();
panel.update_in(cx, |panel, window, cx| {
panel.expand_all_entries(&ExpandAllEntries, window, cx)
});
cx.executor().run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..20, cx),
&[
"v root",
" v dir1",
" v subdir1",
" file1.txt",
" v dir2 <== selected",
" file2.txt",
" v empty1",
" v empty2",
" v empty3",
" file.txt",
],
"Initial state with directories expanded"
);
select_path(&panel, "root/dir1", cx);
cx.run_until_parked();
panel.update_in(cx, |panel, window, cx| {
panel.collapse_all_for_root(window, cx);
});
cx.run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..20, cx),
&[
"v root",
" v dir1 <== selected",
" v subdir1",
" file1.txt",
" v dir2",
" file2.txt",
],
"collapse_all_for_root should be a no-op when called on a non-root directory"
"expand all should unfold auto-folded directories"
);
}
+2 -1
View File
@@ -17,7 +17,8 @@ status bar.
Use the arrow keys to move through entries. {#kb
project_panel::ExpandSelectedEntry} expands a directory and {#kb
project_panel::CollapseSelectedEntry} collapses it. {#kb
project_panel::CollapseAllEntries} collapses every directory at once. Press {#kb
project_panel::CollapseAllEntries} collapses every directory at once. {#kb
project_panel::ExpandAllEntries} expands every directory at once. Press {#kb
project_panel::Open} or click to preview a selected file, without giving it a
permanent tab. Editing the file or double-clicking it promotes it to a permanent tab.