mod keymap_file; pub mod settings_file; pub mod watched_json; use anyhow::{bail, Result}; use gpui::{ font_cache::{FamilyId, FontCache}, fonts, AssetSource, }; use schemars::{ gen::{SchemaGenerator, SchemaSettings}, schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec}, JsonSchema, }; use serde::{de::DeserializeOwned, Deserialize, Deserializer, Serialize}; use serde_json::Value; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; use std::{collections::HashMap, num::NonZeroU32, str, sync::Arc}; use theme::{Theme, ThemeRegistry}; use tree_sitter::Query; use util::{RangeExt, ResultExt as _}; pub use keymap_file::{keymap_file_json_schema, KeymapFileContent}; pub use watched_json::watch_files; #[derive(Clone)] pub struct Settings { pub features: Features, pub buffer_font_family_name: String, pub buffer_font_features: fonts::Features, pub buffer_font_family: FamilyId, pub default_buffer_font_size: f32, pub buffer_font_size: f32, pub active_pane_magnification: f32, pub cursor_blink: bool, pub confirm_quit: bool, pub hover_popover_enabled: bool, pub show_completions_on_input: bool, pub show_call_status_icon: bool, pub vim_mode: bool, pub autosave: Autosave, pub default_dock_anchor: DockAnchor, pub editor_defaults: EditorSettings, pub editor_overrides: EditorSettings, pub git: GitSettings, pub git_overrides: GitSettings, pub journal_defaults: JournalSettings, pub journal_overrides: JournalSettings, pub terminal_defaults: TerminalSettings, pub terminal_overrides: TerminalSettings, pub language_defaults: HashMap, EditorSettings>, pub language_overrides: HashMap, EditorSettings>, pub lsp: HashMap, LspSettings>, pub theme: Arc, pub telemetry_defaults: TelemetrySettings, pub telemetry_overrides: TelemetrySettings, pub auto_update: bool, pub base_keymap: BaseKeymap, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] #[serde(rename_all = "snake_case")] pub enum CopilotSettings { #[default] On, Off, } impl From for bool { fn from(value: CopilotSettings) -> Self { match value { CopilotSettings::On => true, CopilotSettings::Off => false, } } } impl CopilotSettings { pub fn is_on(&self) -> bool { >::into(*self) } } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] pub enum BaseKeymap { #[default] VSCode, JetBrains, SublimeText, Atom, TextMate, } impl BaseKeymap { pub const OPTIONS: [(&'static str, Self); 5] = [ ("VSCode (Default)", Self::VSCode), ("Atom", Self::Atom), ("JetBrains", Self::JetBrains), ("Sublime Text", Self::SublimeText), ("TextMate", Self::TextMate), ]; pub fn asset_path(&self) -> Option<&'static str> { match self { BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"), BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"), BaseKeymap::Atom => Some("keymaps/atom.json"), BaseKeymap::TextMate => Some("keymaps/textmate.json"), BaseKeymap::VSCode => None, } } pub fn names() -> impl Iterator { Self::OPTIONS.iter().map(|(name, _)| *name) } pub fn from_names(option: &str) -> BaseKeymap { Self::OPTIONS .iter() .copied() .find_map(|(name, value)| (name == option).then(|| value)) .unwrap_or_default() } } #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct TelemetrySettings { diagnostics: Option, metrics: Option, } impl TelemetrySettings { pub fn metrics(&self) -> bool { self.metrics.unwrap() } pub fn diagnostics(&self) -> bool { self.diagnostics.unwrap() } pub fn set_metrics(&mut self, value: bool) { self.metrics = Some(value); } pub fn set_diagnostics(&mut self, value: bool) { self.diagnostics = Some(value); } } #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct GitSettings { pub git_gutter: Option, pub gutter_debounce: Option, } #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum GitGutter { #[default] TrackedFiles, Hide, } pub struct GitGutterConfig {} #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct EditorSettings { pub tab_size: Option, pub hard_tabs: Option, pub soft_wrap: Option, pub preferred_line_length: Option, pub format_on_save: Option, pub remove_trailing_whitespace_on_save: Option, pub ensure_final_newline_on_save: Option, pub formatter: Option, pub enable_language_server: Option, pub show_copilot_suggestions: Option, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum SoftWrap { None, EditorWidth, PreferredLineLength, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum FormatOnSave { On, Off, LanguageServer, External { command: String, arguments: Vec, }, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum Formatter { LanguageServer, External { command: String, arguments: Vec, }, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum Autosave { Off, AfterDelay { milliseconds: u64 }, OnFocusChange, OnWindowChange, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct JournalSettings { pub path: Option, pub hour_format: Option, } impl Default for JournalSettings { fn default() -> Self { Self { path: Some("~".into()), hour_format: Some(Default::default()), } } } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum HourFormat { Hour12, Hour24, } impl Default for HourFormat { fn default() -> Self { Self::Hour12 } } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct TerminalSettings { pub shell: Option, pub working_directory: Option, pub font_size: Option, pub font_family: Option, pub line_height: Option, pub font_features: Option, pub env: Option>, pub blinking: Option, pub alternate_scroll: Option, pub option_as_meta: Option, pub copy_on_select: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] #[serde(rename_all = "snake_case")] pub enum TerminalLineHeight { #[default] Comfortable, Standard, Custom(f32), } impl TerminalLineHeight { fn value(&self) -> f32 { match self { TerminalLineHeight::Comfortable => 1.618, TerminalLineHeight::Standard => 1.3, TerminalLineHeight::Custom(line_height) => *line_height, } } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum TerminalBlink { Off, TerminalControlled, On, } impl Default for TerminalBlink { fn default() -> Self { TerminalBlink::TerminalControlled } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum Shell { System, Program(String), WithArguments { program: String, args: Vec }, } impl Default for Shell { fn default() -> Self { Shell::System } } #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum AlternateScroll { On, Off, } impl Default for AlternateScroll { fn default() -> Self { AlternateScroll::On } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum WorkingDirectory { CurrentProjectDirectory, FirstProjectDirectory, AlwaysHome, Always { directory: String }, } impl Default for WorkingDirectory { fn default() -> Self { Self::CurrentProjectDirectory } } impl TerminalSettings { fn line_height(&self) -> Option { self.line_height .to_owned() .map(|line_height| line_height.value()) } } #[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum DockAnchor { #[default] Bottom, Right, Expanded, } impl StaticColumnCount for DockAnchor {} impl Bind for DockAnchor { fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { match self { DockAnchor::Bottom => "Bottom", DockAnchor::Right => "Right", DockAnchor::Expanded => "Expanded", } .bind(statement, start_index) } } impl Column for DockAnchor { fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { String::column(statement, start_index).and_then(|(anchor_text, next_index)| { Ok(( match anchor_text.as_ref() { "Bottom" => DockAnchor::Bottom, "Right" => DockAnchor::Right, "Expanded" => DockAnchor::Expanded, _ => bail!("Stored dock anchor is incorrect"), }, next_index, )) }) } } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct SettingsFileContent { #[serde(default)] pub buffer_font_family: Option, #[serde(default)] pub buffer_font_size: Option, #[serde(default)] pub buffer_font_features: Option, #[serde(default)] pub active_pane_magnification: Option, #[serde(default)] pub cursor_blink: Option, #[serde(default)] pub confirm_quit: Option, #[serde(default)] pub hover_popover_enabled: Option, #[serde(default)] pub show_completions_on_input: Option, #[serde(default)] pub show_call_status_icon: Option, #[serde(default)] pub vim_mode: Option, #[serde(default)] pub autosave: Option, #[serde(default)] pub default_dock_anchor: Option, #[serde(flatten)] pub editor: EditorSettings, #[serde(default)] pub journal: JournalSettings, #[serde(default)] pub terminal: TerminalSettings, #[serde(default)] pub git: Option, #[serde(default)] #[serde(alias = "language_overrides")] pub languages: HashMap, EditorSettings>, #[serde(default)] pub lsp: HashMap, LspSettings>, #[serde(default)] pub theme: Option, #[serde(default)] pub telemetry: TelemetrySettings, #[serde(default)] pub auto_update: Option, #[serde(default)] pub base_keymap: Option, #[serde(default)] pub features: FeaturesContent, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct LspSettings { pub initialization_options: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct Features { pub copilot: bool, } #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct FeaturesContent { pub copilot: Option, } impl Settings { /// Fill out the settings corresponding to the default.json file, overrides will be set later pub fn defaults( assets: impl AssetSource, font_cache: &FontCache, themes: &ThemeRegistry, ) -> Self { #[track_caller] fn required(value: Option) -> Option { assert!(value.is_some(), "missing default setting value"); value } let defaults: SettingsFileContent = parse_json_with_comments( str::from_utf8(assets.load("settings/default.json").unwrap().as_ref()).unwrap(), ) .unwrap(); let buffer_font_features = defaults.buffer_font_features.unwrap(); Self { buffer_font_family: font_cache .load_family( &[defaults.buffer_font_family.as_ref().unwrap()], &buffer_font_features, ) .unwrap(), buffer_font_family_name: defaults.buffer_font_family.unwrap(), buffer_font_features, buffer_font_size: defaults.buffer_font_size.unwrap(), active_pane_magnification: defaults.active_pane_magnification.unwrap(), default_buffer_font_size: defaults.buffer_font_size.unwrap(), confirm_quit: defaults.confirm_quit.unwrap(), cursor_blink: defaults.cursor_blink.unwrap(), hover_popover_enabled: defaults.hover_popover_enabled.unwrap(), show_completions_on_input: defaults.show_completions_on_input.unwrap(), show_call_status_icon: defaults.show_call_status_icon.unwrap(), vim_mode: defaults.vim_mode.unwrap(), autosave: defaults.autosave.unwrap(), default_dock_anchor: defaults.default_dock_anchor.unwrap(), editor_defaults: EditorSettings { tab_size: required(defaults.editor.tab_size), hard_tabs: required(defaults.editor.hard_tabs), soft_wrap: required(defaults.editor.soft_wrap), preferred_line_length: required(defaults.editor.preferred_line_length), remove_trailing_whitespace_on_save: required( defaults.editor.remove_trailing_whitespace_on_save, ), ensure_final_newline_on_save: required( defaults.editor.ensure_final_newline_on_save, ), format_on_save: required(defaults.editor.format_on_save), formatter: required(defaults.editor.formatter), enable_language_server: required(defaults.editor.enable_language_server), show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions), }, editor_overrides: Default::default(), git: defaults.git.unwrap(), git_overrides: Default::default(), journal_defaults: defaults.journal, journal_overrides: Default::default(), terminal_defaults: defaults.terminal, terminal_overrides: Default::default(), language_defaults: defaults.languages, language_overrides: Default::default(), lsp: defaults.lsp.clone(), theme: themes.get(&defaults.theme.unwrap()).unwrap(), telemetry_defaults: defaults.telemetry, telemetry_overrides: Default::default(), auto_update: defaults.auto_update.unwrap(), base_keymap: Default::default(), features: Features { copilot: defaults.features.copilot.unwrap(), }, } } // Fill out the overrride and etc. settings from the user's settings.json pub fn set_user_settings( &mut self, data: SettingsFileContent, theme_registry: &ThemeRegistry, font_cache: &FontCache, ) { let mut family_changed = false; if let Some(value) = data.buffer_font_family { self.buffer_font_family_name = value; family_changed = true; } if let Some(value) = data.buffer_font_features { self.buffer_font_features = value; family_changed = true; } if family_changed { if let Some(id) = font_cache .load_family(&[&self.buffer_font_family_name], &self.buffer_font_features) .log_err() { self.buffer_font_family = id; } } if let Some(value) = &data.theme { if let Some(theme) = theme_registry.get(value).log_err() { self.theme = theme; } } merge(&mut self.buffer_font_size, data.buffer_font_size); merge( &mut self.active_pane_magnification, data.active_pane_magnification, ); merge(&mut self.default_buffer_font_size, data.buffer_font_size); merge(&mut self.cursor_blink, data.cursor_blink); merge(&mut self.confirm_quit, data.confirm_quit); merge(&mut self.hover_popover_enabled, data.hover_popover_enabled); merge( &mut self.show_completions_on_input, data.show_completions_on_input, ); merge(&mut self.vim_mode, data.vim_mode); merge(&mut self.autosave, data.autosave); merge(&mut self.default_dock_anchor, data.default_dock_anchor); merge(&mut self.base_keymap, data.base_keymap); merge(&mut self.features.copilot, data.features.copilot); self.editor_overrides = data.editor; self.git_overrides = data.git.unwrap_or_default(); self.journal_overrides = data.journal; self.terminal_defaults.font_size = data.terminal.font_size; self.terminal_overrides.copy_on_select = data.terminal.copy_on_select; self.terminal_overrides = data.terminal; self.language_overrides = data.languages; self.telemetry_overrides = data.telemetry; self.lsp = data.lsp; merge(&mut self.auto_update, data.auto_update); } pub fn with_language_defaults( mut self, language_name: impl Into>, overrides: EditorSettings, ) -> Self { self.language_defaults .insert(language_name.into(), overrides); self } pub fn features(&self) -> &Features { &self.features } pub fn show_copilot_suggestions(&self, language: Option<&str>) -> bool { self.features.copilot && self.language_setting(language, |settings| { settings.show_copilot_suggestions.map(Into::into) }) } pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 { self.language_setting(language, |settings| settings.tab_size) } pub fn hard_tabs(&self, language: Option<&str>) -> bool { self.language_setting(language, |settings| settings.hard_tabs) } pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap { self.language_setting(language, |settings| settings.soft_wrap) } pub fn preferred_line_length(&self, language: Option<&str>) -> u32 { self.language_setting(language, |settings| settings.preferred_line_length) } pub fn remove_trailing_whitespace_on_save(&self, language: Option<&str>) -> bool { self.language_setting(language, |settings| { settings.remove_trailing_whitespace_on_save.clone() }) } pub fn ensure_final_newline_on_save(&self, language: Option<&str>) -> bool { self.language_setting(language, |settings| { settings.ensure_final_newline_on_save.clone() }) } pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave { self.language_setting(language, |settings| settings.format_on_save.clone()) } pub fn formatter(&self, language: Option<&str>) -> Formatter { self.language_setting(language, |settings| settings.formatter.clone()) } pub fn enable_language_server(&self, language: Option<&str>) -> bool { self.language_setting(language, |settings| settings.enable_language_server) } fn language_setting(&self, language: Option<&str>, f: F) -> R where F: Fn(&EditorSettings) -> Option, { None.or_else(|| language.and_then(|l| self.language_overrides.get(l).and_then(&f))) .or_else(|| f(&self.editor_overrides)) .or_else(|| language.and_then(|l| self.language_defaults.get(l).and_then(&f))) .or_else(|| f(&self.editor_defaults)) .expect("missing default") } pub fn git_gutter(&self) -> GitGutter { self.git_overrides.git_gutter.unwrap_or_else(|| { self.git .git_gutter .expect("git_gutter should be some by setting setup") }) } pub fn telemetry(&self) -> TelemetrySettings { TelemetrySettings { diagnostics: Some(self.telemetry_diagnostics()), metrics: Some(self.telemetry_metrics()), } } pub fn telemetry_diagnostics(&self) -> bool { self.telemetry_overrides .diagnostics .or(self.telemetry_defaults.diagnostics) .expect("missing default") } pub fn telemetry_metrics(&self) -> bool { self.telemetry_overrides .metrics .or(self.telemetry_defaults.metrics) .expect("missing default") } fn terminal_setting(&self, f: F) -> R where F: Fn(&TerminalSettings) -> Option, { None.or_else(|| f(&self.terminal_overrides)) .or_else(|| f(&self.terminal_defaults)) .expect("missing default") } pub fn terminal_line_height(&self) -> f32 { self.terminal_setting(|terminal_setting| terminal_setting.line_height()) } pub fn terminal_scroll(&self) -> AlternateScroll { self.terminal_setting(|terminal_setting| terminal_setting.alternate_scroll.to_owned()) } pub fn terminal_shell(&self) -> Shell { self.terminal_setting(|terminal_setting| terminal_setting.shell.to_owned()) } pub fn terminal_env(&self) -> HashMap { self.terminal_setting(|terminal_setting| terminal_setting.env.to_owned()) } pub fn terminal_strategy(&self) -> WorkingDirectory { self.terminal_setting(|terminal_setting| terminal_setting.working_directory.to_owned()) } #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &gpui::AppContext) -> Settings { Settings { buffer_font_family_name: "Monaco".to_string(), buffer_font_features: Default::default(), buffer_font_family: cx .font_cache() .load_family(&["Monaco"], &Default::default()) .unwrap(), buffer_font_size: 14., active_pane_magnification: 1., default_buffer_font_size: 14., confirm_quit: false, cursor_blink: true, hover_popover_enabled: true, show_completions_on_input: true, show_call_status_icon: true, vim_mode: false, autosave: Autosave::Off, default_dock_anchor: DockAnchor::Bottom, editor_defaults: EditorSettings { tab_size: Some(4.try_into().unwrap()), hard_tabs: Some(false), soft_wrap: Some(SoftWrap::None), preferred_line_length: Some(80), remove_trailing_whitespace_on_save: Some(true), ensure_final_newline_on_save: Some(true), format_on_save: Some(FormatOnSave::On), formatter: Some(Formatter::LanguageServer), enable_language_server: Some(true), show_copilot_suggestions: Some(true), }, editor_overrides: Default::default(), journal_defaults: Default::default(), journal_overrides: Default::default(), terminal_defaults: Default::default(), terminal_overrides: Default::default(), git: Default::default(), git_overrides: Default::default(), language_defaults: Default::default(), language_overrides: Default::default(), lsp: Default::default(), theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), Default::default), telemetry_defaults: TelemetrySettings { diagnostics: Some(true), metrics: Some(true), }, telemetry_overrides: Default::default(), auto_update: true, base_keymap: Default::default(), features: Features { copilot: true }, } } #[cfg(any(test, feature = "test-support"))] pub fn test_async(cx: &mut gpui::TestAppContext) { cx.update(|cx| { let settings = Self::test(cx); cx.set_global(settings); }); } } pub fn settings_file_json_schema( theme_names: Vec, language_names: &[String], ) -> serde_json::Value { let settings = SchemaSettings::draft07().with(|settings| { settings.option_add_null_type = false; }); let generator = SchemaGenerator::new(settings); let mut root_schema = generator.into_root_schema_for::(); // Create a schema for a theme name. let theme_name_schema = SchemaObject { instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))), enum_values: Some(theme_names.into_iter().map(Value::String).collect()), ..Default::default() }; // Create a schema for a 'languages overrides' object, associating editor // settings with specific langauges. assert!(root_schema.definitions.contains_key("EditorSettings")); let languages_object_schema = SchemaObject { instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))), object: Some(Box::new(ObjectValidation { properties: language_names .iter() .map(|name| { ( name.clone(), Schema::new_ref("#/definitions/EditorSettings".into()), ) }) .collect(), ..Default::default() })), ..Default::default() }; // Add these new schemas as definitions, and modify properties of the root // schema to reference them. root_schema.definitions.extend([ ("ThemeName".into(), theme_name_schema.into()), ("Languages".into(), languages_object_schema.into()), ]); let root_schema_object = &mut root_schema.schema.object.as_mut().unwrap(); root_schema_object.properties.extend([ ( "theme".to_owned(), Schema::new_ref("#/definitions/ThemeName".into()), ), ( "languages".to_owned(), Schema::new_ref("#/definitions/Languages".into()), ), // For backward compatibility ( "language_overrides".to_owned(), Schema::new_ref("#/definitions/Languages".into()), ), ]); serde_json::to_value(root_schema).unwrap() } fn merge(target: &mut T, value: Option) { if let Some(value) = value { *target = value; } } pub fn parse_json_with_comments(content: &str) -> Result { Ok(serde_json::from_reader( json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()), )?) } fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_value: &Value) { const LANGUAGE_OVERRIDES: &'static str = "language_overrides"; const LANGAUGES: &'static str = "languages"; let mut parser = tree_sitter::Parser::new(); parser.set_language(tree_sitter_json::language()).unwrap(); let tree = parser.parse(&settings_content, None).unwrap(); let mut cursor = tree_sitter::QueryCursor::new(); let query = Query::new( tree_sitter_json::language(), " (pair key: (string) @key value: (_) @value) ", ) .unwrap(); let has_language_overrides = settings_content.contains(LANGUAGE_OVERRIDES); let mut depth = 0; let mut last_value_range = 0..0; let mut first_key_start = None; let mut existing_value_range = 0..settings_content.len(); let matches = cursor.matches(&query, tree.root_node(), settings_content.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_with(|| key_range.start); let found_key = settings_content .get(key_range.clone()) .map(|key_text| { if key_path[depth] == LANGAUGES && has_language_overrides { return key_text == format!("\"{}\"", LANGUAGE_OVERRIDES); } else { return 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; } else { first_key_start = None; } } } // We found the exact key we want, insert the new value if depth == key_path.len() { let new_val = serde_json::to_string_pretty(new_value) .expect("Could not serialize new json field to string"); settings_content.replace_range(existing_value_range, &new_val); } else { // We have key paths, construct the sub objects let new_key = if has_language_overrides && key_path[depth] == LANGAUGES { LANGUAGE_OVERRIDES } else { 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() { if has_language_overrides && key == &LANGAUGES { new_value = serde_json::json!({ LANGUAGE_OVERRIDES.to_string(): new_value }); } else { 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 settings_content.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 content = format!(r#""{new_key}": {new_val},"#); settings_content.insert_str(first_key_start, &content); settings_content.insert_str( first_key_start + content.len(), &format!("\n{:width$}", ' ', width = column), ) } else { let new_val = serde_json::to_string(&new_value).unwrap(); let mut content = format!(r#""{new_key}": {new_val},"#); content.push(' '); settings_content.insert_str(first_key_start, &content); } } else { new_value = serde_json::json!({ new_key.to_string(): new_value }); let indent_prefix_len = 4 * depth; let new_val = to_pretty_json(&new_value, 4, indent_prefix_len); settings_content.replace_range(existing_value_range, &new_val); if depth == 0 { settings_content.push('\n'); } } } } fn to_pretty_json( value: &serde_json::Value, 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 update_settings_file( mut text: String, mut old_file_content: SettingsFileContent, update: impl FnOnce(&mut SettingsFileContent), ) -> String { let mut new_file_content = old_file_content.clone(); update(&mut new_file_content); if new_file_content.languages.len() != old_file_content.languages.len() { for language in new_file_content.languages.keys() { old_file_content .languages .entry(language.clone()) .or_default(); } for language in old_file_content.languages.keys() { new_file_content .languages .entry(language.clone()) .or_default(); } } let old_object = to_json_object(old_file_content); let new_object = to_json_object(new_file_content); fn apply_changes_to_json_text( old_object: &serde_json::Map, new_object: &serde_json::Map, current_key_path: Vec<&str>, json_text: &mut String, ) { for (key, old_value) in old_object.iter() { // We know that these two are from the same shape of object, so we can just unwrap let new_value = new_object.get(key).unwrap(); if old_value != new_value { match new_value { Value::Bool(_) | Value::Number(_) | Value::String(_) => { let mut key_path = current_key_path.clone(); key_path.push(key); write_settings_key(json_text, &key_path, &new_value); } Value::Object(new_sub_object) => { let mut key_path = current_key_path.clone(); key_path.push(key); if let Value::Object(old_sub_object) = old_value { apply_changes_to_json_text( old_sub_object, new_sub_object, key_path, json_text, ); } else { unimplemented!("This function doesn't support changing values from simple values to objects yet"); } } Value::Null | Value::Array(_) => { unimplemented!("We only support objects and simple values"); } } } } } apply_changes_to_json_text(&old_object, &new_object, vec![], &mut text); text } fn to_json_object(settings_file: SettingsFileContent) -> serde_json::Map { let tmp = serde_json::to_value(settings_file).unwrap(); match tmp { Value::Object(map) => map, _ => unreachable!("SettingsFileContent represents a JSON map"), } } #[cfg(test)] mod tests { use super::*; use unindent::Unindent; fn assert_new_settings, S2: Into>( old_json: S1, update: fn(&mut SettingsFileContent), expected_new_json: S2, ) { let old_json = old_json.into(); let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default(); let new_json = update_settings_file(old_json, old_content, update); pretty_assertions::assert_eq!(new_json, expected_new_json.into()); } #[test] fn test_update_language_overrides_copilot() { assert_new_settings( r#" { "language_overrides": { "JSON": { "show_copilot_suggestions": false } } } "# .unindent(), |settings| { settings.languages.insert( "Rust".into(), EditorSettings { show_copilot_suggestions: Some(true), ..Default::default() }, ); }, r#" { "language_overrides": { "Rust": { "show_copilot_suggestions": true }, "JSON": { "show_copilot_suggestions": false } } } "# .unindent(), ); } #[test] fn test_update_copilot() { assert_new_settings( r#" { "languages": { "JSON": { "show_copilot_suggestions": false } } } "# .unindent(), |settings| { settings.editor.show_copilot_suggestions = Some(true); }, r#" { "show_copilot_suggestions": true, "languages": { "JSON": { "show_copilot_suggestions": false } } } "# .unindent(), ); } #[test] fn test_update_language_copilot() { assert_new_settings( r#" { "languages": { "JSON": { "show_copilot_suggestions": false } } } "# .unindent(), |settings| { settings.languages.insert( "Rust".into(), EditorSettings { show_copilot_suggestions: Some(true), ..Default::default() }, ); }, r#" { "languages": { "Rust": { "show_copilot_suggestions": true }, "JSON": { "show_copilot_suggestions": false } } } "# .unindent(), ); } #[test] fn test_update_telemetry_setting_multiple_fields() { assert_new_settings( r#" { "telemetry": { "metrics": false, "diagnostics": false } } "# .unindent(), |settings| { settings.telemetry.set_diagnostics(true); settings.telemetry.set_metrics(true); }, r#" { "telemetry": { "metrics": true, "diagnostics": true } } "# .unindent(), ); } #[test] fn test_update_telemetry_setting_weird_formatting() { assert_new_settings( r#"{ "telemetry": { "metrics": false, "diagnostics": true } }"# .unindent(), |settings| settings.telemetry.set_diagnostics(false), r#"{ "telemetry": { "metrics": false, "diagnostics": false } }"# .unindent(), ); } #[test] fn test_update_telemetry_setting_other_fields() { assert_new_settings( r#" { "telemetry": { "metrics": false, "diagnostics": true } } "# .unindent(), |settings| settings.telemetry.set_diagnostics(false), r#" { "telemetry": { "metrics": false, "diagnostics": false } } "# .unindent(), ); } #[test] fn test_update_telemetry_setting_empty_telemetry() { assert_new_settings( r#" { "telemetry": {} } "# .unindent(), |settings| settings.telemetry.set_diagnostics(false), r#" { "telemetry": { "diagnostics": false } } "# .unindent(), ); } #[test] fn test_update_telemetry_setting_pre_existing() { assert_new_settings( r#" { "telemetry": { "diagnostics": true } } "# .unindent(), |settings| settings.telemetry.set_diagnostics(false), r#" { "telemetry": { "diagnostics": false } } "# .unindent(), ); } #[test] fn test_update_telemetry_setting() { assert_new_settings( "{}", |settings| settings.telemetry.set_diagnostics(true), r#" { "telemetry": { "diagnostics": true } } "# .unindent(), ); } #[test] fn test_update_object_empty_doc() { assert_new_settings( "", |settings| settings.telemetry.set_diagnostics(true), r#" { "telemetry": { "diagnostics": true } } "# .unindent(), ); } #[test] fn test_write_theme_into_settings_with_theme() { assert_new_settings( r#" { "theme": "One Dark" } "# .unindent(), |settings| settings.theme = Some("summerfruit-light".to_string()), r#" { "theme": "summerfruit-light" } "# .unindent(), ); } #[test] fn test_write_theme_into_empty_settings() { assert_new_settings( r#" { } "# .unindent(), |settings| settings.theme = Some("summerfruit-light".to_string()), r#" { "theme": "summerfruit-light" } "# .unindent(), ); } #[test] fn write_key_no_document() { assert_new_settings( "", |settings| settings.theme = Some("summerfruit-light".to_string()), r#" { "theme": "summerfruit-light" } "# .unindent(), ); } #[test] fn test_write_theme_into_single_line_settings_without_theme() { assert_new_settings( r#"{ "a": "", "ok": true }"#, |settings| settings.theme = Some("summerfruit-light".to_string()), r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#, ); } #[test] fn test_write_theme_pre_object_whitespace() { assert_new_settings( r#" { "a": "", "ok": true }"#, |settings| settings.theme = Some("summerfruit-light".to_string()), r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#.unindent(), ); } #[test] fn test_write_theme_into_multi_line_settings_without_theme() { assert_new_settings( r#" { "a": "b" } "# .unindent(), |settings| settings.theme = Some("summerfruit-light".to_string()), r#" { "theme": "summerfruit-light", "a": "b" } "# .unindent(), ); } }