use anyhow::{Context as _, Result}; use collections::{BTreeMap, HashMap, btree_map, hash_map}; use ec4rs::{ConfigParser, PropertiesSource, Section}; use fs::Fs; use futures::{ FutureExt, StreamExt, channel::{mpsc, oneshot}, future::LocalBoxFuture, }; use gpui::{App, AsyncApp, BorrowAppContext, Global, SharedString, Task, UpdateGlobal}; use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; use schemars::JsonSchema; use serde_json::Value; use smallvec::SmallVec; use std::{ any::{Any, TypeId, type_name}, fmt::Debug, ops::Range, path::{Path, PathBuf}, str::{self, FromStr}, sync::Arc, }; use util::{ResultExt as _, schemars::DefaultDenyUnknownFields}; pub type EditorconfigProperties = ec4rs::Properties; use crate::{ ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, SettingsUiEntry, VsCodeSettings, WorktreeId, parse_json_with_comments, replace_value_in_json_text, settings_content::{ ExtensionsSettingsContent, ProjectSettingsContent, ServerSettingsContent, SettingsContent, UserSettingsContent, }, update_value_in_json_text, }; pub trait SettingsKey: 'static + Send + Sync { /// The name of a key within the JSON file from which this setting should /// be deserialized. If this is `None`, then the setting will be deserialized /// from the root object. const KEY: Option<&'static str>; const FALLBACK_KEY: Option<&'static str> = None; } /// A value that can be defined as a user setting. /// /// Settings can be loaded from a combination of multiple JSON files. pub trait Settings: 'static + Send + Sync + Sized { /// The name of the keys in the [`FileContent`](Self::FileContent) that should /// always be written to a settings file, even if their value matches the default /// value. /// /// This is useful for tagged [`FileContent`](Self::FileContent)s where the tag /// is a "version" field that should always be persisted, even if the current /// user settings match the current version of the settings. const PRESERVED_KEYS: Option<&'static [&'static str]> = None; /// Read the value from default.json. /// This function *should* panic if default values are missing, /// and you should add a default to default.json for documentation. fn from_defaults(content: &SettingsContent, cx: &mut App) -> Self; /// Update the value based on the content from the current file. /// /// This function *should not* panic if there are problems, as the /// content of user-provided settings files may be incomplete or invalid. fn refine(&mut self, content: &SettingsContent, cx: &mut App); fn missing_default() -> anyhow::Error { anyhow::anyhow!("missing default for: {}", std::any::type_name::()) } /// Use [the helpers in the vscode_import module](crate::vscode_import) to apply known /// equivalent settings from a vscode config to our config fn import_from_vscode(_vscode: &VsCodeSettings, _current: &mut SettingsContent) {} #[track_caller] fn register(cx: &mut App) where Self: Sized, { SettingsStore::update_global(cx, |store, cx| { store.register_setting::(cx); }); } #[track_caller] fn get<'a>(path: Option, cx: &'a App) -> &'a Self where Self: Sized, { cx.global::().get(path) } #[track_caller] fn get_global(cx: &App) -> &Self where Self: Sized, { cx.global::().get(None) } #[track_caller] fn try_get(cx: &App) -> Option<&Self> where Self: Sized, { if cx.has_global::() { cx.global::().try_get(None) } else { None } } #[track_caller] fn try_read_global(cx: &AsyncApp, f: impl FnOnce(&Self) -> R) -> Option where Self: Sized, { cx.try_read_global(|s: &SettingsStore, _| f(s.get(None))) } #[track_caller] fn override_global(settings: Self, cx: &mut App) where Self: Sized, { cx.global_mut::().override_global(settings) } } #[derive(Clone, Copy, Debug)] pub struct SettingsLocation<'a> { pub worktree_id: WorktreeId, pub path: &'a Path, } /// A set of strongly-typed setting values defined via multiple config files. pub struct SettingsStore { setting_values: HashMap>, default_settings: Box, user_settings: Option, global_settings: Option>, extension_settings: Option>, server_settings: Option>, local_settings: BTreeMap<(WorktreeId, Arc), SettingsContent>, raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc), (String, Option)>, _setting_file_updates: Task<()>, setting_file_updates_tx: mpsc::UnboundedSender LocalBoxFuture<'static, Result<()>>>>, } #[derive(Clone)] pub struct Editorconfig { pub is_root: bool, pub sections: SmallVec<[Section; 5]>, } impl FromStr for Editorconfig { type Err = anyhow::Error; fn from_str(contents: &str) -> Result { let parser = ConfigParser::new_buffered(contents.as_bytes()) .context("creating editorconfig parser")?; let is_root = parser.is_root; let sections = parser .collect::, _>>() .context("parsing editorconfig sections")?; Ok(Self { is_root, sections }) } } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum LocalSettingsKind { Settings, Tasks, Editorconfig, Debug, } impl Global for SettingsStore {} #[derive(Debug)] struct SettingValue { global_value: Option, local_values: Vec<(WorktreeId, Arc, T)>, } trait AnySettingValue: 'static + Send + Sync { fn setting_type_name(&self) -> &'static str; fn from_default(&self, s: &SettingsContent, cx: &mut App) -> Box; fn refine(&self, value: &mut dyn Any, s: &[&SettingsContent], cx: &mut App); fn value_for_path(&self, path: Option) -> &dyn Any; fn all_local_values(&self) -> Vec<(WorktreeId, Arc, &dyn Any)>; fn set_global_value(&mut self, value: Box); fn set_local_value(&mut self, root_id: WorktreeId, path: Arc, value: Box); fn import_from_vscode( &self, vscode_settings: &VsCodeSettings, settings_content: &mut SettingsContent, ); } impl SettingsStore { pub fn new(cx: &App, default_settings: &str) -> Self { let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded(); let default_settings = parse_json_with_comments(default_settings).unwrap(); Self { setting_values: Default::default(), default_settings, global_settings: None, server_settings: None, user_settings: None, extension_settings: None, local_settings: BTreeMap::default(), raw_editorconfig_settings: BTreeMap::default(), setting_file_updates_tx, _setting_file_updates: cx.spawn(async move |cx| { while let Some(setting_file_update) = setting_file_updates_rx.next().await { (setting_file_update)(cx.clone()).await.log_err(); } }), } } pub fn observe_active_settings_profile_name(cx: &mut App) -> gpui::Subscription { cx.observe_global::(|cx| { Self::update_global(cx, |store, cx| { store.recompute_values(None, cx).log_err(); }); }) } pub fn update(cx: &mut C, f: impl FnOnce(&mut Self, &mut C) -> R) -> R where C: BorrowAppContext, { cx.update_global(f) } /// Add a new type of setting to the store. pub fn register_setting(&mut self, cx: &mut App) { let setting_type_id = TypeId::of::(); let entry = self.setting_values.entry(setting_type_id); if matches!(entry, hash_map::Entry::Occupied(_)) { return; } let setting_value = entry.or_insert(Box::new(SettingValue:: { global_value: None, local_values: Vec::new(), })); let mut refinements = Vec::default(); if let Some(extension_settings) = self.extension_settings.as_deref() { refinements.push(extension_settings) } if let Some(global_settings) = self.global_settings.as_deref() { refinements.push(global_settings) } if let Some(user_settings) = self.user_settings.as_ref() { refinements.push(&user_settings.content); if let Some(release_channel) = user_settings.for_release_channel() { refinements.push(release_channel) } if let Some(os) = user_settings.for_os() { refinements.push(os) } if let Some(profile) = user_settings.for_profile(cx) { refinements.push(profile) } } if let Some(server_settings) = self.server_settings.as_ref() { refinements.push(server_settings) } let mut value = T::from_defaults(&self.default_settings, cx); for refinement in refinements { value.refine(refinement, cx) } setting_value.set_global_value(Box::new(value)); } /// Get the value of a setting. /// /// Panics if the given setting type has not been registered, or if there is no /// value for this setting. pub fn get(&self, path: Option) -> &T { self.setting_values .get(&TypeId::of::()) .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) .value_for_path(path) .downcast_ref::() .expect("no default value for setting type") } /// Get the value of a setting. /// /// Does not panic pub fn try_get(&self, path: Option) -> Option<&T> { self.setting_values .get(&TypeId::of::()) .map(|value| value.value_for_path(path)) .and_then(|value| value.downcast_ref::()) } /// Get all values from project specific settings pub fn get_all_locals(&self) -> Vec<(WorktreeId, Arc, &T)> { self.setting_values .get(&TypeId::of::()) .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) .all_local_values() .into_iter() .map(|(id, path, any)| { ( id, path, any.downcast_ref::() .expect("wrong value type for setting"), ) }) .collect() } /// Override the global value for a setting. /// /// The given value will be overwritten if the user settings file changes. pub fn override_global(&mut self, value: T) { self.setting_values .get_mut(&TypeId::of::()) .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) .set_global_value(Box::new(value)) } /// Get the user's settings as a raw JSON value. /// /// For user-facing functionality use the typed setting interface. /// (e.g. ProjectSettings::get_global(cx)) pub fn raw_user_settings(&self) -> Option<&UserSettingsContent> { self.user_settings.as_ref() } /// Replaces current settings with the values from the given JSON. pub fn set_raw_user_settings( &mut self, new_settings: UserSettingsContent, cx: &mut App, ) -> Result<()> { self.user_settings = Some(new_settings); self.recompute_values(None, cx)?; Ok(()) } /// Replaces current settings with the values from the given JSON. pub fn set_raw_server_settings( &mut self, new_settings: Option, cx: &mut App, ) -> Result<()> { // Rewrite the server settings into a content type self.server_settings = new_settings .map(|settings| settings.to_string()) .and_then(|str| parse_json_with_comments::(&str).ok()) .map(Box::new); self.recompute_values(None, cx)?; Ok(()) } /// Get the configured settings profile names. pub fn configured_settings_profiles(&self) -> impl Iterator { self.user_settings .iter() .flat_map(|settings| settings.profiles.keys().map(|k| k.as_str())) } /// Access the raw JSON value of the default settings. pub fn raw_default_settings(&self) -> &SettingsContent { &self.default_settings } #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut App) -> Self { Self::new(cx, &crate::test_settings()) } /// Updates the value of a setting in the user's global configuration. /// /// This is only for tests. Normally, settings are only loaded from /// JSON files. #[cfg(any(test, feature = "test-support"))] pub fn update_user_settings( &mut self, cx: &mut App, update: impl FnOnce(&mut SettingsContent), ) { let mut content = self.user_settings.clone().unwrap_or_default().content; update(&mut content); let new_text = serde_json::to_string(&UserSettingsContent { content, ..Default::default() }) .unwrap(); self.set_user_settings(&new_text, cx).unwrap(); } pub async fn load_settings(fs: &Arc) -> Result { match fs.load(paths::settings_file()).await { result @ Ok(_) => result, Err(err) => { if let Some(e) = err.downcast_ref::() && e.kind() == std::io::ErrorKind::NotFound { return Ok(crate::initial_user_settings_content().to_string()); } Err(err) } } } fn update_settings_file_inner( &self, fs: Arc, update: impl 'static + Send + FnOnce(String, AsyncApp) -> Result, ) -> oneshot::Receiver> { let (tx, rx) = oneshot::channel::>(); self.setting_file_updates_tx .unbounded_send(Box::new(move |cx: AsyncApp| { async move { let res = async move { let old_text = Self::load_settings(&fs).await?; let new_text = update(old_text, cx)?; let settings_path = paths::settings_file().as_path(); if fs.is_file(settings_path).await { let resolved_path = fs.canonicalize(settings_path).await.with_context(|| { format!( "Failed to canonicalize settings path {:?}", settings_path ) })?; fs.atomic_write(resolved_path.clone(), new_text) .await .with_context(|| { format!("Failed to write settings to file {:?}", resolved_path) })?; } else { fs.atomic_write(settings_path.to_path_buf(), new_text) .await .with_context(|| { format!("Failed to write settings to file {:?}", settings_path) })?; } anyhow::Ok(()) } .await; let new_res = match &res { Ok(_) => anyhow::Ok(()), Err(e) => Err(anyhow::anyhow!("Failed to write settings to file {:?}", e)), }; _ = tx.send(new_res); res } .boxed_local() })) .map_err(|err| anyhow::format_err!("Failed to update settings file: {}", err)) .log_with_level(log::Level::Warn); return rx; } pub fn update_settings_file_at_path( &self, fs: Arc, path: &[impl AsRef], new_value: serde_json::Value, ) -> oneshot::Receiver> { let key_path = path .into_iter() .map(AsRef::as_ref) .map(SharedString::new) .collect::>(); let update = move |mut old_text: String, cx: AsyncApp| { cx.read_global(|store: &SettingsStore, _cx| { // todo(settings_ui) use `update_value_in_json_text` for merging new and old objects with comment preservation, needs old value though... let (range, replacement) = replace_value_in_json_text( &old_text, key_path.as_slice(), store.json_tab_size(), Some(&new_value), None, ); old_text.replace_range(range, &replacement); old_text }) }; self.update_settings_file_inner(fs, update) } pub fn update_settings_file( &self, fs: Arc, update: impl 'static + Send + FnOnce(&mut SettingsContent, &App), ) { _ = self.update_settings_file_inner(fs, move |old_text: String, cx: AsyncApp| { cx.read_global(|store: &SettingsStore, cx| { store.new_text_for_update(old_text, |content| update(content, cx)) }) }); } pub fn import_vscode_settings( &self, fs: Arc, vscode_settings: VsCodeSettings, ) -> oneshot::Receiver> { self.update_settings_file_inner(fs, move |old_text: String, cx: AsyncApp| { cx.read_global(|store: &SettingsStore, _cx| { store.get_vscode_edits(old_text, &vscode_settings) }) }) } pub fn settings_ui_items(&self) -> impl IntoIterator { [].into_iter() } } impl SettingsStore { /// Updates the value of a setting in a JSON file, returning the new text /// for that JSON file. pub fn new_text_for_update( &self, old_text: String, update: impl FnOnce(&mut SettingsContent), ) -> String { let edits = self.edits_for_update(&old_text, update); let mut new_text = old_text; for (range, replacement) in edits.into_iter() { new_text.replace_range(range, &replacement); } new_text } pub fn get_vscode_edits(&self, old_text: String, vscode: &VsCodeSettings) -> String { self.new_text_for_update(old_text, |settings_content| { for v in self.setting_values.values() { v.import_from_vscode(vscode, settings_content) } }) } /// Updates the value of a setting in a JSON file, returning a list /// of edits to apply to the JSON file. pub fn edits_for_update( &self, text: &str, update: impl FnOnce(&mut SettingsContent), ) -> Vec<(Range, String)> { let old_content: UserSettingsContent = parse_json_with_comments(text).log_err().unwrap_or_default(); let mut new_content = old_content.clone(); update(&mut new_content.content); let old_value = serde_json::to_value(&old_content).unwrap(); let new_value = serde_json::to_value(new_content).unwrap(); let mut key_path = Vec::new(); let mut edits = Vec::new(); let tab_size = self.json_tab_size(); let mut text = text.to_string(); update_value_in_json_text( &mut text, &mut key_path, tab_size, &old_value, &new_value, &mut edits, ); edits } pub fn json_tab_size(&self) -> usize { 2 } /// Sets the default settings via a JSON string. /// /// The string should contain a JSON object with a default value for every setting. pub fn set_default_settings( &mut self, default_settings_content: &str, cx: &mut App, ) -> Result<()> { self.default_settings = parse_json_with_comments(default_settings_content)?; self.recompute_values(None, cx)?; Ok(()) } /// Sets the user settings via a JSON string. pub fn set_user_settings(&mut self, user_settings_content: &str, cx: &mut App) -> Result<()> { let settings: UserSettingsContent = if user_settings_content.is_empty() { parse_json_with_comments("{}")? } else { parse_json_with_comments(user_settings_content)? }; self.user_settings = Some(settings); self.recompute_values(None, cx)?; Ok(()) } /// Sets the global settings via a JSON string. pub fn set_global_settings( &mut self, global_settings_content: &str, cx: &mut App, ) -> Result<()> { let settings: SettingsContent = if global_settings_content.is_empty() { parse_json_with_comments("{}")? } else { parse_json_with_comments(global_settings_content)? }; self.global_settings = Some(Box::new(settings)); self.recompute_values(None, cx)?; Ok(()) } pub fn set_server_settings( &mut self, server_settings_content: &str, cx: &mut App, ) -> Result<()> { let settings: Option = if server_settings_content.is_empty() { None } else { parse_json_with_comments(server_settings_content)? }; // Rewrite the server settings into a content type self.server_settings = settings.map(|settings| { Box::new(SettingsContent { project: settings.project, ..Default::default() }) }); self.recompute_values(None, cx)?; Ok(()) } /// Add or remove a set of local settings via a JSON string. pub fn set_local_settings( &mut self, root_id: WorktreeId, directory_path: Arc, kind: LocalSettingsKind, settings_content: Option<&str>, cx: &mut App, ) -> std::result::Result<(), InvalidSettingsError> { let mut zed_settings_changed = false; match ( kind, settings_content .map(|content| content.trim()) .filter(|content| !content.is_empty()), ) { (LocalSettingsKind::Tasks, _) => { return Err(InvalidSettingsError::Tasks { message: "Attempted to submit tasks into the settings store".to_string(), path: directory_path.join(task_file_name()), }); } (LocalSettingsKind::Debug, _) => { return Err(InvalidSettingsError::Debug { message: "Attempted to submit debugger config into the settings store" .to_string(), path: directory_path.join(task_file_name()), }); } (LocalSettingsKind::Settings, None) => { zed_settings_changed = self .local_settings .remove(&(root_id, directory_path.clone())) .is_some() } (LocalSettingsKind::Editorconfig, None) => { self.raw_editorconfig_settings .remove(&(root_id, directory_path.clone())); } (LocalSettingsKind::Settings, Some(settings_contents)) => { let new_settings = parse_json_with_comments::( settings_contents, ) .map_err(|e| InvalidSettingsError::LocalSettings { path: directory_path.join(local_settings_file_relative_path()), message: e.to_string(), })?; match self.local_settings.entry((root_id, directory_path.clone())) { btree_map::Entry::Vacant(v) => { v.insert(SettingsContent { project: new_settings, ..Default::default() }); zed_settings_changed = true; } btree_map::Entry::Occupied(mut o) => { if &o.get().project != &new_settings { o.insert(SettingsContent { project: new_settings, ..Default::default() }); zed_settings_changed = true; } } } } (LocalSettingsKind::Editorconfig, Some(editorconfig_contents)) => { match self .raw_editorconfig_settings .entry((root_id, directory_path.clone())) { btree_map::Entry::Vacant(v) => match editorconfig_contents.parse() { Ok(new_contents) => { v.insert((editorconfig_contents.to_owned(), Some(new_contents))); } Err(e) => { v.insert((editorconfig_contents.to_owned(), None)); return Err(InvalidSettingsError::Editorconfig { message: e.to_string(), path: directory_path.join(EDITORCONFIG_NAME), }); } }, btree_map::Entry::Occupied(mut o) => { if o.get().0 != editorconfig_contents { match editorconfig_contents.parse() { Ok(new_contents) => { o.insert(( editorconfig_contents.to_owned(), Some(new_contents), )); } Err(e) => { o.insert((editorconfig_contents.to_owned(), None)); return Err(InvalidSettingsError::Editorconfig { message: e.to_string(), path: directory_path.join(EDITORCONFIG_NAME), }); } } } } } } }; if zed_settings_changed { self.recompute_values(Some((root_id, &directory_path)), cx)?; } Ok(()) } pub fn set_extension_settings( &mut self, content: ExtensionsSettingsContent, cx: &mut App, ) -> Result<()> { self.extension_settings = Some(Box::new(SettingsContent { project: ProjectSettingsContent { all_languages: content.all_languages, ..Default::default() }, ..Default::default() })); self.recompute_values(None, cx)?; Ok(()) } /// Add or remove a set of local settings via a JSON string. pub fn clear_local_settings(&mut self, root_id: WorktreeId, cx: &mut App) -> Result<()> { self.local_settings .retain(|(worktree_id, _), _| worktree_id != &root_id); self.recompute_values(Some((root_id, "".as_ref())), cx)?; Ok(()) } pub fn local_settings( &self, root_id: WorktreeId, ) -> impl '_ + Iterator, &ProjectSettingsContent)> { self.local_settings .range( (root_id, Path::new("").into()) ..( WorktreeId::from_usize(root_id.to_usize() + 1), Path::new("").into(), ), ) .map(|((_, path), content)| (path.clone(), &content.project)) } pub fn local_editorconfig_settings( &self, root_id: WorktreeId, ) -> impl '_ + Iterator, String, Option)> { self.raw_editorconfig_settings .range( (root_id, Path::new("").into()) ..( WorktreeId::from_usize(root_id.to_usize() + 1), Path::new("").into(), ), ) .map(|((_, path), (content, parsed_content))| { (path.clone(), content.clone(), parsed_content.clone()) }) } pub fn json_schema(&self, schema_params: &SettingsJsonSchemaParams, cx: &App) -> Value { let mut generator = schemars::generate::SchemaSettings::draft2019_09() .with_transform(DefaultDenyUnknownFields) .into_generator(); let schema = UserSettingsContent::json_schema(&mut generator); // add schemas which are determined at runtime for parameterized_json_schema in inventory::iter::() { (parameterized_json_schema.add_and_get_ref)(&mut generator, schema_params, cx); } schema.to_value() } fn recompute_values( &mut self, changed_local_path: Option<(WorktreeId, &Path)>, cx: &mut App, ) -> std::result::Result<(), InvalidSettingsError> { // Reload the global and local values for every setting. let mut project_settings_stack = Vec::<&SettingsContent>::new(); let mut paths_stack = Vec::>::new(); let mut refinements = Vec::default(); if let Some(extension_settings) = self.extension_settings.as_deref() { refinements.push(extension_settings) } if let Some(global_settings) = self.global_settings.as_deref() { refinements.push(global_settings) } if let Some(user_settings) = self.user_settings.as_ref() { refinements.push(&user_settings.content); if let Some(release_channel) = user_settings.for_release_channel() { refinements.push(release_channel) } if let Some(os) = user_settings.for_os() { refinements.push(os) } if let Some(profile) = user_settings.for_profile(cx) { refinements.push(profile) } } if let Some(server_settings) = self.server_settings.as_ref() { refinements.push(server_settings) } for setting_value in self.setting_values.values_mut() { // If the global settings file changed, reload the global value for the field. if changed_local_path.is_none() { let mut value = setting_value.from_default(&self.default_settings, cx); setting_value.refine(value.as_mut(), &refinements, cx); setting_value.set_global_value(value); } // Reload the local values for the setting. paths_stack.clear(); project_settings_stack.clear(); for ((root_id, directory_path), local_settings) in &self.local_settings { // Build a stack of all of the local values for that setting. while let Some(prev_entry) = paths_stack.last() { if let Some((prev_root_id, prev_path)) = prev_entry && (root_id != prev_root_id || !directory_path.starts_with(prev_path)) { paths_stack.pop(); project_settings_stack.pop(); continue; } break; } paths_stack.push(Some((*root_id, directory_path.as_ref()))); project_settings_stack.push(local_settings); // If a local settings file changed, then avoid recomputing local // settings for any path outside of that directory. if changed_local_path.is_some_and(|(changed_root_id, changed_local_path)| { *root_id != changed_root_id || !directory_path.starts_with(changed_local_path) }) { continue; } let mut value = setting_value.from_default(&self.default_settings, cx); setting_value.refine(value.as_mut(), &refinements, cx); setting_value.refine(value.as_mut(), &project_settings_stack, cx); setting_value.set_local_value(*root_id, directory_path.clone(), value); } } Ok(()) } pub fn editorconfig_properties( &self, for_worktree: WorktreeId, for_path: &Path, ) -> Option { let mut properties = EditorconfigProperties::new(); for (directory_with_config, _, parsed_editorconfig) in self.local_editorconfig_settings(for_worktree) { if !for_path.starts_with(&directory_with_config) { properties.use_fallbacks(); return Some(properties); } let parsed_editorconfig = parsed_editorconfig?; if parsed_editorconfig.is_root { properties = EditorconfigProperties::new(); } for section in parsed_editorconfig.sections { section.apply_to(&mut properties, for_path).log_err()?; } } properties.use_fallbacks(); Some(properties) } } #[derive(Debug, Clone, PartialEq)] pub enum InvalidSettingsError { LocalSettings { path: PathBuf, message: String }, UserSettings { message: String }, ServerSettings { message: String }, DefaultSettings { message: String }, Editorconfig { path: PathBuf, message: String }, Tasks { path: PathBuf, message: String }, Debug { path: PathBuf, message: String }, } impl std::fmt::Display for InvalidSettingsError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { InvalidSettingsError::LocalSettings { message, .. } | InvalidSettingsError::UserSettings { message } | InvalidSettingsError::ServerSettings { message } | InvalidSettingsError::DefaultSettings { message } | InvalidSettingsError::Tasks { message, .. } | InvalidSettingsError::Editorconfig { message, .. } | InvalidSettingsError::Debug { message, .. } => { write!(f, "{message}") } } } } impl std::error::Error for InvalidSettingsError {} impl Debug for SettingsStore { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SettingsStore") .field( "types", &self .setting_values .values() .map(|value| value.setting_type_name()) .collect::>(), ) .field("default_settings", &self.default_settings) .field("user_settings", &self.user_settings) .field("local_settings", &self.local_settings) .finish_non_exhaustive() } } impl AnySettingValue for SettingValue { fn from_default(&self, s: &SettingsContent, cx: &mut App) -> Box { Box::new(T::from_defaults(s, cx)) as _ } fn refine(&self, value: &mut dyn Any, refinements: &[&SettingsContent], cx: &mut App) { let value = value.downcast_mut::().unwrap(); for refinement in refinements { value.refine(refinement, cx) } } fn setting_type_name(&self) -> &'static str { type_name::() } fn all_local_values(&self) -> Vec<(WorktreeId, Arc, &dyn Any)> { self.local_values .iter() .map(|(id, path, value)| (*id, path.clone(), value as _)) .collect() } fn value_for_path(&self, path: Option) -> &dyn Any { if let Some(SettingsLocation { worktree_id, path }) = path { for (settings_root_id, settings_path, value) in self.local_values.iter().rev() { if worktree_id == *settings_root_id && path.starts_with(settings_path) { return value; } } } self.global_value .as_ref() .unwrap_or_else(|| panic!("no default value for setting {}", self.setting_type_name())) } fn set_global_value(&mut self, value: Box) { self.global_value = Some(*value.downcast().unwrap()); } fn set_local_value(&mut self, root_id: WorktreeId, path: Arc, value: Box) { let value = *value.downcast().unwrap(); match self .local_values .binary_search_by_key(&(root_id, &path), |e| (e.0, &e.1)) { Ok(ix) => self.local_values[ix].2 = value, Err(ix) => self.local_values.insert(ix, (root_id, path, value)), } } fn import_from_vscode( &self, vscode_settings: &VsCodeSettings, settings_content: &mut SettingsContent, ) { T::import_from_vscode(vscode_settings, settings_content); } } #[cfg(test)] mod tests { use std::num::NonZeroU32; use crate::{ TitleBarSettingsContent, TitleBarVisibility, VsCodeSettingsSource, default_settings, settings_content::LanguageSettingsContent, test_settings, }; use super::*; use unindent::Unindent; use util::MergeFrom; #[derive(Debug, PartialEq)] struct AutoUpdateSetting { auto_update: bool, } impl Settings for AutoUpdateSetting { fn from_defaults(content: &SettingsContent, _: &mut App) -> Self { AutoUpdateSetting { auto_update: content.auto_update.unwrap(), } } fn refine(&mut self, content: &SettingsContent, _: &mut App) { if let Some(auto_update) = content.auto_update { self.auto_update = auto_update; } } fn import_from_vscode(_: &VsCodeSettings, _: &mut SettingsContent) {} } #[derive(Debug, PartialEq)] struct TitleBarSettings { show: TitleBarVisibility, show_branch_name: bool, } impl Settings for TitleBarSettings { fn from_defaults(content: &SettingsContent, _: &mut App) -> Self { let content = content.title_bar.clone().unwrap(); TitleBarSettings { show: content.show.unwrap(), show_branch_name: content.show_branch_name.unwrap(), } } fn refine(&mut self, content: &SettingsContent, _: &mut App) { let Some(content) = content.title_bar.as_ref() else { return; }; self.show.merge_from(&content.show) } fn import_from_vscode(vscode: &VsCodeSettings, content: &mut SettingsContent) { let mut show = None; vscode.enum_setting("window.titleBarStyle", &mut show, |value| match value { "never" => Some(TitleBarVisibility::Never), "always" => Some(TitleBarVisibility::Always), _ => None, }); if let Some(show) = show { content.title_bar.get_or_insert_default().show.replace(show); } } } #[derive(Debug, PartialEq)] struct DefaultLanguageSettings { tab_size: NonZeroU32, preferred_line_length: u32, } impl Settings for DefaultLanguageSettings { fn from_defaults(content: &SettingsContent, _: &mut App) -> Self { let content = &content.project.all_languages.defaults; DefaultLanguageSettings { tab_size: content.tab_size.unwrap(), preferred_line_length: content.preferred_line_length.unwrap(), } } fn refine(&mut self, content: &SettingsContent, _: &mut App) { let content = &content.project.all_languages.defaults; self.tab_size.merge_from(&content.tab_size); self.preferred_line_length .merge_from(&content.preferred_line_length); } fn import_from_vscode(vscode: &VsCodeSettings, content: &mut SettingsContent) { let content = &mut content.project.all_languages.defaults; if let Some(size) = vscode .read_value("editor.tabSize") .and_then(|v| v.as_u64()) .and_then(|n| NonZeroU32::new(n as u32)) { content.tab_size = Some(size); } } } #[gpui::test] fn test_settings_store_basic(cx: &mut App) { let mut store = SettingsStore::new(cx, &default_settings()); store.register_setting::(cx); store.register_setting::(cx); store.register_setting::(cx); assert_eq!( store.get::(None), &AutoUpdateSetting { auto_update: true } ); assert_eq!( store.get::(None).show, TitleBarVisibility::Always ); store .set_user_settings( r#"{ "auto_update": false, "title_bar": { "show": "never" } }"#, cx, ) .unwrap(); assert_eq!( store.get::(None), &AutoUpdateSetting { auto_update: false } ); assert_eq!( store.get::(None).show, TitleBarVisibility::Never ); store .set_local_settings( WorktreeId::from_usize(1), Path::new("/root1").into(), LocalSettingsKind::Settings, Some(r#"{ "tab_size": 5 }"#), cx, ) .unwrap(); store .set_local_settings( WorktreeId::from_usize(1), Path::new("/root1/subdir").into(), LocalSettingsKind::Settings, Some(r#"{ "preferred_line_length": 50 }"#), cx, ) .unwrap(); store .set_local_settings( WorktreeId::from_usize(1), Path::new("/root2").into(), LocalSettingsKind::Settings, Some(r#"{ "tab_size": 9, "title_bar": { "show_branch_name": false } }"#), cx, ) .unwrap(); assert_eq!( store.get::(Some(SettingsLocation { worktree_id: WorktreeId::from_usize(1), path: Path::new("/root1/something"), })), &DefaultLanguageSettings { preferred_line_length: 80, tab_size: 5.try_into().unwrap(), } ); assert_eq!( store.get::(Some(SettingsLocation { worktree_id: WorktreeId::from_usize(1), path: Path::new("/root1/subdir/something") })), &DefaultLanguageSettings { preferred_line_length: 50, tab_size: 5.try_into().unwrap(), } ); assert_eq!( store.get::(Some(SettingsLocation { worktree_id: WorktreeId::from_usize(1), path: Path::new("/root2/something") })), &DefaultLanguageSettings { preferred_line_length: 80, tab_size: 9.try_into().unwrap(), } ); assert_eq!( store.get::(Some(SettingsLocation { worktree_id: WorktreeId::from_usize(1), path: Path::new("/root2/something") })), &TitleBarSettings { show: TitleBarVisibility::Never, show_branch_name: true, } ); } #[gpui::test] fn test_setting_store_assign_json_before_register(cx: &mut App) { let mut store = SettingsStore::new(cx, &test_settings()); store .set_user_settings(r#"{ "auto_update": false }"#, cx) .unwrap(); store.register_setting::(cx); store.register_setting::(cx); assert_eq!( store.get::(None), &AutoUpdateSetting { auto_update: false } ); assert_eq!( store.get::(None).show, TitleBarVisibility::Always, ); } #[track_caller] fn check_settings_update( store: &mut SettingsStore, old_json: String, update: fn(&mut SettingsContent), expected_new_json: String, cx: &mut App, ) { store.set_user_settings(&old_json, cx).ok(); let edits = store.edits_for_update(&old_json, update); let mut new_json = old_json; for (range, replacement) in edits.into_iter() { new_json.replace_range(range, &replacement); } pretty_assertions::assert_eq!(new_json, expected_new_json); } #[gpui::test] fn test_setting_store_update(cx: &mut App) { let mut store = SettingsStore::new(cx, &test_settings()); // entries added and updated check_settings_update( &mut store, r#"{ "languages": { "JSON": { "auto_indent": true } } }"# .unindent(), |settings| { settings .languages_mut() .get_mut("JSON") .unwrap() .auto_indent = Some(false); settings.languages_mut().insert( "Rust".into(), LanguageSettingsContent { auto_indent: Some(true), ..Default::default() }, ); }, r#"{ "languages": { "Rust": { "auto_indent": true }, "JSON": { "auto_indent": false } } }"# .unindent(), cx, ); // entries removed check_settings_update( &mut store, r#"{ "languages": { "Rust": { "language_setting_2": true }, "JSON": { "language_setting_1": false } } }"# .unindent(), |settings| { settings.languages_mut().remove("JSON").unwrap(); }, r#"{ "languages": { "Rust": { "language_setting_2": true } } }"# .unindent(), cx, ); check_settings_update( &mut store, r#"{ "languages": { "Rust": { "language_setting_2": true }, "JSON": { "language_setting_1": false } } }"# .unindent(), |settings| { settings.languages_mut().remove("Rust").unwrap(); }, r#"{ "languages": { "JSON": { "language_setting_1": false } } }"# .unindent(), cx, ); // weird formatting check_settings_update( &mut store, r#"{ "title_bar": { "show": "always", "name": "Max" } }"# .unindent(), |settings| { settings.title_bar.as_mut().unwrap().show = Some(TitleBarVisibility::Never); }, r#"{ "title_bar": { "show": "never", "name": "Max" } }"# .unindent(), cx, ); // single-line formatting, other keys check_settings_update( &mut store, r#"{ "one": 1, "two": 2 }"#.to_owned(), |settings| settings.auto_update = Some(true), r#"{ "auto_update": true, "one": 1, "two": 2 }"#.to_owned(), cx, ); // empty object check_settings_update( &mut store, r#"{ "title_bar": {} }"# .unindent(), |settings| settings.title_bar.as_mut().unwrap().show_menus = Some(true), r#"{ "title_bar": { "show_menus": true } }"# .unindent(), cx, ); // no content check_settings_update( &mut store, r#""#.unindent(), |settings| { settings.title_bar = Some(TitleBarSettingsContent { show_branch_name: Some(true), ..Default::default() }) }, r#"{ "title_bar": { "show_branch_name": true } } "# .unindent(), cx, ); check_settings_update( &mut store, r#"{ } "# .unindent(), |settings| settings.title_bar.get_or_insert_default().show_branch_name = Some(true), r#"{ "title_bar": { "show_branch_name": true } } "# .unindent(), cx, ); } #[gpui::test] fn test_vscode_import(cx: &mut App) { let mut store = SettingsStore::new(cx, &test_settings()); store.register_setting::(cx); store.register_setting::(cx); store.register_setting::(cx); // create settings that werent present check_vscode_import( &mut store, r#"{ } "# .unindent(), r#" { "editor.tabSize": 37 } "#.to_owned(), r#"{ "tab_size": 37 } "# .unindent(), cx, ); // persist settings that were present check_vscode_import( &mut store, r#"{ "preferred_line_length": 99, } "# .unindent(), r#"{ "editor.tabSize": 42 }"#.to_owned(), r#"{ "tab_size": 42, "preferred_line_length": 99, } "# .unindent(), cx, ); // don't clobber settings that aren't present in vscode check_vscode_import( &mut store, r#"{ "preferred_line_length": 99, "tab_size": 42 } "# .unindent(), r#"{}"#.to_owned(), r#"{ "preferred_line_length": 99, "tab_size": 42 } "# .unindent(), cx, ); // custom enum check_vscode_import( &mut store, r#"{ "title_bar": { "show": "always" } } "# .unindent(), r#"{ "window.titleBarStyle": "never" }"#.to_owned(), r#"{ "title_bar": { "show": "never" } } "# .unindent(), cx, ); } #[track_caller] fn check_vscode_import( store: &mut SettingsStore, old: String, vscode: String, expected: String, cx: &mut App, ) { store.set_user_settings(&old, cx).ok(); let new = store.get_vscode_edits( old, &VsCodeSettings::from_str(&vscode, VsCodeSettingsSource::VsCode).unwrap(), ); pretty_assertions::assert_eq!(new, expected); } #[gpui::test] fn test_update_git_settings(cx: &mut App) { let store = SettingsStore::new(cx, &test_settings()); let actual = store.new_text_for_update("{}".to_string(), |current| { current .git .get_or_insert_default() .inline_blame .get_or_insert_default() .enabled = Some(true); }); assert_eq!( actual, r#"{ "git": { "inline_blame": { "enabled": true } } } "# .unindent() ); } #[gpui::test] fn test_global_settings(cx: &mut App) { let mut store = SettingsStore::new(cx, &test_settings()); store.register_setting::(cx); // Set global settings - these should override defaults but not user settings store .set_global_settings( r#"{ "title_bar": { "show": "never", } }"#, cx, ) .unwrap(); // Before user settings, global settings should apply assert_eq!( store.get::(None), &TitleBarSettings { show: TitleBarVisibility::Never, show_branch_name: true, } ); // Set user settings - these should override both defaults and global store .set_user_settings( r#"{ "title_bar": { "show": "always" } }"#, cx, ) .unwrap(); // User settings should override global settings assert_eq!( store.get::(None), &TitleBarSettings { show: TitleBarVisibility::Always, show_branch_name: true, // Staff from global settings } ); } }