From ef973c67c0e8b5ae6e4d1abc87f81b68c0392490 Mon Sep 17 00:00:00 2001 From: Dom Porada Date: Tue, 18 Aug 2026 09:57:38 +0000 Subject: [PATCH] Add support for the `"..."` entry in `file_scan_exclusions` (#62769) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Objective `file_scan_exclusions` replaces the defaults instead of adding to them, so excluding one extra directory means restating all eleven default globs and never picking up defaults added in later Zed releases. ## Solution `file_scan_exclusions` now accepts the `"..."` entry, which expands to the value it overrides, so `["**/node_modules", "..."]` adds to the inherited globs instead of replacing them. Entries listed by name keep their position, and leaving `"..."` out still replaces the list outright, so existing settings behave exactly as they do today. ## Testing - Four unit tests in `crates/settings_content/src/project.rs` cover splicing versus replacing, accumulation across successive layers, and edge cases: a repeated `"..."`, an empty list clearing the value, and a bare `["..."]` leaving it unchanged. - To check by hand: set `"file_scan_exclusions": ["**/node_modules", "..."]` in user settings and confirm `node_modules` disappears from the project panel and file finder while `.git` and `.DS_Store` stay excluded. Remove `"..."` and confirm only `node_modules` is excluded. Repeat in a project's `.zed/settings.json` to confirm it splices the resolved user settings rather than the defaults. - Tested on macOS. This is platform-independent settings-merge logic with no OS-specific code paths, so I did not test Linux or Windows. ## 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 the `"..."` entry in `file_scan_exclusions`. Custom exclusions can now extend the defaults instead of replacing them. --- crates/agent/src/tools/grep_tool.rs | 12 ++- crates/agent/src/tools/list_directory_tool.rs | 12 ++- crates/agent/src/tools/read_file_tool.rs | 12 ++- .../tests/integration/following_tests.rs | 4 +- .../tests/integration/project_tests.rs | 8 +- .../project_panel/src/project_panel_tests.rs | 32 +++--- crates/project_panel/src/tests/undo.rs | 6 +- crates/settings/src/vscode_import.rs | 3 +- crates/settings_content/src/project.rs | 98 ++++++++++++++++++- .../settings_content/src/settings_content.rs | 40 ++++++++ crates/worktree/src/worktree_settings.rs | 2 +- .../tests/integration/worktree_tests.rs | 22 +++-- crates/zed/src/zed.rs | 9 +- docs/src/reference/all-settings.md | 20 +++- 14 files changed, 228 insertions(+), 52 deletions(-) diff --git a/crates/agent/src/tools/grep_tool.rs b/crates/agent/src/tools/grep_tool.rs index 5af92b2e45..c173941781 100644 --- a/crates/agent/src/tools/grep_tool.rs +++ b/crates/agent/src/tools/grep_tool.rs @@ -398,7 +398,7 @@ mod tests { use gpui::{TestAppContext, UpdateGlobal}; use project::{FakeFs, Project}; use serde_json::json; - use settings::SettingsStore; + use settings::{SettingsStore, SplicingVec}; use std::path::PathBuf; use unindent::Unindent; use util::path; @@ -1068,10 +1068,10 @@ mod tests { use settings::SettingsStore; SettingsStore::update_global(cx, |store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![ + settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![ "**/.secretdir".to_string(), "**/.mymetadata".to_string(), - ]); + ])); settings.project.worktree.private_files = Some( vec![ "**/.mysecrets".to_string(), @@ -1303,8 +1303,10 @@ mod tests { cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); + settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![ + "**/.git".to_string(), + "**/node_modules".to_string(), + ])); settings.project.worktree.private_files = Some(vec!["**/.env".to_string()].into()); }); diff --git a/crates/agent/src/tools/list_directory_tool.rs b/crates/agent/src/tools/list_directory_tool.rs index fa61ac4d1b..0fd86f2ad3 100644 --- a/crates/agent/src/tools/list_directory_tool.rs +++ b/crates/agent/src/tools/list_directory_tool.rs @@ -342,7 +342,7 @@ mod tests { use indoc::indoc; use project::{FakeFs, Project}; use serde_json::json; - use settings::SettingsStore; + use settings::{SettingsStore, SplicingVec}; use std::path::PathBuf; use util::path; @@ -578,11 +578,11 @@ mod tests { cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![ + settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![ "**/.secretdir".to_string(), "**/.mymetadata".to_string(), "**/.hidden_subdir".to_string(), - ]); + ])); settings.project.worktree.private_files = Some( vec![ "**/.mysecrets".to_string(), @@ -740,8 +740,10 @@ mod tests { cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); + settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![ + "**/.git".to_string(), + "**/node_modules".to_string(), + ])); settings.project.worktree.private_files = Some(vec!["**/.env".to_string()].into()); }); diff --git a/crates/agent/src/tools/read_file_tool.rs b/crates/agent/src/tools/read_file_tool.rs index df48508fbd..19b1c86093 100644 --- a/crates/agent/src/tools/read_file_tool.rs +++ b/crates/agent/src/tools/read_file_tool.rs @@ -542,7 +542,7 @@ mod test { use gpui::{AppContext, TestAppContext, UpdateGlobal as _}; use project::{FakeFs, Project}; use serde_json::json; - use settings::SettingsStore; + use settings::{SettingsStore, SplicingVec}; use std::path::PathBuf; use std::sync::Arc; use util::path; @@ -1051,10 +1051,10 @@ mod test { use settings::SettingsStore; SettingsStore::update_global(cx, |store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![ + settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![ "**/.secretdir".to_string(), "**/.mymetadata".to_string(), - ]); + ])); settings.project.worktree.private_files = Some( vec![ "**/.mysecrets".to_string(), @@ -1357,8 +1357,10 @@ mod test { cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); + settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![ + "**/.git".to_string(), + "**/node_modules".to_string(), + ])); settings.project.worktree.private_files = Some(vec!["**/.env".to_string()].into()); }); diff --git a/crates/collab/tests/integration/following_tests.rs b/crates/collab/tests/integration/following_tests.rs index 90370b55e6..4c9fa7abc7 100644 --- a/crates/collab/tests/integration/following_tests.rs +++ b/crates/collab/tests/integration/following_tests.rs @@ -15,7 +15,7 @@ use gpui::{ use language::Capability; use rpc::proto::PeerId; use serde_json::json; -use settings::SettingsStore; +use settings::{SettingsStore, SplicingVec}; use text::{Point, ToPoint}; use util::{path, rel_path::rel_path, test::sample_text}; use workspace::{ @@ -1762,7 +1762,7 @@ async fn test_following_into_excluded_file( cx.update_global::(|store, cx| { store.update_user_settings(cx, |settings| { settings.project.worktree.file_scan_exclusions = - Some(vec!["**/.git".to_string()]); + Some(SplicingVec::from(vec!["**/.git".to_string()])); }); }); }); diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index c2ec56e0dc..9f9ebeef87 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -67,7 +67,7 @@ use project::{ }; use rand::{Rng as _, rngs::StdRng}; use serde_json::json; -use settings::{GlobalLspSettingsContent, SettingsStore}; +use settings::{GlobalLspSettingsContent, SettingsStore, SplicingVec}; #[cfg(target_os = "linux")] use settings::{LocalSettingsKind, LocalSettingsPath}; #[cfg(not(windows))] @@ -13420,7 +13420,8 @@ async fn test_git_events_after_project_excludes_dot_git(cx: &mut gpui::TestAppCo cx.update(|cx| { SettingsStore::update_global(cx, |store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec!["foo".to_string()]); + settings.project.worktree.file_scan_exclusions = + Some(SplicingVec::from(vec!["foo".to_string()])); }); }); }); @@ -15268,7 +15269,8 @@ async fn test_rescan_with_gitignore(cx: &mut gpui::TestAppContext) { cx.update(|cx| { cx.update_global::(|store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(Vec::new()); + settings.project.worktree.file_scan_exclusions = + Some(SplicingVec::from(Vec::new())); }); }); }); diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index 1c308d707a..2ee38f5b9e 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -11,7 +11,7 @@ use menu::Cancel; use pretty_assertions::assert_eq; use project::{FakeFs, ProjectPath}; use serde_json::json; -use settings::{ProjectPanelAutoOpenSettings, SettingsStore}; +use settings::{ProjectPanelAutoOpenSettings, SettingsStore, SplicingVec}; use smallvec::smallvec; use std::path::{Path, PathBuf}; use util::{path, paths::PathStyle, rel_path::rel_path}; @@ -426,8 +426,10 @@ async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) { cx.update(|cx| { cx.update_global::(|store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/4/**".to_string()]); + settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![ + "**/.git".to_string(), + "**/4/**".to_string(), + ])); }); }); }); @@ -5562,7 +5564,8 @@ async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) { cx.update(|cx| { cx.update_global::(|store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(Vec::new()); + settings.project.worktree.file_scan_exclusions = + Some(SplicingVec::from(Vec::new())); settings .project_panel .get_or_insert_default() @@ -5864,7 +5867,8 @@ async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) { cx.update(|cx| { cx.update_global::(|store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(Vec::new()); + settings.project.worktree.file_scan_exclusions = + Some(SplicingVec::from(Vec::new())); settings.project.worktree.file_scan_inclusions = Some(vec!["always_included_but_ignored_dir/*".to_string()]); settings @@ -5988,7 +5992,8 @@ async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) { cx.update(|cx| { cx.update_global::(|store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(Vec::new()); + settings.project.worktree.file_scan_exclusions = + Some(SplicingVec::from(Vec::new())); settings .project_panel .get_or_insert_default() @@ -6585,8 +6590,10 @@ async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) { cx.update(|cx| { cx.update_global::(|store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]); + settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![ + "excluded_dir".to_string(), + "**/.git".to_string(), + ])); }); }); }); @@ -11325,7 +11332,8 @@ pub(crate) fn init_test(cx: &mut TestAppContext) { .project_panel .get_or_insert_default() .auto_fold_dirs = Some(false); - settings.project.worktree.file_scan_exclusions = Some(Vec::new()); + settings.project.worktree.file_scan_exclusions = + Some(SplicingVec::from(Vec::new())); settings.project.worktree.file_scan_depth = Some(0); }); }); @@ -11346,7 +11354,8 @@ fn init_test_with_editor(cx: &mut TestAppContext) { .project_panel .get_or_insert_default() .auto_fold_dirs = Some(false); - settings.project.worktree.file_scan_exclusions = Some(Vec::new()); + settings.project.worktree.file_scan_exclusions = + Some(SplicingVec::from(Vec::new())); settings.project.worktree.file_scan_depth = Some(0); }); }); @@ -11368,7 +11377,8 @@ fn init_test_with_git_ui(cx: &mut TestAppContext) { .project_panel .get_or_insert_default() .auto_fold_dirs = Some(false); - settings.project.worktree.file_scan_exclusions = Some(Vec::new()); + settings.project.worktree.file_scan_exclusions = + Some(SplicingVec::from(Vec::new())); settings.project.worktree.file_scan_depth = Some(0); }); }); diff --git a/crates/project_panel/src/tests/undo.rs b/crates/project_panel/src/tests/undo.rs index ca2ec15331..918258d69a 100644 --- a/crates/project_panel/src/tests/undo.rs +++ b/crates/project_panel/src/tests/undo.rs @@ -7,7 +7,7 @@ use fs::{FakeFs, Fs}; use gpui::{App, BorrowAppContext, Context, Entity, VisualTestContext, Window}; use project::Project; use serde_json::{Value, json}; -use settings::SettingsStore; +use settings::{SettingsStore, SplicingVec}; use std::path::Path; use std::sync::Arc; use workspace::{Item, MultiWorkspace, register_project_item}; @@ -617,10 +617,10 @@ async fn excluded_create_is_not_recorded(cx: &mut gpui::TestAppContext) { cx.update_app(|cx| { cx.update_global::(|store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![ + settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![ "**/token.secret".to_string(), "**/banana.secret".to_string(), - ]); + ])); }); }); diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 5ac3a5c9a5..f662654a7b 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -1108,7 +1108,8 @@ impl VsCodeSettings { .filter_map(|n| n.as_str().map(str::to_owned)) .collect::>() }) - .filter(|r| !r.is_empty()), + .filter(|r| !r.is_empty()) + .map(SplicingVec::from), file_scan_inclusions: self .read_value("files.watcherInclude") .and_then(|v| v.as_array()) diff --git a/crates/settings_content/src/project.rs b/crates/settings_content/src/project.rs index 59498358c4..c70d640324 100644 --- a/crates/settings_content/src/project.rs +++ b/crates/settings_content/src/project.rs @@ -14,7 +14,7 @@ use util::serde::default_true; use crate::{ AllLanguageSettingsContent, DelayMs, ExtendingVec, ParseStatus, ProjectTerminalSettingsContent, - RootUserSettings, SaturatingBool, fallible_options, + RootUserSettings, SaturatingBool, SplicingVec, fallible_options, }; #[with_fallible_options] @@ -123,6 +123,10 @@ pub struct WorktreeSettingsContent { /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides /// `file_scan_inclusions`. /// + /// A `"..."` entry expands to the value being overridden, so + /// `["**/node_modules", "..."]` adds to the inherited globs instead of + /// replacing them. Leave `"..."` out to replace them. + /// /// Default: [ /// "**/.git", /// "**/.svn", @@ -134,7 +138,7 @@ pub struct WorktreeSettingsContent { /// "**/.classpath", /// "**/.settings" /// ] - pub file_scan_exclusions: Option>, + pub file_scan_exclusions: Option, /// Always include files that match these globs when scanning for files, even if they're /// ignored by git. This setting is overridden by `file_scan_exclusions`. @@ -936,6 +940,96 @@ pub enum GitHostingProviderKind { #[cfg(test)] mod tests { use super::*; + use crate::{REST_OF_FILE_SCAN_EXCLUSIONS, merge_from::MergeFrom}; + + fn exclusions(globs: &[&str]) -> WorktreeSettingsContent { + WorktreeSettingsContent { + file_scan_exclusions: Some(SplicingVec::from( + globs + .iter() + .map(|glob| glob.to_string()) + .collect::>(), + )), + ..Default::default() + } + } + + #[test] + fn test_file_scan_exclusions_splice_rest_of_list() { + let defaults = exclusions(&["**/.git", "**/.DS_Store"]); + + let mut extended = defaults.clone(); + extended.merge_from(&exclusions(&[ + "**/node_modules", + REST_OF_FILE_SCAN_EXCLUSIONS, + ])); + assert_eq!( + extended.file_scan_exclusions.unwrap().0, + vec!["**/node_modules", "**/.git", "**/.DS_Store"] + ); + + let mut replaced = defaults; + replaced.merge_from(&exclusions(&["**/node_modules"])); + assert_eq!( + replaced.file_scan_exclusions.unwrap().0, + vec!["**/node_modules"] + ); + } + + #[test] + fn test_file_scan_exclusions_splice_each_layer() { + let mut settings = exclusions(&["**/.git"]); + settings.merge_from(&exclusions(&[REST_OF_FILE_SCAN_EXCLUSIONS, "**/target"])); + settings.merge_from(&exclusions(&[REST_OF_FILE_SCAN_EXCLUSIONS, "**/dist"])); + + assert_eq!( + settings.file_scan_exclusions.unwrap().0, + vec!["**/.git", "**/target", "**/dist"] + ); + } + + #[test] + fn test_file_scan_exclusions_splice_edge_cases() { + let mut repeated = exclusions(&["**/.git"]); + repeated.merge_from(&exclusions(&[ + REST_OF_FILE_SCAN_EXCLUSIONS, + REST_OF_FILE_SCAN_EXCLUSIONS, + ])); + assert_eq!(repeated.file_scan_exclusions.unwrap().0, vec!["**/.git"]); + + let mut relisted = exclusions(&["**/.git", "**/.DS_Store"]); + relisted.merge_from(&exclusions(&["**/.git", REST_OF_FILE_SCAN_EXCLUSIONS])); + assert_eq!( + relisted.file_scan_exclusions.unwrap().0, + vec!["**/.git", "**/.DS_Store"] + ); + + let mut cleared = exclusions(&["**/.git"]); + cleared.merge_from(&exclusions(&[])); + assert!(cleared.file_scan_exclusions.unwrap().0.is_empty()); + + let mut unchanged = exclusions(&["**/.git", "**/.DS_Store"]); + unchanged.merge_from(&exclusions(&[REST_OF_FILE_SCAN_EXCLUSIONS])); + assert_eq!( + unchanged.file_scan_exclusions.unwrap().0, + vec!["**/.git", "**/.DS_Store"] + ); + } + + #[test] + fn test_file_scan_exclusions_splice_without_a_base_layer() { + let mut settings = WorktreeSettingsContent::default(); + settings.merge_from(&exclusions(&[REST_OF_FILE_SCAN_EXCLUSIONS, "**/target"])); + + // `Option::merge_from` replaces a `None` base outright rather than + // splicing, so the sentinel survives here. `assets/settings/default.json` + // always populates this field, and `WorktreeSettings::from_settings` + // unwraps it, so no glob is ever compiled from this state. + assert_eq!( + settings.file_scan_exclusions.unwrap().0, + vec![REST_OF_FILE_SCAN_EXCLUSIONS, "**/target"] + ); + } #[test] fn test_stdio_context_server_without_args() { diff --git a/crates/settings_content/src/settings_content.rs b/crates/settings_content/src/settings_content.rs index 9e97232291..bb5df9679c 100644 --- a/crates/settings_content/src/settings_content.rs +++ b/crates/settings_content/src/settings_content.rs @@ -1399,6 +1399,46 @@ impl merge_from::MergeFrom for ExtendingVec { } } +pub const REST_OF_FILE_SCAN_EXCLUSIONS: &str = "..."; + +// A SplicingVec in the settings replaces the value it merges over, except that +// a `...` entry expands to that previous value. +// +// This lets a settings file add to a list without restating what it inherits, +// while omitting `...` still replaces the list outright. Unlike ExtendingVec, +// entries can be dropped by leaving `...` out and listing what to keep. +// +// Entries collapse to their first occurrence, so naming a value that `...` +// already covers keeps it at the position it was written in rather than +// repeating it. +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct SplicingVec(pub Vec); + +impl From> for SplicingVec { + fn from(vec: Vec) -> Self { + SplicingVec(vec) + } +} + +impl merge_from::MergeFrom for SplicingVec { + fn merge_from(&mut self, other: &Self) { + let inherited = std::mem::take(&mut self.0); + self.0 = other + .0 + .iter() + .flat_map(|entry| { + if entry == REST_OF_FILE_SCAN_EXCLUSIONS { + inherited.clone() + } else { + vec![entry.clone()] + } + }) + .collect::>() + .into_iter() + .collect(); + } +} + // An ExtendingSet in the settings can only accumulate new values, and ignores // values that are already present, so merging the same source more than once // (e.g. re-importing VS Code settings) is idempotent. diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs index c2fc915aa6..7a439125bb 100644 --- a/crates/worktree/src/worktree_settings.rs +++ b/crates/worktree/src/worktree_settings.rs @@ -60,7 +60,7 @@ impl WorktreeSettings { impl Settings for WorktreeSettings { fn from_settings(content: &settings::SettingsContent) -> Self { let worktree = content.project.worktree.clone(); - let file_scan_exclusions = worktree.file_scan_exclusions.unwrap(); + let file_scan_exclusions = worktree.file_scan_exclusions.unwrap().0; let file_scan_inclusions = worktree.file_scan_inclusions.unwrap(); let private_files = worktree.private_files.unwrap().0; let hidden_files = worktree.hidden_files.unwrap(); diff --git a/crates/worktree/tests/integration/worktree_tests.rs b/crates/worktree/tests/integration/worktree_tests.rs index bad6c26520..62ce80e4f2 100644 --- a/crates/worktree/tests/integration/worktree_tests.rs +++ b/crates/worktree/tests/integration/worktree_tests.rs @@ -15,7 +15,7 @@ use rpc::{AnyProtoClient, NoopProtoClient, proto}; use worktree::{Entry, EntryKind, Event, PathChange, Worktree, WorktreeModelHandle}; use serde_json::json; -use settings::{LocalSettingsKind, LocalSettingsPath, SettingsStore, WorktreeId}; +use settings::{LocalSettingsKind, LocalSettingsPath, SettingsStore, SplicingVec, WorktreeId}; use std::{ cell::Cell, env, @@ -2070,7 +2070,7 @@ async fn test_file_scan_inclusions(cx: &mut TestAppContext) { cx.update(|cx| { cx.update_global::(|store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![]); + settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![])); settings.project.worktree.file_scan_inclusions = Some(vec![ "node_modules/**/package.json".to_string(), "**/.DS_Store".to_string(), @@ -2141,7 +2141,7 @@ async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext) cx.update_global::(|store, cx| { store.update_user_settings(cx, |settings| { settings.project.worktree.file_scan_exclusions = - Some(vec!["**/.DS_Store".to_string()]); + Some(SplicingVec::from(vec!["**/.DS_Store".to_string()])); settings.project.worktree.file_scan_inclusions = Some(vec!["**/.DS_Store".to_string()]); }); @@ -2204,7 +2204,7 @@ async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppC cx.update(|cx| { cx.update_global::(|store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![]); + settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![])); settings.project.worktree.file_scan_inclusions = Some(vec!["node_modules/**".to_string()]); }); @@ -2239,7 +2239,7 @@ async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppC cx.update(|cx| { cx.update_global::(|store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![]); + settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![])); settings.project.worktree.file_scan_inclusions = Some(vec![]); }); }); @@ -2291,8 +2291,10 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) { cx.update(|cx| { cx.update_global::(|store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]); + settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![ + "**/foo/**".to_string(), + "**/.DS_Store".to_string(), + ])); }); }); }); @@ -2333,7 +2335,7 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) { cx.update_global::(|store, cx| { store.update_user_settings(cx, |settings| { settings.project.worktree.file_scan_exclusions = - Some(vec!["**/node_modules/**".to_string()]); + Some(SplicingVec::from(vec!["**/node_modules/**".to_string()])); }); }); }); @@ -2487,11 +2489,11 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { cx.update(|cx| { cx.update_global::(|store, cx| { store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![ + settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![ "**/.git".to_string(), "node_modules/".to_string(), "build_output".to_string(), - ]); + ])); }); }); }); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 27cbd24704..5b95dd32a5 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2938,7 +2938,7 @@ mod tests { use remote_server::{HeadlessAppState, HeadlessProject}; use semver::Version; use serde_json::json; - use settings::{SaturatingBool, SettingsStore, watch_config_file}; + use settings::{SaturatingBool, SettingsStore, SplicingVec, watch_config_file}; use std::{ path::{Path, PathBuf}, sync::Arc, @@ -4283,7 +4283,10 @@ mod tests { cx.update_global::(|store, cx| { store.update_user_settings(cx, |project_settings| { project_settings.project.worktree.file_scan_exclusions = - Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]); + Some(SplicingVec::from(vec![ + "excluded_dir".to_string(), + "**/.git".to_string(), + ])); }); }); }); @@ -6319,7 +6322,7 @@ mod tests { cx.update_global::(|store, cx| { store.update_user_settings(cx, |worktree_settings| { worktree_settings.project.worktree.file_scan_exclusions = - Some(vec![".zed".to_string()]); + Some(SplicingVec::from(vec![".zed".to_string()])); }); }); diff --git a/docs/src/reference/all-settings.md b/docs/src/reference/all-settings.md index adaf3a9813..94d7fe0c95 100644 --- a/docs/src/reference/all-settings.md +++ b/docs/src/reference/all-settings.md @@ -2152,6 +2152,8 @@ The result is still `)))` and not `))))))`, which is what it would be by default "**/.svn", "**/.hg", "**/.jj", + "**/.sl", + "**/.repo", "**/CVS", "**/.DS_Store", "**/Thumbs.db", @@ -2161,7 +2163,23 @@ The result is still `)))` and not `))))))`, which is what it would be by default } ``` -Note, specifying `file_scan_exclusions` in settings.json will override the defaults (shown above). If you are looking to exclude additional items you will need to include all the default values in your settings. +Note that specifying `file_scan_exclusions` in `settings.json` will override the defaults (listed above). Use `"..."` to keep them: + +```json [settings] +{ + "file_scan_exclusions": ["**/node_modules", "..."] +} +``` + +The `"..."` entry expands to the list you are overriding, so the example above excludes `node_modules` in addition to every default. Entries you list by name keep their position, and `"..."` fills in the inherited ones at that point in the list. If you want full control over what is excluded, omit `"..."` — only the entries you list by name will be used. + +| Configuration | Result | +| ---------------------------- | ------------------------------------ | +| `["..."]` | The defaults, unchanged | +| `["**/node_modules", "..."]` | `**/node_modules`, then the defaults | +| `["**/node_modules"]` | `**/node_modules` only | + +> Note: `"..."` resolves one settings layer at a time. In a project’s `.zed/settings.json` it expands to whatever your user settings resolved to, rather than to Zed’s defaults. ## File Scan Inclusions