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