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:
co-authored by
Gaauwe Rombouts
Gaauwe Rombouts
parent
abe1fd5e16
commit
998fece3af
@@ -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
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user