settings.rs

  1mod keymap_file;
  2pub mod settings_file;
  3pub mod watched_json;
  4
  5use anyhow::Result;
  6use gpui::{
  7    font_cache::{FamilyId, FontCache},
  8    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 std::{collections::HashMap, fmt::Write as _, num::NonZeroU32, str, sync::Arc};
 18use theme::{Theme, ThemeRegistry};
 19use tree_sitter::Query;
 20use util::ResultExt as _;
 21
 22pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
 23
 24#[derive(Clone)]
 25pub struct Settings {
 26    pub experiments: FeatureFlags,
 27    pub projects_online_by_default: bool,
 28    pub buffer_font_family: FamilyId,
 29    pub default_buffer_font_size: f32,
 30    pub buffer_font_size: f32,
 31    pub cursor_blink: bool,
 32    pub hover_popover_enabled: bool,
 33    pub show_completions_on_input: bool,
 34    pub vim_mode: bool,
 35    pub autosave: Autosave,
 36    pub default_dock_anchor: DockAnchor,
 37    pub editor_defaults: EditorSettings,
 38    pub editor_overrides: EditorSettings,
 39    pub git: GitSettings,
 40    pub git_overrides: GitSettings,
 41    pub journal_defaults: JournalSettings,
 42    pub journal_overrides: JournalSettings,
 43    pub terminal_defaults: TerminalSettings,
 44    pub terminal_overrides: TerminalSettings,
 45    pub language_defaults: HashMap<Arc<str>, EditorSettings>,
 46    pub language_overrides: HashMap<Arc<str>, EditorSettings>,
 47    pub lsp: HashMap<Arc<str>, LspSettings>,
 48    pub theme: Arc<Theme>,
 49    pub staff_mode: bool,
 50}
 51
 52#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 53pub struct FeatureFlags {
 54    pub experimental_themes: bool,
 55}
 56
 57impl FeatureFlags {
 58    pub fn keymap_files(&self) -> Vec<&'static str> {
 59        vec![]
 60    }
 61}
 62
 63#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 64pub struct GitSettings {
 65    pub git_gutter: Option<GitGutter>,
 66    pub gutter_debounce: Option<u64>,
 67}
 68
 69#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
 70#[serde(rename_all = "snake_case")]
 71pub enum GitGutter {
 72    #[default]
 73    TrackedFiles,
 74    Hide,
 75}
 76
 77pub struct GitGutterConfig {}
 78
 79#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 80pub struct EditorSettings {
 81    pub tab_size: Option<NonZeroU32>,
 82    pub hard_tabs: Option<bool>,
 83    pub soft_wrap: Option<SoftWrap>,
 84    pub preferred_line_length: Option<u32>,
 85    pub format_on_save: Option<FormatOnSave>,
 86    pub formatter: Option<Formatter>,
 87    pub enable_language_server: Option<bool>,
 88}
 89
 90#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 91#[serde(rename_all = "snake_case")]
 92pub enum SoftWrap {
 93    None,
 94    EditorWidth,
 95    PreferredLineLength,
 96}
 97#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
 98#[serde(rename_all = "snake_case")]
 99pub enum FormatOnSave {
100    On,
101    Off,
102    LanguageServer,
103    External {
104        command: String,
105        arguments: Vec<String>,
106    },
107}
108
109#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
110#[serde(rename_all = "snake_case")]
111pub enum Formatter {
112    LanguageServer,
113    External {
114        command: String,
115        arguments: Vec<String>,
116    },
117}
118
119#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
120#[serde(rename_all = "snake_case")]
121pub enum Autosave {
122    Off,
123    AfterDelay { milliseconds: u64 },
124    OnFocusChange,
125    OnWindowChange,
126}
127
128#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
129pub struct JournalSettings {
130    pub path: Option<String>,
131    pub hour_format: Option<HourFormat>,
132}
133
134impl Default for JournalSettings {
135    fn default() -> Self {
136        Self {
137            path: Some("~".into()),
138            hour_format: Some(Default::default()),
139        }
140    }
141}
142
143#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
144#[serde(rename_all = "snake_case")]
145pub enum HourFormat {
146    Hour12,
147    Hour24,
148}
149
150impl Default for HourFormat {
151    fn default() -> Self {
152        Self::Hour12
153    }
154}
155
156#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
157pub struct TerminalSettings {
158    pub shell: Option<Shell>,
159    pub working_directory: Option<WorkingDirectory>,
160    pub font_size: Option<f32>,
161    pub font_family: Option<String>,
162    pub env: Option<HashMap<String, String>>,
163    pub blinking: Option<TerminalBlink>,
164    pub alternate_scroll: Option<AlternateScroll>,
165    pub option_as_meta: Option<bool>,
166    pub copy_on_select: Option<bool>,
167}
168
169#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
170#[serde(rename_all = "snake_case")]
171pub enum TerminalBlink {
172    Off,
173    TerminalControlled,
174    On,
175}
176
177impl Default for TerminalBlink {
178    fn default() -> Self {
179        TerminalBlink::TerminalControlled
180    }
181}
182
183#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
184#[serde(rename_all = "snake_case")]
185pub enum Shell {
186    System,
187    Program(String),
188    WithArguments { program: String, args: Vec<String> },
189}
190
191impl Default for Shell {
192    fn default() -> Self {
193        Shell::System
194    }
195}
196
197#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
198#[serde(rename_all = "snake_case")]
199pub enum AlternateScroll {
200    On,
201    Off,
202}
203
204impl Default for AlternateScroll {
205    fn default() -> Self {
206        AlternateScroll::On
207    }
208}
209
210#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
211#[serde(rename_all = "snake_case")]
212pub enum WorkingDirectory {
213    CurrentProjectDirectory,
214    FirstProjectDirectory,
215    AlwaysHome,
216    Always { directory: String },
217}
218
219#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)]
220#[serde(rename_all = "snake_case")]
221pub enum DockAnchor {
222    #[default]
223    Bottom,
224    Right,
225    Expanded,
226}
227
228#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
229pub struct SettingsFileContent {
230    pub experiments: Option<FeatureFlags>,
231    #[serde(default)]
232    pub projects_online_by_default: Option<bool>,
233    #[serde(default)]
234    pub buffer_font_family: Option<String>,
235    #[serde(default)]
236    pub buffer_font_size: Option<f32>,
237    #[serde(default)]
238    pub cursor_blink: Option<bool>,
239    #[serde(default)]
240    pub hover_popover_enabled: Option<bool>,
241    #[serde(default)]
242    pub show_completions_on_input: Option<bool>,
243    #[serde(default)]
244    pub vim_mode: Option<bool>,
245    #[serde(default)]
246    pub autosave: Option<Autosave>,
247    #[serde(default)]
248    pub default_dock_anchor: Option<DockAnchor>,
249    #[serde(flatten)]
250    pub editor: EditorSettings,
251    #[serde(default)]
252    pub journal: JournalSettings,
253    #[serde(default)]
254    pub terminal: TerminalSettings,
255    #[serde(default)]
256    pub git: Option<GitSettings>,
257    #[serde(default)]
258    #[serde(alias = "language_overrides")]
259    pub languages: HashMap<Arc<str>, EditorSettings>,
260    #[serde(default)]
261    pub lsp: HashMap<Arc<str>, LspSettings>,
262    #[serde(default)]
263    pub theme: Option<String>,
264    #[serde(default)]
265    pub staff_mode: Option<bool>,
266}
267
268#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
269#[serde(rename_all = "snake_case")]
270pub struct LspSettings {
271    pub initialization_options: Option<Value>,
272}
273
274impl Settings {
275    pub fn defaults(
276        assets: impl AssetSource,
277        font_cache: &FontCache,
278        themes: &ThemeRegistry,
279    ) -> Self {
280        #[track_caller]
281        fn required<T>(value: Option<T>) -> Option<T> {
282            assert!(value.is_some(), "missing default setting value");
283            value
284        }
285
286        let defaults: SettingsFileContent = parse_json_with_comments(
287            str::from_utf8(assets.load("settings/default.json").unwrap().as_ref()).unwrap(),
288        )
289        .unwrap();
290
291        Self {
292            experiments: FeatureFlags::default(),
293            buffer_font_family: font_cache
294                .load_family(&[defaults.buffer_font_family.as_ref().unwrap()])
295                .unwrap(),
296            buffer_font_size: defaults.buffer_font_size.unwrap(),
297            default_buffer_font_size: defaults.buffer_font_size.unwrap(),
298            cursor_blink: defaults.cursor_blink.unwrap(),
299            hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
300            show_completions_on_input: defaults.show_completions_on_input.unwrap(),
301            projects_online_by_default: defaults.projects_online_by_default.unwrap(),
302            vim_mode: defaults.vim_mode.unwrap(),
303            autosave: defaults.autosave.unwrap(),
304            default_dock_anchor: defaults.default_dock_anchor.unwrap(),
305            editor_defaults: EditorSettings {
306                tab_size: required(defaults.editor.tab_size),
307                hard_tabs: required(defaults.editor.hard_tabs),
308                soft_wrap: required(defaults.editor.soft_wrap),
309                preferred_line_length: required(defaults.editor.preferred_line_length),
310                format_on_save: required(defaults.editor.format_on_save),
311                formatter: required(defaults.editor.formatter),
312                enable_language_server: required(defaults.editor.enable_language_server),
313            },
314            editor_overrides: Default::default(),
315            git: defaults.git.unwrap(),
316            git_overrides: Default::default(),
317            journal_defaults: defaults.journal,
318            journal_overrides: Default::default(),
319            terminal_defaults: defaults.terminal,
320            terminal_overrides: Default::default(),
321            language_defaults: defaults.languages,
322            language_overrides: Default::default(),
323            lsp: defaults.lsp.clone(),
324            theme: themes.get(&defaults.theme.unwrap()).unwrap(),
325
326            staff_mode: false,
327        }
328    }
329
330    pub fn set_user_settings(
331        &mut self,
332        data: SettingsFileContent,
333        theme_registry: &ThemeRegistry,
334        font_cache: &FontCache,
335    ) {
336        if let Some(value) = &data.buffer_font_family {
337            if let Some(id) = font_cache.load_family(&[value]).log_err() {
338                self.buffer_font_family = id;
339            }
340        }
341        if let Some(value) = &data.theme {
342            if let Some(theme) = theme_registry.get(value).log_err() {
343                self.theme = theme;
344            }
345        }
346
347        merge(
348            &mut self.projects_online_by_default,
349            data.projects_online_by_default,
350        );
351        merge(&mut self.buffer_font_size, data.buffer_font_size);
352        merge(&mut self.default_buffer_font_size, data.buffer_font_size);
353        merge(&mut self.cursor_blink, data.cursor_blink);
354        merge(&mut self.hover_popover_enabled, data.hover_popover_enabled);
355        merge(
356            &mut self.show_completions_on_input,
357            data.show_completions_on_input,
358        );
359        merge(&mut self.vim_mode, data.vim_mode);
360        merge(&mut self.autosave, data.autosave);
361        merge(&mut self.experiments, data.experiments);
362        merge(&mut self.staff_mode, data.staff_mode);
363        merge(&mut self.default_dock_anchor, data.default_dock_anchor);
364
365        // Ensure terminal font is loaded, so we can request it in terminal_element layout
366        if let Some(terminal_font) = &data.terminal.font_family {
367            font_cache.load_family(&[terminal_font]).log_err();
368        }
369
370        self.editor_overrides = data.editor;
371        self.git_overrides = data.git.unwrap_or_default();
372        self.journal_overrides = data.journal;
373        self.terminal_defaults.font_size = data.terminal.font_size;
374        self.terminal_overrides.copy_on_select = data.terminal.copy_on_select;
375        self.terminal_overrides = data.terminal;
376        self.language_overrides = data.languages;
377        self.lsp = data.lsp;
378    }
379
380    pub fn with_language_defaults(
381        mut self,
382        language_name: impl Into<Arc<str>>,
383        overrides: EditorSettings,
384    ) -> Self {
385        self.language_defaults
386            .insert(language_name.into(), overrides);
387        self
388    }
389
390    pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
391        self.language_setting(language, |settings| settings.tab_size)
392    }
393
394    pub fn hard_tabs(&self, language: Option<&str>) -> bool {
395        self.language_setting(language, |settings| settings.hard_tabs)
396    }
397
398    pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
399        self.language_setting(language, |settings| settings.soft_wrap)
400    }
401
402    pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
403        self.language_setting(language, |settings| settings.preferred_line_length)
404    }
405
406    pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
407        self.language_setting(language, |settings| settings.format_on_save.clone())
408    }
409
410    pub fn formatter(&self, language: Option<&str>) -> Formatter {
411        self.language_setting(language, |settings| settings.formatter.clone())
412    }
413
414    pub fn enable_language_server(&self, language: Option<&str>) -> bool {
415        self.language_setting(language, |settings| settings.enable_language_server)
416    }
417
418    fn language_setting<F, R>(&self, language: Option<&str>, f: F) -> R
419    where
420        F: Fn(&EditorSettings) -> Option<R>,
421    {
422        None.or_else(|| language.and_then(|l| self.language_overrides.get(l).and_then(&f)))
423            .or_else(|| f(&self.editor_overrides))
424            .or_else(|| language.and_then(|l| self.language_defaults.get(l).and_then(&f)))
425            .or_else(|| f(&self.editor_defaults))
426            .expect("missing default")
427    }
428
429    pub fn git_gutter(&self) -> GitGutter {
430        self.git_overrides.git_gutter.unwrap_or_else(|| {
431            self.git
432                .git_gutter
433                .expect("git_gutter should be some by setting setup")
434        })
435    }
436
437    #[cfg(any(test, feature = "test-support"))]
438    pub fn test(cx: &gpui::AppContext) -> Settings {
439        Settings {
440            experiments: FeatureFlags::default(),
441            buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
442            buffer_font_size: 14.,
443            default_buffer_font_size: 14.,
444            cursor_blink: true,
445            hover_popover_enabled: true,
446            show_completions_on_input: true,
447            vim_mode: false,
448            autosave: Autosave::Off,
449            default_dock_anchor: DockAnchor::Bottom,
450            editor_defaults: EditorSettings {
451                tab_size: Some(4.try_into().unwrap()),
452                hard_tabs: Some(false),
453                soft_wrap: Some(SoftWrap::None),
454                preferred_line_length: Some(80),
455                format_on_save: Some(FormatOnSave::On),
456                formatter: Some(Formatter::LanguageServer),
457                enable_language_server: Some(true),
458            },
459            editor_overrides: Default::default(),
460            journal_defaults: Default::default(),
461            journal_overrides: Default::default(),
462            terminal_defaults: Default::default(),
463            terminal_overrides: Default::default(),
464            git: Default::default(),
465            git_overrides: Default::default(),
466            language_defaults: Default::default(),
467            language_overrides: Default::default(),
468            lsp: Default::default(),
469            projects_online_by_default: true,
470            theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), Default::default),
471            staff_mode: false,
472        }
473    }
474
475    #[cfg(any(test, feature = "test-support"))]
476    pub fn test_async(cx: &mut gpui::TestAppContext) {
477        cx.update(|cx| {
478            let settings = Self::test(cx);
479            cx.set_global(settings);
480        });
481    }
482}
483
484pub fn settings_file_json_schema(
485    theme_names: Vec<String>,
486    language_names: &[String],
487) -> serde_json::Value {
488    let settings = SchemaSettings::draft07().with(|settings| {
489        settings.option_add_null_type = false;
490    });
491    let generator = SchemaGenerator::new(settings);
492    let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
493
494    // Create a schema for a theme name.
495    let theme_name_schema = SchemaObject {
496        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
497        enum_values: Some(theme_names.into_iter().map(Value::String).collect()),
498        ..Default::default()
499    };
500
501    // Create a schema for a 'languages overrides' object, associating editor
502    // settings with specific langauges.
503    assert!(root_schema.definitions.contains_key("EditorSettings"));
504    let languages_object_schema = SchemaObject {
505        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
506        object: Some(Box::new(ObjectValidation {
507            properties: language_names
508                .iter()
509                .map(|name| {
510                    (
511                        name.clone(),
512                        Schema::new_ref("#/definitions/EditorSettings".into()),
513                    )
514                })
515                .collect(),
516            ..Default::default()
517        })),
518        ..Default::default()
519    };
520
521    // Add these new schemas as definitions, and modify properties of the root
522    // schema to reference them.
523    root_schema.definitions.extend([
524        ("ThemeName".into(), theme_name_schema.into()),
525        ("Languages".into(), languages_object_schema.into()),
526    ]);
527    let root_schema_object = &mut root_schema.schema.object.as_mut().unwrap();
528
529    // Avoid automcomplete for non-user facing settings
530    root_schema_object.properties.remove("staff_mode");
531    root_schema_object.properties.extend([
532        (
533            "theme".to_owned(),
534            Schema::new_ref("#/definitions/ThemeName".into()),
535        ),
536        (
537            "languages".to_owned(),
538            Schema::new_ref("#/definitions/Languages".into()),
539        ),
540        // For backward compatibility
541        (
542            "language_overrides".to_owned(),
543            Schema::new_ref("#/definitions/Languages".into()),
544        ),
545    ]);
546
547    serde_json::to_value(root_schema).unwrap()
548}
549
550/// Expects the key to be unquoted, and the value to be valid JSON
551/// (e.g. values should be unquoted for numbers and bools, quoted for strings)
552pub fn write_top_level_setting(
553    mut settings_content: String,
554    top_level_key: &str,
555    new_val: &str,
556) -> String {
557    let mut parser = tree_sitter::Parser::new();
558    parser.set_language(tree_sitter_json::language()).unwrap();
559    let tree = parser.parse(&settings_content, None).unwrap();
560
561    let mut cursor = tree_sitter::QueryCursor::new();
562
563    let query = Query::new(
564        tree_sitter_json::language(),
565        "
566        (document
567            (object
568                (pair
569                    key: (string) @key
570                    value: (_) @value)))
571    ",
572    )
573    .unwrap();
574
575    let mut first_key_start = None;
576    let mut existing_value_range = None;
577    let matches = cursor.matches(&query, tree.root_node(), settings_content.as_bytes());
578    for mat in matches {
579        if mat.captures.len() != 2 {
580            continue;
581        }
582
583        let key = mat.captures[0];
584        let value = mat.captures[1];
585
586        first_key_start.get_or_insert_with(|| key.node.start_byte());
587
588        if let Some(key_text) = settings_content.get(key.node.byte_range()) {
589            if key_text == format!("\"{top_level_key}\"") {
590                existing_value_range = Some(value.node.byte_range());
591                break;
592            }
593        }
594    }
595
596    match (first_key_start, existing_value_range) {
597        (None, None) => {
598            // No document, create a new object and overwrite
599            settings_content.clear();
600            write!(
601                settings_content,
602                "{{\n    \"{}\": {new_val}\n}}\n",
603                top_level_key
604            )
605            .unwrap();
606        }
607
608        (_, Some(existing_value_range)) => {
609            // Existing theme key, overwrite
610            settings_content.replace_range(existing_value_range, &new_val);
611        }
612
613        (Some(first_key_start), None) => {
614            // No existing theme key, but other settings. Prepend new theme settings and
615            // match style of first key
616            let mut row = 0;
617            let mut column = 0;
618            for (ix, char) in settings_content.char_indices() {
619                if ix == first_key_start {
620                    break;
621                }
622                if char == '\n' {
623                    row += 1;
624                    column = 0;
625                } else {
626                    column += char.len_utf8();
627                }
628            }
629
630            let content = format!(r#""{top_level_key}": {new_val},"#);
631            settings_content.insert_str(first_key_start, &content);
632
633            if row > 0 {
634                settings_content.insert_str(
635                    first_key_start + content.len(),
636                    &format!("\n{:width$}", ' ', width = column),
637                )
638            } else {
639                settings_content.insert_str(first_key_start + content.len(), " ")
640            }
641        }
642    }
643
644    settings_content
645}
646
647fn merge<T: Copy>(target: &mut T, value: Option<T>) {
648    if let Some(value) = value {
649        *target = value;
650    }
651}
652
653pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
654    Ok(serde_json::from_reader(
655        json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
656    )?)
657}
658
659#[cfg(test)]
660mod tests {
661    use crate::write_top_level_setting;
662    use unindent::Unindent;
663
664    #[test]
665    fn test_write_theme_into_settings_with_theme() {
666        let settings = r#"
667            {
668                "theme": "One Dark"
669            }
670        "#
671        .unindent();
672
673        let new_settings = r#"
674            {
675                "theme": "summerfruit-light"
676            }
677        "#
678        .unindent();
679
680        let settings_after_theme =
681            write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
682
683        assert_eq!(settings_after_theme, new_settings)
684    }
685
686    #[test]
687    fn test_write_theme_into_empty_settings() {
688        let settings = r#"
689            {
690            }
691        "#
692        .unindent();
693
694        let new_settings = r#"
695            {
696                "theme": "summerfruit-light"
697            }
698        "#
699        .unindent();
700
701        let settings_after_theme =
702            write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
703
704        assert_eq!(settings_after_theme, new_settings)
705    }
706
707    #[test]
708    fn test_write_theme_into_no_settings() {
709        let settings = "".to_string();
710
711        let new_settings = r#"
712            {
713                "theme": "summerfruit-light"
714            }
715        "#
716        .unindent();
717
718        let settings_after_theme =
719            write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
720
721        assert_eq!(settings_after_theme, new_settings)
722    }
723
724    #[test]
725    fn test_write_theme_into_single_line_settings_without_theme() {
726        let settings = r#"{ "a": "", "ok": true }"#.to_string();
727        let new_settings = r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#;
728
729        let settings_after_theme =
730            write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
731
732        assert_eq!(settings_after_theme, new_settings)
733    }
734
735    #[test]
736    fn test_write_theme_pre_object_whitespace() {
737        let settings = r#"          { "a": "", "ok": true }"#.to_string();
738        let new_settings = r#"          { "theme": "summerfruit-light", "a": "", "ok": true }"#;
739
740        let settings_after_theme =
741            write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
742
743        assert_eq!(settings_after_theme, new_settings)
744    }
745
746    #[test]
747    fn test_write_theme_into_multi_line_settings_without_theme() {
748        let settings = r#"
749            {
750                "a": "b"
751            }
752        "#
753        .unindent();
754
755        let new_settings = r#"
756            {
757                "theme": "summerfruit-light",
758                "a": "b"
759            }
760        "#
761        .unindent();
762
763        let settings_after_theme =
764            write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
765
766        assert_eq!(settings_after_theme, new_settings)
767    }
768}