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