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