From 387281fa5b4e6b0f3cb629d1f889c4eae9d1b0ab Mon Sep 17 00:00:00 2001
From: Angelk90 <20476002+Angelk90@users.noreply.github.com>
Date: Mon, 9 Jun 2025 13:16:31 +0200
Subject: [PATCH] project_panel: Add `hide_root` when only one folder in the
project (#25289)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Closes #24188
Todo:
- [x] Hide root when only one worktree
- [x] Basic tests
- [x] Docs
- [x] Fix `select_first` + tests
- [x] Fix auto collapse dir + tests
- [x] Fix file / dir creation + tests
- [x] Fix root rename case
| Show root | Hide root |
|--------|--------|
|
|
|
|
|
|
Release Notes:
- Added support to hide the root entry of the Project Panel when there’s
only one folder in the project. This can be enabled by setting
`hide_root` to `true` in the `project_panel` config.
---------
Co-authored-by: Smit Barmase
---
assets/settings/default.json | 4 +-
crates/project_panel/src/project_panel.rs | 104 ++--
.../src/project_panel_settings.rs | 5 +
.../project_panel/src/project_panel_tests.rs | 524 ++++++++++++++++++
docs/src/configuring-zed.md | 3 +-
5 files changed, 605 insertions(+), 35 deletions(-)
diff --git a/assets/settings/default.json b/assets/settings/default.json
index a486b2a50d..4f04a7abdf 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -604,7 +604,9 @@
// 2. Never show indent guides:
// "never"
"show": "always"
- }
+ },
+ // Whether to hide the root entry when only one folder is open in the window.
+ "hide_root": false
},
"outline_panel": {
// Whether to show the outline panel button in the status bar
diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs
index ed27b11e8a..7effb96ac0 100644
--- a/crates/project_panel/src/project_panel.rs
+++ b/crates/project_panel/src/project_panel.rs
@@ -464,6 +464,9 @@ impl ProjectPanel {
if project_panel_settings.hide_gitignore != new_settings.hide_gitignore {
this.update_visible_entries(None, cx);
}
+ if project_panel_settings.hide_root != new_settings.hide_root {
+ this.update_visible_entries(None, cx);
+ }
project_panel_settings = new_settings;
this.update_diagnostics(cx);
cx.notify();
@@ -768,6 +771,12 @@ impl ProjectPanel {
let is_remote = project.is_via_collab();
let is_local = project.is_local();
+ let settings = ProjectPanelSettings::get_global(cx);
+ let visible_worktrees_count = project.visible_worktrees(cx).count();
+ let should_hide_rename = is_root
+ && (cfg!(target_os = "windows")
+ || (settings.hide_root && visible_worktrees_count == 1));
+
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
menu.context(self.focus_handle.clone()).map(|menu| {
if is_read_only {
@@ -817,7 +826,7 @@ impl ProjectPanel {
Box::new(zed_actions::workspace::CopyRelativePath),
)
.separator()
- .when(!is_root || !cfg!(target_os = "windows"), |menu| {
+ .when(!should_hide_rename, |menu| {
menu.action("Rename", Box::new(Rename))
})
.when(!is_root & !is_remote, |menu| {
@@ -1538,6 +1547,16 @@ impl ProjectPanel {
if Some(entry) == worktree.read(cx).root_entry() {
return;
}
+
+ if Some(entry) == worktree.read(cx).root_entry() {
+ let settings = ProjectPanelSettings::get_global(cx);
+ let visible_worktrees_count =
+ self.project.read(cx).visible_worktrees(cx).count();
+ if settings.hide_root && visible_worktrees_count == 1 {
+ return;
+ }
+ }
+
self.edit_state = Some(EditState {
worktree_id,
entry_id: sub_entry_id,
@@ -2106,19 +2125,11 @@ impl ProjectPanel {
}
fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context) {
- let worktree = self
- .visible_entries
- .first()
- .and_then(|(worktree_id, _, _)| {
- self.project.read(cx).worktree_for_id(*worktree_id, cx)
- });
- if let Some(worktree) = worktree {
- let worktree = worktree.read(cx);
- let worktree_id = worktree.id();
- if let Some(root_entry) = worktree.root_entry() {
+ if let Some((worktree_id, visible_worktree_entries, _)) = self.visible_entries.first() {
+ if let Some(entry) = visible_worktree_entries.first() {
let selection = SelectedEntry {
- worktree_id,
- entry_id: root_entry.id,
+ worktree_id: *worktree_id,
+ entry_id: entry.id,
};
self.selection = Some(selection);
if window.modifiers().shift {
@@ -2771,6 +2782,31 @@ impl ProjectPanel {
Some(())
}
+ fn create_new_git_entry(
+ parent_entry: &Entry,
+ git_summary: GitSummary,
+ new_entry_kind: EntryKind,
+ ) -> GitEntry {
+ GitEntry {
+ entry: Entry {
+ id: NEW_ENTRY_ID,
+ kind: new_entry_kind,
+ path: parent_entry.path.join("\0").into(),
+ inode: 0,
+ mtime: parent_entry.mtime,
+ size: parent_entry.size,
+ is_ignored: parent_entry.is_ignored,
+ is_external: false,
+ is_private: false,
+ is_always_included: parent_entry.is_always_included,
+ canonical_path: parent_entry.canonical_path.clone(),
+ char_bag: parent_entry.char_bag,
+ is_fifo: parent_entry.is_fifo,
+ },
+ git_summary,
+ }
+ }
+
fn update_visible_entries(
&mut self,
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
@@ -2790,7 +2826,10 @@ impl ProjectPanel {
let old_ancestors = std::mem::take(&mut self.ancestors);
self.visible_entries.clear();
let mut max_width_item = None;
- for worktree in project.visible_worktrees(cx) {
+
+ let visible_worktrees: Vec<_> = project.visible_worktrees(cx).collect();
+ let hide_root = settings.hide_root && visible_worktrees.len() == 1;
+ for worktree in visible_worktrees {
let worktree_snapshot = worktree.read(cx).snapshot();
let worktree_id = worktree_snapshot.id();
@@ -2825,6 +2864,18 @@ impl ProjectPanel {
GitTraversal::new(&repo_snapshots, worktree_snapshot.entries(true, 0));
let mut auto_folded_ancestors = vec![];
while let Some(entry) = entry_iter.entry() {
+ if hide_root && Some(entry.entry) == worktree.read(cx).root_entry() {
+ if new_entry_parent_id == Some(entry.id) {
+ visible_worktree_entries.push(Self::create_new_git_entry(
+ &entry.entry,
+ entry.git_summary,
+ new_entry_kind,
+ ));
+ new_entry_parent_id = None;
+ }
+ entry_iter.advance();
+ continue;
+ }
if auto_collapse_dirs && entry.kind.is_dir() {
auto_folded_ancestors.push(entry.id);
if !self.unfolded_dir_ids.contains(&entry.id) {
@@ -2878,24 +2929,11 @@ impl ProjectPanel {
false
};
if precedes_new_entry && (!hide_gitignore || !entry.is_ignored) {
- visible_worktree_entries.push(GitEntry {
- entry: Entry {
- id: NEW_ENTRY_ID,
- kind: new_entry_kind,
- path: entry.path.join("\0").into(),
- inode: 0,
- mtime: entry.mtime,
- size: entry.size,
- is_ignored: entry.is_ignored,
- is_external: false,
- is_private: false,
- is_always_included: entry.is_always_included,
- canonical_path: entry.canonical_path.clone(),
- char_bag: entry.char_bag,
- is_fifo: entry.is_fifo,
- },
- git_summary: entry.git_summary,
- });
+ visible_worktree_entries.push(Self::create_new_git_entry(
+ &entry.entry,
+ entry.git_summary,
+ new_entry_kind,
+ ));
}
let worktree_abs_path = worktree.read(cx).abs_path();
let (depth, path) = if Some(entry.entry) == worktree.read(cx).root_entry() {
@@ -3729,7 +3767,7 @@ impl ProjectPanel {
None
}
})
- .unwrap_or((0, 0));
+ .unwrap_or_else(|| (0, entry.path.components().count()));
(depth, difference)
}
diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs
index 54b4a4840a..31f4a21b09 100644
--- a/crates/project_panel/src/project_panel_settings.rs
+++ b/crates/project_panel/src/project_panel_settings.rs
@@ -44,6 +44,7 @@ pub struct ProjectPanelSettings {
pub auto_fold_dirs: bool,
pub scrollbar: ScrollbarSettings,
pub show_diagnostics: ShowDiagnostics,
+ pub hide_root: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -145,6 +146,10 @@ pub struct ProjectPanelSettingsContent {
pub show_diagnostics: Option,
/// Settings related to indent guides in the project panel.
pub indent_guides: Option,
+ /// Whether to hide the root entry when only one folder is open in the window.
+ ///
+ /// Default: false
+ pub hide_root: Option,
}
impl Settings for ProjectPanelSettings {
diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs
index 9a1eda72d9..9604755d1e 100644
--- a/crates/project_panel/src/project_panel_tests.rs
+++ b/crates/project_panel/src/project_panel_tests.rs
@@ -309,6 +309,7 @@ async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) {
)
.await;
+ // Test 1: Multiple worktrees with auto_fold_dirs = true
let project = Project::test(
fs.clone(),
[path!("/root1").as_ref(), path!("/root2").as_ref()],
@@ -392,6 +393,66 @@ async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) {
separator!(" file_1.java"),
]
);
+
+ // Test 2: Single worktree with auto_fold_dirs = true and hide_root = true
+ {
+ let project = Project::test(fs.clone(), [path!("/root1").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);
+ cx.update(|_, cx| {
+ let settings = *ProjectPanelSettings::get_global(cx);
+ ProjectPanelSettings::override_global(
+ ProjectPanelSettings {
+ auto_fold_dirs: true,
+ hide_root: true,
+ ..settings
+ },
+ cx,
+ );
+ });
+ let panel = workspace.update(cx, ProjectPanel::new).unwrap();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[separator!("> dir_1/nested_dir_1/nested_dir_2/nested_dir_3")],
+ "Single worktree with hide_root=true should hide root and show auto-folded paths"
+ );
+
+ toggle_expand_dir(
+ &panel,
+ "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3",
+ cx,
+ );
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ separator!("v dir_1/nested_dir_1/nested_dir_2/nested_dir_3 <== selected"),
+ separator!(" > nested_dir_4/nested_dir_5"),
+ separator!(" file_a.java"),
+ separator!(" file_b.java"),
+ separator!(" file_c.java"),
+ ],
+ "Expanded auto-folded path with hidden root should show contents without root prefix"
+ );
+
+ toggle_expand_dir(
+ &panel,
+ "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5",
+ cx,
+ );
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ separator!("v dir_1/nested_dir_1/nested_dir_2/nested_dir_3"),
+ separator!(" v nested_dir_4/nested_dir_5 <== selected"),
+ separator!(" file_d.java"),
+ separator!(" file_a.java"),
+ separator!(" file_b.java"),
+ separator!(" file_c.java"),
+ ],
+ "Nested expansion with hidden root should maintain proper indentation"
+ );
+ }
}
#[gpui::test(iterations = 30)]
@@ -2475,6 +2536,7 @@ async fn test_select_directory(cx: &mut gpui::TestAppContext) {
]
);
}
+
#[gpui::test]
async fn test_select_first_last(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);
@@ -2543,6 +2605,46 @@ async fn test_select_first_last(cx: &mut gpui::TestAppContext) {
" file_2.py <== selected",
]
);
+
+ cx.update(|_, cx| {
+ let settings = *ProjectPanelSettings::get_global(cx);
+ ProjectPanelSettings::override_global(
+ ProjectPanelSettings {
+ hide_root: true,
+ ..settings
+ },
+ cx,
+ );
+ });
+
+ let panel = workspace.update(cx, ProjectPanel::new).unwrap();
+
+ #[rustfmt::skip]
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "> dir_1",
+ "> zdir_2",
+ " file_1.py",
+ " file_2.py",
+ ],
+ "With hide_root=true, root should be hidden"
+ );
+
+ panel.update_in(cx, |panel, window, cx| {
+ panel.select_first(&SelectFirst, window, cx)
+ });
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "> dir_1 <== selected",
+ "> zdir_2",
+ " file_1.py",
+ " file_2.py",
+ ],
+ "With hide_root=true, first entry should be dir_1, not the hidden root"
+ );
}
#[gpui::test]
@@ -2789,6 +2891,101 @@ async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
);
}
+#[gpui::test]
+async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) {
+ init_test_with_editor(cx);
+
+ let fs = FakeFs::new(cx.executor().clone());
+ fs.insert_tree(
+ "/root1",
+ json!({
+ "dir1": { "file1.txt": "content" },
+ "file2.txt": "content",
+ }),
+ )
+ .await;
+ fs.insert_tree("/root2", json!({ "file3.txt": "content" }))
+ .await;
+
+ // Test 1: Single worktree, hide_root=true - rename should be blocked
+ {
+ let project = Project::test(fs.clone(), ["/root1".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);
+
+ cx.update(|_, cx| {
+ let settings = *ProjectPanelSettings::get_global(cx);
+ ProjectPanelSettings::override_global(
+ ProjectPanelSettings {
+ hide_root: true,
+ ..settings
+ },
+ cx,
+ );
+ });
+
+ let panel = workspace.update(cx, ProjectPanel::new).unwrap();
+
+ panel.update(cx, |panel, cx| {
+ let project = panel.project.read(cx);
+ let worktree = project.visible_worktrees(cx).next().unwrap();
+ let root_entry = worktree.read(cx).root_entry().unwrap();
+ panel.selection = Some(SelectedEntry {
+ worktree_id: worktree.read(cx).id(),
+ entry_id: root_entry.id,
+ });
+ });
+
+ panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
+
+ assert!(
+ panel.read_with(cx, |panel, _| panel.edit_state.is_none()),
+ "Rename should be blocked when hide_root=true with single worktree"
+ );
+ }
+
+ // Test 2: Multiple worktrees, hide_root=true - rename should work
+ {
+ let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".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);
+
+ cx.update(|_, cx| {
+ let settings = *ProjectPanelSettings::get_global(cx);
+ ProjectPanelSettings::override_global(
+ ProjectPanelSettings {
+ hide_root: true,
+ ..settings
+ },
+ cx,
+ );
+ });
+
+ let panel = workspace.update(cx, ProjectPanel::new).unwrap();
+ select_path(&panel, "root1", cx);
+ panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
+
+ #[cfg(target_os = "windows")]
+ assert!(
+ panel.read_with(cx, |panel, _| panel.edit_state.is_none()),
+ "Rename should be blocked on Windows even with multiple worktrees"
+ );
+
+ #[cfg(not(target_os = "windows"))]
+ {
+ assert!(
+ panel.read_with(cx, |panel, _| panel.edit_state.is_some()),
+ "Rename should work with multiple worktrees on non-Windows when hide_root=true"
+ );
+ panel.update_in(cx, |panel, window, cx| {
+ panel.cancel(&menu::Cancel, window, cx)
+ });
+ }
+ }
+}
+
#[gpui::test]
async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);
@@ -5098,6 +5295,155 @@ async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) {
);
}
+#[gpui::test]
+async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor().clone());
+ fs.insert_tree(
+ path!("/root"),
+ json!({
+ "existing_dir": {
+ "existing_file.txt": "",
+ },
+ "existing_file.txt": "",
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), [path!("/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);
+
+ cx.update(|_, cx| {
+ let settings = *ProjectPanelSettings::get_global(cx);
+ ProjectPanelSettings::override_global(
+ ProjectPanelSettings {
+ hide_root: true,
+ ..settings
+ },
+ cx,
+ );
+ });
+
+ let panel = workspace
+ .update(cx, |workspace, window, cx| {
+ let panel = ProjectPanel::new(workspace, window, cx);
+ workspace.add_panel(panel.clone(), window, cx);
+ panel
+ })
+ .unwrap();
+
+ #[rustfmt::skip]
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..20, cx),
+ &[
+ "> existing_dir",
+ " existing_file.txt",
+ ],
+ "Initial state with hide_root=true, root should be hidden and nothing selected"
+ );
+
+ panel.update(cx, |panel, _| {
+ assert!(
+ panel.selection.is_none(),
+ "Should have no selection initially"
+ );
+ });
+
+ // Test 1: Create new file when no entry is selected
+ panel.update_in(cx, |panel, window, cx| {
+ panel.new_file(&NewFile, window, cx);
+ });
+ panel.update_in(cx, |panel, window, cx| {
+ assert!(panel.filename_editor.read(cx).is_focused(window));
+ });
+
+ #[rustfmt::skip]
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..20, cx),
+ &[
+ "> existing_dir",
+ " [EDITOR: ''] <== selected",
+ " existing_file.txt",
+ ],
+ "Editor should appear at root level when hide_root=true and no selection"
+ );
+
+ let confirm = panel.update_in(cx, |panel, window, cx| {
+ panel.filename_editor.update(cx, |editor, cx| {
+ editor.set_text("new_file_at_root.txt", window, cx)
+ });
+ panel.confirm_edit(window, cx).unwrap()
+ });
+ confirm.await.unwrap();
+
+ #[rustfmt::skip]
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..20, cx),
+ &[
+ "> existing_dir",
+ " existing_file.txt",
+ " new_file_at_root.txt <== selected <== marked",
+ ],
+ "New file should be created at root level and visible without root prefix"
+ );
+
+ assert!(
+ fs.is_file(Path::new("/root/new_file_at_root.txt")).await,
+ "File should be created in the actual root directory"
+ );
+
+ // Test 2: Create new directory when no entry is selected
+ panel.update(cx, |panel, _| {
+ panel.selection = None;
+ });
+
+ panel.update_in(cx, |panel, window, cx| {
+ panel.new_directory(&NewDirectory, window, cx);
+ });
+ panel.update_in(cx, |panel, window, cx| {
+ assert!(panel.filename_editor.read(cx).is_focused(window));
+ });
+
+ #[rustfmt::skip]
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..20, cx),
+ &[
+ "> [EDITOR: ''] <== selected",
+ "> existing_dir",
+ " existing_file.txt",
+ " new_file_at_root.txt",
+ ],
+ "Directory editor should appear at root level when hide_root=true and no selection"
+ );
+
+ let confirm = panel.update_in(cx, |panel, window, cx| {
+ panel.filename_editor.update(cx, |editor, cx| {
+ editor.set_text("new_dir_at_root", window, cx)
+ });
+ panel.confirm_edit(window, cx).unwrap()
+ });
+ confirm.await.unwrap();
+
+ #[rustfmt::skip]
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..20, cx),
+ &[
+ "> existing_dir",
+ "v new_dir_at_root <== selected",
+ " existing_file.txt",
+ " new_file_at_root.txt",
+ ],
+ "New directory should be created at root level and visible without root prefix"
+ );
+
+ assert!(
+ fs.is_dir(Path::new("/root/new_dir_at_root")).await,
+ "Directory should be created in the actual root directory"
+ );
+}
+
#[gpui::test]
async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) {
init_test(cx);
@@ -5297,6 +5643,184 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext)
});
}
+#[gpui::test]
+async fn test_hide_root(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor().clone());
+ fs.insert_tree(
+ "/root1",
+ json!({
+ "dir1": {
+ "file1.txt": "content",
+ "file2.txt": "content",
+ },
+ "dir2": {
+ "file3.txt": "content",
+ },
+ "file4.txt": "content",
+ }),
+ )
+ .await;
+
+ fs.insert_tree(
+ "/root2",
+ json!({
+ "dir3": {
+ "file5.txt": "content",
+ },
+ "file6.txt": "content",
+ }),
+ )
+ .await;
+
+ // Test 1: Single worktree with hide_root = false
+ {
+ let project = Project::test(fs.clone(), ["/root1".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);
+
+ cx.update(|_, cx| {
+ let settings = *ProjectPanelSettings::get_global(cx);
+ ProjectPanelSettings::override_global(
+ ProjectPanelSettings {
+ hide_root: false,
+ ..settings
+ },
+ cx,
+ );
+ });
+
+ let panel = workspace.update(cx, ProjectPanel::new).unwrap();
+
+ #[rustfmt::skip]
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > dir1",
+ " > dir2",
+ " file4.txt",
+ ],
+ "With hide_root=false and single worktree, root should be visible"
+ );
+ }
+
+ // Test 2: Single worktree with hide_root = true
+ {
+ let project = Project::test(fs.clone(), ["/root1".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);
+
+ // Set hide_root to true
+ cx.update(|_, cx| {
+ let settings = *ProjectPanelSettings::get_global(cx);
+ ProjectPanelSettings::override_global(
+ ProjectPanelSettings {
+ hide_root: true,
+ ..settings
+ },
+ cx,
+ );
+ });
+
+ let panel = workspace.update(cx, ProjectPanel::new).unwrap();
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &["> dir1", "> dir2", " file4.txt",],
+ "With hide_root=true and single worktree, root should be hidden"
+ );
+
+ // Test expanding directories still works without root
+ toggle_expand_dir(&panel, "root1/dir1", cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v dir1 <== selected",
+ " file1.txt",
+ " file2.txt",
+ "> dir2",
+ " file4.txt",
+ ],
+ "Should be able to expand directories even when root is hidden"
+ );
+ }
+
+ // Test 3: Multiple worktrees with hide_root = true
+ {
+ let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".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);
+
+ // Set hide_root to true
+ cx.update(|_, cx| {
+ let settings = *ProjectPanelSettings::get_global(cx);
+ ProjectPanelSettings::override_global(
+ ProjectPanelSettings {
+ hide_root: true,
+ ..settings
+ },
+ cx,
+ );
+ });
+
+ let panel = workspace.update(cx, ProjectPanel::new).unwrap();
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > dir1",
+ " > dir2",
+ " file4.txt",
+ "v root2",
+ " > dir3",
+ " file6.txt",
+ ],
+ "With hide_root=true and multiple worktrees, roots should still be visible"
+ );
+ }
+
+ // Test 4: Multiple worktrees with hide_root = false
+ {
+ let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".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);
+
+ cx.update(|_, cx| {
+ let settings = *ProjectPanelSettings::get_global(cx);
+ ProjectPanelSettings::override_global(
+ ProjectPanelSettings {
+ hide_root: false,
+ ..settings
+ },
+ cx,
+ );
+ });
+
+ let panel = workspace.update(cx, ProjectPanel::new).unwrap();
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > dir1",
+ " > dir2",
+ " file4.txt",
+ "v root2",
+ " > dir3",
+ " file6.txt",
+ ],
+ "With hide_root=false and multiple worktrees, roots should be visible"
+ );
+ }
+}
+
fn select_path(panel: &Entity, path: impl AsRef, cx: &mut VisualTestContext) {
let path = path.as_ref();
panel.update(cx, |panel, cx| {
diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md
index e383e31b2d..4587a70ac1 100644
--- a/docs/src/configuring-zed.md
+++ b/docs/src/configuring-zed.md
@@ -3098,7 +3098,8 @@ Run the `theme selector: toggle` action in the command palette to see a current
"show_diagnostics": "all",
"indent_guides": {
"show": "always"
- }
+ },
+ "hide_root": false
}
}
```