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