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