Import VS Code files.exclude into file_scan_exclusions (#64492)

## Objective

Import VS Code’s `files.exclude` into `file_scan_exclusions`.

The previous `files.watcherExclude` mapping was dead code: it expected
an array, while VS Code stores an object of `pattern: bool`.
`files.watcherExclude` only stops watching, and the files stay visible
in VS Code, so it is not a match for `file_scan_exclusions`.
`files.exclude` hides files from the explorer and search, which matches
`file_scan_exclusions`, and its default set is nearly identical to
Zed's.

Related to #62769, reported during the review of #64484. A meta
follow-up to #64399.

## Solution

Import nonempty `files.exclude` patterns enabled with `true`, ignoring
disabled and malformed entries, including VS Code's `{"when": ...}`
form.
Append `"..."` so the imported list extends inherited exclusions instead
of replacing them.
Inputs with no usable patterns leave the inherited list unchanged.
`files.watcherExclude` is no longer imported.

## Testing

The new regression tests reproduce the bug and verify the fix.

To check manually, import
`{"files.exclude":{"**/build/**":true,"**/cache/**":false}}` and verify
that `file_scan_exclusions` becomes `["**/build/**", "..."]`, and that
`.git` stays hidden in the project panel.

## 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:

- Fixed importing VS Code `files.exclude` into `file_scan_exclusions`.

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
Dom PoradaandKirill Bulatov authored and GitHub committed 2026-09-20 19:33:06 +00:00
1 parent 313f63eb71
commit 24f3009720
2 files changed
+103 -31

No files matched your search

+102 -30
View File
@@ -1126,25 +1126,15 @@ impl VsCodeSettings {
WorktreeSettingsContent {
prevent_sharing_in_public_channels: false,
file_scan_depth: None,
file_scan_exclusions: self
.read_value("files.watcherExclude")
.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())
.map(SplicingVec::from),
file_scan_exclusions: Self::enabled_patterns(self.read_value("files.exclude")),
// `files.watcherInclude` adds watch roots, not Git-ignore overrides
file_scan_inclusions: None,
scan_symlinks: None,
private_files: None,
hidden_files: None,
// Zed cannot represent the writable exceptions in `files.readonlyExclude`
read_only_files: self
.read_value("files.readonlyInclude")
.filter(|_| {
read_only_files: Self::enabled_patterns(
self.read_value("files.readonlyInclude").filter(|_| {
!self
.read_value("files.readonlyExclude")
.and_then(Value::as_object)
@@ -1153,26 +1143,32 @@ impl VsCodeSettings {
.values()
.any(|enabled| enabled.as_bool() == Some(true))
})
})
.and_then(Value::as_object)
.map(|patterns| {
patterns
.iter()
.filter(|(pattern, enabled)| {
!pattern.is_empty()
&& pattern.as_str() != SplicingVec::REST
&& enabled.as_bool() == Some(true)
})
.map(|(pattern, _)| pattern.to_owned())
.collect::<Vec<_>>()
})
.filter(|patterns| !patterns.is_empty())
.map(|mut patterns| {
patterns.push(SplicingVec::REST.to_owned());
SplicingVec::from(patterns)
}),
),
}
}
fn enabled_patterns(value: Option<&Value>) -> Option<SplicingVec> {
value
.and_then(Value::as_object)
.map(|patterns| {
patterns
.iter()
.filter(|(pattern, enabled)| {
// Zed reserves `...` for inheritance, not a literal path
!pattern.is_empty()
&& pattern.as_str() != SplicingVec::REST
&& enabled.as_bool() == Some(true)
})
.map(|(pattern, _)| pattern.to_owned())
.collect::<Vec<_>>()
})
.filter(|patterns| !patterns.is_empty())
.map(|mut patterns| {
patterns.push(SplicingVec::REST.to_owned());
SplicingVec::from(patterns)
})
}
}
fn translate_vscode_window_title_format(template: &str) -> String {
@@ -1239,6 +1235,82 @@ mod tests {
.reduce_motion
}
#[test]
fn test_import_file_exclusions() -> Result<()> {
let imported = VsCodeSettings::from_str(
r#"{
"files.exclude": {
"": true,
"**/array/**": [],
"**/build/**": true,
"**/cache/**": false,
"**/null/**": null,
"**/number/**": 1,
"**/object/**": {"enabled": true},
"**/string/**": "true",
"**/target/**": true,
"**/*.js": {"when": "$(basename).ts"},
"...": true
}
}"#,
VsCodeSettingsSource::VsCode,
)?
.settings_content();
assert_eq!(
serde_json::to_value(&imported.project.worktree.file_scan_exclusions)?,
serde_json::json!(["**/build/**", "**/target/**", "..."])
);
let mut inherited = WorktreeSettingsContent {
file_scan_exclusions: Some(SplicingVec::from(vec!["**/inherited/**".to_string()])),
..Default::default()
};
inherited.merge_from(&imported.project.worktree);
assert_eq!(
serde_json::to_value(&inherited.file_scan_exclusions)?,
serde_json::json!(["**/build/**", "**/target/**", "**/inherited/**"])
);
Ok(())
}
#[test]
fn test_import_file_exclusions_without_usable_patterns() -> Result<()> {
let inherited = WorktreeSettingsContent {
file_scan_exclusions: Some(SplicingVec::from(vec!["**/inherited/**".to_string()])),
..Default::default()
};
for content in [
r#"{"files.exclude": "**/cache/**"}"#,
r#"{"files.exclude": 1}"#,
r#"{"files.exclude": ["**/cache/**"]}"#,
r#"{"files.exclude": []}"#,
r#"{"files.exclude": null}"#,
r#"{"files.exclude": true}"#,
r#"{"files.exclude": {"": true}}"#,
r#"{"files.exclude": {"**/cache/**": "true"}}"#,
r#"{"files.exclude": {"**/cache/**": false}}"#,
r#"{"files.exclude": {"**/*.js": {"when": "$(basename).ts"}}}"#,
r#"{"files.exclude": {"...": true}}"#,
r#"{"files.exclude": {}}"#,
r#"{"files.watcherExclude": {"**/cache/**": true}}"#,
r#"{}"#,
] {
let imported =
VsCodeSettings::from_str(content, VsCodeSettingsSource::VsCode)?.settings_content();
assert_eq!(
imported.project.worktree.file_scan_exclusions, None,
"{content}"
);
let mut unchanged = inherited.clone();
unchanged.merge_from(&imported.project.worktree);
assert_eq!(
unchanged.file_scan_exclusions, inherited.file_scan_exclusions,
"{content}"
);
}
Ok(())
}
#[test]
fn test_import_watcher_include_preserves_file_scan_inclusions() -> Result<()> {
let inherited = WorktreeSettingsContent {
+1 -1
View File
@@ -87,10 +87,10 @@ The following VS Code settings are automatically imported when you use **Import
| `files.associations` | `file_types` |
| `files.autoSave` | `autosave` |
| `files.autoSaveDelay` | `autosave.milliseconds` |
| `files.exclude` | `file_scan_exclusions` |
| `files.insertFinalNewline` | `ensure_final_newline_on_save` |
| `files.readonlyInclude` | `read_only_files` |
| `files.simpleDialog.enable` | `use_system_path_prompts` |
| `files.watcherExclude` | `file_scan_exclusions` |
| `search.smartCase` | `use_smartcase_search` |
| `search.useIgnoreFiles` | `search.include_ignored` |