use anyhow::{anyhow, Context, Result}; use collections::{btree_map, hash_map, BTreeMap, HashMap}; use fs::Fs; use futures::{channel::mpsc, future::LocalBoxFuture, FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, BorrowAppContext, Global, Task, UpdateGlobal}; use paths::local_settings_file_relative_path; use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema}; use serde::{de::DeserializeOwned, Deserialize as _, Serialize}; use smallvec::SmallVec; use std::{ any::{type_name, Any, TypeId}, fmt::Debug, ops::Range, path::{Path, PathBuf}, str, sync::{Arc, LazyLock}, }; use tree_sitter::Query; use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _}; use crate::{SettingsJsonSchemaParams, WorktreeId}; /// 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 { /// 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>; /// The name of the keys in the [`FileContent`] that should always be written to /// a settings file, even if their value matches the default value. /// /// This is useful for tagged [`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; /// The type that is stored in an individual JSON file. type FileContent: Clone + Default + Serialize + DeserializeOwned + JsonSchema; /// The logic for combining together values from one or more JSON files into the /// final value for this setting. fn load(sources: SettingsSources, cx: &mut AppContext) -> Result where Self: Sized; fn json_schema( generator: &mut SchemaGenerator, _: &SettingsJsonSchemaParams, _: &AppContext, ) -> RootSchema { generator.root_schema_for::() } fn missing_default() -> anyhow::Error { anyhow::anyhow!("missing default") } fn register(cx: &mut AppContext) where Self: Sized, { SettingsStore::update_global(cx, |store, cx| { store.register_setting::(cx); }); } #[track_caller] fn get<'a>(path: Option, cx: &'a AppContext) -> &'a Self where Self: Sized, { cx.global::().get(path) } #[track_caller] fn get_global(cx: &AppContext) -> &Self where Self: Sized, { cx.global::().get(None) } #[track_caller] fn try_read_global(cx: &AsyncAppContext, 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 AppContext) where Self: Sized, { cx.global_mut::().override_global(settings) } } #[derive(Clone, Copy, Debug)] pub struct SettingsSources<'a, T> { /// The default Zed settings. pub default: &'a T, /// Settings provided by extensions. pub extensions: Option<&'a T>, /// The user settings. pub user: Option<&'a T>, /// The user settings for the current release channel. pub release_channel: Option<&'a T>, /// The server's settings. pub server: Option<&'a T>, /// The project settings, ordered from least specific to most specific. pub project: &'a [&'a T], } impl<'a, T: Serialize> SettingsSources<'a, T> { /// Returns an iterator over the default settings as well as all settings customizations. pub fn defaults_and_customizations(&self) -> impl Iterator { [self.default].into_iter().chain(self.customizations()) } /// Returns an iterator over all of the settings customizations. pub fn customizations(&self) -> impl Iterator { self.extensions .into_iter() .chain(self.user) .chain(self.release_channel) .chain(self.server) .chain(self.project.iter().copied()) } /// Returns the settings after performing a JSON merge of the provided customizations. /// /// Customizations later in the iterator win out over the earlier ones. pub fn json_merge_with( customizations: impl Iterator, ) -> Result { let mut merged = serde_json::Value::Null; for value in customizations { merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged); } Ok(serde_json::from_value(merged)?) } /// Returns the settings after performing a JSON merge of the customizations into the /// default settings. /// /// More-specific customizations win out over the less-specific ones. pub fn json_merge(&'a self) -> Result { Self::json_merge_with(self.defaults_and_customizations()) } } #[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>, raw_default_settings: serde_json::Value, raw_user_settings: serde_json::Value, raw_server_settings: Option, raw_extension_settings: serde_json::Value, raw_local_settings: BTreeMap<(WorktreeId, Arc), HashMap>, tab_size_callback: Option<( TypeId, Box Option + Send + Sync + 'static>, )>, _setting_file_updates: Task<()>, setting_file_updates_tx: mpsc::UnboundedSender< Box LocalBoxFuture<'static, Result<()>>>, >, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum LocalSettingsKind { Settings, Tasks, Editorconfig, } impl Global for SettingsStore {} #[derive(Debug)] struct SettingValue { global_value: Option, local_values: Vec<(WorktreeId, Arc, T)>, } trait AnySettingValue: 'static + Send + Sync { fn key(&self) -> Option<&'static str>; fn setting_type_name(&self) -> &'static str; fn deserialize_setting(&self, json: &serde_json::Value) -> Result; fn load_setting( &self, sources: SettingsSources, cx: &mut AppContext, ) -> Result>; fn value_for_path(&self, path: Option) -> &dyn Any; fn set_global_value(&mut self, value: Box); fn set_local_value(&mut self, root_id: WorktreeId, path: Arc, value: Box); fn json_schema( &self, generator: &mut SchemaGenerator, _: &SettingsJsonSchemaParams, cx: &AppContext, ) -> RootSchema; } struct DeserializedSetting(Box); impl SettingsStore { pub fn new(cx: &AppContext) -> Self { let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded(); Self { setting_values: Default::default(), raw_default_settings: serde_json::json!({}), raw_user_settings: serde_json::json!({}), raw_server_settings: None, raw_extension_settings: serde_json::json!({}), raw_local_settings: Default::default(), tab_size_callback: Default::default(), setting_file_updates_tx, _setting_file_updates: cx.spawn(|cx| async move { while let Some(setting_file_update) = setting_file_updates_rx.next().await { (setting_file_update)(cx.clone()).await.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 AppContext) { 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(), })); if let Some(default_settings) = setting_value .deserialize_setting(&self.raw_default_settings) .log_err() { let user_value = setting_value .deserialize_setting(&self.raw_user_settings) .log_err(); let mut release_channel_value = None; if let Some(release_settings) = &self .raw_user_settings .get(release_channel::RELEASE_CHANNEL.dev_name()) { release_channel_value = setting_value .deserialize_setting(release_settings) .log_err(); } let server_value = self .raw_server_settings .as_ref() .and_then(|server_setting| { setting_value.deserialize_setting(server_setting).log_err() }); let extension_value = setting_value .deserialize_setting(&self.raw_extension_settings) .log_err(); if let Some(setting) = setting_value .load_setting( SettingsSources { default: &default_settings, extensions: extension_value.as_ref(), user: user_value.as_ref(), release_channel: release_channel_value.as_ref(), server: server_value.as_ref(), project: &[], }, cx, ) .context("A default setting must be added to the `default.json` file") .log_err() { setting_value.set_global_value(setting); } } } /// 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") } /// 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) -> &serde_json::Value { &self.raw_user_settings } #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut AppContext) -> Self { let mut this = Self::new(cx); this.set_default_settings(&crate::test_settings(), cx) .unwrap(); this.set_user_settings("{}", cx).unwrap(); this } /// 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 AppContext, update: impl FnOnce(&mut T::FileContent), ) { let old_text = serde_json::to_string(&self.raw_user_settings).unwrap(); let new_text = self.new_text_for_update::(old_text, update); self.set_user_settings(&new_text, cx).unwrap(); } 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::() { if e.kind() == std::io::ErrorKind::NotFound { return Ok(crate::initial_user_settings_content().to_string()); } } Err(err) } } } pub fn update_settings_file( &self, fs: Arc, update: impl 'static + Send + FnOnce(&mut T::FileContent, &AppContext), ) { self.setting_file_updates_tx .unbounded_send(Box::new(move |cx: AsyncAppContext| { async move { let old_text = Self::load_settings(&fs).await?; let new_text = cx.read_global(|store: &SettingsStore, cx| { store.new_text_for_update::(old_text, |content| update(content, cx)) })?; let initial_path = paths::settings_file().as_path(); if fs.is_file(initial_path).await { let resolved_path = fs.canonicalize(initial_path).await.with_context(|| { format!("Failed to canonicalize settings path {:?}", initial_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(initial_path.to_path_buf(), new_text) .await .with_context(|| { format!("Failed to write settings to file {:?}", initial_path) })?; } anyhow::Ok(()) } .boxed_local() })) .ok(); } /// 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 T::FileContent), ) -> 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 } /// 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 T::FileContent), ) -> Vec<(Range, String)> { let setting_type_id = TypeId::of::(); let preserved_keys = T::PRESERVED_KEYS.unwrap_or_default(); let setting = self .setting_values .get(&setting_type_id) .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())); let raw_settings = parse_json_with_comments::(text).unwrap_or_default(); let old_content = match setting.deserialize_setting(&raw_settings) { Ok(content) => content.0.downcast::().unwrap(), Err(_) => Box::<::FileContent>::default(), }; let mut new_content = old_content.clone(); update(&mut new_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(); if let Some(key) = T::KEY { key_path.push(key); } 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, preserved_keys, &mut edits, ); edits } /// Configure the tab sized when updating JSON files. pub fn set_json_tab_size_callback( &mut self, get_tab_size: fn(&T) -> Option, ) { self.tab_size_callback = Some(( TypeId::of::(), Box::new(move |value| get_tab_size(value.downcast_ref::().unwrap())), )); } fn json_tab_size(&self) -> usize { const DEFAULT_JSON_TAB_SIZE: usize = 2; if let Some((setting_type_id, callback)) = &self.tab_size_callback { let setting_value = self.setting_values.get(setting_type_id).unwrap(); let value = setting_value.value_for_path(None); if let Some(value) = callback(value) { return value; } } DEFAULT_JSON_TAB_SIZE } /// 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 AppContext, ) -> Result<()> { let settings: serde_json::Value = parse_json_with_comments(default_settings_content)?; if settings.is_object() { self.raw_default_settings = settings; self.recompute_values(None, cx)?; Ok(()) } else { Err(anyhow!("settings must be an object")) } } /// Sets the user settings via a JSON string. pub fn set_user_settings( &mut self, user_settings_content: &str, cx: &mut AppContext, ) -> Result<()> { let settings: serde_json::Value = if user_settings_content.is_empty() { parse_json_with_comments("{}")? } else { parse_json_with_comments(user_settings_content)? }; anyhow::ensure!(settings.is_object(), "settings must be an object"); self.raw_user_settings = settings; self.recompute_values(None, cx)?; Ok(()) } pub fn set_server_settings( &mut self, server_settings_content: &str, cx: &mut AppContext, ) -> Result<()> { let settings: Option = if server_settings_content.is_empty() { None } else { parse_json_with_comments(server_settings_content)? }; anyhow::ensure!( settings .as_ref() .map(|value| value.is_object()) .unwrap_or(true), "settings must be an object" ); self.raw_server_settings = settings; 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 AppContext, ) -> std::result::Result<(), InvalidSettingsError> { debug_assert!( kind != LocalSettingsKind::Tasks, "Attempted to submit tasks into the settings store" ); let raw_local_settings = self .raw_local_settings .entry((root_id, directory_path.clone())) .or_default(); let changed = if settings_content.is_some_and(|content| !content.is_empty()) { let new_contents = parse_json_with_comments(settings_content.unwrap()).map_err(|e| { InvalidSettingsError::LocalSettings { path: directory_path.join(local_settings_file_relative_path()), message: e.to_string(), } })?; if Some(&new_contents) == raw_local_settings.get(&kind) { false } else { raw_local_settings.insert(kind, new_contents); true } } else { raw_local_settings.remove(&kind).is_some() }; if changed { self.recompute_values(Some((root_id, &directory_path)), cx)?; } Ok(()) } pub fn set_extension_settings( &mut self, content: T, cx: &mut AppContext, ) -> Result<()> { let settings: serde_json::Value = serde_json::to_value(content)?; if settings.is_object() { self.raw_extension_settings = settings; self.recompute_values(None, cx)?; Ok(()) } else { Err(anyhow!("settings must be an object")) } } /// Add or remove a set of local settings via a JSON string. pub fn clear_local_settings(&mut self, root_id: WorktreeId, cx: &mut AppContext) -> Result<()> { self.raw_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, LocalSettingsKind, String)> { self.raw_local_settings .range( (root_id, Path::new("").into()) ..( WorktreeId::from_usize(root_id.to_usize() + 1), Path::new("").into(), ), ) .flat_map(|((_, path), content)| { content.iter().filter_map(|(&kind, raw_content)| { let parsed_content = serde_json::to_string(raw_content).log_err()?; Some((path.clone(), kind, parsed_content)) }) }) } pub fn json_schema( &self, schema_params: &SettingsJsonSchemaParams, cx: &AppContext, ) -> serde_json::Value { use schemars::{ gen::SchemaSettings, schema::{Schema, SchemaObject}, }; let settings = SchemaSettings::draft07().with(|settings| { settings.option_add_null_type = false; }); let mut generator = SchemaGenerator::new(settings); let mut combined_schema = RootSchema::default(); for setting_value in self.setting_values.values() { let setting_schema = setting_value.json_schema(&mut generator, schema_params, cx); combined_schema .definitions .extend(setting_schema.definitions); let target_schema = if let Some(key) = setting_value.key() { let key_schema = combined_schema .schema .object() .properties .entry(key.to_string()) .or_insert_with(|| Schema::Object(SchemaObject::default())); if let Schema::Object(key_schema) = key_schema { key_schema } else { continue; } } else { &mut combined_schema.schema }; merge_schema(target_schema, setting_schema.schema); } fn merge_schema(target: &mut SchemaObject, mut source: SchemaObject) { let source_subschemas = source.subschemas(); let target_subschemas = target.subschemas(); if let Some(all_of) = source_subschemas.all_of.take() { target_subschemas .all_of .get_or_insert(Vec::new()) .extend(all_of); } if let Some(any_of) = source_subschemas.any_of.take() { target_subschemas .any_of .get_or_insert(Vec::new()) .extend(any_of); } if let Some(one_of) = source_subschemas.one_of.take() { target_subschemas .one_of .get_or_insert(Vec::new()) .extend(one_of); } if let Some(source) = source.object { let target_properties = &mut target.object().properties; for (key, value) in source.properties { match target_properties.entry(key) { btree_map::Entry::Vacant(e) => { e.insert(value); } btree_map::Entry::Occupied(e) => { if let (Schema::Object(target), Schema::Object(src)) = (e.into_mut(), value) { merge_schema(target, src); } } } } } overwrite(&mut target.instance_type, source.instance_type); overwrite(&mut target.string, source.string); overwrite(&mut target.number, source.number); overwrite(&mut target.reference, source.reference); overwrite(&mut target.array, source.array); overwrite(&mut target.enum_values, source.enum_values); fn overwrite(target: &mut Option, source: Option) { if let Some(source) = source { *target = Some(source); } } } for release_stage in ["dev", "nightly", "stable", "preview"] { let schema = combined_schema.schema.clone(); combined_schema .schema .object() .properties .insert(release_stage.to_string(), schema.into()); } serde_json::to_value(&combined_schema).unwrap() } fn recompute_values( &mut self, changed_local_path: Option<(WorktreeId, &Path)>, cx: &mut AppContext, ) -> Result<(), InvalidSettingsError> { // Reload the global and local values for every setting. let mut project_settings_stack = Vec::::new(); let mut paths_stack = Vec::>::new(); for setting_value in self.setting_values.values_mut() { let default_settings = setting_value .deserialize_setting(&self.raw_default_settings) .map_err(|e| InvalidSettingsError::DefaultSettings { message: e.to_string(), })?; let extension_settings = setting_value .deserialize_setting(&self.raw_extension_settings) .log_err(); let user_settings = match setting_value.deserialize_setting(&self.raw_user_settings) { Ok(settings) => Some(settings), Err(error) => { return Err(InvalidSettingsError::UserSettings { message: error.to_string(), }); } }; let server_settings = self .raw_server_settings .as_ref() .and_then(|setting| setting_value.deserialize_setting(setting).log_err()); let mut release_channel_settings = None; if let Some(release_settings) = &self .raw_user_settings .get(release_channel::RELEASE_CHANNEL.dev_name()) { if let Some(release_settings) = setting_value .deserialize_setting(release_settings) .log_err() { release_channel_settings = Some(release_settings); } } // If the global settings file changed, reload the global value for the field. if changed_local_path.is_none() { if let Some(value) = setting_value .load_setting( SettingsSources { default: &default_settings, extensions: extension_settings.as_ref(), user: user_settings.as_ref(), release_channel: release_channel_settings.as_ref(), server: server_settings.as_ref(), project: &[], }, cx, ) .log_err() { 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.raw_local_settings { if let Some(local_settings) = local_settings.get(&LocalSettingsKind::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 { if root_id != prev_root_id || !directory_path.starts_with(prev_path) { paths_stack.pop(); project_settings_stack.pop(); continue; } } break; } match setting_value.deserialize_setting(local_settings) { Ok(local_settings) => { 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.map_or( false, |(changed_root_id, changed_local_path)| { *root_id != changed_root_id || !directory_path.starts_with(changed_local_path) }, ) { continue; } if let Some(value) = setting_value .load_setting( SettingsSources { default: &default_settings, extensions: extension_settings.as_ref(), user: user_settings.as_ref(), release_channel: release_channel_settings.as_ref(), server: server_settings.as_ref(), project: &project_settings_stack.iter().collect::>(), }, cx, ) .log_err() { setting_value.set_local_value( *root_id, directory_path.clone(), value, ); } } Err(error) => { return Err(InvalidSettingsError::LocalSettings { path: directory_path.join(local_settings_file_relative_path()), message: error.to_string(), }); } } } } } Ok(()) } } #[derive(Debug, Clone, PartialEq)] pub enum InvalidSettingsError { LocalSettings { path: PathBuf, message: String }, UserSettings { message: String }, ServerSettings { message: String }, DefaultSettings { 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 } => { 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.raw_default_settings) .field("user_settings", &self.raw_user_settings) .field("local_settings", &self.raw_local_settings) .finish_non_exhaustive() } } impl AnySettingValue for SettingValue { fn key(&self) -> Option<&'static str> { T::KEY } fn setting_type_name(&self) -> &'static str { type_name::() } fn load_setting( &self, values: SettingsSources, cx: &mut AppContext, ) -> Result> { Ok(Box::new(T::load( SettingsSources { default: values.default.0.downcast_ref::().unwrap(), extensions: values .extensions .map(|value| value.0.downcast_ref::().unwrap()), user: values .user .map(|value| value.0.downcast_ref::().unwrap()), release_channel: values .release_channel .map(|value| value.0.downcast_ref::().unwrap()), server: values .server .map(|value| value.0.downcast_ref::().unwrap()), project: values .project .iter() .map(|value| value.0.downcast_ref().unwrap()) .collect::>() .as_slice(), }, cx, )?)) } fn deserialize_setting(&self, mut json: &serde_json::Value) -> Result { if let Some(key) = T::KEY { if let Some(value) = json.get(key) { json = value; } else { let value = T::FileContent::default(); return Ok(DeserializedSetting(Box::new(value))); } } let value = T::FileContent::deserialize(json)?; Ok(DeserializedSetting(Box::new(value))) } 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 json_schema( &self, generator: &mut SchemaGenerator, params: &SettingsJsonSchemaParams, cx: &AppContext, ) -> RootSchema { T::json_schema(generator, params, cx) } } fn update_value_in_json_text<'a>( text: &mut String, key_path: &mut Vec<&'a str>, tab_size: usize, old_value: &'a serde_json::Value, new_value: &'a serde_json::Value, preserved_keys: &[&str], edits: &mut Vec<(Range, String)>, ) { // If the old and new values are both objects, then compare them key by key, // preserving the comments and formatting of the unchanged parts. Otherwise, // replace the old value with the new value. if let (serde_json::Value::Object(old_object), serde_json::Value::Object(new_object)) = (old_value, new_value) { for (key, old_sub_value) in old_object.iter() { key_path.push(key); let new_sub_value = new_object.get(key).unwrap_or(&serde_json::Value::Null); update_value_in_json_text( text, key_path, tab_size, old_sub_value, new_sub_value, preserved_keys, edits, ); key_path.pop(); } for (key, new_sub_value) in new_object.iter() { key_path.push(key); if !old_object.contains_key(key) { update_value_in_json_text( text, key_path, tab_size, &serde_json::Value::Null, new_sub_value, preserved_keys, edits, ); } key_path.pop(); } } else if key_path .last() .map_or(false, |key| preserved_keys.contains(key)) || old_value != new_value { let mut new_value = new_value.clone(); if let Some(new_object) = new_value.as_object_mut() { new_object.retain(|_, v| !v.is_null()); } let (range, replacement) = replace_value_in_json_text(text, key_path, tab_size, &new_value); text.replace_range(range.clone(), &replacement); edits.push((range, replacement)); } } fn replace_value_in_json_text( text: &str, key_path: &[&str], tab_size: usize, new_value: &serde_json::Value, ) -> (Range, String) { static PAIR_QUERY: LazyLock = LazyLock::new(|| { Query::new( &tree_sitter_json::LANGUAGE.into(), "(pair key: (string) @key value: (_) @value)", ) .expect("Failed to create PAIR_QUERY") }); let mut parser = tree_sitter::Parser::new(); parser .set_language(&tree_sitter_json::LANGUAGE.into()) .unwrap(); let syntax_tree = parser.parse(text, None).unwrap(); let mut cursor = tree_sitter::QueryCursor::new(); let mut depth = 0; let mut last_value_range = 0..0; let mut first_key_start = None; let mut existing_value_range = 0..text.len(); let matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes()); for mat in matches { if mat.captures.len() != 2 { continue; } let key_range = mat.captures[0].node.byte_range(); let value_range = mat.captures[1].node.byte_range(); // Don't enter sub objects until we find an exact // match for the current keypath if last_value_range.contains_inclusive(&value_range) { continue; } last_value_range = value_range.clone(); if key_range.start > existing_value_range.end { break; } first_key_start.get_or_insert(key_range.start); let found_key = text .get(key_range.clone()) .map(|key_text| key_text == format!("\"{}\"", key_path[depth])) .unwrap_or(false); if found_key { existing_value_range = value_range; // Reset last value range when increasing in depth last_value_range = existing_value_range.start..existing_value_range.start; depth += 1; if depth == key_path.len() { break; } first_key_start = None; } } // We found the exact key we want, insert the new value if depth == key_path.len() { let new_val = to_pretty_json(&new_value, tab_size, tab_size * depth); (existing_value_range, new_val) } else { // We have key paths, construct the sub objects let new_key = key_path[depth]; // We don't have the key, construct the nested objects let mut new_value = serde_json::to_value(new_value).unwrap(); for key in key_path[(depth + 1)..].iter().rev() { new_value = serde_json::json!({ key.to_string(): new_value }); } if let Some(first_key_start) = first_key_start { let mut row = 0; let mut column = 0; for (ix, char) in text.char_indices() { if ix == first_key_start { break; } if char == '\n' { row += 1; column = 0; } else { column += char.len_utf8(); } } if row > 0 { // depth is 0 based, but division needs to be 1 based. let new_val = to_pretty_json(&new_value, column / (depth + 1), column); let space = ' '; let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column); (first_key_start..first_key_start, content) } else { let new_val = serde_json::to_string(&new_value).unwrap(); let mut content = format!(r#""{new_key}": {new_val},"#); content.push(' '); (first_key_start..first_key_start, content) } } else { new_value = serde_json::json!({ new_key.to_string(): new_value }); let indent_prefix_len = 4 * depth; let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len); if depth == 0 { new_val.push('\n'); } (existing_value_range, new_val) } } } fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len: usize) -> String { const SPACES: [u8; 32] = [b' '; 32]; debug_assert!(indent_size <= SPACES.len()); debug_assert!(indent_prefix_len <= SPACES.len()); let mut output = Vec::new(); let mut ser = serde_json::Serializer::with_formatter( &mut output, serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]), ); value.serialize(&mut ser).unwrap(); let text = String::from_utf8(output).unwrap(); let mut adjusted_text = String::new(); for (i, line) in text.split('\n').enumerate() { if i > 0 { adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap()); } adjusted_text.push_str(line); adjusted_text.push('\n'); } adjusted_text.pop(); adjusted_text } pub fn parse_json_with_comments(content: &str) -> Result { Ok(serde_json_lenient::from_str(content)?) } #[cfg(test)] mod tests { use super::*; use serde_derive::Deserialize; use unindent::Unindent; #[gpui::test] fn test_settings_store_basic(cx: &mut AppContext) { let mut store = SettingsStore::new(cx); store.register_setting::(cx); store.register_setting::(cx); store.register_setting::(cx); store .set_default_settings( r#"{ "turbo": false, "user": { "name": "John Doe", "age": 30, "staff": false } }"#, cx, ) .unwrap(); assert_eq!(store.get::(None), &TurboSetting(false)); assert_eq!( store.get::(None), &UserSettings { name: "John Doe".to_string(), age: 30, staff: false, } ); assert_eq!( store.get::(None), &MultiKeySettings { key1: String::new(), key2: String::new(), } ); store .set_user_settings( r#"{ "turbo": true, "user": { "age": 31 }, "key1": "a" }"#, cx, ) .unwrap(); assert_eq!(store.get::(None), &TurboSetting(true)); assert_eq!( store.get::(None), &UserSettings { name: "John Doe".to_string(), age: 31, staff: false } ); store .set_local_settings( WorktreeId::from_usize(1), Path::new("/root1").into(), LocalSettingsKind::Settings, Some(r#"{ "user": { "staff": true } }"#), cx, ) .unwrap(); store .set_local_settings( WorktreeId::from_usize(1), Path::new("/root1/subdir").into(), LocalSettingsKind::Settings, Some(r#"{ "user": { "name": "Jane Doe" } }"#), cx, ) .unwrap(); store .set_local_settings( WorktreeId::from_usize(1), Path::new("/root2").into(), LocalSettingsKind::Settings, Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#), cx, ) .unwrap(); assert_eq!( store.get::(Some(SettingsLocation { worktree_id: WorktreeId::from_usize(1), path: Path::new("/root1/something"), })), &UserSettings { name: "John Doe".to_string(), age: 31, staff: true } ); assert_eq!( store.get::(Some(SettingsLocation { worktree_id: WorktreeId::from_usize(1), path: Path::new("/root1/subdir/something") })), &UserSettings { name: "Jane Doe".to_string(), age: 31, staff: true } ); assert_eq!( store.get::(Some(SettingsLocation { worktree_id: WorktreeId::from_usize(1), path: Path::new("/root2/something") })), &UserSettings { name: "John Doe".to_string(), age: 42, staff: false } ); assert_eq!( store.get::(Some(SettingsLocation { worktree_id: WorktreeId::from_usize(1), path: Path::new("/root2/something") })), &MultiKeySettings { key1: "a".to_string(), key2: "b".to_string(), } ); } #[gpui::test] fn test_setting_store_assign_json_before_register(cx: &mut AppContext) { let mut store = SettingsStore::new(cx); store .set_default_settings( r#"{ "turbo": true, "user": { "name": "John Doe", "age": 30, "staff": false }, "key1": "x" }"#, cx, ) .unwrap(); store .set_user_settings(r#"{ "turbo": false }"#, cx) .unwrap(); store.register_setting::(cx); store.register_setting::(cx); assert_eq!(store.get::(None), &TurboSetting(false)); assert_eq!( store.get::(None), &UserSettings { name: "John Doe".to_string(), age: 30, staff: false, } ); store.register_setting::(cx); assert_eq!( store.get::(None), &MultiKeySettings { key1: "x".into(), key2: String::new(), } ); } #[gpui::test] fn test_setting_store_update(cx: &mut AppContext) { let mut store = SettingsStore::new(cx); store.register_setting::(cx); store.register_setting::(cx); store.register_setting::(cx); // entries added and updated check_settings_update::( &mut store, r#"{ "languages": { "JSON": { "language_setting_1": true } } }"# .unindent(), |settings| { settings .languages .get_mut("JSON") .unwrap() .language_setting_1 = Some(false); settings.languages.insert( "Rust".into(), LanguageSettingEntry { language_setting_2: Some(true), ..Default::default() }, ); }, r#"{ "languages": { "Rust": { "language_setting_2": true }, "JSON": { "language_setting_1": false } } }"# .unindent(), cx, ); // weird formatting check_settings_update::( &mut store, r#"{ "user": { "age": 36, "name": "Max", "staff": true } }"# .unindent(), |settings| settings.age = Some(37), r#"{ "user": { "age": 37, "name": "Max", "staff": true } }"# .unindent(), cx, ); // single-line formatting, other keys check_settings_update::( &mut store, r#"{ "one": 1, "two": 2 }"#.unindent(), |settings| settings.key1 = Some("x".into()), r#"{ "key1": "x", "one": 1, "two": 2 }"#.unindent(), cx, ); // empty object check_settings_update::( &mut store, r#"{ "user": {} }"# .unindent(), |settings| settings.age = Some(37), r#"{ "user": { "age": 37 } }"# .unindent(), cx, ); // no content check_settings_update::( &mut store, r#""#.unindent(), |settings| settings.age = Some(37), r#"{ "user": { "age": 37 } } "# .unindent(), cx, ); check_settings_update::( &mut store, r#"{ } "# .unindent(), |settings| settings.age = Some(37), r#"{ "user": { "age": 37 } } "# .unindent(), cx, ); } fn check_settings_update( store: &mut SettingsStore, old_json: String, update: fn(&mut T::FileContent), expected_new_json: String, cx: &mut AppContext, ) { 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); } #[derive(Debug, PartialEq, Deserialize)] struct UserSettings { name: String, age: u32, staff: bool, } #[derive(Default, Clone, Serialize, Deserialize, JsonSchema)] struct UserSettingsJson { name: Option, age: Option, staff: Option, } impl Settings for UserSettings { const KEY: Option<&'static str> = Some("user"); type FileContent = UserSettingsJson; fn load(sources: SettingsSources, _: &mut AppContext) -> Result { sources.json_merge() } } #[derive(Debug, Deserialize, PartialEq)] struct TurboSetting(bool); impl Settings for TurboSetting { const KEY: Option<&'static str> = Some("turbo"); type FileContent = Option; fn load(sources: SettingsSources, _: &mut AppContext) -> Result { sources.json_merge() } } #[derive(Clone, Debug, PartialEq, Deserialize)] struct MultiKeySettings { #[serde(default)] key1: String, #[serde(default)] key2: String, } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] struct MultiKeySettingsJson { key1: Option, key2: Option, } impl Settings for MultiKeySettings { const KEY: Option<&'static str> = None; type FileContent = MultiKeySettingsJson; fn load(sources: SettingsSources, _: &mut AppContext) -> Result { sources.json_merge() } } #[derive(Debug, Deserialize)] struct JournalSettings { pub path: String, pub hour_format: HourFormat, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] enum HourFormat { Hour12, Hour24, } #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] struct JournalSettingsJson { pub path: Option, pub hour_format: Option, } impl Settings for JournalSettings { const KEY: Option<&'static str> = Some("journal"); type FileContent = JournalSettingsJson; fn load(sources: SettingsSources, _: &mut AppContext) -> Result { sources.json_merge() } } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] struct LanguageSettings { #[serde(default)] languages: HashMap, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] struct LanguageSettingEntry { language_setting_1: Option, language_setting_2: Option, } impl Settings for LanguageSettings { const KEY: Option<&'static str> = None; type FileContent = Self; fn load(sources: SettingsSources, _: &mut AppContext) -> Result { sources.json_merge() } } }