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