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