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