project_panel: Add ability to hide hidden files (#39843)

Closes #5185

Release Notes:

- Added an option to hide hidden files in the project panel by setting
`hide_hidden` in the project panel settings.

---------

Co-authored-by: Gaauwe Rombouts <gromdroid@gmail.com>
Co-authored-by: Gaauwe Rombouts <mail@grombouts.nl>
This commit is contained in:
Smit Barmase
2025-10-12 18:31:55 +05:30
committed by GitHub
co-authored by Gaauwe Rombouts Gaauwe Rombouts
parent abe1fd5e16
commit 998fece3af
16 changed files with 223 additions and 4 deletions
+3 -1
View File
@@ -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;
+3
View File
@@ -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
+1
View File
@@ -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>,
+1
View File
@@ -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,
};
+12 -2
View File
@@ -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| {
+1
View File
@@ -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
+21
View File
@@ -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",
+31
View File
@@ -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,
+1
View File
@@ -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
}
}
+3 -1
View File
@@ -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
}
```