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: ProjectPanelSettings,
48 pub editor_defaults: EditorSettings,
49 pub editor_overrides: EditorSettings,
50 pub git: GitSettings,
51 pub git_overrides: GitSettings,
52 pub copilot: CopilotSettings,
53 pub journal_defaults: JournalSettings,
54 pub journal_overrides: JournalSettings,
55 pub terminal_defaults: TerminalSettings,
56 pub terminal_overrides: TerminalSettings,
57 pub language_defaults: HashMap<Arc<str>, EditorSettings>,
58 pub language_overrides: HashMap<Arc<str>, EditorSettings>,
59 pub lsp: HashMap<Arc<str>, LspSettings>,
60 pub theme: Arc<Theme>,
61 pub telemetry_defaults: TelemetrySettings,
62 pub telemetry_overrides: TelemetrySettings,
63 pub auto_update: bool,
64 pub base_keymap: BaseKeymap,
65}
66
67#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
68pub enum BaseKeymap {
69 #[default]
70 VSCode,
71 JetBrains,
72 SublimeText,
73 Atom,
74 TextMate,
75}
76
77impl BaseKeymap {
78 pub const OPTIONS: [(&'static str, Self); 5] = [
79 ("VSCode (Default)", Self::VSCode),
80 ("Atom", Self::Atom),
81 ("JetBrains", Self::JetBrains),
82 ("Sublime Text", Self::SublimeText),
83 ("TextMate", Self::TextMate),
84 ];
85
86 pub fn asset_path(&self) -> Option<&'static str> {
87 match self {
88 BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
89 BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"),
90 BaseKeymap::Atom => Some("keymaps/atom.json"),
91 BaseKeymap::TextMate => Some("keymaps/textmate.json"),
92 BaseKeymap::VSCode => None,
93 }
94 }
95
96 pub fn names() -> impl Iterator<Item = &'static str> {
97 Self::OPTIONS.iter().map(|(name, _)| *name)
98 }
99
100 pub fn from_names(option: &str) -> BaseKeymap {
101 Self::OPTIONS
102 .iter()
103 .copied()
104 .find_map(|(name, value)| (name == option).then(|| value))
105 .unwrap_or_default()
106 }
107}
108
109#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
110pub struct TelemetrySettings {
111 diagnostics: Option<bool>,
112 metrics: Option<bool>,
113}
114
115impl TelemetrySettings {
116 pub fn metrics(&self) -> bool {
117 self.metrics.unwrap()
118 }
119
120 pub fn diagnostics(&self) -> bool {
121 self.diagnostics.unwrap()
122 }
123
124 pub fn set_metrics(&mut self, value: bool) {
125 self.metrics = Some(value);
126 }
127
128 pub fn set_diagnostics(&mut self, value: bool) {
129 self.diagnostics = Some(value);
130 }
131}
132
133#[derive(Clone, Debug, Default)]
134pub struct CopilotSettings {
135 pub disabled_globs: Vec<glob::Pattern>,
136}
137
138#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
139pub struct CopilotSettingsContent {
140 #[serde(default)]
141 pub disabled_globs: Option<Vec<String>>,
142}
143
144#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
145pub struct GitSettings {
146 pub git_gutter: Option<GitGutter>,
147 pub gutter_debounce: Option<u64>,
148}
149
150#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
151#[serde(rename_all = "snake_case")]
152pub enum GitGutter {
153 #[default]
154 TrackedFiles,
155 Hide,
156}
157
158pub struct GitGutterConfig {}
159
160#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
161pub struct ProjectPanelSettings {
162 pub dock: ProjectPanelDockPosition,
163 pub default_width: f32,
164}
165#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
166pub struct ProjectPanelSettingsContent {
167 pub dock: Option<ProjectPanelDockPosition>,
168 pub default_width: Option<f32>,
169}
170
171#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
172#[serde(rename_all = "lowercase")]
173pub enum ProjectPanelDockPosition {
174 Left,
175 Right,
176}
177
178#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
179pub struct EditorSettings {
180 pub tab_size: Option<NonZeroU32>,
181 pub hard_tabs: Option<bool>,
182 pub soft_wrap: Option<SoftWrap>,
183 pub preferred_line_length: Option<u32>,
184 pub format_on_save: Option<FormatOnSave>,
185 pub remove_trailing_whitespace_on_save: Option<bool>,
186 pub ensure_final_newline_on_save: Option<bool>,
187 pub formatter: Option<Formatter>,
188 pub enable_language_server: Option<bool>,
189 pub show_copilot_suggestions: Option<bool>,
190 pub show_whitespaces: Option<ShowWhitespaces>,
191}
192
193#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
194#[serde(rename_all = "snake_case")]
195pub enum SoftWrap {
196 None,
197 EditorWidth,
198 PreferredLineLength,
199}
200#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
201#[serde(rename_all = "snake_case")]
202pub enum FormatOnSave {
203 On,
204 Off,
205 LanguageServer,
206 External {
207 command: String,
208 arguments: Vec<String>,
209 },
210}
211
212#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
213#[serde(rename_all = "snake_case")]
214pub enum Formatter {
215 LanguageServer,
216 External {
217 command: String,
218 arguments: Vec<String>,
219 },
220}
221
222#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
223#[serde(rename_all = "snake_case")]
224pub enum Autosave {
225 Off,
226 AfterDelay { milliseconds: u64 },
227 OnFocusChange,
228 OnWindowChange,
229}
230
231#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
232pub struct JournalSettings {
233 pub path: Option<String>,
234 pub hour_format: Option<HourFormat>,
235}
236
237impl Default for JournalSettings {
238 fn default() -> Self {
239 Self {
240 path: Some("~".into()),
241 hour_format: Some(Default::default()),
242 }
243 }
244}
245
246#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
247#[serde(rename_all = "snake_case")]
248pub enum HourFormat {
249 Hour12,
250 Hour24,
251}
252
253impl Default for HourFormat {
254 fn default() -> Self {
255 Self::Hour12
256 }
257}
258
259#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
260pub struct TerminalSettings {
261 pub default_width: Option<f32>,
262 pub default_height: Option<f32>,
263 pub shell: Option<Shell>,
264 pub working_directory: Option<WorkingDirectory>,
265 pub font_size: Option<f32>,
266 pub font_family: Option<String>,
267 pub line_height: Option<TerminalLineHeight>,
268 pub font_features: Option<fonts::Features>,
269 pub env: Option<HashMap<String, String>>,
270 pub blinking: Option<TerminalBlink>,
271 pub alternate_scroll: Option<AlternateScroll>,
272 pub option_as_meta: Option<bool>,
273 pub copy_on_select: Option<bool>,
274 pub dock: Option<TerminalDockPosition>,
275}
276
277#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
278#[serde(rename_all = "lowercase")]
279pub enum TerminalDockPosition {
280 Left,
281 Bottom,
282 Right,
283}
284
285#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
286#[serde(rename_all = "snake_case")]
287pub enum TerminalLineHeight {
288 #[default]
289 Comfortable,
290 Standard,
291 Custom(f32),
292}
293
294impl TerminalLineHeight {
295 fn value(&self) -> f32 {
296 match self {
297 TerminalLineHeight::Comfortable => 1.618,
298 TerminalLineHeight::Standard => 1.3,
299 TerminalLineHeight::Custom(line_height) => *line_height,
300 }
301 }
302}
303
304#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
305#[serde(rename_all = "snake_case")]
306pub enum TerminalBlink {
307 Off,
308 TerminalControlled,
309 On,
310}
311
312impl Default for TerminalBlink {
313 fn default() -> Self {
314 TerminalBlink::TerminalControlled
315 }
316}
317
318#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
319#[serde(rename_all = "snake_case")]
320pub enum Shell {
321 System,
322 Program(String),
323 WithArguments { program: String, args: Vec<String> },
324}
325
326impl Default for Shell {
327 fn default() -> Self {
328 Shell::System
329 }
330}
331
332#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
333#[serde(rename_all = "snake_case")]
334pub enum AlternateScroll {
335 On,
336 Off,
337}
338
339impl Default for AlternateScroll {
340 fn default() -> Self {
341 AlternateScroll::On
342 }
343}
344
345#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
346#[serde(rename_all = "snake_case")]
347pub enum WorkingDirectory {
348 CurrentProjectDirectory,
349 FirstProjectDirectory,
350 AlwaysHome,
351 Always { directory: String },
352}
353
354impl Default for WorkingDirectory {
355 fn default() -> Self {
356 Self::CurrentProjectDirectory
357 }
358}
359
360impl TerminalSettings {
361 fn line_height(&self) -> Option<f32> {
362 self.line_height
363 .to_owned()
364 .map(|line_height| line_height.value())
365 }
366}
367
368#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
369pub struct SettingsFileContent {
370 #[serde(default)]
371 pub buffer_font_family: Option<String>,
372 #[serde(default)]
373 pub buffer_font_size: Option<f32>,
374 #[serde(default)]
375 pub buffer_font_features: Option<fonts::Features>,
376 #[serde(default)]
377 pub copilot: Option<CopilotSettingsContent>,
378 #[serde(default)]
379 pub active_pane_magnification: Option<f32>,
380 #[serde(default)]
381 pub cursor_blink: Option<bool>,
382 #[serde(default)]
383 pub confirm_quit: Option<bool>,
384 #[serde(default)]
385 pub hover_popover_enabled: Option<bool>,
386 #[serde(default)]
387 pub show_completions_on_input: Option<bool>,
388 #[serde(default)]
389 pub show_call_status_icon: Option<bool>,
390 #[serde(default)]
391 pub vim_mode: Option<bool>,
392 #[serde(default)]
393 pub autosave: Option<Autosave>,
394 #[serde(flatten)]
395 pub editor: EditorSettings,
396 #[serde(default)]
397 pub project_panel: ProjectPanelSettingsContent,
398 #[serde(default)]
399 pub journal: JournalSettings,
400 #[serde(default)]
401 pub terminal: TerminalSettings,
402 #[serde(default)]
403 pub git: Option<GitSettings>,
404 #[serde(default)]
405 #[serde(alias = "language_overrides")]
406 pub languages: HashMap<Arc<str>, EditorSettings>,
407 #[serde(default)]
408 pub lsp: HashMap<Arc<str>, LspSettings>,
409 #[serde(default)]
410 pub theme: Option<String>,
411 #[serde(default)]
412 pub telemetry: TelemetrySettings,
413 #[serde(default)]
414 pub auto_update: Option<bool>,
415 #[serde(default)]
416 pub base_keymap: Option<BaseKeymap>,
417 #[serde(default)]
418 pub features: FeaturesContent,
419}
420
421#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
422#[serde(rename_all = "snake_case")]
423pub struct LspSettings {
424 pub initialization_options: Option<Value>,
425}
426
427#[derive(Clone, Debug, PartialEq, Eq)]
428pub struct Features {
429 pub copilot: bool,
430}
431
432#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
433pub struct FeaturesContent {
434 pub copilot: Option<bool>,
435}
436
437#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
438#[serde(rename_all = "snake_case")]
439pub enum ShowWhitespaces {
440 #[default]
441 Selection,
442 None,
443 All,
444}
445
446impl Settings {
447 pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> {
448 match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() {
449 Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
450 Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
451 }
452 }
453
454 /// Fill out the settings corresponding to the default.json file, overrides will be set later
455 pub fn defaults(
456 assets: impl AssetSource,
457 font_cache: &FontCache,
458 themes: &ThemeRegistry,
459 ) -> Self {
460 #[track_caller]
461 fn required<T>(value: Option<T>) -> Option<T> {
462 assert!(value.is_some(), "missing default setting value");
463 value
464 }
465
466 let defaults: SettingsFileContent = parse_json_with_comments(
467 str::from_utf8(assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap().as_ref()).unwrap(),
468 )
469 .unwrap();
470
471 let buffer_font_features = defaults.buffer_font_features.unwrap();
472 Self {
473 buffer_font_family: font_cache
474 .load_family(
475 &[defaults.buffer_font_family.as_ref().unwrap()],
476 &buffer_font_features,
477 )
478 .unwrap(),
479 buffer_font_family_name: defaults.buffer_font_family.unwrap(),
480 buffer_font_features,
481 buffer_font_size: defaults.buffer_font_size.unwrap(),
482 active_pane_magnification: defaults.active_pane_magnification.unwrap(),
483 default_buffer_font_size: defaults.buffer_font_size.unwrap(),
484 confirm_quit: defaults.confirm_quit.unwrap(),
485 cursor_blink: defaults.cursor_blink.unwrap(),
486 hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
487 show_completions_on_input: defaults.show_completions_on_input.unwrap(),
488 show_call_status_icon: defaults.show_call_status_icon.unwrap(),
489 vim_mode: defaults.vim_mode.unwrap(),
490 autosave: defaults.autosave.unwrap(),
491 project_panel: ProjectPanelSettings {
492 dock: defaults.project_panel.dock.unwrap(),
493 default_width: defaults.project_panel.default_width.unwrap(),
494 },
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 show_whitespaces: required(defaults.editor.show_whitespaces),
511 },
512 editor_overrides: Default::default(),
513 copilot: CopilotSettings {
514 disabled_globs: defaults
515 .copilot
516 .unwrap()
517 .disabled_globs
518 .unwrap()
519 .into_iter()
520 .map(|s| glob::Pattern::new(&s).unwrap())
521 .collect(),
522 },
523 git: defaults.git.unwrap(),
524 git_overrides: Default::default(),
525 journal_defaults: defaults.journal,
526 journal_overrides: Default::default(),
527 terminal_defaults: defaults.terminal,
528 terminal_overrides: Default::default(),
529 language_defaults: defaults.languages,
530 language_overrides: Default::default(),
531 lsp: defaults.lsp.clone(),
532 theme: themes.get(&defaults.theme.unwrap()).unwrap(),
533 telemetry_defaults: defaults.telemetry,
534 telemetry_overrides: Default::default(),
535 auto_update: defaults.auto_update.unwrap(),
536 base_keymap: Default::default(),
537 features: Features {
538 copilot: defaults.features.copilot.unwrap(),
539 },
540 }
541 }
542
543 // Fill out the overrride and etc. settings from the user's settings.json
544 pub fn set_user_settings(
545 &mut self,
546 data: SettingsFileContent,
547 theme_registry: &ThemeRegistry,
548 font_cache: &FontCache,
549 ) {
550 let mut family_changed = false;
551 if let Some(value) = data.buffer_font_family {
552 self.buffer_font_family_name = value;
553 family_changed = true;
554 }
555 if let Some(value) = data.buffer_font_features {
556 self.buffer_font_features = value;
557 family_changed = true;
558 }
559 if family_changed {
560 if let Some(id) = font_cache
561 .load_family(&[&self.buffer_font_family_name], &self.buffer_font_features)
562 .log_err()
563 {
564 self.buffer_font_family = id;
565 }
566 }
567
568 if let Some(value) = &data.theme {
569 if let Some(theme) = theme_registry.get(value).log_err() {
570 self.theme = theme;
571 }
572 }
573
574 merge(&mut self.buffer_font_size, data.buffer_font_size);
575 merge(
576 &mut self.active_pane_magnification,
577 data.active_pane_magnification,
578 );
579 merge(&mut self.default_buffer_font_size, data.buffer_font_size);
580 merge(&mut self.cursor_blink, data.cursor_blink);
581 merge(&mut self.confirm_quit, data.confirm_quit);
582 merge(&mut self.hover_popover_enabled, data.hover_popover_enabled);
583 merge(
584 &mut self.show_completions_on_input,
585 data.show_completions_on_input,
586 );
587 merge(&mut self.vim_mode, data.vim_mode);
588 merge(&mut self.autosave, data.autosave);
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 merge(&mut self.project_panel.dock, data.project_panel.dock);
602 merge(&mut self.project_panel.default_width, data.project_panel.default_width);
603 self.git_overrides = data.git.unwrap_or_default();
604 self.journal_overrides = data.journal;
605 self.terminal_defaults.font_size = data.terminal.font_size;
606 self.terminal_overrides.copy_on_select = data.terminal.copy_on_select;
607 self.terminal_overrides = data.terminal;
608 self.language_overrides = data.languages;
609 self.telemetry_overrides = data.telemetry;
610 self.lsp = data.lsp;
611 merge(&mut self.auto_update, data.auto_update);
612 }
613
614 pub fn with_language_defaults(
615 mut self,
616 language_name: impl Into<Arc<str>>,
617 overrides: EditorSettings,
618 ) -> Self {
619 self.language_defaults
620 .insert(language_name.into(), overrides);
621 self
622 }
623
624 pub fn features(&self) -> &Features {
625 &self.features
626 }
627
628 pub fn show_copilot_suggestions(&self, language: Option<&str>, path: Option<&Path>) -> bool {
629 if !self.features.copilot {
630 return false;
631 }
632
633 if !self.copilot_enabled_for_language(language) {
634 return false;
635 }
636
637 if let Some(path) = path {
638 if !self.copilot_enabled_for_path(path) {
639 return false;
640 }
641 }
642
643 true
644 }
645
646 pub fn copilot_enabled_for_path(&self, path: &Path) -> bool {
647 !self
648 .copilot
649 .disabled_globs
650 .iter()
651 .any(|glob| glob.matches_path(path))
652 }
653
654 pub fn copilot_enabled_for_language(&self, language: Option<&str>) -> bool {
655 self.language_setting(language, |settings| settings.show_copilot_suggestions)
656 }
657
658 pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
659 self.language_setting(language, |settings| settings.tab_size)
660 }
661
662 pub fn show_whitespaces(&self, language: Option<&str>) -> ShowWhitespaces {
663 self.language_setting(language, |settings| settings.show_whitespaces)
664 }
665
666 pub fn hard_tabs(&self, language: Option<&str>) -> bool {
667 self.language_setting(language, |settings| settings.hard_tabs)
668 }
669
670 pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
671 self.language_setting(language, |settings| settings.soft_wrap)
672 }
673
674 pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
675 self.language_setting(language, |settings| settings.preferred_line_length)
676 }
677
678 pub fn remove_trailing_whitespace_on_save(&self, language: Option<&str>) -> bool {
679 self.language_setting(language, |settings| {
680 settings.remove_trailing_whitespace_on_save.clone()
681 })
682 }
683
684 pub fn ensure_final_newline_on_save(&self, language: Option<&str>) -> bool {
685 self.language_setting(language, |settings| {
686 settings.ensure_final_newline_on_save.clone()
687 })
688 }
689
690 pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
691 self.language_setting(language, |settings| settings.format_on_save.clone())
692 }
693
694 pub fn formatter(&self, language: Option<&str>) -> Formatter {
695 self.language_setting(language, |settings| settings.formatter.clone())
696 }
697
698 pub fn enable_language_server(&self, language: Option<&str>) -> bool {
699 self.language_setting(language, |settings| settings.enable_language_server)
700 }
701
702 fn language_setting<F, R>(&self, language: Option<&str>, f: F) -> R
703 where
704 F: Fn(&EditorSettings) -> Option<R>,
705 {
706 None.or_else(|| language.and_then(|l| self.language_overrides.get(l).and_then(&f)))
707 .or_else(|| f(&self.editor_overrides))
708 .or_else(|| language.and_then(|l| self.language_defaults.get(l).and_then(&f)))
709 .or_else(|| f(&self.editor_defaults))
710 .expect("missing default")
711 }
712
713 pub fn git_gutter(&self) -> GitGutter {
714 self.git_overrides.git_gutter.unwrap_or_else(|| {
715 self.git
716 .git_gutter
717 .expect("git_gutter should be some by setting setup")
718 })
719 }
720
721 pub fn telemetry(&self) -> TelemetrySettings {
722 TelemetrySettings {
723 diagnostics: Some(self.telemetry_diagnostics()),
724 metrics: Some(self.telemetry_metrics()),
725 }
726 }
727
728 pub fn telemetry_diagnostics(&self) -> bool {
729 self.telemetry_overrides
730 .diagnostics
731 .or(self.telemetry_defaults.diagnostics)
732 .expect("missing default")
733 }
734
735 pub fn telemetry_metrics(&self) -> bool {
736 self.telemetry_overrides
737 .metrics
738 .or(self.telemetry_defaults.metrics)
739 .expect("missing default")
740 }
741
742 fn terminal_setting<F, R>(&self, f: F) -> R
743 where
744 F: Fn(&TerminalSettings) -> Option<R>,
745 {
746 None.or_else(|| f(&self.terminal_overrides))
747 .or_else(|| f(&self.terminal_defaults))
748 .expect("missing default")
749 }
750
751 pub fn terminal_line_height(&self) -> f32 {
752 self.terminal_setting(|terminal_setting| terminal_setting.line_height())
753 }
754
755 pub fn terminal_scroll(&self) -> AlternateScroll {
756 self.terminal_setting(|terminal_setting| terminal_setting.alternate_scroll.to_owned())
757 }
758
759 pub fn terminal_shell(&self) -> Shell {
760 self.terminal_setting(|terminal_setting| terminal_setting.shell.to_owned())
761 }
762
763 pub fn terminal_env(&self) -> HashMap<String, String> {
764 self.terminal_setting(|terminal_setting| terminal_setting.env.to_owned())
765 }
766
767 pub fn terminal_strategy(&self) -> WorkingDirectory {
768 self.terminal_setting(|terminal_setting| terminal_setting.working_directory.to_owned())
769 }
770
771 #[cfg(any(test, feature = "test-support"))]
772 pub fn test(cx: &gpui::AppContext) -> Settings {
773 Settings {
774 buffer_font_family_name: "Monaco".to_string(),
775 buffer_font_features: Default::default(),
776 buffer_font_family: cx
777 .font_cache()
778 .load_family(&["Monaco"], &Default::default())
779 .unwrap(),
780 buffer_font_size: 14.,
781 active_pane_magnification: 1.,
782 default_buffer_font_size: 14.,
783 confirm_quit: false,
784 cursor_blink: true,
785 hover_popover_enabled: true,
786 show_completions_on_input: true,
787 show_call_status_icon: true,
788 vim_mode: false,
789 autosave: Autosave::Off,
790 project_panel: ProjectPanelSettings {
791 dock: ProjectPanelDockPosition::Left,
792 default_width: 240.,
793 },
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}