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