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