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