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