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