Detailed changes
@@ -43,7 +43,8 @@
"f11": "zed::ToggleFullScreen",
"ctrl-alt-z": "edit_prediction::RateCompletions",
"ctrl-alt-shift-i": "edit_prediction::ToggleMenu",
- "ctrl-alt-l": "lsp_tool::ToggleMenu"
+ "ctrl-alt-l": "lsp_tool::ToggleMenu",
+ "ctrl-alt-.": "project_panel::ToggleHideHidden"
}
},
{
@@ -49,7 +49,8 @@
"ctrl-cmd-f": "zed::ToggleFullScreen",
"ctrl-cmd-z": "edit_prediction::RateCompletions",
"ctrl-cmd-i": "edit_prediction::ToggleMenu",
- "ctrl-cmd-l": "lsp_tool::ToggleMenu"
+ "ctrl-cmd-l": "lsp_tool::ToggleMenu",
+ "cmd-alt-.": "project_panel::ToggleHideHidden"
}
},
{
@@ -41,7 +41,8 @@
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-shift-i": "edit_prediction::ToggleMenu",
- "shift-alt-l": "lsp_tool::ToggleMenu"
+ "shift-alt-l": "lsp_tool::ToggleMenu",
+ "ctrl-alt-.": "project_panel::ToggleHideHidden"
}
},
{
@@ -1247,6 +1247,9 @@
// that are overly broad can slow down Zed's file scanning. `file_scan_exclusions` takes
// precedence over these inclusions.
"file_scan_inclusions": [".env*"],
+ // Globs to match files that will be considered "hidden". These files can be hidden from the
+ // project panel by toggling the "hide_hidden" setting.
+ "hidden_files": ["**/.*"],
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
@@ -294,6 +294,8 @@ actions!(
ToggleFocus,
/// Toggles visibility of git-ignored files.
ToggleHideGitIgnore,
+ /// Toggles visibility of hidden files.
+ ToggleHideHidden,
/// Starts a new search in the selected directory.
NewSearchInDirectory,
/// Unfolds the selected directory.
@@ -383,6 +385,19 @@ pub fn init(cx: &mut App) {
})
});
+ workspace.register_action(|workspace, _: &ToggleHideHidden, _, cx| {
+ let fs = workspace.app_state().fs.clone();
+ update_settings_file(fs, cx, move |setting, _| {
+ setting.project_panel.get_or_insert_default().hide_hidden = Some(
+ !setting
+ .project_panel
+ .get_or_insert_default()
+ .hide_hidden
+ .unwrap_or(false),
+ );
+ })
+ });
+
workspace.register_action(|workspace, action: &CollapseAllEntries, window, cx| {
if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
panel.update(cx, |panel, cx| {
@@ -97,6 +97,10 @@ pub struct WorktreeSettingsContent {
/// Treat the files matching these globs as `.env` files.
/// Default: ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"]
pub private_files: Option<ExtendingVec<String>>,
+
+ /// Treat the files matching these globs as hidden files. You can hide hidden files in the project panel.
+ /// Default: ["**/.*"]
+ pub hidden_files: Option<Vec<String>>,
}
#[skip_serializing_none]
@@ -876,6 +876,7 @@ impl VsCodeSettings {
})
.filter(|r| !r.is_empty()),
private_files: None,
+ hidden_files: None,
}
}
}
@@ -3743,6 +3743,24 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
metadata: None,
files: USER,
}),
+ SettingsPageItem::SettingItem(SettingItem {
+ title: "Hidden Files",
+ description: "Globs to match files that will be considered \"hidden\" and can be hidden from the project panel.",
+ field: Box::new(
+ SettingField {
+ json_path: Some("worktree.hidden_files"),
+ pick: |settings_content| {
+ settings_content.project.worktree.hidden_files.as_ref()
+ },
+ write: |settings_content, value| {
+ settings_content.project.worktree.hidden_files = value;
+ },
+ }
+ .unimplemented(),
+ ),
+ metadata: None,
+ files: USER,
+ }),
SettingsPageItem::SettingItem(SettingItem {
title: "Open File on Paste",
description: "Whether to automatically open files when pasting them in the project panel.",
@@ -438,12 +438,9 @@ impl Worktree {
&& let Ok(path) = RelPath::unix(file_name)
{
entry.is_private = !share_private_files && settings.is_path_private(path);
+ entry.is_hidden = settings.is_path_hidden(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());
}
@@ -2685,7 +2682,6 @@ impl BackgroundScannerState {
scan_queue: scan_job_tx.clone(),
ancestor_inodes,
is_external: entry.is_external,
- is_hidden: entry.is_hidden,
})
.unwrap();
}
@@ -4285,11 +4281,6 @@ 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 =
@@ -4306,7 +4297,6 @@ 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 {
@@ -4330,6 +4320,10 @@ impl BackgroundScanner {
log::debug!("detected private file: {relative_path:?}");
child_entry.is_private = true;
}
+ if self.settings.is_path_hidden(&relative_path) {
+ log::debug!("detected hidden file: {relative_path:?}");
+ child_entry.is_hidden = true;
+ }
}
new_entries.push(child_entry);
@@ -4456,13 +4450,7 @@ impl BackgroundScanner {
fs_entry.is_private = self.is_path_private(path);
fs_entry.is_always_included =
self.settings.is_path_always_included(path, is_dir);
-
- 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));
+ fs_entry.is_hidden = self.settings.is_path_hidden(path);
if let (Some(scan_queue_tx), true) = (&scan_queue_tx, is_dir) {
if state.should_scan_directory(&fs_entry)
@@ -5035,10 +5023,6 @@ 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>,
@@ -5047,7 +5031,6 @@ struct ScanJob {
scan_queue: Sender<ScanJob>,
ancestor_inodes: TreeSet<u64>,
is_external: bool,
- is_hidden: bool,
}
struct UpdateIgnoreStatusJob {
@@ -19,6 +19,7 @@ pub struct WorktreeSettings {
/// determine whether to terminate worktree scanning for a given dir.
pub parent_dir_scan_inclusions: PathMatcher,
pub private_files: PathMatcher,
+ pub hidden_files: PathMatcher,
}
impl WorktreeSettings {
@@ -39,6 +40,11 @@ impl WorktreeSettings {
self.file_scan_inclusions.is_match(path.as_std_path())
}
}
+
+ pub fn is_path_hidden(&self, path: &RelPath) -> bool {
+ path.ancestors()
+ .any(|ancestor| self.hidden_files.is_match(ancestor.as_std_path()))
+ }
}
impl Settings for WorktreeSettings {
@@ -47,6 +53,7 @@ impl Settings for WorktreeSettings {
let file_scan_exclusions = worktree.file_scan_exclusions.unwrap();
let file_scan_inclusions = worktree.file_scan_inclusions.unwrap();
let private_files = worktree.private_files.unwrap().0;
+ let hidden_files = worktree.hidden_files.unwrap();
let parsed_file_scan_inclusions: Vec<String> = file_scan_inclusions
.iter()
.flat_map(|glob| {
@@ -74,6 +81,9 @@ impl Settings for WorktreeSettings {
private_files: path_matchers(private_files, "private_files")
.log_err()
.unwrap_or_default(),
+ hidden_files: path_matchers(hidden_files, "hidden_files")
+ .log_err()
+ .unwrap_or_default(),
}
}
}
@@ -1057,6 +1057,92 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
});
}
+#[gpui::test]
+async fn test_hidden_files(cx: &mut TestAppContext) {
+ init_test(cx);
+ cx.executor().allow_parking();
+ let dir = TempTree::new(json!({
+ ".gitignore": "**/target\n",
+ ".hidden_file": "content",
+ ".hidden_dir": {
+ "nested.rs": "code",
+ },
+ "src": {
+ "visible.rs": "code",
+ },
+ "logs": {
+ "app.log": "logs",
+ "debug.log": "logs",
+ },
+ "visible.txt": "content",
+ }));
+
+ let tree = Worktree::local(
+ dir.path(),
+ true,
+ Arc::new(RealFs::new(None, cx.executor())),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+ tree.flush_fs_events(cx).await;
+
+ tree.read_with(cx, |tree, _| {
+ assert_eq!(
+ tree.entries(true, 0)
+ .map(|entry| (entry.path.as_ref(), entry.is_hidden))
+ .collect::<Vec<_>>(),
+ vec![
+ (rel_path(""), false),
+ (rel_path(".gitignore"), true),
+ (rel_path(".hidden_dir"), true),
+ (rel_path(".hidden_dir/nested.rs"), true),
+ (rel_path(".hidden_file"), true),
+ (rel_path("logs"), false),
+ (rel_path("logs/app.log"), false),
+ (rel_path("logs/debug.log"), false),
+ (rel_path("src"), false),
+ (rel_path("src/visible.rs"), false),
+ (rel_path("visible.txt"), false),
+ ]
+ );
+ });
+
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _>(|store, cx| {
+ store.update_user_settings(cx, |settings| {
+ settings.project.worktree.hidden_files = Some(vec!["**/*.log".to_string()]);
+ });
+ });
+ });
+ tree.flush_fs_events(cx).await;
+ cx.executor().run_until_parked();
+
+ tree.read_with(cx, |tree, _| {
+ assert_eq!(
+ tree.entries(true, 0)
+ .map(|entry| (entry.path.as_ref(), entry.is_hidden))
+ .collect::<Vec<_>>(),
+ vec![
+ (rel_path(""), false),
+ (rel_path(".gitignore"), false),
+ (rel_path(".hidden_dir"), false),
+ (rel_path(".hidden_dir/nested.rs"), false),
+ (rel_path(".hidden_file"), false),
+ (rel_path("logs"), false),
+ (rel_path("logs/app.log"), true),
+ (rel_path("logs/debug.log"), true),
+ (rel_path("src"), false),
+ (rel_path("src/visible.rs"), false),
+ (rel_path("visible.txt"), false),
+ ]
+ );
+ });
+}
+
#[gpui::test]
async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
init_test(cx);