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