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