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