Detailed changes
@@ -722,7 +722,9 @@
// Whether to enable drag-and-drop operations in the project panel.
"drag_and_drop": true,
// Whether to hide the root entry when only one folder is open in the window.
- "hide_root": false
+ "hide_root": false,
+ // Whether to hide the hidden entries in the project panel.
+ "hide_hidden": false
},
"outline_panel": {
// Whether to show the outline panel button in the status bar
@@ -97,6 +97,7 @@ CREATE TABLE "worktree_entries" (
"is_external" BOOL NOT NULL,
"is_ignored" BOOL NOT NULL,
"is_deleted" BOOL NOT NULL,
+ "is_hidden" BOOL NOT NULL,
"git_status" INTEGER,
"is_fifo" BOOL NOT NULL,
PRIMARY KEY (project_id, worktree_id, id),
@@ -0,0 +1,2 @@
+ALTER TABLE "worktree_entries"
+ADD "is_hidden" BOOL NOT NULL DEFAULT FALSE;
@@ -282,6 +282,7 @@ impl Database {
git_status: ActiveValue::set(None),
is_external: ActiveValue::set(entry.is_external),
is_deleted: ActiveValue::set(false),
+ is_hidden: ActiveValue::set(entry.is_hidden),
scan_id: ActiveValue::set(update.scan_id as i64),
is_fifo: ActiveValue::set(entry.is_fifo),
}
@@ -300,6 +301,7 @@ impl Database {
worktree_entry::Column::MtimeNanos,
worktree_entry::Column::CanonicalPath,
worktree_entry::Column::IsIgnored,
+ worktree_entry::Column::IsHidden,
worktree_entry::Column::ScanId,
])
.to_owned(),
@@ -905,6 +907,7 @@ impl Database {
canonical_path: db_entry.canonical_path,
is_ignored: db_entry.is_ignored,
is_external: db_entry.is_external,
+ is_hidden: db_entry.is_hidden,
// This is only used in the summarization backlog, so if it's None,
// that just means we won't be able to detect when to resummarize
// based on total number of backlogged bytes - instead, we'd go
@@ -671,6 +671,7 @@ impl Database {
canonical_path: db_entry.canonical_path,
is_ignored: db_entry.is_ignored,
is_external: db_entry.is_external,
+ is_hidden: db_entry.is_hidden,
// This is only used in the summarization backlog, so if it's None,
// that just means we won't be able to detect when to resummarize
// based on total number of backlogged bytes - instead, we'd go
@@ -19,6 +19,7 @@ pub struct Model {
pub is_ignored: bool,
pub is_external: bool,
pub is_deleted: bool,
+ pub is_hidden: bool,
pub scan_id: i64,
pub is_fifo: bool,
pub canonical_path: Option<String>,
@@ -29,6 +29,7 @@ fn load_linux_repo_snapshot() -> Vec<GitEntry> {
is_always_included: false,
is_external: false,
is_private: false,
+ is_hidden: false,
char_bag: Default::default(),
is_fifo: false,
};
@@ -676,6 +676,9 @@ impl ProjectPanel {
if project_panel_settings.hide_root != new_settings.hide_root {
this.update_visible_entries(None, false, false, window, cx);
}
+ if project_panel_settings.hide_hidden != new_settings.hide_hidden {
+ this.update_visible_entries(None, false, false, window, cx);
+ }
if project_panel_settings.sticky_scroll && !new_settings.sticky_scroll {
this.sticky_items_count = 0;
}
@@ -3172,6 +3175,7 @@ impl ProjectPanel {
mtime: parent_entry.mtime,
size: parent_entry.size,
is_ignored: parent_entry.is_ignored,
+ is_hidden: parent_entry.is_hidden,
is_external: false,
is_private: false,
is_always_included: parent_entry.is_always_included,
@@ -3212,6 +3216,7 @@ impl ProjectPanel {
.map(|worktree| worktree.read(cx).snapshot())
.collect();
let hide_root = settings.hide_root && visible_worktrees.len() == 1;
+ let hide_hidden = settings.hide_hidden;
self.update_visible_entries_task = cx.spawn_in(window, async move |this, cx| {
let new_state = cx
.background_spawn(async move {
@@ -3303,7 +3308,9 @@ impl ProjectPanel {
}
}
auto_folded_ancestors.clear();
- if !hide_gitignore || !entry.is_ignored {
+ if (!hide_gitignore || !entry.is_ignored)
+ && (!hide_hidden || !entry.is_hidden)
+ {
visible_worktree_entries.push(entry.to_owned());
}
let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id
@@ -3316,7 +3323,10 @@ impl ProjectPanel {
} else {
false
};
- if precedes_new_entry && (!hide_gitignore || !entry.is_ignored) {
+ if precedes_new_entry
+ && (!hide_gitignore || !entry.is_ignored)
+ && (!hide_hidden || !entry.is_hidden)
+ {
visible_worktree_entries.push(Self::create_new_git_entry(
entry.entry,
entry.git_summary,
@@ -30,6 +30,7 @@ pub struct ProjectPanelSettings {
pub scrollbar: ScrollbarSettings,
pub show_diagnostics: ShowDiagnostics,
pub hide_root: bool,
+ pub hide_hidden: bool,
pub drag_and_drop: bool,
}
@@ -79,6 +80,7 @@ impl Settings for ProjectPanelSettings {
},
show_diagnostics: project_panel.show_diagnostics.unwrap(),
hide_root: project_panel.hide_root.unwrap(),
+ hide_hidden: project_panel.hide_hidden.unwrap(),
drag_and_drop: project_panel.drag_and_drop.unwrap(),
}
}
@@ -6678,6 +6678,142 @@ async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) {
}
}
+#[gpui::test]
+async fn test_hide_hidden_entries(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(
+ "/root",
+ json!({
+ ".hidden-file.txt": "hidden file content",
+ "visible-file.txt": "visible file content",
+ ".hidden-parent-dir": {
+ "nested-dir": {
+ "file.txt": "file content",
+ }
+ },
+ "visible-dir": {
+ "file-in-visible.txt": "file content",
+ "nested": {
+ ".hidden-nested-dir": {
+ ".double-hidden-dir": {
+ "deep-file-1.txt": "deep content 1",
+ "deep-file-2.txt": "deep content 2"
+ },
+ "hidden-nested-file-1.txt": "hidden nested 1",
+ "hidden-nested-file-2.txt": "hidden nested 2"
+ },
+ "visible-nested-file.txt": "visible nested content"
+ }
+ }
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/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_hidden: false,
+ ..settings
+ },
+ cx,
+ );
+ });
+
+ let panel = workspace.update(cx, ProjectPanel::new).unwrap();
+ cx.run_until_parked();
+
+ toggle_expand_dir(&panel, "root/.hidden-parent-dir", cx);
+ toggle_expand_dir(&panel, "root/.hidden-parent-dir/nested-dir", cx);
+ toggle_expand_dir(&panel, "root/visible-dir", cx);
+ toggle_expand_dir(&panel, "root/visible-dir/nested", cx);
+ toggle_expand_dir(&panel, "root/visible-dir/nested/.hidden-nested-dir", cx);
+ toggle_expand_dir(
+ &panel,
+ "root/visible-dir/nested/.hidden-nested-dir/.double-hidden-dir",
+ cx,
+ );
+
+ let expanded = [
+ "v root",
+ " v .hidden-parent-dir",
+ " v nested-dir",
+ " file.txt",
+ " v visible-dir",
+ " v nested",
+ " v .hidden-nested-dir",
+ " v .double-hidden-dir <== selected",
+ " deep-file-1.txt",
+ " deep-file-2.txt",
+ " hidden-nested-file-1.txt",
+ " hidden-nested-file-2.txt",
+ " visible-nested-file.txt",
+ " file-in-visible.txt",
+ " .hidden-file.txt",
+ " visible-file.txt",
+ ];
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..30, cx),
+ &expanded,
+ "With hide_hidden=false, contents of hidden nested directory should be visible"
+ );
+
+ cx.update(|_, cx| {
+ let settings = *ProjectPanelSettings::get_global(cx);
+ ProjectPanelSettings::override_global(
+ ProjectPanelSettings {
+ hide_hidden: true,
+ ..settings
+ },
+ cx,
+ );
+ });
+
+ panel.update_in(cx, |panel, window, cx| {
+ panel.update_visible_entries(None, false, false, window, cx);
+ });
+ cx.run_until_parked();
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..30, cx),
+ &[
+ "v root",
+ " v visible-dir",
+ " v nested",
+ " visible-nested-file.txt",
+ " file-in-visible.txt",
+ " visible-file.txt",
+ ],
+ "With hide_hidden=false, contents of hidden nested directory should be visible"
+ );
+
+ panel.update_in(cx, |panel, window, cx| {
+ let settings = *ProjectPanelSettings::get_global(cx);
+ ProjectPanelSettings::override_global(
+ ProjectPanelSettings {
+ hide_hidden: false,
+ ..settings
+ },
+ cx,
+ );
+ panel.update_visible_entries(None, false, false, window, cx);
+ });
+ cx.run_until_parked();
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..30, cx),
+ &expanded,
+ "With hide_hidden=false, deeply nested hidden directories and their contents should be visible"
+ );
+}
+
fn select_path(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestContext) {
let path = rel_path(path);
panel.update_in(cx, |panel, window, cx| {
@@ -27,6 +27,7 @@ message Entry {
bool is_fifo = 10;
optional uint64 size = 11;
optional string canonical_path = 12;
+ bool is_hidden = 13;
}
message AddWorktree {
@@ -530,6 +530,10 @@ pub struct ProjectPanelSettingsContent {
///
/// Default: false
pub hide_root: Option<bool>,
+ /// Whether to hide the hidden entries in the project panel.
+ ///
+ /// Default: false
+ pub hide_hidden: Option<bool>,
/// Whether to stick parent directories at top of the project panel.
///
/// Default: true
@@ -2657,6 +2657,27 @@ pub(crate) fn settings_data() -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
+ SettingsPageItem::SettingItem(SettingItem {
+ title: "Hide Hidden",
+ description: "Whether to hide the hidden entries in the project panel",
+ field: Box::new(SettingField {
+ pick: |settings_content| {
+ if let Some(project_panel) = &settings_content.project_panel {
+ &project_panel.hide_hidden
+ } else {
+ &None
+ }
+ },
+ pick_mut: |settings_content| {
+ &mut settings_content
+ .project_panel
+ .get_or_insert_default()
+ .hide_hidden
+ },
+ }),
+ metadata: None,
+ files: USER,
+ }),
SettingsPageItem::SectionHeader("Terminal Panel"),
SettingsPageItem::SettingItem(SettingItem {
title: "Terminal Dock",
@@ -439,6 +439,10 @@ impl Worktree {
entry.is_private = !share_private_files && settings.is_path_private(path);
}
}
+ entry.is_hidden = abs_path
+ .file_name()
+ .and_then(|name| name.to_str())
+ .map_or(false, |name| is_path_hidden(name));
snapshot.insert_entry(entry, fs.as_ref());
}
@@ -2668,6 +2672,7 @@ impl BackgroundScannerState {
scan_queue: scan_job_tx.clone(),
ancestor_inodes,
is_external: entry.is_external,
+ is_hidden: entry.is_hidden,
})
.unwrap();
}
@@ -3177,6 +3182,11 @@ pub struct Entry {
/// exclude them from searches.
pub is_ignored: bool,
+ /// Whether this entry is hidden or inside hidden directory.
+ ///
+ /// We only scan hidden entries once the directory is expanded.
+ pub is_hidden: bool,
+
/// Whether this entry is always included in searches.
///
/// This is used for entries that are always included in searches, even
@@ -3351,6 +3361,7 @@ impl Entry {
size: metadata.len,
canonical_path,
is_ignored: false,
+ is_hidden: false,
is_always_included: false,
is_external: false,
is_private: false,
@@ -4219,6 +4230,11 @@ impl BackgroundScanner {
child_entry.canonical_path = Some(canonical_path.into());
}
+ child_entry.is_hidden = job.is_hidden
+ || child_name
+ .to_str()
+ .map_or(false, |name| is_path_hidden(name));
+
if child_entry.is_dir() {
child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true);
child_entry.is_always_included = self.settings.is_path_always_included(&child_path);
@@ -4234,6 +4250,7 @@ impl BackgroundScanner {
abs_path: child_abs_path.clone(),
path: child_path,
is_external: child_entry.is_external,
+ is_hidden: child_entry.is_hidden,
ignore_stack: if child_entry.is_ignored {
IgnoreStack::all()
} else {
@@ -4384,6 +4401,13 @@ impl BackgroundScanner {
fs_entry.is_private = self.is_path_private(path);
fs_entry.is_always_included = self.settings.is_path_always_included(path);
+ let parent_is_hidden = path
+ .parent()
+ .and_then(|parent| state.snapshot.entry_for_path(parent))
+ .map_or(false, |parent_entry| parent_entry.is_hidden);
+ fs_entry.is_hidden = parent_is_hidden
+ || path.file_name().map_or(false, |name| is_path_hidden(name));
+
if let (Some(scan_queue_tx), true) = (&scan_queue_tx, is_dir) {
if state.should_scan_directory(&fs_entry)
|| (fs_entry.path.is_empty()
@@ -4945,6 +4969,10 @@ fn char_bag_for_path(root_char_bag: CharBag, path: &RelPath) -> CharBag {
result
}
+fn is_path_hidden(name: &str) -> bool {
+ name.starts_with('.')
+}
+
#[derive(Debug)]
struct ScanJob {
abs_path: Arc<Path>,
@@ -4953,6 +4981,7 @@ struct ScanJob {
scan_queue: Sender<ScanJob>,
ancestor_inodes: TreeSet<u64>,
is_external: bool,
+ is_hidden: bool,
}
struct UpdateIgnoreStatusJob {
@@ -5374,6 +5403,7 @@ impl<'a> From<&'a Entry> for proto::Entry {
inode: entry.inode,
mtime: entry.mtime.map(|time| time.into()),
is_ignored: entry.is_ignored,
+ is_hidden: entry.is_hidden,
is_external: entry.is_external,
is_fifo: entry.is_fifo,
size: Some(entry.size),
@@ -5412,6 +5442,7 @@ impl TryFrom<(&CharBag, &PathMatcher, proto::Entry)> for Entry {
.canonical_path
.map(|path_string| Arc::from(PathBuf::from(path_string))),
is_ignored: entry.is_ignored,
+ is_hidden: entry.is_hidden,
is_always_included,
is_external: entry.is_external,
is_private: false,
@@ -4154,6 +4154,7 @@ Run the {#action theme_selector::Toggle} action in the command palette to see a
"show": "always"
},
"hide_root": false,
+ "hide_hidden": false,
"starts_open": true
}
}
@@ -443,7 +443,9 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k
"show": "always"
},
// Whether to hide the root entry when only one folder is open in the window.
- "hide_root": false
+ "hide_root": false,
+ // Whether to hide the hidden entries in the project panel.
+ "hide_hidden": false
}
```