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