settings.rs

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