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