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