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