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 schemars::{
11 gen::{SchemaGenerator, SchemaSettings},
12 schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
13 JsonSchema,
14};
15use serde::{de::DeserializeOwned, Deserialize, Serialize};
16use serde_json::Value;
17use sqlez::{
18 bindable::{Bind, Column, StaticColumnCount},
19 statement::Statement,
20};
21use std::{collections::HashMap, num::NonZeroU32, str, sync::Arc};
22use theme::{Theme, ThemeRegistry};
23use tree_sitter::Query;
24use util::ResultExt as _;
25
26pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
27pub use watched_json::watch_files;
28
29#[derive(Clone)]
30pub struct Settings {
31 pub buffer_font_family_name: String,
32 pub buffer_font_features: fonts::Features,
33 pub buffer_font_family: FamilyId,
34 pub default_buffer_font_size: f32,
35 pub buffer_font_size: f32,
36 pub active_pane_magnification: f32,
37 pub cursor_blink: bool,
38 pub confirm_quit: bool,
39 pub hover_popover_enabled: bool,
40 pub show_completions_on_input: bool,
41 pub show_call_status_icon: bool,
42 pub vim_mode: bool,
43 pub autosave: Autosave,
44 pub default_dock_anchor: DockAnchor,
45 pub editor_defaults: EditorSettings,
46 pub editor_overrides: EditorSettings,
47 pub git: GitSettings,
48 pub git_overrides: GitSettings,
49 pub journal_defaults: JournalSettings,
50 pub journal_overrides: JournalSettings,
51 pub terminal_defaults: TerminalSettings,
52 pub terminal_overrides: TerminalSettings,
53 pub language_defaults: HashMap<Arc<str>, EditorSettings>,
54 pub language_overrides: HashMap<Arc<str>, EditorSettings>,
55 pub lsp: HashMap<Arc<str>, LspSettings>,
56 pub theme: Arc<Theme>,
57 pub telemetry_defaults: TelemetrySettings,
58 pub telemetry_overrides: TelemetrySettings,
59 pub auto_update: bool,
60 pub base_keymap: BaseKeymap,
61}
62
63#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
64pub enum BaseKeymap {
65 #[default]
66 VSCode,
67 JetBrains,
68 SublimeText,
69 Atom,
70 TextMate,
71}
72
73impl BaseKeymap {
74 pub const OPTIONS: [(&'static str, Self); 5] = [
75 ("VSCode (Default)", Self::VSCode),
76 ("Atom", Self::Atom),
77 ("JetBrains", Self::JetBrains),
78 ("Sublime Text", Self::SublimeText),
79 ("TextMate", Self::TextMate),
80 ];
81
82 pub fn asset_path(&self) -> Option<&'static str> {
83 match self {
84 BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
85 BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"),
86 BaseKeymap::Atom => Some("keymaps/atom.json"),
87 BaseKeymap::TextMate => Some("keymaps/textmate.json"),
88 BaseKeymap::VSCode => None,
89 }
90 }
91
92 pub fn names() -> impl Iterator<Item = &'static str> {
93 Self::OPTIONS.iter().map(|(name, _)| *name)
94 }
95
96 pub fn from_names(option: &str) -> BaseKeymap {
97 Self::OPTIONS
98 .iter()
99 .copied()
100 .find_map(|(name, value)| (name == option).then(|| value))
101 .unwrap_or_default()
102 }
103}
104
105#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
106pub struct TelemetrySettings {
107 diagnostics: Option<bool>,
108 metrics: Option<bool>,
109}
110
111impl TelemetrySettings {
112 pub fn metrics(&self) -> bool {
113 self.metrics.unwrap()
114 }
115
116 pub fn diagnostics(&self) -> bool {
117 self.diagnostics.unwrap()
118 }
119
120 pub fn set_metrics(&mut self, value: bool) {
121 self.metrics = Some(value);
122 }
123
124 pub fn set_diagnostics(&mut self, value: bool) {
125 self.diagnostics = Some(value);
126 }
127}
128
129#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
130pub struct GitSettings {
131 pub git_gutter: Option<GitGutter>,
132 pub gutter_debounce: Option<u64>,
133}
134
135#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
136#[serde(rename_all = "snake_case")]
137pub enum GitGutter {
138 #[default]
139 TrackedFiles,
140 Hide,
141}
142
143pub struct GitGutterConfig {}
144
145#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
146pub struct EditorSettings {
147 pub tab_size: Option<NonZeroU32>,
148 pub hard_tabs: Option<bool>,
149 pub soft_wrap: Option<SoftWrap>,
150 pub preferred_line_length: Option<u32>,
151 pub format_on_save: Option<FormatOnSave>,
152 pub remove_trailing_whitespace_on_save: Option<bool>,
153 pub ensure_final_newline_on_save: Option<bool>,
154 pub formatter: Option<Formatter>,
155 pub enable_language_server: Option<bool>,
156}
157
158#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
159#[serde(rename_all = "snake_case")]
160pub enum SoftWrap {
161 None,
162 EditorWidth,
163 PreferredLineLength,
164}
165#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
166#[serde(rename_all = "snake_case")]
167pub enum FormatOnSave {
168 On,
169 Off,
170 LanguageServer,
171 External {
172 command: String,
173 arguments: Vec<String>,
174 },
175}
176
177#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
178#[serde(rename_all = "snake_case")]
179pub enum Formatter {
180 LanguageServer,
181 External {
182 command: String,
183 arguments: Vec<String>,
184 },
185}
186
187#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
188#[serde(rename_all = "snake_case")]
189pub enum Autosave {
190 Off,
191 AfterDelay { milliseconds: u64 },
192 OnFocusChange,
193 OnWindowChange,
194}
195
196#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
197pub struct JournalSettings {
198 pub path: Option<String>,
199 pub hour_format: Option<HourFormat>,
200}
201
202impl Default for JournalSettings {
203 fn default() -> Self {
204 Self {
205 path: Some("~".into()),
206 hour_format: Some(Default::default()),
207 }
208 }
209}
210
211#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
212#[serde(rename_all = "snake_case")]
213pub enum HourFormat {
214 Hour12,
215 Hour24,
216}
217
218impl Default for HourFormat {
219 fn default() -> Self {
220 Self::Hour12
221 }
222}
223
224#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
225pub struct TerminalSettings {
226 pub shell: Option<Shell>,
227 pub working_directory: Option<WorkingDirectory>,
228 pub font_size: Option<f32>,
229 pub font_family: Option<String>,
230 pub env: Option<HashMap<String, String>>,
231 pub blinking: Option<TerminalBlink>,
232 pub alternate_scroll: Option<AlternateScroll>,
233 pub option_as_meta: Option<bool>,
234 pub copy_on_select: Option<bool>,
235}
236
237#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
238#[serde(rename_all = "snake_case")]
239pub enum TerminalBlink {
240 Off,
241 TerminalControlled,
242 On,
243}
244
245impl Default for TerminalBlink {
246 fn default() -> Self {
247 TerminalBlink::TerminalControlled
248 }
249}
250
251#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
252#[serde(rename_all = "snake_case")]
253pub enum Shell {
254 System,
255 Program(String),
256 WithArguments { program: String, args: Vec<String> },
257}
258
259impl Default for Shell {
260 fn default() -> Self {
261 Shell::System
262 }
263}
264
265#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
266#[serde(rename_all = "snake_case")]
267pub enum AlternateScroll {
268 On,
269 Off,
270}
271
272impl Default for AlternateScroll {
273 fn default() -> Self {
274 AlternateScroll::On
275 }
276}
277
278#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
279#[serde(rename_all = "snake_case")]
280pub enum WorkingDirectory {
281 CurrentProjectDirectory,
282 FirstProjectDirectory,
283 AlwaysHome,
284 Always { directory: String },
285}
286
287impl Default for WorkingDirectory {
288 fn default() -> Self {
289 Self::CurrentProjectDirectory
290 }
291}
292
293#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)]
294#[serde(rename_all = "snake_case")]
295pub enum DockAnchor {
296 #[default]
297 Bottom,
298 Right,
299 Expanded,
300}
301
302impl StaticColumnCount for DockAnchor {}
303impl Bind for DockAnchor {
304 fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
305 match self {
306 DockAnchor::Bottom => "Bottom",
307 DockAnchor::Right => "Right",
308 DockAnchor::Expanded => "Expanded",
309 }
310 .bind(statement, start_index)
311 }
312}
313
314impl Column for DockAnchor {
315 fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
316 String::column(statement, start_index).and_then(|(anchor_text, next_index)| {
317 Ok((
318 match anchor_text.as_ref() {
319 "Bottom" => DockAnchor::Bottom,
320 "Right" => DockAnchor::Right,
321 "Expanded" => DockAnchor::Expanded,
322 _ => bail!("Stored dock anchor is incorrect"),
323 },
324 next_index,
325 ))
326 })
327 }
328}
329
330#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
331pub struct SettingsFileContent {
332 #[serde(default)]
333 pub buffer_font_family: Option<String>,
334 #[serde(default)]
335 pub buffer_font_size: Option<f32>,
336 #[serde(default)]
337 pub buffer_font_features: Option<fonts::Features>,
338 #[serde(default)]
339 pub active_pane_magnification: Option<f32>,
340 #[serde(default)]
341 pub cursor_blink: Option<bool>,
342 #[serde(default)]
343 pub confirm_quit: Option<bool>,
344 #[serde(default)]
345 pub hover_popover_enabled: Option<bool>,
346 #[serde(default)]
347 pub show_completions_on_input: Option<bool>,
348 #[serde(default)]
349 pub show_call_status_icon: Option<bool>,
350 #[serde(default)]
351 pub vim_mode: Option<bool>,
352 #[serde(default)]
353 pub autosave: Option<Autosave>,
354 #[serde(default)]
355 pub default_dock_anchor: Option<DockAnchor>,
356 #[serde(flatten)]
357 pub editor: EditorSettings,
358 #[serde(default)]
359 pub journal: JournalSettings,
360 #[serde(default)]
361 pub terminal: TerminalSettings,
362 #[serde(default)]
363 pub git: Option<GitSettings>,
364 #[serde(default)]
365 #[serde(alias = "language_overrides")]
366 pub languages: HashMap<Arc<str>, EditorSettings>,
367 #[serde(default)]
368 pub lsp: HashMap<Arc<str>, LspSettings>,
369 #[serde(default)]
370 pub theme: Option<String>,
371 #[serde(default)]
372 pub telemetry: TelemetrySettings,
373 #[serde(default)]
374 pub auto_update: Option<bool>,
375 #[serde(default)]
376 pub base_keymap: Option<BaseKeymap>,
377}
378
379#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
380#[serde(rename_all = "snake_case")]
381pub struct LspSettings {
382 pub initialization_options: Option<Value>,
383}
384
385impl Settings {
386 /// Fill out the settings corresponding to the default.json file, overrides will be set later
387 pub fn defaults(
388 assets: impl AssetSource,
389 font_cache: &FontCache,
390 themes: &ThemeRegistry,
391 ) -> Self {
392 #[track_caller]
393 fn required<T>(value: Option<T>) -> Option<T> {
394 assert!(value.is_some(), "missing default setting value");
395 value
396 }
397
398 let defaults: SettingsFileContent = parse_json_with_comments(
399 str::from_utf8(assets.load("settings/default.json").unwrap().as_ref()).unwrap(),
400 )
401 .unwrap();
402
403 let buffer_font_features = defaults.buffer_font_features.unwrap();
404 Self {
405 buffer_font_family: font_cache
406 .load_family(
407 &[defaults.buffer_font_family.as_ref().unwrap()],
408 buffer_font_features,
409 )
410 .unwrap(),
411 buffer_font_family_name: defaults.buffer_font_family.unwrap(),
412 buffer_font_features,
413 buffer_font_size: defaults.buffer_font_size.unwrap(),
414 active_pane_magnification: defaults.active_pane_magnification.unwrap(),
415 default_buffer_font_size: defaults.buffer_font_size.unwrap(),
416 confirm_quit: defaults.confirm_quit.unwrap(),
417 cursor_blink: defaults.cursor_blink.unwrap(),
418 hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
419 show_completions_on_input: defaults.show_completions_on_input.unwrap(),
420 show_call_status_icon: defaults.show_call_status_icon.unwrap(),
421 vim_mode: defaults.vim_mode.unwrap(),
422 autosave: defaults.autosave.unwrap(),
423 default_dock_anchor: defaults.default_dock_anchor.unwrap(),
424 editor_defaults: EditorSettings {
425 tab_size: required(defaults.editor.tab_size),
426 hard_tabs: required(defaults.editor.hard_tabs),
427 soft_wrap: required(defaults.editor.soft_wrap),
428 preferred_line_length: required(defaults.editor.preferred_line_length),
429 remove_trailing_whitespace_on_save: required(
430 defaults.editor.remove_trailing_whitespace_on_save,
431 ),
432 ensure_final_newline_on_save: required(
433 defaults.editor.ensure_final_newline_on_save,
434 ),
435 format_on_save: required(defaults.editor.format_on_save),
436 formatter: required(defaults.editor.formatter),
437 enable_language_server: required(defaults.editor.enable_language_server),
438 },
439 editor_overrides: Default::default(),
440 git: defaults.git.unwrap(),
441 git_overrides: Default::default(),
442 journal_defaults: defaults.journal,
443 journal_overrides: Default::default(),
444 terminal_defaults: defaults.terminal,
445 terminal_overrides: Default::default(),
446 language_defaults: defaults.languages,
447 language_overrides: Default::default(),
448 lsp: defaults.lsp.clone(),
449 theme: themes.get(&defaults.theme.unwrap()).unwrap(),
450 telemetry_defaults: defaults.telemetry,
451 telemetry_overrides: Default::default(),
452 auto_update: defaults.auto_update.unwrap(),
453 base_keymap: Default::default(),
454 }
455 }
456
457 // Fill out the overrride and etc. settings from the user's settings.json
458 pub fn set_user_settings(
459 &mut self,
460 data: SettingsFileContent,
461 theme_registry: &ThemeRegistry,
462 font_cache: &FontCache,
463 ) {
464 let mut family_changed = false;
465 if let Some(value) = data.buffer_font_family {
466 self.buffer_font_family_name = value;
467 family_changed = true;
468 }
469 if let Some(value) = data.buffer_font_features {
470 self.buffer_font_features = value;
471 family_changed = true;
472 }
473 if family_changed {
474 if let Some(id) = font_cache
475 .load_family(&[&self.buffer_font_family_name], self.buffer_font_features)
476 .log_err()
477 {
478 self.buffer_font_family = id;
479 }
480 }
481
482 if let Some(value) = &data.theme {
483 if let Some(theme) = theme_registry.get(value).log_err() {
484 self.theme = theme;
485 }
486 }
487
488 merge(&mut self.buffer_font_size, data.buffer_font_size);
489 merge(
490 &mut self.active_pane_magnification,
491 data.active_pane_magnification,
492 );
493 merge(&mut self.default_buffer_font_size, data.buffer_font_size);
494 merge(&mut self.cursor_blink, data.cursor_blink);
495 merge(&mut self.confirm_quit, data.confirm_quit);
496 merge(&mut self.hover_popover_enabled, data.hover_popover_enabled);
497 merge(
498 &mut self.show_completions_on_input,
499 data.show_completions_on_input,
500 );
501 merge(&mut self.vim_mode, data.vim_mode);
502 merge(&mut self.autosave, data.autosave);
503 merge(&mut self.default_dock_anchor, data.default_dock_anchor);
504 merge(&mut self.base_keymap, data.base_keymap);
505
506 // Ensure terminal font is loaded, so we can request it in terminal_element layout
507 if let Some(terminal_font) = &data.terminal.font_family {
508 // TODO: enable font features for the terminal as well.
509 font_cache
510 .load_family(&[terminal_font], Default::default())
511 .log_err();
512 }
513
514 self.editor_overrides = data.editor;
515 self.git_overrides = data.git.unwrap_or_default();
516 self.journal_overrides = data.journal;
517 self.terminal_defaults.font_size = data.terminal.font_size;
518 self.terminal_overrides.copy_on_select = data.terminal.copy_on_select;
519 self.terminal_overrides = data.terminal;
520 self.language_overrides = data.languages;
521 self.telemetry_overrides = data.telemetry;
522 self.lsp = data.lsp;
523 merge(&mut self.auto_update, data.auto_update);
524 }
525
526 pub fn with_language_defaults(
527 mut self,
528 language_name: impl Into<Arc<str>>,
529 overrides: EditorSettings,
530 ) -> Self {
531 self.language_defaults
532 .insert(language_name.into(), overrides);
533 self
534 }
535
536 pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
537 self.language_setting(language, |settings| settings.tab_size)
538 }
539
540 pub fn hard_tabs(&self, language: Option<&str>) -> bool {
541 self.language_setting(language, |settings| settings.hard_tabs)
542 }
543
544 pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
545 self.language_setting(language, |settings| settings.soft_wrap)
546 }
547
548 pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
549 self.language_setting(language, |settings| settings.preferred_line_length)
550 }
551
552 pub fn remove_trailing_whitespace_on_save(&self, language: Option<&str>) -> bool {
553 self.language_setting(language, |settings| {
554 settings.remove_trailing_whitespace_on_save.clone()
555 })
556 }
557
558 pub fn ensure_final_newline_on_save(&self, language: Option<&str>) -> bool {
559 self.language_setting(language, |settings| {
560 settings.ensure_final_newline_on_save.clone()
561 })
562 }
563
564 pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
565 self.language_setting(language, |settings| settings.format_on_save.clone())
566 }
567
568 pub fn formatter(&self, language: Option<&str>) -> Formatter {
569 self.language_setting(language, |settings| settings.formatter.clone())
570 }
571
572 pub fn enable_language_server(&self, language: Option<&str>) -> bool {
573 self.language_setting(language, |settings| settings.enable_language_server)
574 }
575
576 fn language_setting<F, R>(&self, language: Option<&str>, f: F) -> R
577 where
578 F: Fn(&EditorSettings) -> Option<R>,
579 {
580 None.or_else(|| language.and_then(|l| self.language_overrides.get(l).and_then(&f)))
581 .or_else(|| f(&self.editor_overrides))
582 .or_else(|| language.and_then(|l| self.language_defaults.get(l).and_then(&f)))
583 .or_else(|| f(&self.editor_defaults))
584 .expect("missing default")
585 }
586
587 pub fn git_gutter(&self) -> GitGutter {
588 self.git_overrides.git_gutter.unwrap_or_else(|| {
589 self.git
590 .git_gutter
591 .expect("git_gutter should be some by setting setup")
592 })
593 }
594
595 fn terminal_setting<F, R: Default + Clone>(&self, f: F) -> R
596 where
597 F: Fn(&TerminalSettings) -> Option<&R>,
598 {
599 f(&self.terminal_overrides)
600 .or_else(|| f(&self.terminal_defaults))
601 .cloned()
602 .unwrap_or_else(|| R::default())
603 }
604
605 pub fn telemetry(&self) -> TelemetrySettings {
606 TelemetrySettings {
607 diagnostics: Some(self.telemetry_diagnostics()),
608 metrics: Some(self.telemetry_metrics()),
609 }
610 }
611
612 pub fn telemetry_diagnostics(&self) -> bool {
613 self.telemetry_overrides
614 .diagnostics
615 .or(self.telemetry_defaults.diagnostics)
616 .expect("missing default")
617 }
618
619 pub fn telemetry_metrics(&self) -> bool {
620 self.telemetry_overrides
621 .metrics
622 .or(self.telemetry_defaults.metrics)
623 .expect("missing default")
624 }
625
626 pub fn terminal_scroll(&self) -> AlternateScroll {
627 self.terminal_setting(|terminal_setting| terminal_setting.alternate_scroll.as_ref())
628 }
629
630 pub fn terminal_shell(&self) -> Shell {
631 self.terminal_setting(|terminal_setting| terminal_setting.shell.as_ref())
632 }
633
634 pub fn terminal_env(&self) -> HashMap<String, String> {
635 self.terminal_setting(|terminal_setting| terminal_setting.env.as_ref())
636 }
637
638 pub fn terminal_strategy(&self) -> WorkingDirectory {
639 self.terminal_setting(|terminal_setting| terminal_setting.working_directory.as_ref())
640 }
641
642 #[cfg(any(test, feature = "test-support"))]
643 pub fn test(cx: &gpui::AppContext) -> Settings {
644 Settings {
645 buffer_font_family_name: "Monaco".to_string(),
646 buffer_font_features: Default::default(),
647 buffer_font_family: cx
648 .font_cache()
649 .load_family(&["Monaco"], Default::default())
650 .unwrap(),
651 buffer_font_size: 14.,
652 active_pane_magnification: 1.,
653 default_buffer_font_size: 14.,
654 confirm_quit: false,
655 cursor_blink: true,
656 hover_popover_enabled: true,
657 show_completions_on_input: true,
658 show_call_status_icon: true,
659 vim_mode: false,
660 autosave: Autosave::Off,
661 default_dock_anchor: DockAnchor::Bottom,
662 editor_defaults: EditorSettings {
663 tab_size: Some(4.try_into().unwrap()),
664 hard_tabs: Some(false),
665 soft_wrap: Some(SoftWrap::None),
666 preferred_line_length: Some(80),
667 remove_trailing_whitespace_on_save: Some(true),
668 ensure_final_newline_on_save: Some(true),
669 format_on_save: Some(FormatOnSave::On),
670 formatter: Some(Formatter::LanguageServer),
671 enable_language_server: Some(true),
672 },
673 editor_overrides: Default::default(),
674 journal_defaults: Default::default(),
675 journal_overrides: Default::default(),
676 terminal_defaults: Default::default(),
677 terminal_overrides: Default::default(),
678 git: Default::default(),
679 git_overrides: Default::default(),
680 language_defaults: Default::default(),
681 language_overrides: Default::default(),
682 lsp: Default::default(),
683 theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), Default::default),
684 telemetry_defaults: TelemetrySettings {
685 diagnostics: Some(true),
686 metrics: Some(true),
687 },
688 telemetry_overrides: Default::default(),
689 auto_update: true,
690 base_keymap: Default::default(),
691 }
692 }
693
694 #[cfg(any(test, feature = "test-support"))]
695 pub fn test_async(cx: &mut gpui::TestAppContext) {
696 cx.update(|cx| {
697 let settings = Self::test(cx);
698 cx.set_global(settings);
699 });
700 }
701}
702
703pub fn settings_file_json_schema(
704 theme_names: Vec<String>,
705 language_names: &[String],
706) -> serde_json::Value {
707 let settings = SchemaSettings::draft07().with(|settings| {
708 settings.option_add_null_type = false;
709 });
710 let generator = SchemaGenerator::new(settings);
711 let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
712
713 // Create a schema for a theme name.
714 let theme_name_schema = SchemaObject {
715 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
716 enum_values: Some(theme_names.into_iter().map(Value::String).collect()),
717 ..Default::default()
718 };
719
720 // Create a schema for a 'languages overrides' object, associating editor
721 // settings with specific langauges.
722 assert!(root_schema.definitions.contains_key("EditorSettings"));
723 let languages_object_schema = SchemaObject {
724 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
725 object: Some(Box::new(ObjectValidation {
726 properties: language_names
727 .iter()
728 .map(|name| {
729 (
730 name.clone(),
731 Schema::new_ref("#/definitions/EditorSettings".into()),
732 )
733 })
734 .collect(),
735 ..Default::default()
736 })),
737 ..Default::default()
738 };
739
740 // Add these new schemas as definitions, and modify properties of the root
741 // schema to reference them.
742 root_schema.definitions.extend([
743 ("ThemeName".into(), theme_name_schema.into()),
744 ("Languages".into(), languages_object_schema.into()),
745 ]);
746 let root_schema_object = &mut root_schema.schema.object.as_mut().unwrap();
747
748 root_schema_object.properties.extend([
749 (
750 "theme".to_owned(),
751 Schema::new_ref("#/definitions/ThemeName".into()),
752 ),
753 (
754 "languages".to_owned(),
755 Schema::new_ref("#/definitions/Languages".into()),
756 ),
757 // For backward compatibility
758 (
759 "language_overrides".to_owned(),
760 Schema::new_ref("#/definitions/Languages".into()),
761 ),
762 ]);
763
764 serde_json::to_value(root_schema).unwrap()
765}
766
767fn merge<T: Copy>(target: &mut T, value: Option<T>) {
768 if let Some(value) = value {
769 *target = value;
770 }
771}
772
773pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
774 Ok(serde_json::from_reader(
775 json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
776 )?)
777}
778
779fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_value: &Value) {
780 let mut parser = tree_sitter::Parser::new();
781 parser.set_language(tree_sitter_json::language()).unwrap();
782 let tree = parser.parse(&settings_content, None).unwrap();
783
784 let mut cursor = tree_sitter::QueryCursor::new();
785
786 let query = Query::new(
787 tree_sitter_json::language(),
788 "
789 (pair
790 key: (string) @key
791 value: (_) @value)
792 ",
793 )
794 .unwrap();
795
796 let mut depth = 0;
797 let mut first_key_start = None;
798 let mut existing_value_range = 0..settings_content.len();
799 let matches = cursor.matches(&query, tree.root_node(), settings_content.as_bytes());
800 for mat in matches {
801 if mat.captures.len() != 2 {
802 continue;
803 }
804
805 let key_range = mat.captures[0].node.byte_range();
806 let value_range = mat.captures[1].node.byte_range();
807
808 if key_range.start > existing_value_range.end {
809 break;
810 }
811
812 first_key_start.get_or_insert_with(|| key_range.start);
813
814 let found_key = settings_content
815 .get(key_range.clone())
816 .map(|key_text| key_text == format!("\"{}\"", key_path[depth]))
817 .unwrap_or(false);
818
819 if found_key {
820 existing_value_range = value_range;
821 depth += 1;
822
823 if depth == key_path.len() {
824 break;
825 } else {
826 first_key_start = None;
827 }
828 }
829 }
830
831 // We found the exact key we want, insert the new value
832 if depth == key_path.len() {
833 let new_val = serde_json::to_string_pretty(new_value)
834 .expect("Could not serialize new json field to string");
835 settings_content.replace_range(existing_value_range, &new_val);
836 } else {
837 // We have key paths, construct the sub objects
838 let new_key = key_path[depth];
839
840 // We don't have the key, construct the nested objects
841 let mut new_value = serde_json::to_value(new_value).unwrap();
842 for key in key_path[(depth + 1)..].iter().rev() {
843 new_value = serde_json::json!({ key.to_string(): new_value });
844 }
845
846 if let Some(first_key_start) = first_key_start {
847 let mut row = 0;
848 let mut column = 0;
849 for (ix, char) in settings_content.char_indices() {
850 if ix == first_key_start {
851 break;
852 }
853 if char == '\n' {
854 row += 1;
855 column = 0;
856 } else {
857 column += char.len_utf8();
858 }
859 }
860
861 if row > 0 {
862 let new_val = to_pretty_json(&new_value, column, column);
863 let content = format!(r#""{new_key}": {new_val},"#);
864 settings_content.insert_str(first_key_start, &content);
865
866 settings_content.insert_str(
867 first_key_start + content.len(),
868 &format!("\n{:width$}", ' ', width = column),
869 )
870 } else {
871 let new_val = serde_json::to_string(&new_value).unwrap();
872 let mut content = format!(r#""{new_key}": {new_val},"#);
873 content.push(' ');
874 settings_content.insert_str(first_key_start, &content);
875 }
876 } else {
877 new_value = serde_json::json!({ new_key.to_string(): new_value });
878 let indent_prefix_len = 4 * depth;
879 let new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
880
881 settings_content.replace_range(existing_value_range, &new_val);
882 if depth == 0 {
883 settings_content.push('\n');
884 }
885 }
886 }
887}
888
889fn to_pretty_json(
890 value: &serde_json::Value,
891 indent_size: usize,
892 indent_prefix_len: usize,
893) -> String {
894 const SPACES: [u8; 32] = [b' '; 32];
895
896 debug_assert!(indent_size <= SPACES.len());
897 debug_assert!(indent_prefix_len <= SPACES.len());
898
899 let mut output = Vec::new();
900 let mut ser = serde_json::Serializer::with_formatter(
901 &mut output,
902 serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
903 );
904
905 value.serialize(&mut ser).unwrap();
906 let text = String::from_utf8(output).unwrap();
907
908 let mut adjusted_text = String::new();
909 for (i, line) in text.split('\n').enumerate() {
910 if i > 0 {
911 adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
912 }
913 adjusted_text.push_str(line);
914 adjusted_text.push('\n');
915 }
916 adjusted_text.pop();
917 adjusted_text
918}
919
920pub fn update_settings_file(
921 mut text: String,
922 old_file_content: SettingsFileContent,
923 update: impl FnOnce(&mut SettingsFileContent),
924) -> String {
925 let mut new_file_content = old_file_content.clone();
926
927 update(&mut new_file_content);
928
929 let old_object = to_json_object(old_file_content);
930 let new_object = to_json_object(new_file_content);
931
932 fn apply_changes_to_json_text(
933 old_object: &serde_json::Map<String, Value>,
934 new_object: &serde_json::Map<String, Value>,
935 current_key_path: Vec<&str>,
936 json_text: &mut String,
937 ) {
938 for (key, old_value) in old_object.iter() {
939 // We know that these two are from the same shape of object, so we can just unwrap
940 let new_value = new_object.get(key).unwrap();
941 if old_value != new_value {
942 match new_value {
943 Value::Bool(_) | Value::Number(_) | Value::String(_) => {
944 let mut key_path = current_key_path.clone();
945 key_path.push(key);
946 write_settings_key(json_text, &key_path, &new_value);
947 }
948 Value::Object(new_sub_object) => {
949 let mut key_path = current_key_path.clone();
950 key_path.push(key);
951 if let Value::Object(old_sub_object) = old_value {
952 apply_changes_to_json_text(
953 old_sub_object,
954 new_sub_object,
955 key_path,
956 json_text,
957 );
958 } else {
959 unimplemented!("This function doesn't support changing values from simple values to objects yet");
960 }
961 }
962 Value::Null | Value::Array(_) => {
963 unimplemented!("We only support objects and simple values");
964 }
965 }
966 }
967 }
968 }
969
970 apply_changes_to_json_text(&old_object, &new_object, vec![], &mut text);
971
972 text
973}
974
975fn to_json_object(settings_file: SettingsFileContent) -> serde_json::Map<String, Value> {
976 let tmp = serde_json::to_value(settings_file).unwrap();
977 match tmp {
978 Value::Object(map) => map,
979 _ => unreachable!("SettingsFileContent represents a JSON map"),
980 }
981}
982
983#[cfg(test)]
984mod tests {
985 use super::*;
986 use unindent::Unindent;
987
988 fn assert_new_settings<S1: Into<String>, S2: Into<String>>(
989 old_json: S1,
990 update: fn(&mut SettingsFileContent),
991 expected_new_json: S2,
992 ) {
993 let old_json = old_json.into();
994 let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default();
995 let new_json = update_settings_file(old_json, old_content, update);
996 assert_eq!(new_json, expected_new_json.into());
997 }
998
999 #[test]
1000 fn test_update_telemetry_setting_multiple_fields() {
1001 assert_new_settings(
1002 r#"
1003 {
1004 "telemetry": {
1005 "metrics": false,
1006 "diagnostics": false
1007 }
1008 }
1009 "#
1010 .unindent(),
1011 |settings| {
1012 settings.telemetry.set_diagnostics(true);
1013 settings.telemetry.set_metrics(true);
1014 },
1015 r#"
1016 {
1017 "telemetry": {
1018 "metrics": true,
1019 "diagnostics": true
1020 }
1021 }
1022 "#
1023 .unindent(),
1024 );
1025 }
1026
1027 #[test]
1028 fn test_update_telemetry_setting_weird_formatting() {
1029 assert_new_settings(
1030 r#"{
1031 "telemetry": { "metrics": false, "diagnostics": true }
1032 }"#
1033 .unindent(),
1034 |settings| settings.telemetry.set_diagnostics(false),
1035 r#"{
1036 "telemetry": { "metrics": false, "diagnostics": false }
1037 }"#
1038 .unindent(),
1039 );
1040 }
1041
1042 #[test]
1043 fn test_update_telemetry_setting_other_fields() {
1044 assert_new_settings(
1045 r#"
1046 {
1047 "telemetry": {
1048 "metrics": false,
1049 "diagnostics": true
1050 }
1051 }
1052 "#
1053 .unindent(),
1054 |settings| settings.telemetry.set_diagnostics(false),
1055 r#"
1056 {
1057 "telemetry": {
1058 "metrics": false,
1059 "diagnostics": false
1060 }
1061 }
1062 "#
1063 .unindent(),
1064 );
1065 }
1066
1067 #[test]
1068 fn test_update_telemetry_setting_empty_telemetry() {
1069 assert_new_settings(
1070 r#"
1071 {
1072 "telemetry": {}
1073 }
1074 "#
1075 .unindent(),
1076 |settings| settings.telemetry.set_diagnostics(false),
1077 r#"
1078 {
1079 "telemetry": {
1080 "diagnostics": false
1081 }
1082 }
1083 "#
1084 .unindent(),
1085 );
1086 }
1087
1088 #[test]
1089 fn test_update_telemetry_setting_pre_existing() {
1090 assert_new_settings(
1091 r#"
1092 {
1093 "telemetry": {
1094 "diagnostics": true
1095 }
1096 }
1097 "#
1098 .unindent(),
1099 |settings| settings.telemetry.set_diagnostics(false),
1100 r#"
1101 {
1102 "telemetry": {
1103 "diagnostics": false
1104 }
1105 }
1106 "#
1107 .unindent(),
1108 );
1109 }
1110
1111 #[test]
1112 fn test_update_telemetry_setting() {
1113 assert_new_settings(
1114 "{}",
1115 |settings| settings.telemetry.set_diagnostics(true),
1116 r#"
1117 {
1118 "telemetry": {
1119 "diagnostics": true
1120 }
1121 }
1122 "#
1123 .unindent(),
1124 );
1125 }
1126
1127 #[test]
1128 fn test_update_object_empty_doc() {
1129 assert_new_settings(
1130 "",
1131 |settings| settings.telemetry.set_diagnostics(true),
1132 r#"
1133 {
1134 "telemetry": {
1135 "diagnostics": true
1136 }
1137 }
1138 "#
1139 .unindent(),
1140 );
1141 }
1142
1143 #[test]
1144 fn test_write_theme_into_settings_with_theme() {
1145 assert_new_settings(
1146 r#"
1147 {
1148 "theme": "One Dark"
1149 }
1150 "#
1151 .unindent(),
1152 |settings| settings.theme = Some("summerfruit-light".to_string()),
1153 r#"
1154 {
1155 "theme": "summerfruit-light"
1156 }
1157 "#
1158 .unindent(),
1159 );
1160 }
1161
1162 #[test]
1163 fn test_write_theme_into_empty_settings() {
1164 assert_new_settings(
1165 r#"
1166 {
1167 }
1168 "#
1169 .unindent(),
1170 |settings| settings.theme = Some("summerfruit-light".to_string()),
1171 r#"
1172 {
1173 "theme": "summerfruit-light"
1174 }
1175 "#
1176 .unindent(),
1177 );
1178 }
1179
1180 #[test]
1181 fn write_key_no_document() {
1182 assert_new_settings(
1183 "",
1184 |settings| settings.theme = Some("summerfruit-light".to_string()),
1185 r#"
1186 {
1187 "theme": "summerfruit-light"
1188 }
1189 "#
1190 .unindent(),
1191 );
1192 }
1193
1194 #[test]
1195 fn test_write_theme_into_single_line_settings_without_theme() {
1196 assert_new_settings(
1197 r#"{ "a": "", "ok": true }"#,
1198 |settings| settings.theme = Some("summerfruit-light".to_string()),
1199 r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#,
1200 );
1201 }
1202
1203 #[test]
1204 fn test_write_theme_pre_object_whitespace() {
1205 assert_new_settings(
1206 r#" { "a": "", "ok": true }"#,
1207 |settings| settings.theme = Some("summerfruit-light".to_string()),
1208 r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#.unindent(),
1209 );
1210 }
1211
1212 #[test]
1213 fn test_write_theme_into_multi_line_settings_without_theme() {
1214 assert_new_settings(
1215 r#"
1216 {
1217 "a": "b"
1218 }
1219 "#
1220 .unindent(),
1221 |settings| settings.theme = Some("summerfruit-light".to_string()),
1222 r#"
1223 {
1224 "theme": "summerfruit-light",
1225 "a": "b"
1226 }
1227 "#
1228 .unindent(),
1229 );
1230 }
1231}