diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index b73822723d..da7f73dbd5 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -892,7 +892,10 @@ impl settings::Settings for AllLanguageSettings { file_types.insert( language.clone(), - (builder.build().unwrap(), patterns.0.clone()), + ( + builder.build().unwrap(), + patterns.0.iter().cloned().collect(), + ), ); } diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 8729a7db2c..8ef05e6fef 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -36,7 +36,7 @@ use crate::{ LanguageToSettingsMap, LspSettings, LspSettingsMap, SemanticTokenRules, ThemeName, UserSettingsContentExt, VsCodeSettings, WorktreeId, settings_content::{ - ExtendingVec, ExtensionsSettingsContent, ProfileBase, ProjectSettingsContent, + ExtendingSet, ExtensionsSettingsContent, ProfileBase, ProjectSettingsContent, RootUserSettings, SettingsContent, UserSettingsContent, merge_from::MergeFrom, }, }; @@ -1203,7 +1203,7 @@ impl SettingsStore { }); let file_type_patterns_ref = - generator.subschema_for::>().to_value(); + generator.subschema_for::>().to_value(); replace_subschema::(generator, || { json_schema!({ "type": "object", @@ -2616,6 +2616,32 @@ mod tests { .unindent(), cx, ); + + // re-importing a file association that is already present should be + // idempotent rather than appending a duplicate extension (#56536) + check_vscode_import( + &mut store, + r#"{ + "file_types": { + "c": ["*.keymap"] + } + } + "# + .unindent(), + r#"{ "files.associations": { "*.keymap": "c" } }"#.to_owned(), + r#"{ + "base_keymap": "VSCode", + "minimap": { + "show": "always" + }, + "file_types": { + "c": ["*.keymap"] + } + } + "# + .unindent(), + cx, + ); } #[track_caller] diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 4c2d3d7f63..6db45b1e29 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -644,11 +644,15 @@ impl VsCodeSettings { fn file_types(&self) -> Option { // vscodes file association map is inverted from ours, so we flip the mapping before merging - let mut associations: HashMap, ExtendingVec> = HashMap::default(); + let mut associations: HashMap, ExtendingSet> = HashMap::default(); let map = self.read_value("files.associations")?.as_object()?; for (k, v) in map { let Some(v) = v.as_str() else { continue }; - associations.entry(v.into()).or_default().0.push(k.clone()); + associations + .entry(v.into()) + .or_default() + .0 + .insert(k.clone()); } skip_default(FileTypeMap(associations)) } diff --git a/crates/settings_content/src/language.rs b/crates/settings_content/src/language.rs index 922b5211eb..326513c28e 100644 --- a/crates/settings_content/src/language.rs +++ b/crates/settings_content/src/language.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use settings_macros::{MergeFrom, with_fallible_options}; use std::sync::Arc; -use crate::{DocumentFoldingRanges, DocumentSymbols, ExtendingVec, SemanticTokens, merge_from}; +use crate::{DocumentFoldingRanges, DocumentSymbols, ExtendingSet, SemanticTokens, merge_from}; /// The state of the modifier keys at some point in time #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)] @@ -1203,11 +1203,11 @@ pub struct LanguageToSettingsMap(pub HashMap); /// Map from language name to file patterns. #[with_fallible_options] #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct FileTypeMap(pub HashMap, ExtendingVec>); +pub struct FileTypeMap(pub HashMap, ExtendingSet>); impl<'a> IntoIterator for &'a FileTypeMap { - type Item = (&'a Arc, &'a ExtendingVec); - type IntoIter = std::collections::hash_map::Iter<'a, Arc, ExtendingVec>; + type Item = (&'a Arc, &'a ExtendingSet); + type IntoIter = std::collections::hash_map::Iter<'a, Arc, ExtendingSet>; fn into_iter(self) -> Self::IntoIter { self.0.iter() diff --git a/crates/settings_content/src/settings_content.rs b/crates/settings_content/src/settings_content.rs index 0edd11fdea..42d1a242df 100644 --- a/crates/settings_content/src/settings_content.rs +++ b/crates/settings_content/src/settings_content.rs @@ -32,7 +32,7 @@ pub use theme::*; pub use title_bar::*; pub use workspace::*; -use collections::{HashMap, IndexMap}; +use collections::{HashMap, IndexMap, IndexSet}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings_macros::{MergeFrom, with_fallible_options}; @@ -1397,6 +1397,27 @@ impl merge_from::MergeFrom for ExtendingVec { } } +// 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. +// +// Insertion order is preserved, so it round-trips through the user's settings +// file without reordering their entries. +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct ExtendingSet(pub IndexSet); + +impl From> for ExtendingSet { + fn from(vec: Vec) -> Self { + ExtendingSet(vec.into_iter().collect()) + } +} + +impl merge_from::MergeFrom for ExtendingSet { + fn merge_from(&mut self, other: &Self) { + self.0.extend(other.0.iter().cloned()); + } +} + // A SaturatingBool in the settings can only ever be set to true, // later attempts to set it to false will be ignored. //