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