Add support for the "..." entry in file_scan_exclusions (#62769)
## 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.
This commit is contained in:
1 parent
6706fa4651
commit
ef973c67c0
14 files changed
+228
-52
No files matched your search
@@ -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());
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
@@ -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::<SettingsStore, _>(|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()]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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::<SettingsStore, _>(|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()));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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::<SettingsStore, _>(|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(),
|
||||
]);
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1108,7 +1108,8 @@ impl VsCodeSettings {
|
||||
.filter_map(|n| n.as_str().map(str::to_owned))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.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())
|
||||
|
||||
@@ -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<Vec<String>>,
|
||||
pub file_scan_exclusions: Option<SplicingVec>,
|
||||
|
||||
/// 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::<Vec<_>>(),
|
||||
)),
|
||||
..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() {
|
||||
|
||||
@@ -1399,6 +1399,46 @@ impl<T: Clone> merge_from::MergeFrom for ExtendingVec<T> {
|
||||
}
|
||||
}
|
||||
|
||||
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<String>);
|
||||
|
||||
impl From<Vec<String>> for SplicingVec {
|
||||
fn from(vec: Vec<String>) -> 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::<IndexSet<_>>()
|
||||
.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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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::<SettingsStore, _>(|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(),
|
||||
]);
|
||||
]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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::<SettingsStore, _>(|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::<SettingsStore, _>(|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()]));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user