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