Add support for the "..." entry in file_scan_inclusions (#64440)
## Objective Let projects add inclusion patterns without repeating defaults or patterns inherited from user settings. Part of #64399. ## Solution Extend the `"..."` behavior from #62769 and #64222 to `file_scan_inclusions` using the shared merge implementation. `"..."` includes the resolved inherited list, omitting it replaces the list, and `[]` clears it. Duplicate entries retain their first occurrence. Stop importing VS Code’s `files.watcherInclude` as inclusion patterns because it configures additional watch roots rather than overriding Git ignores. ## Testing Tested on macOS with settings merge, VS Code import, and worktree integration tests. Coverage includes preservation of the `.env*` default, layered inheritance, replacement, clearing, and deduplication. The worktree test verifies that inherited and project-specific patterns include Git-ignored files while exclusions still take precedence. To check manually, configure different user and project inclusion patterns and add ignored files matching each. Include `"..."` to retain inherited patterns, then repeat without it and with `[]` to verify replacement and clearing. Confirm that excluded files remain absent. ## 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 `file_scan_inclusions` so project settings can extend inherited inclusion patterns.
This commit is contained in:
1 parent
18960e899a
commit
2229b4e2d6
8 files changed
+279
-34
No files matched your search
@@ -7,7 +7,7 @@ use menu::{Cancel, Confirm, SelectNext, SelectPrevious};
|
||||
use pretty_assertions::{assert_eq, assert_matches};
|
||||
use project::{FS_WATCH_LATENCY, RemoveOptions};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use settings::{SettingsStore, SplicingVec};
|
||||
use util::{path, rel_path::rel_path};
|
||||
use workspace::{
|
||||
AppState, CloseActiveItem, Item, MultiWorkspace, OpenOptions, ToggleFileFinder, Workspace,
|
||||
@@ -1003,10 +1003,10 @@ async fn test_ignored_root_with_file_inclusions(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.worktree.file_scan_inclusions = Some(vec![
|
||||
"height_demo/**/hi_bonjour".to_string(),
|
||||
settings.project.worktree.file_scan_inclusions = Some(SplicingVec::from(vec![
|
||||
"**/height_1".to_string(),
|
||||
]);
|
||||
"height_demo/**/hi_bonjour".to_string(),
|
||||
]));
|
||||
});
|
||||
})
|
||||
});
|
||||
@@ -1097,7 +1097,8 @@ async fn test_ignored_root_with_file_inclusions_repro(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.worktree.file_scan_inclusions = Some(vec!["**/.env".to_string()]);
|
||||
settings.project.worktree.file_scan_inclusions =
|
||||
Some(SplicingVec::from(vec!["**/.env".to_string()]));
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
@@ -6206,8 +6206,9 @@ async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
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.project.worktree.file_scan_inclusions = Some(SplicingVec::from(vec![
|
||||
"always_included_but_ignored_dir/*".to_string(),
|
||||
]));
|
||||
settings
|
||||
.project_panel
|
||||
.get_or_insert_default()
|
||||
|
||||
@@ -1136,15 +1136,8 @@ impl VsCodeSettings {
|
||||
})
|
||||
.filter(|r| !r.is_empty())
|
||||
.map(SplicingVec::from),
|
||||
file_scan_inclusions: self
|
||||
.read_value("files.watcherInclude")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|v| {
|
||||
v.iter()
|
||||
.filter_map(|n| n.as_str().map(str::to_owned))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.filter(|r| !r.is_empty()),
|
||||
// `files.watcherInclude` adds watch roots, not Git-ignore overrides
|
||||
file_scan_inclusions: None,
|
||||
scan_symlinks: None,
|
||||
private_files: None,
|
||||
hidden_files: None,
|
||||
@@ -1243,6 +1236,46 @@ mod tests {
|
||||
.reduce_motion
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_import_watcher_include_preserves_file_scan_inclusions() -> Result<()> {
|
||||
let inherited = WorktreeSettingsContent {
|
||||
file_scan_inclusions: Some(SplicingVec::from(vec![
|
||||
".env*".to_string(),
|
||||
"**/*.local".to_string(),
|
||||
])),
|
||||
..Default::default()
|
||||
};
|
||||
for content in [
|
||||
r#"{}"#,
|
||||
r#"{"files.watcherInclude": []}"#,
|
||||
r#"{"files.watcherInclude": ["linked-folder"]}"#,
|
||||
r#"{"files.watcherInclude": ["..."]}"#,
|
||||
r#"{"files.watcherInclude": ["linked-folder", false]}"#,
|
||||
r#"{"files.watcherInclude": {"linked-folder": true}}"#,
|
||||
] {
|
||||
let mut content: Value = serde_json::from_str(content)?;
|
||||
content["editor.tabSize"] = serde_json::json!(8);
|
||||
let imported = VsCodeSettings::from_str(
|
||||
&serde_json::to_string(&content)?,
|
||||
VsCodeSettingsSource::VsCode,
|
||||
)?
|
||||
.settings_content();
|
||||
assert_eq!(imported.project.worktree.file_scan_inclusions, None);
|
||||
assert_eq!(
|
||||
imported.project.all_languages.defaults.tab_size,
|
||||
NonZeroU32::new(8)
|
||||
);
|
||||
|
||||
let mut unchanged = inherited.clone();
|
||||
unchanged.merge_from(&imported.project.worktree);
|
||||
assert_eq!(
|
||||
unchanged.file_scan_inclusions,
|
||||
inherited.file_scan_inclusions
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_import_read_only_files() -> Result<()> {
|
||||
let inherited = WorktreeSettingsContent {
|
||||
|
||||
@@ -148,13 +148,15 @@ pub struct WorktreeSettingsContent {
|
||||
/// ]
|
||||
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`.
|
||||
/// Default: [
|
||||
/// ".env*",
|
||||
/// "docker-compose.*.yml",
|
||||
/// ]
|
||||
pub file_scan_inclusions: Option<Vec<String>>,
|
||||
/// 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`.
|
||||
///
|
||||
/// A "..." entry expands to the value being overridden. Leave "..." out to
|
||||
/// replace the inherited globs, or use an empty list to clear them.
|
||||
///
|
||||
/// Default: [".env*"]
|
||||
pub file_scan_inclusions: Option<SplicingVec>,
|
||||
|
||||
/// When to scan content of linked directories.
|
||||
///
|
||||
@@ -1044,6 +1046,74 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
fn inclusions(globs: &[&str]) -> WorktreeSettingsContent {
|
||||
WorktreeSettingsContent {
|
||||
file_scan_inclusions: Some(SplicingVec::from(
|
||||
globs
|
||||
.iter()
|
||||
.map(|glob| glob.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_scan_inclusions_splice_each_layer() {
|
||||
let mut settings = inclusions(&[".env*"]);
|
||||
settings.merge_from(&inclusions(&[SplicingVec::REST, "**/*.user"]));
|
||||
assert_eq!(settings, inclusions(&[".env*", "**/*.user"]));
|
||||
|
||||
settings.merge_from(&inclusions(&["**/*.project", SplicingVec::REST]));
|
||||
assert_eq!(
|
||||
settings,
|
||||
inclusions(&["**/*.project", ".env*", "**/*.user"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_scan_inclusions_replace_and_clear() {
|
||||
let mut settings = inclusions(&[".env*"]);
|
||||
settings.merge_from(&inclusions(&["**/*.user"]));
|
||||
assert_eq!(settings, inclusions(&["**/*.user"]));
|
||||
|
||||
settings.merge_from(&inclusions(&["**/*.project"]));
|
||||
assert_eq!(settings, inclusions(&["**/*.project"]));
|
||||
|
||||
settings.merge_from(&inclusions(&[]));
|
||||
assert_eq!(settings, inclusions(&[]));
|
||||
|
||||
settings.merge_from(&inclusions(&[SplicingVec::REST, "**/*.next"]));
|
||||
assert_eq!(settings, inclusions(&["**/*.next"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_scan_inclusions_splice_preserves_first_occurrence() {
|
||||
let inherited = inclusions(&[".env*", "**/*.user"]);
|
||||
let mut settings = inherited.clone();
|
||||
settings.merge_from(&WorktreeSettingsContent::default());
|
||||
assert_eq!(settings, inherited);
|
||||
|
||||
settings.merge_from(&inclusions(&[SplicingVec::REST, SplicingVec::REST]));
|
||||
assert_eq!(settings, inherited);
|
||||
|
||||
settings.merge_from(&inclusions(&[
|
||||
"**/*.user",
|
||||
SplicingVec::REST,
|
||||
".env*",
|
||||
"**/*.project",
|
||||
SplicingVec::REST,
|
||||
"**/*.project",
|
||||
]));
|
||||
assert_eq!(
|
||||
settings,
|
||||
inclusions(&["**/*.user", ".env*", "**/*.project"])
|
||||
);
|
||||
|
||||
settings.merge_from(&inclusions(&["**/*.project", "**/*.user", "**/*.project"]));
|
||||
assert_eq!(settings, inclusions(&["**/*.project", "**/*.user"]));
|
||||
}
|
||||
|
||||
fn read_only_files(globs: &[&str]) -> WorktreeSettingsContent {
|
||||
WorktreeSettingsContent {
|
||||
read_only_files: Some(SplicingVec::from(
|
||||
|
||||
@@ -61,7 +61,7 @@ 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().0;
|
||||
let file_scan_inclusions = worktree.file_scan_inclusions.unwrap();
|
||||
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 read_only_files = worktree.read_only_files.unwrap_or_default().0;
|
||||
|
||||
@@ -2067,10 +2067,10 @@ async fn test_file_scan_inclusions(cx: &mut TestAppContext) {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![]));
|
||||
settings.project.worktree.file_scan_inclusions = Some(vec![
|
||||
"node_modules/**/package.json".to_string(),
|
||||
settings.project.worktree.file_scan_inclusions = Some(SplicingVec::from(vec![
|
||||
"**/.DS_Store".to_string(),
|
||||
]);
|
||||
"node_modules/**/package.json".to_string(),
|
||||
]));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2108,6 +2108,135 @@ async fn test_file_scan_inclusions(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_file_scan_inclusions_from_project_settings(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let worktree_id = WorktreeId::from_proto(0);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![
|
||||
"...".to_string(),
|
||||
"ignored/**/excluded.project".to_string(),
|
||||
]));
|
||||
settings.project.worktree.file_scan_inclusions = Some(SplicingVec::from(vec![
|
||||
"...".to_string(),
|
||||
"ignored/**/*.user".to_string(),
|
||||
]));
|
||||
});
|
||||
store
|
||||
.set_local_settings(
|
||||
worktree_id,
|
||||
LocalSettingsPath::InWorktree(Arc::from(RelPath::empty())),
|
||||
LocalSettingsKind::Settings,
|
||||
Some(r#"{ "file_scan_inclusions": ["...", "ignored/**/*.project"] }"#),
|
||||
cx,
|
||||
)
|
||||
.expect("valid project settings");
|
||||
});
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
".env.local": "",
|
||||
".git": {},
|
||||
".gitignore": ".env*\n/ignored/\nunmatched.txt\n",
|
||||
"ignored": {
|
||||
"nested": {
|
||||
"excluded.project": "",
|
||||
"included.project": "",
|
||||
"included.user": "",
|
||||
"unmatched.txt": ""
|
||||
}
|
||||
},
|
||||
"unmatched.txt": ""
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let tree = build_worktree(fs, path!("/root"), cx).await;
|
||||
tree.read_with(cx, |tree, _| {
|
||||
for path in [
|
||||
".env.local",
|
||||
"ignored",
|
||||
"ignored/nested",
|
||||
"ignored/nested/included.project",
|
||||
"ignored/nested/included.user",
|
||||
] {
|
||||
assert!(
|
||||
tree.entry_for_path(rel_path(path))
|
||||
.is_some_and(|entry| entry.is_ignored && entry.is_always_included),
|
||||
"expected {path} to be ignored by Git and included by settings"
|
||||
);
|
||||
}
|
||||
for path in ["ignored/nested/unmatched.txt", "unmatched.txt"] {
|
||||
assert!(
|
||||
tree.entry_for_path(rel_path(path))
|
||||
.is_some_and(|entry| entry.is_ignored && !entry.is_always_included),
|
||||
"expected {path} to remain ignored"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
tree.entry_for_path(rel_path("ignored/nested/excluded.project")),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
tree.files(false, 0)
|
||||
.map(|entry| entry.path.as_ref())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
rel_path(".env.local"),
|
||||
rel_path(".gitignore"),
|
||||
rel_path("ignored/nested/included.project"),
|
||||
rel_path("ignored/nested/included.user"),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
for (settings, included_paths) in [
|
||||
(
|
||||
r#"{ "file_scan_inclusions": ["ignored/**/*.project"] }"#,
|
||||
vec![
|
||||
rel_path(".gitignore"),
|
||||
rel_path("ignored/nested/included.project"),
|
||||
],
|
||||
),
|
||||
(
|
||||
r#"{ "file_scan_inclusions": [] }"#,
|
||||
vec![rel_path(".gitignore")],
|
||||
),
|
||||
] {
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store
|
||||
.set_local_settings(
|
||||
worktree_id,
|
||||
LocalSettingsPath::InWorktree(Arc::from(RelPath::empty())),
|
||||
LocalSettingsKind::Settings,
|
||||
Some(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.files(false, 0)
|
||||
.map(|entry| entry.path.as_ref())
|
||||
.collect::<Vec<_>>(),
|
||||
included_paths
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -2139,7 +2268,7 @@ async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext)
|
||||
settings.project.worktree.file_scan_exclusions =
|
||||
Some(SplicingVec::from(vec!["**/.DS_Store".to_string()]));
|
||||
settings.project.worktree.file_scan_inclusions =
|
||||
Some(vec!["**/.DS_Store".to_string()]);
|
||||
Some(SplicingVec::from(vec!["**/.DS_Store".to_string()]));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2202,7 +2331,7 @@ async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppC
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![]));
|
||||
settings.project.worktree.file_scan_inclusions =
|
||||
Some(vec!["node_modules/**".to_string()]);
|
||||
Some(SplicingVec::from(vec!["node_modules/**".to_string()]));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2236,7 +2365,7 @@ async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppC
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.worktree.file_scan_exclusions = Some(SplicingVec::from(vec![]));
|
||||
settings.project.worktree.file_scan_inclusions = Some(vec![]);
|
||||
settings.project.worktree.file_scan_inclusions = Some(SplicingVec::from(vec![]));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6818,7 +6947,7 @@ async fn test_file_scan_depth_pierced_by_inclusions(cx: &mut TestAppContext) {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.worktree.file_scan_depth = Some(1);
|
||||
settings.project.worktree.file_scan_inclusions =
|
||||
Some(vec!["junk/a/b/c/deep.txt".to_string()]);
|
||||
Some(SplicingVec::from(vec!["junk/a/b/c/deep.txt".to_string()]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,13 +84,12 @@ The following VS Code settings are automatically imported when you use **Import
|
||||
|
||||
| VS Code Setting | Zed Setting |
|
||||
| --------------------------- | ------------------------------ |
|
||||
| `files.associations` | `file_types` |
|
||||
| `files.autoSave` | `autosave` |
|
||||
| `files.autoSaveDelay` | `autosave.milliseconds` |
|
||||
| `files.insertFinalNewline` | `ensure_final_newline_on_save` |
|
||||
| `files.associations` | `file_types` |
|
||||
| `files.watcherExclude` | `file_scan_exclusions` |
|
||||
| `files.watcherInclude` | `file_scan_inclusions` |
|
||||
| `files.simpleDialog.enable` | `use_system_path_prompts` |
|
||||
| `files.watcherExclude` | `file_scan_exclusions` |
|
||||
| `search.smartCase` | `use_smartcase_search` |
|
||||
| `search.useIgnoreFiles` | `search.include_ignored` |
|
||||
|
||||
|
||||
@@ -2383,6 +2383,18 @@ The `"..."` entry expands to the list you are overriding, so the example above e
|
||||
}
|
||||
```
|
||||
|
||||
Use `"..."` to extend the inherited list instead of replacing it. In your user settings, it expands to Zed’s defaults. In a project’s `.zed/settings.json`, it expands to the resolved list from your user settings, including any defaults you kept.
|
||||
|
||||
For example, add this to your settings.json to include generated files without repeating inherited patterns:
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
"file_scan_inclusions": ["...", "generated/**"]
|
||||
}
|
||||
```
|
||||
|
||||
With the default user settings, this keeps `.env*` and adds `generated/**`. Omit `"..."` to replace the inherited list, or use `[]` to clear it. Duplicate patterns keep their first occurrence, even if `"..."` appears more than once. `file_scan_exclusions` still takes precedence.
|
||||
|
||||
## File Scan Depth
|
||||
|
||||
- Setting: `file_scan_depth`
|
||||
|
||||
Reference in new issue
Block a user