diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index ac94edba23..3bb2b86b9d 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -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::>() - }) - .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::>() - }) - .filter(|patterns| !patterns.is_empty()) - .map(|mut patterns| { - patterns.push(SplicingVec::REST.to_owned()); - SplicingVec::from(patterns) }), + ), } } + + fn enabled_patterns(value: Option<&Value>) -> Option { + 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::>() + }) + .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 { diff --git a/docs/src/migrate/vs-code.md b/docs/src/migrate/vs-code.md index 78cfaca2b0..820889d060 100644 --- a/docs/src/migrate/vs-code.md +++ b/docs/src/migrate/vs-code.md @@ -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` |