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:
1 parent
d412fb10d7
commit
9fd3e0ca3b
6 files changed
+421
-238
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(¤t_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(¤t_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))
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user