From 94436f0873a10a3ecf8e108326293c429b2a67ab Mon Sep 17 00:00:00 2001 From: Dom Porada Date: Sat, 19 Sep 2026 09:32:40 +0000 Subject: [PATCH] Add support for the `"..."` entry in `hidden_files` (#64484) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Objective Let projects add hidden file patterns without repeating defaults or patterns inherited from user settings. Part of [#64399](https://github.com/zed-industries/zed/discussions/64399). ## Solution Extend the `"..."` behavior from #62769, #64222, and #64440 to `hidden_files`. `"..."` includes the resolved inherited list, omitting it replaces the list, and `[]` clears it. Duplicate entries retain their first occurrence. The default remains `["**/.*"]`, and `project_panel.hide_hidden` still controls visibility. ## Testing Tested on macOS with settings merge, worktree integration, and Settings Editor metadata tests. To check manually, configure different user and project hidden file patterns and enable `project_panel.hide_hidden`. Include `"..."` to retain inherited patterns, then repeat without it and with `[]` to verify replacement and clearing. ## Self-Review Checklist: - [x] I’ve reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed’s UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable --- Release Notes: - Added support for `"..."` in `hidden_files` so project settings can extend inherited hidden-file patterns. --- crates/settings_content/src/project.rs | 123 +++++++++++++++++- crates/settings_ui/src/page_data.rs | 4 +- crates/worktree/src/worktree_settings.rs | 2 +- .../tests/integration/worktree_tests.rs | 121 ++++++++++++++++- docs/src/reference/all-settings.md | 26 ++-- 5 files changed, 256 insertions(+), 20 deletions(-) diff --git a/crates/settings_content/src/project.rs b/crates/settings_content/src/project.rs index 0ad33c260b..37f9bc5b1c 100644 --- a/crates/settings_content/src/project.rs +++ b/crates/settings_content/src/project.rs @@ -211,9 +211,35 @@ pub struct WorktreeSettingsContent { /// 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>, + /// Treat files and folders matching these glob patterns as hidden, including + /// files inside matching folders. To hide these entries, run + /// `project panel: toggle hide hidden` from the command palette or set + /// `project_panel.hide_hidden` to `true`. + /// + /// Default: + /// + /// ```json + /// { + /// "hidden_files": ["**/.*"] + /// } + /// ``` + /// + /// Use `"..."` to add patterns without repeating Zed’s defaults. In project + /// settings, it extends the user or parent configuration value. Omit + /// `"..."` to replace the inherited list. + /// + /// ```json + /// { + /// "hidden_files": ["**/*.log", "..."], + /// "project_panel": { + /// "hide_hidden": true + /// } + /// } + /// ``` + /// + /// Inherited patterns are inserted at `"..."`, and duplicates keep their first + /// occurrence. + pub hidden_files: Option, /// Treat files matching these glob patterns as read-only when opened. You can /// view but not edit them, which is useful for build outputs, external @@ -1163,6 +1189,97 @@ mod tests { assert_eq!(settings, inclusions(&["**/*.project", "**/*.user"])); } + fn hidden_files(globs: &[&str]) -> WorktreeSettingsContent { + WorktreeSettingsContent { + hidden_files: Some(SplicingVec::from( + globs + .iter() + .map(|glob| glob.to_string()) + .collect::>(), + )), + ..Default::default() + } + } + + #[test] + fn test_hidden_files_splice_each_layer() { + let mut settings = hidden_files(&["**/.*"]); + settings.merge_from(&hidden_files(&[SplicingVec::REST, "**/*.user"])); + assert_eq!(settings, hidden_files(&["**/.*", "**/*.user"])); + + settings.merge_from(&hidden_files(&["**/*.project", SplicingVec::REST])); + assert_eq!( + settings, + hidden_files(&["**/*.project", "**/.*", "**/*.user"]) + ); + + settings.merge_from(&hidden_files(&[ + "**/*.before", + SplicingVec::REST, + "**/*.after", + ])); + assert_eq!( + settings, + hidden_files(&[ + "**/*.before", + "**/*.project", + "**/.*", + "**/*.user", + "**/*.after", + ]) + ); + } + + #[test] + fn test_hidden_files_replace_and_clear() { + let mut settings = hidden_files(&["**/.*"]); + settings.merge_from(&hidden_files(&["**/*.user"])); + assert_eq!(settings, hidden_files(&["**/*.user"])); + + settings.merge_from(&hidden_files(&["**/*.project"])); + assert_eq!(settings, hidden_files(&["**/*.project"])); + + settings.merge_from(&hidden_files(&[])); + assert_eq!(settings, hidden_files(&[])); + + settings.merge_from(&hidden_files(&[SplicingVec::REST, "**/*.next"])); + assert_eq!(settings, hidden_files(&["**/*.next"])); + } + + #[test] + fn test_hidden_files_splice_preserves_first_occurrence() { + let inherited = hidden_files(&["**/.*", "**/*.user"]); + let mut settings = inherited.clone(); + settings.merge_from(&WorktreeSettingsContent::default()); + assert_eq!(settings, inherited); + + settings.merge_from(&hidden_files(&[SplicingVec::REST])); + assert_eq!(settings, inherited); + + settings.merge_from(&hidden_files(&[SplicingVec::REST, SplicingVec::REST])); + assert_eq!(settings, inherited); + + settings.merge_from(&hidden_files(&[ + "**/*.user", + SplicingVec::REST, + "**/.*", + "**/*.project", + SplicingVec::REST, + "**/*.project", + ])); + assert_eq!( + settings, + hidden_files(&["**/*.user", "**/.*", "**/*.project"]) + ); + + settings.merge_from(&hidden_files(&[ + "**/*.project", + "**/*.user", + "**/*.project", + ])); + assert_eq!(settings, hidden_files(&["**/*.project", "**/*.user"])); + } + fn read_only_files(globs: &[&str]) -> WorktreeSettingsContent { WorktreeSettingsContent { read_only_files: Some(SplicingVec::from( diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 48e113a308..da09f0d46a 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -252,7 +252,7 @@ fn general_page(cx: &App) -> SettingsPage { field: Box::new( SettingField { organization_override: None, - json_path: Some("worktree.private_files"), + json_path: Some("private_files"), pick: |settings_content| { settings_content.project.worktree.private_files.as_ref() }, @@ -6064,7 +6064,7 @@ fn panels_page() -> SettingsPage { field: Box::new( SettingField { organization_override: None, - json_path: Some("worktree.hidden_files"), + json_path: Some("hidden_files"), pick: |settings_content| { settings_content.project.worktree.hidden_files.as_ref() }, diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index 2c18e2175b..1999ba2092 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -63,7 +63,7 @@ impl Settings for WorktreeSettings { let file_scan_exclusions = worktree.file_scan_exclusions.unwrap().0; let file_scan_inclusions = worktree.file_scan_inclusions.unwrap().0; let private_files = worktree.private_files.unwrap().0; - let hidden_files = worktree.hidden_files.unwrap(); + let hidden_files = worktree.hidden_files.unwrap().0; let read_only_files = worktree.read_only_files.unwrap_or_default().0; let scan_symlinks = worktree.scan_symlinks.unwrap(); let parsed_file_scan_inclusions: Vec = file_scan_inclusions diff --git a/crates/worktree/tests/integration/worktree_tests.rs b/crates/worktree/tests/integration/worktree_tests.rs index 47a841f03e..c1984ad290 100644 --- a/crates/worktree/tests/integration/worktree_tests.rs +++ b/crates/worktree/tests/integration/worktree_tests.rs @@ -2550,7 +2550,8 @@ async fn test_hidden_files(cx: &mut TestAppContext) { cx.update(|cx| { cx.update_global::(|store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.hidden_files = Some(vec!["**/*.log".to_string()]); + settings.project.worktree.hidden_files = + Some(SplicingVec::from(vec!["**/*.log".to_string()])); }); }); }); @@ -2579,6 +2580,124 @@ async fn test_hidden_files(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_hidden_files_from_project_settings(cx: &mut TestAppContext) { + init_test(cx); + let worktree_id = WorktreeId::from_proto(0); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + path!("/root"), + json!({ + ".hidden_dir": { + "nested.rs": "" + }, + ".hidden_file": "", + "app.log": "", + "generated": { + "nested.rs": "" + }, + "visible.rs": "" + }), + ) + .await; + let tree = build_worktree(fs, path!("/root"), cx).await; + tree.read_with(cx, |tree, _| { + assert_eq!( + tree.entries(true, 0) + .filter(|entry| entry.is_hidden) + .map(|entry| entry.path.as_ref()) + .collect::>(), + vec![ + rel_path(".hidden_dir"), + rel_path(".hidden_dir/nested.rs"), + rel_path(".hidden_file"), + ] + ); + }); + + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings(cx, |settings| { + settings.project.worktree.hidden_files = Some(SplicingVec::from(vec![ + "**/*.log".to_string(), + SplicingVec::REST.to_string(), + ])); + }); + }); + }); + + for (settings, hidden_paths) in [ + ( + None, + vec![ + rel_path(".hidden_dir"), + rel_path(".hidden_dir/nested.rs"), + rel_path(".hidden_file"), + rel_path("app.log"), + ], + ), + ( + Some(r#"{ "hidden_files": ["**/generated", "..."] }"#), + vec![ + rel_path(".hidden_dir"), + rel_path(".hidden_dir/nested.rs"), + rel_path(".hidden_file"), + rel_path("app.log"), + rel_path("generated"), + rel_path("generated/nested.rs"), + ], + ), + ( + Some(r#"{ "hidden_files": ["**/generated"] }"#), + vec![rel_path("generated"), rel_path("generated/nested.rs")], + ), + (Some(r#"{ "hidden_files": [] }"#), vec![]), + ] { + cx.update(|cx| { + cx.update_global::(|store, cx| { + store + .set_local_settings( + worktree_id, + LocalSettingsPath::InWorktree(Arc::from(RelPath::empty())), + LocalSettingsKind::Settings, + settings, + cx, + ) + .expect("valid project settings"); + }); + }); + cx.run_until_parked(); + tree.read_with(cx, |tree, _| { + tree.as_local().expect("local worktree").scan_complete() + }) + .await; + tree.read_with(cx, |tree, _| { + assert_eq!( + tree.entries(true, 0) + .filter(|entry| entry.is_hidden) + .map(|entry| entry.path.as_ref()) + .collect::>(), + hidden_paths + ); + assert_eq!( + tree.entries(true, 0) + .map(|entry| entry.path.as_ref()) + .collect::>(), + vec![ + rel_path(""), + rel_path(".hidden_dir"), + rel_path(".hidden_dir/nested.rs"), + rel_path(".hidden_file"), + rel_path("app.log"), + rel_path("generated"), + rel_path("generated/nested.rs"), + rel_path("visible.rs"), + ] + ); + }); + } +} + #[gpui::test] async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { init_test(cx); diff --git a/docs/src/reference/all-settings.md b/docs/src/reference/all-settings.md index acafc5bcb8..9381331ec2 100644 --- a/docs/src/reference/all-settings.md +++ b/docs/src/reference/all-settings.md @@ -2934,29 +2934,29 @@ Example: ## Hidden Files {#hidden-files} -- Description: Glob patterns that mark files and folders as hidden in the Project Panel. - Setting: `hidden_files` -- Default: `["**/.*"]` - -**Options** - -List of `string` glob patterns. - -The default matches files and folders whose names start with a dot. Files inside matching folders are also considered hidden. - -The [Project Panel](../project-panel.md#hiding-files) shows hidden entries by default. To hide them, enable **Hide Hidden** under **Panels > Project Panel** in the Settings Editor, or set `project_panel.hide_hidden` to `true`. - -Customizing `hidden_files` replaces the default patterns. To hide `*.log` files while keeping dotfiles and dotfolders hidden, add this to your `settings.json`: +- Description: Treat files and folders matching these glob patterns as hidden, including files inside matching folders. To [hide these entries](../project-panel.md#hiding-files), run `project panel: toggle hide hidden` from the command palette or set `project_panel.hide_hidden` to `true`. +- Default: ```json [settings] { - "hidden_files": ["**/*.log", "**/.*"], + "hidden_files": ["**/.*"] +} +``` + +Use `"..."` to add patterns without repeating Zed’s defaults. In project settings, it extends the user or parent configuration value. Omit `"..."` to replace the inherited list. + +```json [settings] +{ + "hidden_files": ["**/*.log", "..."], "project_panel": { "hide_hidden": true } } ``` +Inherited patterns are inserted at `"..."`, and duplicates keep their first occurrence. + ## Indent Guides - Description: Configuration related to indent guides. Indent guides can be configured separately for each language.