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