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