diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index a0c190fe7456f00351b609a97361c40b6b2d9b9a..1af01c5d239c8aba15dfd8259d76af4b80f4aece 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -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" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 83a998be35dc38f925f8f764c1801c80e5fb56bc..8c2f596fbdc19b11b1fe1039c2fb61df9c91f8f0 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -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" } }, { diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index ae61bb887ce2e9f956c6d11d7bfc1f8036a00284..59ea8398c5997999eb771df58486f9c7270b0a52 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -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" } }, { diff --git a/assets/settings/default.json b/assets/settings/default.json index 1852ace708c53f6a651420b77d7e83f9afc978c3..370124dcf5ac0a362440108e0b473bf2f0c1dfab 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -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: diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 2dbdfdd22e80cf61de48784470fe3bf8375b624c..5d669b2d9afef4b773244c4d76a3698287788b93 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -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::(cx) { panel.update(cx, |panel, cx| { diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs index a6af4c617ae7e744926e0a7608ff3fddeb2d0cd2..56c3ff1c78ead6b113799c1c11552e0732b62345 100644 --- a/crates/settings/src/settings_content/project.rs +++ b/crates/settings/src/settings_content/project.rs @@ -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>, + + /// Treat the files matching these globs as hidden files. You can hide hidden files in the project panel. + /// Default: ["**/.*"] + pub hidden_files: Option>, } #[skip_serializing_none] diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index ba8392bcd3a3a775af7e4caaa949a79095703817..fe66e6b686a63ca4bd4ab965b8ed9f1325023d82 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -876,6 +876,7 @@ impl VsCodeSettings { }) .filter(|r| !r.is_empty()), private_files: None, + hidden_files: None, } } } diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 89986d925ce88d50629a58fb48faa629f63cab12..0bb1844c31847507f28e6cc7f5c12c1d31f20fdd 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -3743,6 +3743,24 @@ pub(crate) fn settings_data(cx: &App) -> Vec { 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.", diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index bd2437ff56366bad041097ff797f6d05046dafc0..8f6a1d23b82a272452ed90e635c3936f169d1404 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -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, @@ -5047,7 +5031,6 @@ struct ScanJob { scan_queue: Sender, ancestor_inodes: TreeSet, is_external: bool, - is_hidden: bool, } struct UpdateIgnoreStatusJob { diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index 9fb88af9d9cd83c6de7afe1962b2640ae8e81515..3240978cc8694e1ce3cdc809a0929bc558f91766 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -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 = 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(), } } } diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs index a3dab96cc69f553bcf01e4d8357d68cde2f05fce..fcd842846b4308c15cc5f01bbdd47e72598ce821 100644 --- a/crates/worktree/src/worktree_tests.rs +++ b/crates/worktree/src/worktree_tests.rs @@ -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![ + (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::(|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![ + (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);