settings.rs

   1mod keymap_file;
   2pub mod settings_file;
   3pub mod watched_json;
   4
   5use anyhow::Result;
   6use gpui::{
   7    font_cache::{FamilyId, FontCache},
   8    fonts, AssetSource,
   9};
  10use lazy_static::lazy_static;
  11use schemars::{
  12    gen::{SchemaGenerator, SchemaSettings},
  13    schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
  14    JsonSchema,
  15};
  16use serde::{de::DeserializeOwned, Deserialize, Serialize};
  17use serde_json::Value;
  18use std::{
  19    borrow::Cow, collections::HashMap, num::NonZeroU32, ops::Range, path::Path, str, sync::Arc,
  20};
  21use theme::{Theme, ThemeRegistry};
  22use tree_sitter::{Query, Tree};
  23use util::{RangeExt, ResultExt as _};
  24
  25pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
  26pub use watched_json::watch_files;
  27
  28pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
  29pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
  30
  31#[derive(Clone)]
  32pub struct Settings {
  33    pub features: Features,
  34    pub buffer_font_family_name: String,
  35    pub buffer_font_features: fonts::Features,
  36    pub buffer_font_family: FamilyId,
  37    pub default_buffer_font_size: f32,
  38    pub buffer_font_size: f32,
  39    pub active_pane_magnification: f32,
  40    pub cursor_blink: bool,
  41    pub confirm_quit: bool,
  42    pub hover_popover_enabled: bool,
  43    pub show_completions_on_input: bool,
  44    pub show_call_status_icon: bool,
  45    pub vim_mode: bool,
  46    pub autosave: Autosave,
  47    pub project_panel_defaults: ProjectPanelSettings,
  48    pub project_panel_overrides: ProjectPanelSettings,
  49    pub editor_defaults: EditorSettings,
  50    pub editor_overrides: EditorSettings,
  51    pub git: GitSettings,
  52    pub git_overrides: GitSettings,
  53    pub copilot: CopilotSettings,
  54    pub journal_defaults: JournalSettings,
  55    pub journal_overrides: JournalSettings,
  56    pub terminal_defaults: TerminalSettings,
  57    pub terminal_overrides: TerminalSettings,
  58    pub language_defaults: HashMap<Arc<str>, EditorSettings>,
  59    pub language_overrides: HashMap<Arc<str>, EditorSettings>,
  60    pub lsp: HashMap<Arc<str>, LspSettings>,
  61    pub theme: Arc<Theme>,
  62    pub telemetry_defaults: TelemetrySettings,
  63    pub telemetry_overrides: TelemetrySettings,
  64    pub auto_update: bool,
  65    pub base_keymap: BaseKeymap,
  66}
  67
  68#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
  69pub enum BaseKeymap {
  70    #[default]
  71    VSCode,
  72    JetBrains,
  73    SublimeText,
  74    Atom,
  75    TextMate,
  76}
  77
  78impl BaseKeymap {
  79    pub const OPTIONS: [(&'static str, Self); 5] = [
  80        ("VSCode (Default)", Self::VSCode),
  81        ("Atom", Self::Atom),
  82        ("JetBrains", Self::JetBrains),
  83        ("Sublime Text", Self::SublimeText),
  84        ("TextMate", Self::TextMate),
  85    ];
  86
  87    pub fn asset_path(&self) -> Option<&'static str> {
  88        match self {
  89            BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
  90            BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"),
  91            BaseKeymap::Atom => Some("keymaps/atom.json"),
  92            BaseKeymap::TextMate => Some("keymaps/textmate.json"),
  93            BaseKeymap::VSCode => None,
  94        }
  95    }
  96
  97    pub fn names() -> impl Iterator<Item = &'static str> {
  98        Self::OPTIONS.iter().map(|(name, _)| *name)
  99    }
 100
 101    pub fn from_names(option: &str) -> BaseKeymap {
 102        Self::OPTIONS
 103            .iter()
 104            .copied()
 105            .find_map(|(name, value)| (name == option).then(|| value))
 106            .unwrap_or_default()
 107    }
 108}
 109
 110#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 111pub struct TelemetrySettings {
 112    diagnostics: Option<bool>,
 113    metrics: Option<bool>,
 114}
 115
 116impl TelemetrySettings {
 117    pub fn metrics(&self) -> bool {
 118        self.metrics.unwrap()
 119    }
 120
 121    pub fn diagnostics(&self) -> bool {
 122        self.diagnostics.unwrap()
 123    }
 124
 125    pub fn set_metrics(&mut self, value: bool) {
 126        self.metrics = Some(value);
 127    }
 128
 129    pub fn set_diagnostics(&mut self, value: bool) {
 130        self.diagnostics = Some(value);
 131    }
 132}
 133
 134#[derive(Clone, Debug, Default)]
 135pub struct CopilotSettings {
 136    pub disabled_globs: Vec<glob::Pattern>,
 137}
 138
 139#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 140pub struct CopilotSettingsContent {
 141    #[serde(default)]
 142    pub disabled_globs: Option<Vec<String>>,
 143}
 144
 145#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 146pub struct GitSettings {
 147    pub git_gutter: Option<GitGutter>,
 148    pub gutter_debounce: Option<u64>,
 149}
 150
 151#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
 152#[serde(rename_all = "snake_case")]
 153pub enum GitGutter {
 154    #[default]
 155    TrackedFiles,
 156    Hide,
 157}
 158
 159pub struct GitGutterConfig {}
 160
 161#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 162pub struct ProjectPanelSettings {
 163    pub dock: Option<ProjectPanelDockPosition>,
 164}
 165
 166#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
 167#[serde(rename_all = "lowercase")]
 168pub enum ProjectPanelDockPosition {
 169    Left,
 170    Right,
 171}
 172
 173#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 174pub struct EditorSettings {
 175    pub tab_size: Option<NonZeroU32>,
 176    pub hard_tabs: Option<bool>,
 177    pub soft_wrap: Option<SoftWrap>,
 178    pub preferred_line_length: Option<u32>,
 179    pub format_on_save: Option<FormatOnSave>,
 180    pub remove_trailing_whitespace_on_save: Option<bool>,
 181    pub ensure_final_newline_on_save: Option<bool>,
 182    pub formatter: Option<Formatter>,
 183    pub enable_language_server: Option<bool>,
 184    pub show_copilot_suggestions: Option<bool>,
 185    pub show_whitespaces: Option<ShowWhitespaces>,
 186}
 187
 188#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 189#[serde(rename_all = "snake_case")]
 190pub enum SoftWrap {
 191    None,
 192    EditorWidth,
 193    PreferredLineLength,
 194}
 195#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 196#[serde(rename_all = "snake_case")]
 197pub enum FormatOnSave {
 198    On,
 199    Off,
 200    LanguageServer,
 201    External {
 202        command: String,
 203        arguments: Vec<String>,
 204    },
 205}
 206
 207#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 208#[serde(rename_all = "snake_case")]
 209pub enum Formatter {
 210    LanguageServer,
 211    External {
 212        command: String,
 213        arguments: Vec<String>,
 214    },
 215}
 216
 217#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 218#[serde(rename_all = "snake_case")]
 219pub enum Autosave {
 220    Off,
 221    AfterDelay { milliseconds: u64 },
 222    OnFocusChange,
 223    OnWindowChange,
 224}
 225
 226#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
 227pub struct JournalSettings {
 228    pub path: Option<String>,
 229    pub hour_format: Option<HourFormat>,
 230}
 231
 232impl Default for JournalSettings {
 233    fn default() -> Self {
 234        Self {
 235            path: Some("~".into()),
 236            hour_format: Some(Default::default()),
 237        }
 238    }
 239}
 240
 241#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
 242#[serde(rename_all = "snake_case")]
 243pub enum HourFormat {
 244    Hour12,
 245    Hour24,
 246}
 247
 248impl Default for HourFormat {
 249    fn default() -> Self {
 250        Self::Hour12
 251    }
 252}
 253
 254#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 255pub struct TerminalSettings {
 256    pub shell: Option<Shell>,
 257    pub working_directory: Option<WorkingDirectory>,
 258    pub font_size: Option<f32>,
 259    pub font_family: Option<String>,
 260    pub line_height: Option<TerminalLineHeight>,
 261    pub font_features: Option<fonts::Features>,
 262    pub env: Option<HashMap<String, String>>,
 263    pub blinking: Option<TerminalBlink>,
 264    pub alternate_scroll: Option<AlternateScroll>,
 265    pub option_as_meta: Option<bool>,
 266    pub copy_on_select: Option<bool>,
 267    pub dock: Option<TerminalDockPosition>,
 268}
 269
 270#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
 271#[serde(rename_all = "lowercase")]
 272pub enum TerminalDockPosition {
 273    Left,
 274    Bottom,
 275    Right,
 276}
 277
 278#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
 279#[serde(rename_all = "snake_case")]
 280pub enum TerminalLineHeight {
 281    #[default]
 282    Comfortable,
 283    Standard,
 284    Custom(f32),
 285}
 286
 287impl TerminalLineHeight {
 288    fn value(&self) -> f32 {
 289        match self {
 290            TerminalLineHeight::Comfortable => 1.618,
 291            TerminalLineHeight::Standard => 1.3,
 292            TerminalLineHeight::Custom(line_height) => *line_height,
 293        }
 294    }
 295}
 296
 297#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 298#[serde(rename_all = "snake_case")]
 299pub enum TerminalBlink {
 300    Off,
 301    TerminalControlled,
 302    On,
 303}
 304
 305impl Default for TerminalBlink {
 306    fn default() -> Self {
 307        TerminalBlink::TerminalControlled
 308    }
 309}
 310
 311#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 312#[serde(rename_all = "snake_case")]
 313pub enum Shell {
 314    System,
 315    Program(String),
 316    WithArguments { program: String, args: Vec<String> },
 317}
 318
 319impl Default for Shell {
 320    fn default() -> Self {
 321        Shell::System
 322    }
 323}
 324
 325#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 326#[serde(rename_all = "snake_case")]
 327pub enum AlternateScroll {
 328    On,
 329    Off,
 330}
 331
 332impl Default for AlternateScroll {
 333    fn default() -> Self {
 334        AlternateScroll::On
 335    }
 336}
 337
 338#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 339#[serde(rename_all = "snake_case")]
 340pub enum WorkingDirectory {
 341    CurrentProjectDirectory,
 342    FirstProjectDirectory,
 343    AlwaysHome,
 344    Always { directory: String },
 345}
 346
 347impl Default for WorkingDirectory {
 348    fn default() -> Self {
 349        Self::CurrentProjectDirectory
 350    }
 351}
 352
 353impl TerminalSettings {
 354    fn line_height(&self) -> Option<f32> {
 355        self.line_height
 356            .to_owned()
 357            .map(|line_height| line_height.value())
 358    }
 359}
 360
 361#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 362pub struct SettingsFileContent {
 363    #[serde(default)]
 364    pub buffer_font_family: Option<String>,
 365    #[serde(default)]
 366    pub buffer_font_size: Option<f32>,
 367    #[serde(default)]
 368    pub buffer_font_features: Option<fonts::Features>,
 369    #[serde(default)]
 370    pub copilot: Option<CopilotSettingsContent>,
 371    #[serde(default)]
 372    pub active_pane_magnification: Option<f32>,
 373    #[serde(default)]
 374    pub cursor_blink: Option<bool>,
 375    #[serde(default)]
 376    pub confirm_quit: Option<bool>,
 377    #[serde(default)]
 378    pub hover_popover_enabled: Option<bool>,
 379    #[serde(default)]
 380    pub show_completions_on_input: Option<bool>,
 381    #[serde(default)]
 382    pub show_call_status_icon: Option<bool>,
 383    #[serde(default)]
 384    pub vim_mode: Option<bool>,
 385    #[serde(default)]
 386    pub autosave: Option<Autosave>,
 387    #[serde(flatten)]
 388    pub editor: EditorSettings,
 389    #[serde(default)]
 390    pub project_panel: ProjectPanelSettings,
 391    #[serde(default)]
 392    pub journal: JournalSettings,
 393    #[serde(default)]
 394    pub terminal: TerminalSettings,
 395    #[serde(default)]
 396    pub git: Option<GitSettings>,
 397    #[serde(default)]
 398    #[serde(alias = "language_overrides")]
 399    pub languages: HashMap<Arc<str>, EditorSettings>,
 400    #[serde(default)]
 401    pub lsp: HashMap<Arc<str>, LspSettings>,
 402    #[serde(default)]
 403    pub theme: Option<String>,
 404    #[serde(default)]
 405    pub telemetry: TelemetrySettings,
 406    #[serde(default)]
 407    pub auto_update: Option<bool>,
 408    #[serde(default)]
 409    pub base_keymap: Option<BaseKeymap>,
 410    #[serde(default)]
 411    pub features: FeaturesContent,
 412}
 413
 414#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 415#[serde(rename_all = "snake_case")]
 416pub struct LspSettings {
 417    pub initialization_options: Option<Value>,
 418}
 419
 420#[derive(Clone, Debug, PartialEq, Eq)]
 421pub struct Features {
 422    pub copilot: bool,
 423}
 424
 425#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 426#[serde(rename_all = "snake_case")]
 427pub struct FeaturesContent {
 428    pub copilot: Option<bool>,
 429}
 430
 431#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 432#[serde(rename_all = "snake_case")]
 433pub enum ShowWhitespaces {
 434    #[default]
 435    Selection,
 436    None,
 437    All,
 438}
 439
 440impl Settings {
 441    pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> {
 442        match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() {
 443            Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
 444            Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
 445        }
 446    }
 447
 448    /// Fill out the settings corresponding to the default.json file, overrides will be set later
 449    pub fn defaults(
 450        assets: impl AssetSource,
 451        font_cache: &FontCache,
 452        themes: &ThemeRegistry,
 453    ) -> Self {
 454        #[track_caller]
 455        fn required<T>(value: Option<T>) -> Option<T> {
 456            assert!(value.is_some(), "missing default setting value");
 457            value
 458        }
 459
 460        let defaults: SettingsFileContent = parse_json_with_comments(
 461            str::from_utf8(assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap().as_ref()).unwrap(),
 462        )
 463        .unwrap();
 464
 465        let buffer_font_features = defaults.buffer_font_features.unwrap();
 466        Self {
 467            buffer_font_family: font_cache
 468                .load_family(
 469                    &[defaults.buffer_font_family.as_ref().unwrap()],
 470                    &buffer_font_features,
 471                )
 472                .unwrap(),
 473            buffer_font_family_name: defaults.buffer_font_family.unwrap(),
 474            buffer_font_features,
 475            buffer_font_size: defaults.buffer_font_size.unwrap(),
 476            active_pane_magnification: defaults.active_pane_magnification.unwrap(),
 477            default_buffer_font_size: defaults.buffer_font_size.unwrap(),
 478            confirm_quit: defaults.confirm_quit.unwrap(),
 479            cursor_blink: defaults.cursor_blink.unwrap(),
 480            hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
 481            show_completions_on_input: defaults.show_completions_on_input.unwrap(),
 482            show_call_status_icon: defaults.show_call_status_icon.unwrap(),
 483            vim_mode: defaults.vim_mode.unwrap(),
 484            autosave: defaults.autosave.unwrap(),
 485            project_panel_defaults: defaults.project_panel,
 486            project_panel_overrides: Default::default(),
 487            editor_defaults: EditorSettings {
 488                tab_size: required(defaults.editor.tab_size),
 489                hard_tabs: required(defaults.editor.hard_tabs),
 490                soft_wrap: required(defaults.editor.soft_wrap),
 491                preferred_line_length: required(defaults.editor.preferred_line_length),
 492                remove_trailing_whitespace_on_save: required(
 493                    defaults.editor.remove_trailing_whitespace_on_save,
 494                ),
 495                ensure_final_newline_on_save: required(
 496                    defaults.editor.ensure_final_newline_on_save,
 497                ),
 498                format_on_save: required(defaults.editor.format_on_save),
 499                formatter: required(defaults.editor.formatter),
 500                enable_language_server: required(defaults.editor.enable_language_server),
 501                show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions),
 502                show_whitespaces: required(defaults.editor.show_whitespaces),
 503            },
 504            editor_overrides: Default::default(),
 505            copilot: CopilotSettings {
 506                disabled_globs: defaults
 507                    .copilot
 508                    .unwrap()
 509                    .disabled_globs
 510                    .unwrap()
 511                    .into_iter()
 512                    .map(|s| glob::Pattern::new(&s).unwrap())
 513                    .collect(),
 514            },
 515            git: defaults.git.unwrap(),
 516            git_overrides: Default::default(),
 517            journal_defaults: defaults.journal,
 518            journal_overrides: Default::default(),
 519            terminal_defaults: defaults.terminal,
 520            terminal_overrides: Default::default(),
 521            language_defaults: defaults.languages,
 522            language_overrides: Default::default(),
 523            lsp: defaults.lsp.clone(),
 524            theme: themes.get(&defaults.theme.unwrap()).unwrap(),
 525            telemetry_defaults: defaults.telemetry,
 526            telemetry_overrides: Default::default(),
 527            auto_update: defaults.auto_update.unwrap(),
 528            base_keymap: Default::default(),
 529            features: Features {
 530                copilot: defaults.features.copilot.unwrap(),
 531            },
 532        }
 533    }
 534
 535    // Fill out the overrride and etc. settings from the user's settings.json
 536    pub fn set_user_settings(
 537        &mut self,
 538        data: SettingsFileContent,
 539        theme_registry: &ThemeRegistry,
 540        font_cache: &FontCache,
 541    ) {
 542        let mut family_changed = false;
 543        if let Some(value) = data.buffer_font_family {
 544            self.buffer_font_family_name = value;
 545            family_changed = true;
 546        }
 547        if let Some(value) = data.buffer_font_features {
 548            self.buffer_font_features = value;
 549            family_changed = true;
 550        }
 551        if family_changed {
 552            if let Some(id) = font_cache
 553                .load_family(&[&self.buffer_font_family_name], &self.buffer_font_features)
 554                .log_err()
 555            {
 556                self.buffer_font_family = id;
 557            }
 558        }
 559
 560        if let Some(value) = &data.theme {
 561            if let Some(theme) = theme_registry.get(value).log_err() {
 562                self.theme = theme;
 563            }
 564        }
 565
 566        merge(&mut self.buffer_font_size, data.buffer_font_size);
 567        merge(
 568            &mut self.active_pane_magnification,
 569            data.active_pane_magnification,
 570        );
 571        merge(&mut self.default_buffer_font_size, data.buffer_font_size);
 572        merge(&mut self.cursor_blink, data.cursor_blink);
 573        merge(&mut self.confirm_quit, data.confirm_quit);
 574        merge(&mut self.hover_popover_enabled, data.hover_popover_enabled);
 575        merge(
 576            &mut self.show_completions_on_input,
 577            data.show_completions_on_input,
 578        );
 579        merge(&mut self.vim_mode, data.vim_mode);
 580        merge(&mut self.autosave, data.autosave);
 581        merge(&mut self.base_keymap, data.base_keymap);
 582        merge(&mut self.features.copilot, data.features.copilot);
 583
 584        if let Some(copilot) = data.copilot {
 585            if let Some(disabled_globs) = copilot.disabled_globs {
 586                self.copilot.disabled_globs = disabled_globs
 587                    .into_iter()
 588                    .filter_map(|s| glob::Pattern::new(&s).ok())
 589                    .collect()
 590            }
 591        }
 592        self.editor_overrides = data.editor;
 593        self.project_panel_overrides = data.project_panel;
 594        self.git_overrides = data.git.unwrap_or_default();
 595        self.journal_overrides = data.journal;
 596        self.terminal_defaults.font_size = data.terminal.font_size;
 597        self.terminal_overrides.copy_on_select = data.terminal.copy_on_select;
 598        self.terminal_overrides = data.terminal;
 599        self.language_overrides = data.languages;
 600        self.telemetry_overrides = data.telemetry;
 601        self.lsp = data.lsp;
 602        merge(&mut self.auto_update, data.auto_update);
 603    }
 604
 605    pub fn with_language_defaults(
 606        mut self,
 607        language_name: impl Into<Arc<str>>,
 608        overrides: EditorSettings,
 609    ) -> Self {
 610        self.language_defaults
 611            .insert(language_name.into(), overrides);
 612        self
 613    }
 614
 615    pub fn features(&self) -> &Features {
 616        &self.features
 617    }
 618
 619    pub fn show_copilot_suggestions(&self, language: Option<&str>, path: Option<&Path>) -> bool {
 620        if !self.features.copilot {
 621            return false;
 622        }
 623
 624        if !self.copilot_enabled_for_language(language) {
 625            return false;
 626        }
 627
 628        if let Some(path) = path {
 629            if !self.copilot_enabled_for_path(path) {
 630                return false;
 631            }
 632        }
 633
 634        true
 635    }
 636
 637    pub fn copilot_enabled_for_path(&self, path: &Path) -> bool {
 638        !self
 639            .copilot
 640            .disabled_globs
 641            .iter()
 642            .any(|glob| glob.matches_path(path))
 643    }
 644
 645    pub fn copilot_enabled_for_language(&self, language: Option<&str>) -> bool {
 646        self.language_setting(language, |settings| settings.show_copilot_suggestions)
 647    }
 648
 649    pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
 650        self.language_setting(language, |settings| settings.tab_size)
 651    }
 652
 653    pub fn show_whitespaces(&self, language: Option<&str>) -> ShowWhitespaces {
 654        self.language_setting(language, |settings| settings.show_whitespaces)
 655    }
 656
 657    pub fn hard_tabs(&self, language: Option<&str>) -> bool {
 658        self.language_setting(language, |settings| settings.hard_tabs)
 659    }
 660
 661    pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
 662        self.language_setting(language, |settings| settings.soft_wrap)
 663    }
 664
 665    pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
 666        self.language_setting(language, |settings| settings.preferred_line_length)
 667    }
 668
 669    pub fn remove_trailing_whitespace_on_save(&self, language: Option<&str>) -> bool {
 670        self.language_setting(language, |settings| {
 671            settings.remove_trailing_whitespace_on_save.clone()
 672        })
 673    }
 674
 675    pub fn ensure_final_newline_on_save(&self, language: Option<&str>) -> bool {
 676        self.language_setting(language, |settings| {
 677            settings.ensure_final_newline_on_save.clone()
 678        })
 679    }
 680
 681    pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
 682        self.language_setting(language, |settings| settings.format_on_save.clone())
 683    }
 684
 685    pub fn formatter(&self, language: Option<&str>) -> Formatter {
 686        self.language_setting(language, |settings| settings.formatter.clone())
 687    }
 688
 689    pub fn enable_language_server(&self, language: Option<&str>) -> bool {
 690        self.language_setting(language, |settings| settings.enable_language_server)
 691    }
 692
 693    fn language_setting<F, R>(&self, language: Option<&str>, f: F) -> R
 694    where
 695        F: Fn(&EditorSettings) -> Option<R>,
 696    {
 697        None.or_else(|| language.and_then(|l| self.language_overrides.get(l).and_then(&f)))
 698            .or_else(|| f(&self.editor_overrides))
 699            .or_else(|| language.and_then(|l| self.language_defaults.get(l).and_then(&f)))
 700            .or_else(|| f(&self.editor_defaults))
 701            .expect("missing default")
 702    }
 703
 704    pub fn git_gutter(&self) -> GitGutter {
 705        self.git_overrides.git_gutter.unwrap_or_else(|| {
 706            self.git
 707                .git_gutter
 708                .expect("git_gutter should be some by setting setup")
 709        })
 710    }
 711
 712    pub fn telemetry(&self) -> TelemetrySettings {
 713        TelemetrySettings {
 714            diagnostics: Some(self.telemetry_diagnostics()),
 715            metrics: Some(self.telemetry_metrics()),
 716        }
 717    }
 718
 719    pub fn telemetry_diagnostics(&self) -> bool {
 720        self.telemetry_overrides
 721            .diagnostics
 722            .or(self.telemetry_defaults.diagnostics)
 723            .expect("missing default")
 724    }
 725
 726    pub fn telemetry_metrics(&self) -> bool {
 727        self.telemetry_overrides
 728            .metrics
 729            .or(self.telemetry_defaults.metrics)
 730            .expect("missing default")
 731    }
 732
 733    fn terminal_setting<F, R>(&self, f: F) -> R
 734    where
 735        F: Fn(&TerminalSettings) -> Option<R>,
 736    {
 737        None.or_else(|| f(&self.terminal_overrides))
 738            .or_else(|| f(&self.terminal_defaults))
 739            .expect("missing default")
 740    }
 741
 742    pub fn terminal_line_height(&self) -> f32 {
 743        self.terminal_setting(|terminal_setting| terminal_setting.line_height())
 744    }
 745
 746    pub fn terminal_scroll(&self) -> AlternateScroll {
 747        self.terminal_setting(|terminal_setting| terminal_setting.alternate_scroll.to_owned())
 748    }
 749
 750    pub fn terminal_shell(&self) -> Shell {
 751        self.terminal_setting(|terminal_setting| terminal_setting.shell.to_owned())
 752    }
 753
 754    pub fn terminal_env(&self) -> HashMap<String, String> {
 755        self.terminal_setting(|terminal_setting| terminal_setting.env.to_owned())
 756    }
 757
 758    pub fn terminal_strategy(&self) -> WorkingDirectory {
 759        self.terminal_setting(|terminal_setting| terminal_setting.working_directory.to_owned())
 760    }
 761
 762    #[cfg(any(test, feature = "test-support"))]
 763    pub fn test(cx: &gpui::AppContext) -> Settings {
 764        Settings {
 765            buffer_font_family_name: "Monaco".to_string(),
 766            buffer_font_features: Default::default(),
 767            buffer_font_family: cx
 768                .font_cache()
 769                .load_family(&["Monaco"], &Default::default())
 770                .unwrap(),
 771            buffer_font_size: 14.,
 772            active_pane_magnification: 1.,
 773            default_buffer_font_size: 14.,
 774            confirm_quit: false,
 775            cursor_blink: true,
 776            hover_popover_enabled: true,
 777            show_completions_on_input: true,
 778            show_call_status_icon: true,
 779            vim_mode: false,
 780            autosave: Autosave::Off,
 781            project_panel_defaults: Default::default(),
 782            project_panel_overrides: Default::default(),
 783            editor_defaults: EditorSettings {
 784                tab_size: Some(4.try_into().unwrap()),
 785                hard_tabs: Some(false),
 786                soft_wrap: Some(SoftWrap::None),
 787                preferred_line_length: Some(80),
 788                remove_trailing_whitespace_on_save: Some(true),
 789                ensure_final_newline_on_save: Some(true),
 790                format_on_save: Some(FormatOnSave::On),
 791                formatter: Some(Formatter::LanguageServer),
 792                enable_language_server: Some(true),
 793                show_copilot_suggestions: Some(true),
 794                show_whitespaces: Some(ShowWhitespaces::None),
 795            },
 796            editor_overrides: Default::default(),
 797            copilot: Default::default(),
 798            journal_defaults: Default::default(),
 799            journal_overrides: Default::default(),
 800            terminal_defaults: Default::default(),
 801            terminal_overrides: Default::default(),
 802            git: Default::default(),
 803            git_overrides: Default::default(),
 804            language_defaults: Default::default(),
 805            language_overrides: Default::default(),
 806            lsp: Default::default(),
 807            theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), Default::default),
 808            telemetry_defaults: TelemetrySettings {
 809                diagnostics: Some(true),
 810                metrics: Some(true),
 811            },
 812            telemetry_overrides: Default::default(),
 813            auto_update: true,
 814            base_keymap: Default::default(),
 815            features: Features { copilot: true },
 816        }
 817    }
 818
 819    #[cfg(any(test, feature = "test-support"))]
 820    pub fn test_async(cx: &mut gpui::TestAppContext) {
 821        cx.update(|cx| {
 822            let settings = Self::test(cx);
 823            cx.set_global(settings);
 824        });
 825    }
 826}
 827
 828pub fn settings_file_json_schema(
 829    theme_names: Vec<String>,
 830    language_names: &[String],
 831) -> serde_json::Value {
 832    let settings = SchemaSettings::draft07().with(|settings| {
 833        settings.option_add_null_type = false;
 834    });
 835    let generator = SchemaGenerator::new(settings);
 836
 837    let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
 838
 839    // Create a schema for a theme name.
 840    let theme_name_schema = SchemaObject {
 841        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
 842        enum_values: Some(theme_names.into_iter().map(Value::String).collect()),
 843        ..Default::default()
 844    };
 845
 846    // Create a schema for a 'languages overrides' object, associating editor
 847    // settings with specific langauges.
 848    assert!(root_schema.definitions.contains_key("EditorSettings"));
 849
 850    let languages_object_schema = SchemaObject {
 851        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
 852        object: Some(Box::new(ObjectValidation {
 853            properties: language_names
 854                .iter()
 855                .map(|name| {
 856                    (
 857                        name.clone(),
 858                        Schema::new_ref("#/definitions/EditorSettings".into()),
 859                    )
 860                })
 861                .collect(),
 862            ..Default::default()
 863        })),
 864        ..Default::default()
 865    };
 866
 867    // Add these new schemas as definitions, and modify properties of the root
 868    // schema to reference them.
 869    root_schema.definitions.extend([
 870        ("ThemeName".into(), theme_name_schema.into()),
 871        ("Languages".into(), languages_object_schema.into()),
 872    ]);
 873    let root_schema_object = &mut root_schema.schema.object.as_mut().unwrap();
 874
 875    root_schema_object.properties.extend([
 876        (
 877            "theme".to_owned(),
 878            Schema::new_ref("#/definitions/ThemeName".into()),
 879        ),
 880        (
 881            "languages".to_owned(),
 882            Schema::new_ref("#/definitions/Languages".into()),
 883        ),
 884        // For backward compatibility
 885        (
 886            "language_overrides".to_owned(),
 887            Schema::new_ref("#/definitions/Languages".into()),
 888        ),
 889    ]);
 890
 891    serde_json::to_value(root_schema).unwrap()
 892}
 893
 894fn merge<T: Copy>(target: &mut T, value: Option<T>) {
 895    if let Some(value) = value {
 896        *target = value;
 897    }
 898}
 899
 900pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
 901    Ok(serde_json::from_reader(
 902        json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
 903    )?)
 904}
 905
 906lazy_static! {
 907    static ref PAIR_QUERY: Query = Query::new(
 908        tree_sitter_json::language(),
 909        "
 910            (pair
 911                key: (string) @key
 912                value: (_) @value)
 913        ",
 914    )
 915    .unwrap();
 916}
 917
 918fn update_object_in_settings_file<'a>(
 919    old_object: &'a serde_json::Map<String, Value>,
 920    new_object: &'a serde_json::Map<String, Value>,
 921    text: &str,
 922    syntax_tree: &Tree,
 923    tab_size: usize,
 924    key_path: &mut Vec<&'a str>,
 925    edits: &mut Vec<(Range<usize>, String)>,
 926) {
 927    for (key, old_value) in old_object.iter() {
 928        key_path.push(key);
 929        let new_value = new_object.get(key).unwrap_or(&Value::Null);
 930
 931        // If the old and new values are both objects, then compare them key by key,
 932        // preserving the comments and formatting of the unchanged parts. Otherwise,
 933        // replace the old value with the new value.
 934        if let (Value::Object(old_sub_object), Value::Object(new_sub_object)) =
 935            (old_value, new_value)
 936        {
 937            update_object_in_settings_file(
 938                old_sub_object,
 939                new_sub_object,
 940                text,
 941                syntax_tree,
 942                tab_size,
 943                key_path,
 944                edits,
 945            )
 946        } else if old_value != new_value {
 947            let (range, replacement) =
 948                update_key_in_settings_file(text, syntax_tree, &key_path, tab_size, &new_value);
 949            edits.push((range, replacement));
 950        }
 951
 952        key_path.pop();
 953    }
 954}
 955
 956fn update_key_in_settings_file(
 957    text: &str,
 958    syntax_tree: &Tree,
 959    key_path: &[&str],
 960    tab_size: usize,
 961    new_value: impl Serialize,
 962) -> (Range<usize>, String) {
 963    const LANGUAGE_OVERRIDES: &'static str = "language_overrides";
 964    const LANGUAGES: &'static str = "languages";
 965
 966    let mut cursor = tree_sitter::QueryCursor::new();
 967
 968    let has_language_overrides = text.contains(LANGUAGE_OVERRIDES);
 969
 970    let mut depth = 0;
 971    let mut last_value_range = 0..0;
 972    let mut first_key_start = None;
 973    let mut existing_value_range = 0..text.len();
 974    let matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes());
 975    for mat in matches {
 976        if mat.captures.len() != 2 {
 977            continue;
 978        }
 979
 980        let key_range = mat.captures[0].node.byte_range();
 981        let value_range = mat.captures[1].node.byte_range();
 982
 983        // Don't enter sub objects until we find an exact
 984        // match for the current keypath
 985        if last_value_range.contains_inclusive(&value_range) {
 986            continue;
 987        }
 988
 989        last_value_range = value_range.clone();
 990
 991        if key_range.start > existing_value_range.end {
 992            break;
 993        }
 994
 995        first_key_start.get_or_insert_with(|| key_range.start);
 996
 997        let found_key = text
 998            .get(key_range.clone())
 999            .map(|key_text| {
1000                if key_path[depth] == LANGUAGES && has_language_overrides {
1001                    return key_text == format!("\"{}\"", LANGUAGE_OVERRIDES);
1002                } else {
1003                    return key_text == format!("\"{}\"", key_path[depth]);
1004                }
1005            })
1006            .unwrap_or(false);
1007
1008        if found_key {
1009            existing_value_range = value_range;
1010            // Reset last value range when increasing in depth
1011            last_value_range = existing_value_range.start..existing_value_range.start;
1012            depth += 1;
1013
1014            if depth == key_path.len() {
1015                break;
1016            } else {
1017                first_key_start = None;
1018            }
1019        }
1020    }
1021
1022    // We found the exact key we want, insert the new value
1023    if depth == key_path.len() {
1024        let new_val = to_pretty_json(&new_value, tab_size, tab_size * depth);
1025        (existing_value_range, new_val)
1026    } else {
1027        // We have key paths, construct the sub objects
1028        let new_key = if has_language_overrides && key_path[depth] == LANGUAGES {
1029            LANGUAGE_OVERRIDES
1030        } else {
1031            key_path[depth]
1032        };
1033
1034        // We don't have the key, construct the nested objects
1035        let mut new_value = serde_json::to_value(new_value).unwrap();
1036        for key in key_path[(depth + 1)..].iter().rev() {
1037            if has_language_overrides && key == &LANGUAGES {
1038                new_value = serde_json::json!({ LANGUAGE_OVERRIDES.to_string(): new_value });
1039            } else {
1040                new_value = serde_json::json!({ key.to_string(): new_value });
1041            }
1042        }
1043
1044        if let Some(first_key_start) = first_key_start {
1045            let mut row = 0;
1046            let mut column = 0;
1047            for (ix, char) in text.char_indices() {
1048                if ix == first_key_start {
1049                    break;
1050                }
1051                if char == '\n' {
1052                    row += 1;
1053                    column = 0;
1054                } else {
1055                    column += char.len_utf8();
1056                }
1057            }
1058
1059            if row > 0 {
1060                // depth is 0 based, but division needs to be 1 based.
1061                let new_val = to_pretty_json(&new_value, column / (depth + 1), column);
1062                let space = ' ';
1063                let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column);
1064                (first_key_start..first_key_start, content)
1065            } else {
1066                let new_val = serde_json::to_string(&new_value).unwrap();
1067                let mut content = format!(r#""{new_key}": {new_val},"#);
1068                content.push(' ');
1069                (first_key_start..first_key_start, content)
1070            }
1071        } else {
1072            new_value = serde_json::json!({ new_key.to_string(): new_value });
1073            let indent_prefix_len = 4 * depth;
1074            let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
1075            if depth == 0 {
1076                new_val.push('\n');
1077            }
1078
1079            (existing_value_range, new_val)
1080        }
1081    }
1082}
1083
1084fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len: usize) -> String {
1085    const SPACES: [u8; 32] = [b' '; 32];
1086
1087    debug_assert!(indent_size <= SPACES.len());
1088    debug_assert!(indent_prefix_len <= SPACES.len());
1089
1090    let mut output = Vec::new();
1091    let mut ser = serde_json::Serializer::with_formatter(
1092        &mut output,
1093        serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
1094    );
1095
1096    value.serialize(&mut ser).unwrap();
1097    let text = String::from_utf8(output).unwrap();
1098
1099    let mut adjusted_text = String::new();
1100    for (i, line) in text.split('\n').enumerate() {
1101        if i > 0 {
1102            adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
1103        }
1104        adjusted_text.push_str(line);
1105        adjusted_text.push('\n');
1106    }
1107    adjusted_text.pop();
1108    adjusted_text
1109}
1110
1111/// Update the settings file with the given callback.
1112///
1113/// Returns a new JSON string and the offset where the first edit occurred.
1114fn update_settings_file(
1115    text: &str,
1116    mut old_file_content: SettingsFileContent,
1117    tab_size: NonZeroU32,
1118    update: impl FnOnce(&mut SettingsFileContent),
1119) -> Vec<(Range<usize>, String)> {
1120    let mut new_file_content = old_file_content.clone();
1121    update(&mut new_file_content);
1122
1123    if new_file_content.languages.len() != old_file_content.languages.len() {
1124        for language in new_file_content.languages.keys() {
1125            old_file_content
1126                .languages
1127                .entry(language.clone())
1128                .or_default();
1129        }
1130        for language in old_file_content.languages.keys() {
1131            new_file_content
1132                .languages
1133                .entry(language.clone())
1134                .or_default();
1135        }
1136    }
1137
1138    let mut parser = tree_sitter::Parser::new();
1139    parser.set_language(tree_sitter_json::language()).unwrap();
1140    let tree = parser.parse(text, None).unwrap();
1141
1142    let old_object = to_json_object(old_file_content);
1143    let new_object = to_json_object(new_file_content);
1144    let mut key_path = Vec::new();
1145    let mut edits = Vec::new();
1146    update_object_in_settings_file(
1147        &old_object,
1148        &new_object,
1149        &text,
1150        &tree,
1151        tab_size.get() as usize,
1152        &mut key_path,
1153        &mut edits,
1154    );
1155    edits.sort_unstable_by_key(|e| e.0.start);
1156    return edits;
1157}
1158
1159fn to_json_object(settings_file: SettingsFileContent) -> serde_json::Map<String, Value> {
1160    let tmp = serde_json::to_value(settings_file).unwrap();
1161    match tmp {
1162        Value::Object(map) => map,
1163        _ => unreachable!("SettingsFileContent represents a JSON map"),
1164    }
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169    use super::*;
1170    use unindent::Unindent;
1171
1172    fn assert_new_settings(
1173        old_json: String,
1174        update: fn(&mut SettingsFileContent),
1175        expected_new_json: String,
1176    ) {
1177        let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default();
1178        let edits = update_settings_file(&old_json, old_content, 4.try_into().unwrap(), update);
1179        let mut new_json = old_json;
1180        for (range, replacement) in edits.into_iter().rev() {
1181            new_json.replace_range(range, &replacement);
1182        }
1183        pretty_assertions::assert_eq!(new_json, expected_new_json);
1184    }
1185
1186    #[test]
1187    fn test_update_language_overrides_copilot() {
1188        assert_new_settings(
1189            r#"
1190                {
1191                    "language_overrides": {
1192                        "JSON": {
1193                            "show_copilot_suggestions": false
1194                        }
1195                    }
1196                }
1197            "#
1198            .unindent(),
1199            |settings| {
1200                settings.languages.insert(
1201                    "Rust".into(),
1202                    EditorSettings {
1203                        show_copilot_suggestions: Some(true),
1204                        ..Default::default()
1205                    },
1206                );
1207            },
1208            r#"
1209                {
1210                    "language_overrides": {
1211                        "Rust": {
1212                            "show_copilot_suggestions": true
1213                        },
1214                        "JSON": {
1215                            "show_copilot_suggestions": false
1216                        }
1217                    }
1218                }
1219            "#
1220            .unindent(),
1221        );
1222    }
1223
1224    #[test]
1225    fn test_update_copilot_globs() {
1226        assert_new_settings(
1227            r#"
1228                {
1229                }
1230            "#
1231            .unindent(),
1232            |settings| {
1233                settings.copilot = Some(CopilotSettingsContent {
1234                    disabled_globs: Some(vec![]),
1235                });
1236            },
1237            r#"
1238                {
1239                    "copilot": {
1240                        "disabled_globs": []
1241                    }
1242                }
1243            "#
1244            .unindent(),
1245        );
1246
1247        assert_new_settings(
1248            r#"
1249                {
1250                    "copilot": {
1251                        "disabled_globs": [
1252                            "**/*.json"
1253                        ]
1254                    }
1255                }
1256            "#
1257            .unindent(),
1258            |settings| {
1259                settings
1260                    .copilot
1261                    .get_or_insert(Default::default())
1262                    .disabled_globs
1263                    .as_mut()
1264                    .unwrap()
1265                    .push(".env".into());
1266            },
1267            r#"
1268                {
1269                    "copilot": {
1270                        "disabled_globs": [
1271                            "**/*.json",
1272                            ".env"
1273                        ]
1274                    }
1275                }
1276            "#
1277            .unindent(),
1278        );
1279    }
1280
1281    #[test]
1282    fn test_update_copilot() {
1283        assert_new_settings(
1284            r#"
1285                {
1286                    "languages": {
1287                        "JSON": {
1288                            "show_copilot_suggestions": false
1289                        }
1290                    }
1291                }
1292            "#
1293            .unindent(),
1294            |settings| {
1295                settings.editor.show_copilot_suggestions = Some(true);
1296            },
1297            r#"
1298                {
1299                    "show_copilot_suggestions": true,
1300                    "languages": {
1301                        "JSON": {
1302                            "show_copilot_suggestions": false
1303                        }
1304                    }
1305                }
1306            "#
1307            .unindent(),
1308        );
1309    }
1310
1311    #[test]
1312    fn test_update_language_copilot() {
1313        assert_new_settings(
1314            r#"
1315                {
1316                    "languages": {
1317                        "JSON": {
1318                            "show_copilot_suggestions": false
1319                        }
1320                    }
1321                }
1322            "#
1323            .unindent(),
1324            |settings| {
1325                settings.languages.insert(
1326                    "Rust".into(),
1327                    EditorSettings {
1328                        show_copilot_suggestions: Some(true),
1329                        ..Default::default()
1330                    },
1331                );
1332            },
1333            r#"
1334                {
1335                    "languages": {
1336                        "Rust": {
1337                            "show_copilot_suggestions": true
1338                        },
1339                        "JSON": {
1340                            "show_copilot_suggestions": false
1341                        }
1342                    }
1343                }
1344            "#
1345            .unindent(),
1346        );
1347    }
1348
1349    #[test]
1350    fn test_update_telemetry_setting_multiple_fields() {
1351        assert_new_settings(
1352            r#"
1353                {
1354                    "telemetry": {
1355                        "metrics": false,
1356                        "diagnostics": false
1357                    }
1358                }
1359            "#
1360            .unindent(),
1361            |settings| {
1362                settings.telemetry.set_diagnostics(true);
1363                settings.telemetry.set_metrics(true);
1364            },
1365            r#"
1366                {
1367                    "telemetry": {
1368                        "metrics": true,
1369                        "diagnostics": true
1370                    }
1371                }
1372            "#
1373            .unindent(),
1374        );
1375    }
1376
1377    #[test]
1378    fn test_update_telemetry_setting_weird_formatting() {
1379        assert_new_settings(
1380            r#"{
1381                "telemetry":   { "metrics": false, "diagnostics": true }
1382            }"#
1383            .unindent(),
1384            |settings| settings.telemetry.set_diagnostics(false),
1385            r#"{
1386                "telemetry":   { "metrics": false, "diagnostics": false }
1387            }"#
1388            .unindent(),
1389        );
1390    }
1391
1392    #[test]
1393    fn test_update_telemetry_setting_other_fields() {
1394        assert_new_settings(
1395            r#"
1396                {
1397                    "telemetry": {
1398                        "metrics": false,
1399                        "diagnostics": true
1400                    }
1401                }
1402            "#
1403            .unindent(),
1404            |settings| settings.telemetry.set_diagnostics(false),
1405            r#"
1406                {
1407                    "telemetry": {
1408                        "metrics": false,
1409                        "diagnostics": false
1410                    }
1411                }
1412            "#
1413            .unindent(),
1414        );
1415    }
1416
1417    #[test]
1418    fn test_update_telemetry_setting_empty_telemetry() {
1419        assert_new_settings(
1420            r#"
1421                {
1422                    "telemetry": {}
1423                }
1424            "#
1425            .unindent(),
1426            |settings| settings.telemetry.set_diagnostics(false),
1427            r#"
1428                {
1429                    "telemetry": {
1430                        "diagnostics": false
1431                    }
1432                }
1433            "#
1434            .unindent(),
1435        );
1436    }
1437
1438    #[test]
1439    fn test_update_telemetry_setting_pre_existing() {
1440        assert_new_settings(
1441            r#"
1442                {
1443                    "telemetry": {
1444                        "diagnostics": true
1445                    }
1446                }
1447            "#
1448            .unindent(),
1449            |settings| settings.telemetry.set_diagnostics(false),
1450            r#"
1451                {
1452                    "telemetry": {
1453                        "diagnostics": false
1454                    }
1455                }
1456            "#
1457            .unindent(),
1458        );
1459    }
1460
1461    #[test]
1462    fn test_update_telemetry_setting() {
1463        assert_new_settings(
1464            "{}".into(),
1465            |settings| settings.telemetry.set_diagnostics(true),
1466            r#"
1467                {
1468                    "telemetry": {
1469                        "diagnostics": true
1470                    }
1471                }
1472            "#
1473            .unindent(),
1474        );
1475    }
1476
1477    #[test]
1478    fn test_update_object_empty_doc() {
1479        assert_new_settings(
1480            "".into(),
1481            |settings| settings.telemetry.set_diagnostics(true),
1482            r#"
1483                {
1484                    "telemetry": {
1485                        "diagnostics": true
1486                    }
1487                }
1488            "#
1489            .unindent(),
1490        );
1491    }
1492
1493    #[test]
1494    fn test_write_theme_into_settings_with_theme() {
1495        assert_new_settings(
1496            r#"
1497                {
1498                    "theme": "One Dark"
1499                }
1500            "#
1501            .unindent(),
1502            |settings| settings.theme = Some("summerfruit-light".to_string()),
1503            r#"
1504                {
1505                    "theme": "summerfruit-light"
1506                }
1507            "#
1508            .unindent(),
1509        );
1510    }
1511
1512    #[test]
1513    fn test_write_theme_into_empty_settings() {
1514        assert_new_settings(
1515            r#"
1516                {
1517                }
1518            "#
1519            .unindent(),
1520            |settings| settings.theme = Some("summerfruit-light".to_string()),
1521            r#"
1522                {
1523                    "theme": "summerfruit-light"
1524                }
1525            "#
1526            .unindent(),
1527        );
1528    }
1529
1530    #[test]
1531    fn write_key_no_document() {
1532        assert_new_settings(
1533            "".to_string(),
1534            |settings| settings.theme = Some("summerfruit-light".to_string()),
1535            r#"
1536                {
1537                    "theme": "summerfruit-light"
1538                }
1539            "#
1540            .unindent(),
1541        );
1542    }
1543
1544    #[test]
1545    fn test_write_theme_into_single_line_settings_without_theme() {
1546        assert_new_settings(
1547            r#"{ "a": "", "ok": true }"#.to_string(),
1548            |settings| settings.theme = Some("summerfruit-light".to_string()),
1549            r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#.to_string(),
1550        );
1551    }
1552
1553    #[test]
1554    fn test_write_theme_pre_object_whitespace() {
1555        assert_new_settings(
1556            r#"          { "a": "", "ok": true }"#.to_string(),
1557            |settings| settings.theme = Some("summerfruit-light".to_string()),
1558            r#"          { "theme": "summerfruit-light", "a": "", "ok": true }"#.unindent(),
1559        );
1560    }
1561
1562    #[test]
1563    fn test_write_theme_into_multi_line_settings_without_theme() {
1564        assert_new_settings(
1565            r#"
1566                {
1567                    "a": "b"
1568                }
1569            "#
1570            .unindent(),
1571            |settings| settings.theme = Some("summerfruit-light".to_string()),
1572            r#"
1573                {
1574                    "theme": "summerfruit-light",
1575                    "a": "b"
1576                }
1577            "#
1578            .unindent(),
1579        );
1580    }
1581}