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