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