1mod keymap_file;
2
3use anyhow::Result;
4use gpui::{
5 font_cache::{FamilyId, FontCache},
6 AssetSource,
7};
8use schemars::{
9 gen::{SchemaGenerator, SchemaSettings},
10 schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
11 JsonSchema,
12};
13use serde::{de::DeserializeOwned, Deserialize};
14use serde_json::Value;
15use std::{collections::HashMap, fmt::Write as _, num::NonZeroU32, str, sync::Arc};
16use theme::{Theme, ThemeRegistry};
17use tree_sitter::Query;
18use util::ResultExt as _;
19
20pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
21
22#[derive(Clone)]
23pub struct Settings {
24 pub experiments: FeatureFlags,
25 pub projects_online_by_default: bool,
26 pub buffer_font_family: FamilyId,
27 pub default_buffer_font_size: f32,
28 pub buffer_font_size: f32,
29 pub hover_popover_enabled: bool,
30 pub show_completions_on_input: bool,
31 pub vim_mode: bool,
32 pub autosave: Autosave,
33 pub default_dock_anchor: DockAnchor,
34 pub editor_defaults: EditorSettings,
35 pub editor_overrides: EditorSettings,
36 pub git: GitSettings,
37 pub git_overrides: GitSettings,
38 pub terminal_defaults: TerminalSettings,
39 pub terminal_overrides: TerminalSettings,
40 pub language_defaults: HashMap<Arc<str>, EditorSettings>,
41 pub language_overrides: HashMap<Arc<str>, EditorSettings>,
42 pub lsp: HashMap<Arc<str>, LspSettings>,
43 pub theme: Arc<Theme>,
44 pub staff_mode: bool,
45}
46
47#[derive(Copy, Clone, Debug, Default, Deserialize, JsonSchema)]
48pub struct FeatureFlags {
49 pub experimental_themes: bool,
50}
51
52impl FeatureFlags {
53 pub fn keymap_files(&self) -> Vec<&'static str> {
54 vec![]
55 }
56}
57
58#[derive(Copy, Clone, Debug, Default, Deserialize, JsonSchema)]
59pub struct GitSettings {
60 pub git_gutter: Option<GitGutter>,
61 pub gutter_debounce: Option<u64>,
62}
63
64#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
65#[serde(rename_all = "snake_case")]
66pub enum GitGutter {
67 #[default]
68 TrackedFiles,
69 Hide,
70}
71
72pub struct GitGutterConfig {}
73
74#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
75pub struct EditorSettings {
76 pub tab_size: Option<NonZeroU32>,
77 pub hard_tabs: Option<bool>,
78 pub soft_wrap: Option<SoftWrap>,
79 pub preferred_line_length: Option<u32>,
80 pub format_on_save: Option<FormatOnSave>,
81 pub formatter: Option<Formatter>,
82 pub enable_language_server: Option<bool>,
83}
84
85#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
86#[serde(rename_all = "snake_case")]
87pub enum SoftWrap {
88 None,
89 EditorWidth,
90 PreferredLineLength,
91}
92#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
93#[serde(rename_all = "snake_case")]
94pub enum FormatOnSave {
95 On,
96 Off,
97 LanguageServer,
98 External {
99 command: String,
100 arguments: Vec<String>,
101 },
102}
103
104#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
105#[serde(rename_all = "snake_case")]
106pub enum Formatter {
107 LanguageServer,
108 External {
109 command: String,
110 arguments: Vec<String>,
111 },
112}
113
114#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
115#[serde(rename_all = "snake_case")]
116pub enum Autosave {
117 Off,
118 AfterDelay { milliseconds: u64 },
119 OnFocusChange,
120 OnWindowChange,
121}
122
123#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
124pub struct TerminalSettings {
125 pub shell: Option<Shell>,
126 pub working_directory: Option<WorkingDirectory>,
127 pub font_size: Option<f32>,
128 pub font_family: Option<String>,
129 pub env: Option<HashMap<String, String>>,
130 pub blinking: Option<TerminalBlink>,
131 pub alternate_scroll: Option<AlternateScroll>,
132 pub option_as_meta: Option<bool>,
133 pub copy_on_select: Option<bool>,
134}
135
136#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
137#[serde(rename_all = "snake_case")]
138pub enum TerminalBlink {
139 Off,
140 TerminalControlled,
141 On,
142}
143
144impl Default for TerminalBlink {
145 fn default() -> Self {
146 TerminalBlink::TerminalControlled
147 }
148}
149
150#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
151#[serde(rename_all = "snake_case")]
152pub enum Shell {
153 System,
154 Program(String),
155 WithArguments { program: String, args: Vec<String> },
156}
157
158impl Default for Shell {
159 fn default() -> Self {
160 Shell::System
161 }
162}
163
164#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
165#[serde(rename_all = "snake_case")]
166pub enum AlternateScroll {
167 On,
168 Off,
169}
170
171impl Default for AlternateScroll {
172 fn default() -> Self {
173 AlternateScroll::On
174 }
175}
176
177#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
178#[serde(rename_all = "snake_case")]
179pub enum WorkingDirectory {
180 CurrentProjectDirectory,
181 FirstProjectDirectory,
182 AlwaysHome,
183 Always { directory: String },
184}
185
186#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Deserialize, JsonSchema)]
187#[serde(rename_all = "snake_case")]
188pub enum DockAnchor {
189 #[default]
190 Bottom,
191 Right,
192 Expanded,
193}
194
195#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
196pub struct SettingsFileContent {
197 pub experiments: Option<FeatureFlags>,
198 #[serde(default)]
199 pub projects_online_by_default: Option<bool>,
200 #[serde(default)]
201 pub buffer_font_family: Option<String>,
202 #[serde(default)]
203 pub buffer_font_size: Option<f32>,
204 #[serde(default)]
205 pub hover_popover_enabled: Option<bool>,
206 #[serde(default)]
207 pub show_completions_on_input: Option<bool>,
208 #[serde(default)]
209 pub vim_mode: Option<bool>,
210 #[serde(default)]
211 pub autosave: Option<Autosave>,
212 #[serde(default)]
213 pub default_dock_anchor: Option<DockAnchor>,
214 #[serde(flatten)]
215 pub editor: EditorSettings,
216 #[serde(default)]
217 pub terminal: TerminalSettings,
218 #[serde(default)]
219 pub git: Option<GitSettings>,
220 #[serde(default)]
221 #[serde(alias = "language_overrides")]
222 pub languages: HashMap<Arc<str>, EditorSettings>,
223 #[serde(default)]
224 pub lsp: HashMap<Arc<str>, LspSettings>,
225 #[serde(default)]
226 pub theme: Option<String>,
227 #[serde(default)]
228 pub staff_mode: Option<bool>,
229}
230
231#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
232#[serde(rename_all = "snake_case")]
233pub struct LspSettings {
234 pub initialization_options: Option<Value>,
235}
236
237impl Settings {
238 pub fn defaults(
239 assets: impl AssetSource,
240 font_cache: &FontCache,
241 themes: &ThemeRegistry,
242 ) -> Self {
243 #[track_caller]
244 fn required<T>(value: Option<T>) -> Option<T> {
245 assert!(value.is_some(), "missing default setting value");
246 value
247 }
248
249 let defaults: SettingsFileContent = parse_json_with_comments(
250 str::from_utf8(assets.load("settings/default.json").unwrap().as_ref()).unwrap(),
251 )
252 .unwrap();
253
254 Self {
255 experiments: FeatureFlags::default(),
256 buffer_font_family: font_cache
257 .load_family(&[defaults.buffer_font_family.as_ref().unwrap()])
258 .unwrap(),
259 buffer_font_size: defaults.buffer_font_size.unwrap(),
260 default_buffer_font_size: defaults.buffer_font_size.unwrap(),
261 hover_popover_enabled: defaults.hover_popover_enabled.unwrap(),
262 show_completions_on_input: defaults.show_completions_on_input.unwrap(),
263 projects_online_by_default: defaults.projects_online_by_default.unwrap(),
264 vim_mode: defaults.vim_mode.unwrap(),
265 autosave: defaults.autosave.unwrap(),
266 default_dock_anchor: defaults.default_dock_anchor.unwrap(),
267 editor_defaults: EditorSettings {
268 tab_size: required(defaults.editor.tab_size),
269 hard_tabs: required(defaults.editor.hard_tabs),
270 soft_wrap: required(defaults.editor.soft_wrap),
271 preferred_line_length: required(defaults.editor.preferred_line_length),
272 format_on_save: required(defaults.editor.format_on_save),
273 formatter: required(defaults.editor.formatter),
274 enable_language_server: required(defaults.editor.enable_language_server),
275 },
276 editor_overrides: Default::default(),
277 git: defaults.git.unwrap(),
278 git_overrides: Default::default(),
279 terminal_defaults: defaults.terminal,
280 terminal_overrides: Default::default(),
281 language_defaults: defaults.languages,
282 language_overrides: Default::default(),
283 lsp: defaults.lsp.clone(),
284 theme: themes.get(&defaults.theme.unwrap()).unwrap(),
285
286 staff_mode: false,
287 }
288 }
289
290 pub fn set_user_settings(
291 &mut self,
292 data: SettingsFileContent,
293 theme_registry: &ThemeRegistry,
294 font_cache: &FontCache,
295 ) {
296 if let Some(value) = &data.buffer_font_family {
297 if let Some(id) = font_cache.load_family(&[value]).log_err() {
298 self.buffer_font_family = id;
299 }
300 }
301 if let Some(value) = &data.theme {
302 if let Some(theme) = theme_registry.get(value).log_err() {
303 self.theme = theme;
304 }
305 }
306
307 merge(
308 &mut self.projects_online_by_default,
309 data.projects_online_by_default,
310 );
311 merge(&mut self.buffer_font_size, data.buffer_font_size);
312 merge(&mut self.default_buffer_font_size, data.buffer_font_size);
313 merge(&mut self.hover_popover_enabled, data.hover_popover_enabled);
314 merge(
315 &mut self.show_completions_on_input,
316 data.show_completions_on_input,
317 );
318 merge(&mut self.vim_mode, data.vim_mode);
319 merge(&mut self.autosave, data.autosave);
320 merge(&mut self.experiments, data.experiments);
321 merge(&mut self.staff_mode, data.staff_mode);
322 merge(&mut self.default_dock_anchor, data.default_dock_anchor);
323
324 // Ensure terminal font is loaded, so we can request it in terminal_element layout
325 if let Some(terminal_font) = &data.terminal.font_family {
326 font_cache.load_family(&[terminal_font]).log_err();
327 }
328
329 self.editor_overrides = data.editor;
330 self.git_overrides = data.git.unwrap_or_default();
331 self.terminal_defaults.font_size = data.terminal.font_size;
332 self.terminal_overrides.copy_on_select = data.terminal.copy_on_select;
333 self.terminal_overrides = data.terminal;
334 self.language_overrides = data.languages;
335 self.lsp = data.lsp;
336 }
337
338 pub fn with_language_defaults(
339 mut self,
340 language_name: impl Into<Arc<str>>,
341 overrides: EditorSettings,
342 ) -> Self {
343 self.language_defaults
344 .insert(language_name.into(), overrides);
345 self
346 }
347
348 pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
349 self.language_setting(language, |settings| settings.tab_size)
350 }
351
352 pub fn hard_tabs(&self, language: Option<&str>) -> bool {
353 self.language_setting(language, |settings| settings.hard_tabs)
354 }
355
356 pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap {
357 self.language_setting(language, |settings| settings.soft_wrap)
358 }
359
360 pub fn preferred_line_length(&self, language: Option<&str>) -> u32 {
361 self.language_setting(language, |settings| settings.preferred_line_length)
362 }
363
364 pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
365 self.language_setting(language, |settings| settings.format_on_save.clone())
366 }
367
368 pub fn formatter(&self, language: Option<&str>) -> Formatter {
369 self.language_setting(language, |settings| settings.formatter.clone())
370 }
371
372 pub fn enable_language_server(&self, language: Option<&str>) -> bool {
373 self.language_setting(language, |settings| settings.enable_language_server)
374 }
375
376 fn language_setting<F, R>(&self, language: Option<&str>, f: F) -> R
377 where
378 F: Fn(&EditorSettings) -> Option<R>,
379 {
380 None.or_else(|| language.and_then(|l| self.language_overrides.get(l).and_then(&f)))
381 .or_else(|| f(&self.editor_overrides))
382 .or_else(|| language.and_then(|l| self.language_defaults.get(l).and_then(&f)))
383 .or_else(|| f(&self.editor_defaults))
384 .expect("missing default")
385 }
386
387 pub fn git_gutter(&self) -> GitGutter {
388 self.git_overrides.git_gutter.unwrap_or_else(|| {
389 self.git
390 .git_gutter
391 .expect("git_gutter should be some by setting setup")
392 })
393 }
394
395 #[cfg(any(test, feature = "test-support"))]
396 pub fn test(cx: &gpui::AppContext) -> Settings {
397 Settings {
398 experiments: FeatureFlags::default(),
399 buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(),
400 buffer_font_size: 14.,
401 default_buffer_font_size: 14.,
402 hover_popover_enabled: true,
403 show_completions_on_input: true,
404 vim_mode: false,
405 autosave: Autosave::Off,
406 default_dock_anchor: DockAnchor::Bottom,
407 editor_defaults: EditorSettings {
408 tab_size: Some(4.try_into().unwrap()),
409 hard_tabs: Some(false),
410 soft_wrap: Some(SoftWrap::None),
411 preferred_line_length: Some(80),
412 format_on_save: Some(FormatOnSave::On),
413 formatter: Some(Formatter::LanguageServer),
414 enable_language_server: Some(true),
415 },
416 editor_overrides: Default::default(),
417 terminal_defaults: Default::default(),
418 terminal_overrides: Default::default(),
419 git: Default::default(),
420 git_overrides: Default::default(),
421 language_defaults: Default::default(),
422 language_overrides: Default::default(),
423 lsp: Default::default(),
424 projects_online_by_default: true,
425 theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), Default::default),
426 staff_mode: false,
427 }
428 }
429
430 #[cfg(any(test, feature = "test-support"))]
431 pub fn test_async(cx: &mut gpui::TestAppContext) {
432 cx.update(|cx| {
433 let settings = Self::test(cx);
434 cx.set_global(settings);
435 });
436 }
437}
438
439pub fn settings_file_json_schema(
440 theme_names: Vec<String>,
441 language_names: &[String],
442) -> serde_json::Value {
443 let settings = SchemaSettings::draft07().with(|settings| {
444 settings.option_add_null_type = false;
445 });
446 let generator = SchemaGenerator::new(settings);
447 let mut root_schema = generator.into_root_schema_for::<SettingsFileContent>();
448
449 // Create a schema for a theme name.
450 let theme_name_schema = SchemaObject {
451 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
452 enum_values: Some(theme_names.into_iter().map(Value::String).collect()),
453 ..Default::default()
454 };
455
456 // Create a schema for a 'languages overrides' object, associating editor
457 // settings with specific langauges.
458 assert!(root_schema.definitions.contains_key("EditorSettings"));
459 let languages_object_schema = SchemaObject {
460 instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))),
461 object: Some(Box::new(ObjectValidation {
462 properties: language_names
463 .iter()
464 .map(|name| {
465 (
466 name.clone(),
467 Schema::new_ref("#/definitions/EditorSettings".into()),
468 )
469 })
470 .collect(),
471 ..Default::default()
472 })),
473 ..Default::default()
474 };
475
476 // Add these new schemas as definitions, and modify properties of the root
477 // schema to reference them.
478 root_schema.definitions.extend([
479 ("ThemeName".into(), theme_name_schema.into()),
480 ("Languages".into(), languages_object_schema.into()),
481 ]);
482 let root_schema_object = &mut root_schema.schema.object.as_mut().unwrap();
483
484 // Avoid automcomplete for non-user facing settings
485 root_schema_object.properties.remove("staff_mode");
486 root_schema_object.properties.extend([
487 (
488 "theme".to_owned(),
489 Schema::new_ref("#/definitions/ThemeName".into()),
490 ),
491 (
492 "languages".to_owned(),
493 Schema::new_ref("#/definitions/Languages".into()),
494 ),
495 // For backward compatibility
496 (
497 "language_overrides".to_owned(),
498 Schema::new_ref("#/definitions/Languages".into()),
499 ),
500 ]);
501
502 serde_json::to_value(root_schema).unwrap()
503}
504
505pub fn write_theme(mut settings_content: String, new_val: &str) -> String {
506 let mut parser = tree_sitter::Parser::new();
507 parser.set_language(tree_sitter_json::language()).unwrap();
508 let tree = parser.parse(&settings_content, None).unwrap();
509
510 let mut cursor = tree_sitter::QueryCursor::new();
511
512 let query = Query::new(
513 tree_sitter_json::language(),
514 "
515 (document
516 (object
517 (pair
518 key: (string) @key
519 value: (_) @value)))
520 ",
521 )
522 .unwrap();
523
524 let mut first_key_start = None;
525 let mut existing_value_range = None;
526 let matches = cursor.matches(&query, tree.root_node(), settings_content.as_bytes());
527 for mat in matches {
528 if mat.captures.len() != 2 {
529 continue;
530 }
531
532 let key = mat.captures[0];
533 let value = mat.captures[1];
534
535 first_key_start.get_or_insert_with(|| key.node.start_byte());
536
537 if let Some(key_text) = settings_content.get(key.node.byte_range()) {
538 if key_text == "\"theme\"" {
539 existing_value_range = Some(value.node.byte_range());
540 break;
541 }
542 }
543 }
544
545 match (first_key_start, existing_value_range) {
546 (None, None) => {
547 // No document, create a new object and overwrite
548 settings_content.clear();
549 write!(settings_content, "{{\n \"theme\": \"{new_val}\"\n}}\n").unwrap();
550 }
551
552 (_, Some(existing_value_range)) => {
553 // Existing theme key, overwrite
554 settings_content.replace_range(existing_value_range, &format!("\"{new_val}\""));
555 }
556
557 (Some(first_key_start), None) => {
558 // No existing theme key, but other settings. Prepend new theme settings and
559 // match style of first key
560 let mut row = 0;
561 let mut column = 0;
562 for (ix, char) in settings_content.char_indices() {
563 if ix == first_key_start {
564 break;
565 }
566 if char == '\n' {
567 row += 1;
568 column = 0;
569 } else {
570 column += char.len_utf8();
571 }
572 }
573
574 let content = format!(r#""theme": "{new_val}","#);
575 settings_content.insert_str(first_key_start, &content);
576
577 if row > 0 {
578 settings_content.insert_str(
579 first_key_start + content.len(),
580 &format!("\n{:width$}", ' ', width = column),
581 )
582 } else {
583 settings_content.insert_str(first_key_start + content.len(), " ")
584 }
585 }
586 }
587
588 settings_content
589}
590
591fn merge<T: Copy>(target: &mut T, value: Option<T>) {
592 if let Some(value) = value {
593 *target = value;
594 }
595}
596
597pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
598 Ok(serde_json::from_reader(
599 json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
600 )?)
601}
602
603#[cfg(test)]
604mod tests {
605 use crate::write_theme;
606 use unindent::Unindent;
607
608 #[test]
609 fn test_write_theme_into_settings_with_theme() {
610 let settings = r#"
611 {
612 "theme": "one-dark"
613 }
614 "#
615 .unindent();
616
617 let new_settings = r#"
618 {
619 "theme": "summerfruit-light"
620 }
621 "#
622 .unindent();
623
624 let settings_after_theme = write_theme(settings, "summerfruit-light");
625
626 assert_eq!(settings_after_theme, new_settings)
627 }
628
629 #[test]
630 fn test_write_theme_into_empty_settings() {
631 let settings = r#"
632 {
633 }
634 "#
635 .unindent();
636
637 let new_settings = r#"
638 {
639 "theme": "summerfruit-light"
640 }
641 "#
642 .unindent();
643
644 let settings_after_theme = write_theme(settings, "summerfruit-light");
645
646 assert_eq!(settings_after_theme, new_settings)
647 }
648
649 #[test]
650 fn test_write_theme_into_no_settings() {
651 let settings = "".to_string();
652
653 let new_settings = r#"
654 {
655 "theme": "summerfruit-light"
656 }
657 "#
658 .unindent();
659
660 let settings_after_theme = write_theme(settings, "summerfruit-light");
661
662 assert_eq!(settings_after_theme, new_settings)
663 }
664
665 #[test]
666 fn test_write_theme_into_single_line_settings_without_theme() {
667 let settings = r#"{ "a": "", "ok": true }"#.to_string();
668 let new_settings = r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#;
669
670 let settings_after_theme = write_theme(settings, "summerfruit-light");
671
672 assert_eq!(settings_after_theme, new_settings)
673 }
674
675 #[test]
676 fn test_write_theme_pre_object_whitespace() {
677 let settings = r#" { "a": "", "ok": true }"#.to_string();
678 let new_settings = r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#;
679
680 let settings_after_theme = write_theme(settings, "summerfruit-light");
681
682 assert_eq!(settings_after_theme, new_settings)
683 }
684
685 #[test]
686 fn test_write_theme_into_multi_line_settings_without_theme() {
687 let settings = r#"
688 {
689 "a": "b"
690 }
691 "#
692 .unindent();
693
694 let new_settings = r#"
695 {
696 "theme": "summerfruit-light",
697 "a": "b"
698 }
699 "#
700 .unindent();
701
702 let settings_after_theme = write_theme(settings, "summerfruit-light");
703
704 assert_eq!(settings_after_theme, new_settings)
705 }
706}