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