1use crate::types::{context_keys, LspServerConfig, ProcessLimits};
2
3use rust_i18n::t;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use std::borrow::Cow;
7use std::collections::HashMap;
8use std::ops::Deref;
9use std::path::Path;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct ThemeName(pub String);
15
16impl ThemeName {
17 pub const BUILTIN_OPTIONS: &'static [&'static str] =
19 &["dark", "light", "high-contrast", "nostalgia"];
20}
21
22impl Deref for ThemeName {
23 type Target = str;
24 fn deref(&self) -> &Self::Target {
25 &self.0
26 }
27}
28
29impl From<String> for ThemeName {
30 fn from(s: String) -> Self {
31 Self(s)
32 }
33}
34
35impl From<&str> for ThemeName {
36 fn from(s: &str) -> Self {
37 Self(s.to_string())
38 }
39}
40
41impl PartialEq<str> for ThemeName {
42 fn eq(&self, other: &str) -> bool {
43 self.0 == other
44 }
45}
46
47impl PartialEq<ThemeName> for str {
48 fn eq(&self, other: &ThemeName) -> bool {
49 self == other.0
50 }
51}
52
53impl JsonSchema for ThemeName {
54 fn schema_name() -> Cow<'static, str> {
55 Cow::Borrowed("ThemeOptions")
56 }
57
58 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
59 schemars::json_schema!({
60 "description": "Available color themes",
61 "type": "string",
62 "enum": Self::BUILTIN_OPTIONS
63 })
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
70#[serde(transparent)]
71pub struct LocaleName(pub Option<String>);
72
73include!(concat!(env!("OUT_DIR"), "/locale_options.rs"));
75
76impl LocaleName {
77 pub const LOCALE_OPTIONS: &'static [Option<&'static str>] = GENERATED_LOCALE_OPTIONS;
81
82 pub fn as_option(&self) -> Option<&str> {
84 self.0.as_deref()
85 }
86}
87
88impl From<Option<String>> for LocaleName {
89 fn from(s: Option<String>) -> Self {
90 Self(s)
91 }
92}
93
94impl From<Option<&str>> for LocaleName {
95 fn from(s: Option<&str>) -> Self {
96 Self(s.map(|s| s.to_string()))
97 }
98}
99
100impl JsonSchema for LocaleName {
101 fn schema_name() -> Cow<'static, str> {
102 Cow::Borrowed("LocaleOptions")
103 }
104
105 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
106 schemars::json_schema!({
107 "description": "UI locale (language). Use null for auto-detection from environment.",
108 "enum": Self::LOCALE_OPTIONS
109 })
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
115#[serde(rename_all = "snake_case")]
116pub enum CursorStyle {
117 #[default]
119 Default,
120 BlinkingBlock,
122 SteadyBlock,
124 BlinkingBar,
126 SteadyBar,
128 BlinkingUnderline,
130 SteadyUnderline,
132}
133
134impl CursorStyle {
135 pub const OPTIONS: &'static [&'static str] = &[
137 "default",
138 "blinking_block",
139 "steady_block",
140 "blinking_bar",
141 "steady_bar",
142 "blinking_underline",
143 "steady_underline",
144 ];
145
146 pub const DESCRIPTIONS: &'static [&'static str] = &[
148 "Terminal default",
149 "█ Blinking block",
150 "█ Solid block",
151 "│ Blinking bar",
152 "│ Solid bar",
153 "_ Blinking underline",
154 "_ Solid underline",
155 ];
156
157 pub fn is_block(self) -> bool {
161 matches!(
162 self,
163 Self::BlinkingBlock | Self::SteadyBlock | Self::Default
164 )
165 }
166
167 #[cfg(feature = "runtime")]
169 pub fn to_crossterm_style(self) -> crossterm::cursor::SetCursorStyle {
170 use crossterm::cursor::SetCursorStyle;
171 match self {
172 Self::Default => SetCursorStyle::DefaultUserShape,
173 Self::BlinkingBlock => SetCursorStyle::BlinkingBlock,
174 Self::SteadyBlock => SetCursorStyle::SteadyBlock,
175 Self::BlinkingBar => SetCursorStyle::BlinkingBar,
176 Self::SteadyBar => SetCursorStyle::SteadyBar,
177 Self::BlinkingUnderline => SetCursorStyle::BlinkingUnderScore,
178 Self::SteadyUnderline => SetCursorStyle::SteadyUnderScore,
179 }
180 }
181
182 pub fn to_escape_sequence(self) -> &'static [u8] {
185 match self {
186 Self::Default => b"\x1b[0 q",
187 Self::BlinkingBlock => b"\x1b[1 q",
188 Self::SteadyBlock => b"\x1b[2 q",
189 Self::BlinkingUnderline => b"\x1b[3 q",
190 Self::SteadyUnderline => b"\x1b[4 q",
191 Self::BlinkingBar => b"\x1b[5 q",
192 Self::SteadyBar => b"\x1b[6 q",
193 }
194 }
195
196 pub fn parse(s: &str) -> Option<Self> {
198 match s {
199 "default" => Some(CursorStyle::Default),
200 "blinking_block" => Some(CursorStyle::BlinkingBlock),
201 "steady_block" => Some(CursorStyle::SteadyBlock),
202 "blinking_bar" => Some(CursorStyle::BlinkingBar),
203 "steady_bar" => Some(CursorStyle::SteadyBar),
204 "blinking_underline" => Some(CursorStyle::BlinkingUnderline),
205 "steady_underline" => Some(CursorStyle::SteadyUnderline),
206 _ => None,
207 }
208 }
209
210 pub fn as_str(self) -> &'static str {
212 match self {
213 Self::Default => "default",
214 Self::BlinkingBlock => "blinking_block",
215 Self::SteadyBlock => "steady_block",
216 Self::BlinkingBar => "blinking_bar",
217 Self::SteadyBar => "steady_bar",
218 Self::BlinkingUnderline => "blinking_underline",
219 Self::SteadyUnderline => "steady_underline",
220 }
221 }
222}
223
224impl JsonSchema for CursorStyle {
225 fn schema_name() -> Cow<'static, str> {
226 Cow::Borrowed("CursorStyle")
227 }
228
229 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
230 schemars::json_schema!({
231 "description": "Terminal cursor style",
232 "type": "string",
233 "enum": Self::OPTIONS
234 })
235 }
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240#[serde(transparent)]
241pub struct KeybindingMapName(pub String);
242
243impl KeybindingMapName {
244 pub const BUILTIN_OPTIONS: &'static [&'static str] =
246 &["default", "emacs", "vscode", "macos", "macos-gui"];
247}
248
249impl Deref for KeybindingMapName {
250 type Target = str;
251 fn deref(&self) -> &Self::Target {
252 &self.0
253 }
254}
255
256impl From<String> for KeybindingMapName {
257 fn from(s: String) -> Self {
258 Self(s)
259 }
260}
261
262impl From<&str> for KeybindingMapName {
263 fn from(s: &str) -> Self {
264 Self(s.to_string())
265 }
266}
267
268impl PartialEq<str> for KeybindingMapName {
269 fn eq(&self, other: &str) -> bool {
270 self.0 == other
271 }
272}
273
274#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
276#[serde(rename_all = "lowercase")]
277pub enum LineEndingOption {
278 #[default]
280 Lf,
281 Crlf,
283 Cr,
285}
286
287impl LineEndingOption {
288 pub fn to_line_ending(&self) -> crate::model::buffer::LineEnding {
290 match self {
291 Self::Lf => crate::model::buffer::LineEnding::LF,
292 Self::Crlf => crate::model::buffer::LineEnding::CRLF,
293 Self::Cr => crate::model::buffer::LineEnding::CR,
294 }
295 }
296}
297
298impl JsonSchema for LineEndingOption {
299 fn schema_name() -> Cow<'static, str> {
300 Cow::Borrowed("LineEndingOption")
301 }
302
303 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
304 schemars::json_schema!({
305 "description": "Default line ending format for new files",
306 "type": "string",
307 "enum": ["lf", "crlf", "cr"],
308 "default": "lf"
309 })
310 }
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
315#[serde(rename_all = "lowercase")]
316pub enum AcceptSuggestionOnEnter {
317 #[default]
319 On,
320 Off,
322 Smart,
324}
325
326impl JsonSchema for AcceptSuggestionOnEnter {
327 fn schema_name() -> Cow<'static, str> {
328 Cow::Borrowed("AcceptSuggestionOnEnter")
329 }
330
331 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
332 schemars::json_schema!({
333 "description": "Controls whether Enter accepts a completion suggestion",
334 "type": "string",
335 "enum": ["on", "off", "smart"],
336 "default": "on"
337 })
338 }
339}
340
341impl PartialEq<KeybindingMapName> for str {
342 fn eq(&self, other: &KeybindingMapName) -> bool {
343 self == other.0
344 }
345}
346
347impl JsonSchema for KeybindingMapName {
348 fn schema_name() -> Cow<'static, str> {
349 Cow::Borrowed("KeybindingMapOptions")
350 }
351
352 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
353 schemars::json_schema!({
354 "description": "Available keybinding maps",
355 "type": "string",
356 "enum": Self::BUILTIN_OPTIONS
357 })
358 }
359}
360
361#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
363pub struct Config {
364 #[serde(default)]
367 pub version: u32,
368
369 #[serde(default = "default_theme_name")]
371 pub theme: ThemeName,
372
373 #[serde(default)]
376 pub locale: LocaleName,
377
378 #[serde(default = "default_true")]
381 pub check_for_updates: bool,
382
383 #[serde(default)]
385 pub editor: EditorConfig,
386
387 #[serde(default)]
389 pub file_explorer: FileExplorerConfig,
390
391 #[serde(default)]
393 pub file_browser: FileBrowserConfig,
394
395 #[serde(default)]
397 pub clipboard: ClipboardConfig,
398
399 #[serde(default)]
401 pub terminal: TerminalConfig,
402
403 #[serde(default)]
405 pub keybindings: Vec<Keybinding>,
406
407 #[serde(default)]
410 pub keybinding_maps: HashMap<String, KeymapConfig>,
411
412 #[serde(default = "default_keybinding_map_name")]
414 pub active_keybinding_map: KeybindingMapName,
415
416 #[serde(default)]
418 pub languages: HashMap<String, LanguageConfig>,
419
420 #[serde(default)]
422 pub lsp: HashMap<String, LspServerConfig>,
423
424 #[serde(default)]
426 pub warnings: WarningsConfig,
427
428 #[serde(default)]
432 #[schemars(extend("x-standalone-category" = true, "x-no-add" = true))]
433 pub plugins: HashMap<String, PluginConfig>,
434
435 #[serde(default)]
437 pub packages: PackagesConfig,
438}
439
440fn default_keybinding_map_name() -> KeybindingMapName {
441 if cfg!(target_os = "macos") {
444 KeybindingMapName("macos".to_string())
445 } else {
446 KeybindingMapName("default".to_string())
447 }
448}
449
450fn default_theme_name() -> ThemeName {
451 ThemeName("high-contrast".to_string())
452}
453
454#[derive(Debug, Clone, Copy)]
459pub struct WhitespaceVisibility {
460 pub spaces_leading: bool,
461 pub spaces_inner: bool,
462 pub spaces_trailing: bool,
463 pub tabs_leading: bool,
464 pub tabs_inner: bool,
465 pub tabs_trailing: bool,
466}
467
468impl Default for WhitespaceVisibility {
469 fn default() -> Self {
470 Self {
472 spaces_leading: false,
473 spaces_inner: false,
474 spaces_trailing: false,
475 tabs_leading: true,
476 tabs_inner: true,
477 tabs_trailing: true,
478 }
479 }
480}
481
482impl WhitespaceVisibility {
483 pub fn from_editor_config(editor: &EditorConfig) -> Self {
485 if !editor.whitespace_show {
486 return Self {
487 spaces_leading: false,
488 spaces_inner: false,
489 spaces_trailing: false,
490 tabs_leading: false,
491 tabs_inner: false,
492 tabs_trailing: false,
493 };
494 }
495 Self {
496 spaces_leading: editor.whitespace_spaces_leading,
497 spaces_inner: editor.whitespace_spaces_inner,
498 spaces_trailing: editor.whitespace_spaces_trailing,
499 tabs_leading: editor.whitespace_tabs_leading,
500 tabs_inner: editor.whitespace_tabs_inner,
501 tabs_trailing: editor.whitespace_tabs_trailing,
502 }
503 }
504
505 pub fn with_language_tab_override(mut self, show_whitespace_tabs: bool) -> Self {
508 if !show_whitespace_tabs {
509 self.tabs_leading = false;
510 self.tabs_inner = false;
511 self.tabs_trailing = false;
512 }
513 self
514 }
515
516 pub fn any_spaces(&self) -> bool {
518 self.spaces_leading || self.spaces_inner || self.spaces_trailing
519 }
520
521 pub fn any_tabs(&self) -> bool {
523 self.tabs_leading || self.tabs_inner || self.tabs_trailing
524 }
525
526 pub fn any_visible(&self) -> bool {
528 self.any_spaces() || self.any_tabs()
529 }
530
531 pub fn toggle_all(&mut self) {
535 if self.any_visible() {
536 *self = Self {
537 spaces_leading: false,
538 spaces_inner: false,
539 spaces_trailing: false,
540 tabs_leading: false,
541 tabs_inner: false,
542 tabs_trailing: false,
543 };
544 } else {
545 *self = Self::default();
546 }
547 }
548}
549
550#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
552pub struct EditorConfig {
553 #[serde(default = "default_true")]
556 #[schemars(extend("x-section" = "Display"))]
557 pub line_numbers: bool,
558
559 #[serde(default = "default_false")]
561 #[schemars(extend("x-section" = "Display"))]
562 pub relative_line_numbers: bool,
563
564 #[serde(default = "default_true")]
566 #[schemars(extend("x-section" = "Display"))]
567 pub line_wrap: bool,
568
569 #[serde(default = "default_true")]
571 #[schemars(extend("x-section" = "Display"))]
572 pub wrap_indent: bool,
573
574 #[serde(default = "default_true")]
576 #[schemars(extend("x-section" = "Display"))]
577 pub syntax_highlighting: bool,
578
579 #[serde(default = "default_true")]
584 #[schemars(extend("x-section" = "Display"))]
585 pub show_menu_bar: bool,
586
587 #[serde(default = "default_true")]
592 #[schemars(extend("x-section" = "Display"))]
593 pub show_tab_bar: bool,
594
595 #[serde(default = "default_true")]
600 #[schemars(extend("x-section" = "Display"))]
601 pub show_status_bar: bool,
602
603 #[serde(default = "default_true")]
607 #[schemars(extend("x-section" = "Display"))]
608 pub show_vertical_scrollbar: bool,
609
610 #[serde(default = "default_false")]
615 #[schemars(extend("x-section" = "Display"))]
616 pub show_horizontal_scrollbar: bool,
617
618 #[serde(default = "default_false")]
623 #[schemars(extend("x-section" = "Display"))]
624 pub use_terminal_bg: bool,
625
626 #[serde(default)]
630 #[schemars(extend("x-section" = "Display"))]
631 pub cursor_style: CursorStyle,
632
633 #[serde(default)]
638 #[schemars(extend("x-section" = "Display"))]
639 pub rulers: Vec<usize>,
640
641 #[serde(default = "default_true")]
647 #[schemars(extend("x-section" = "Whitespace"))]
648 pub whitespace_show: bool,
649
650 #[serde(default = "default_false")]
654 #[schemars(extend("x-section" = "Whitespace"))]
655 pub whitespace_spaces_leading: bool,
656
657 #[serde(default = "default_false")]
661 #[schemars(extend("x-section" = "Whitespace"))]
662 pub whitespace_spaces_inner: bool,
663
664 #[serde(default = "default_false")]
668 #[schemars(extend("x-section" = "Whitespace"))]
669 pub whitespace_spaces_trailing: bool,
670
671 #[serde(default = "default_true")]
675 #[schemars(extend("x-section" = "Whitespace"))]
676 pub whitespace_tabs_leading: bool,
677
678 #[serde(default = "default_true")]
682 #[schemars(extend("x-section" = "Whitespace"))]
683 pub whitespace_tabs_inner: bool,
684
685 #[serde(default = "default_true")]
689 #[schemars(extend("x-section" = "Whitespace"))]
690 pub whitespace_tabs_trailing: bool,
691
692 #[serde(default = "default_tab_size")]
695 #[schemars(extend("x-section" = "Editing"))]
696 pub tab_size: usize,
697
698 #[serde(default = "default_true")]
700 #[schemars(extend("x-section" = "Editing"))]
701 pub auto_indent: bool,
702
703 #[serde(default = "default_true")]
710 #[schemars(extend("x-section" = "Editing"))]
711 pub auto_close: bool,
712
713 #[serde(default = "default_true")]
718 #[schemars(extend("x-section" = "Editing"))]
719 pub auto_surround: bool,
720
721 #[serde(default = "default_scroll_offset")]
723 #[schemars(extend("x-section" = "Editing"))]
724 pub scroll_offset: usize,
725
726 #[serde(default)]
731 #[schemars(extend("x-section" = "Editing"))]
732 pub default_line_ending: LineEndingOption,
733
734 #[serde(default = "default_false")]
737 #[schemars(extend("x-section" = "Editing"))]
738 pub trim_trailing_whitespace_on_save: bool,
739
740 #[serde(default = "default_false")]
743 #[schemars(extend("x-section" = "Editing"))]
744 pub ensure_final_newline_on_save: bool,
745
746 #[serde(default = "default_true")]
750 #[schemars(extend("x-section" = "Bracket Matching"))]
751 pub highlight_matching_brackets: bool,
752
753 #[serde(default = "default_true")]
757 #[schemars(extend("x-section" = "Bracket Matching"))]
758 pub rainbow_brackets: bool,
759
760 #[serde(default = "default_true")]
766 #[schemars(extend("x-section" = "Completion"))]
767 pub quick_suggestions: bool,
768
769 #[serde(default = "default_quick_suggestions_delay")]
775 #[schemars(extend("x-section" = "Completion"))]
776 pub quick_suggestions_delay_ms: u64,
777
778 #[serde(default = "default_true")]
782 #[schemars(extend("x-section" = "Completion"))]
783 pub suggest_on_trigger_characters: bool,
784
785 #[serde(default = "default_accept_suggestion_on_enter")]
791 #[schemars(extend("x-section" = "Completion"))]
792 pub accept_suggestion_on_enter: AcceptSuggestionOnEnter,
793
794 #[serde(default = "default_true")]
797 #[schemars(extend("x-section" = "LSP"))]
798 pub enable_inlay_hints: bool,
799
800 #[serde(default = "default_false")]
804 #[schemars(extend("x-section" = "LSP"))]
805 pub enable_semantic_tokens_full: bool,
806
807 #[serde(default = "default_false")]
812 #[schemars(extend("x-section" = "Diagnostics"))]
813 pub diagnostics_inline_text: bool,
814
815 #[serde(default = "default_true")]
820 #[schemars(extend("x-section" = "Mouse"))]
821 pub mouse_hover_enabled: bool,
822
823 #[serde(default = "default_mouse_hover_delay")]
827 #[schemars(extend("x-section" = "Mouse"))]
828 pub mouse_hover_delay_ms: u64,
829
830 #[serde(default = "default_double_click_time")]
834 #[schemars(extend("x-section" = "Mouse"))]
835 pub double_click_time_ms: u64,
836
837 #[serde(default = "default_false")]
842 #[schemars(extend("x-section" = "Recovery"))]
843 pub auto_save_enabled: bool,
844
845 #[serde(default = "default_auto_save_interval")]
850 #[schemars(extend("x-section" = "Recovery"))]
851 pub auto_save_interval_secs: u32,
852
853 #[serde(default = "default_true")]
858 #[schemars(extend("x-section" = "Recovery"))]
859 pub recovery_enabled: bool,
860
861 #[serde(default = "default_auto_recovery_save_interval")]
866 #[schemars(extend("x-section" = "Recovery"))]
867 pub auto_recovery_save_interval_secs: u32,
868
869 #[serde(default = "default_auto_revert_poll_interval")]
874 #[schemars(extend("x-section" = "Recovery"))]
875 pub auto_revert_poll_interval_ms: u64,
876
877 #[serde(default = "default_true")]
883 #[schemars(extend("x-section" = "Keyboard"))]
884 pub keyboard_disambiguate_escape_codes: bool,
885
886 #[serde(default = "default_false")]
891 #[schemars(extend("x-section" = "Keyboard"))]
892 pub keyboard_report_event_types: bool,
893
894 #[serde(default = "default_true")]
899 #[schemars(extend("x-section" = "Keyboard"))]
900 pub keyboard_report_alternate_keys: bool,
901
902 #[serde(default = "default_false")]
908 #[schemars(extend("x-section" = "Keyboard"))]
909 pub keyboard_report_all_keys_as_escape_codes: bool,
910
911 #[serde(default = "default_highlight_timeout")]
914 #[schemars(extend("x-section" = "Performance"))]
915 pub highlight_timeout_ms: u64,
916
917 #[serde(default = "default_snapshot_interval")]
919 #[schemars(extend("x-section" = "Performance"))]
920 pub snapshot_interval: usize,
921
922 #[serde(default = "default_highlight_context_bytes")]
927 #[schemars(extend("x-section" = "Performance"))]
928 pub highlight_context_bytes: usize,
929
930 #[serde(default = "default_large_file_threshold")]
937 #[schemars(extend("x-section" = "Performance"))]
938 pub large_file_threshold_bytes: u64,
939
940 #[serde(default = "default_estimated_line_length")]
944 #[schemars(extend("x-section" = "Performance"))]
945 pub estimated_line_length: usize,
946
947 #[serde(default = "default_read_concurrency")]
952 #[schemars(extend("x-section" = "Performance"))]
953 pub read_concurrency: usize,
954
955 #[serde(default = "default_file_tree_poll_interval")]
960 #[schemars(extend("x-section" = "Performance"))]
961 pub file_tree_poll_interval_ms: u64,
962}
963
964fn default_tab_size() -> usize {
965 4
966}
967
968pub const LARGE_FILE_THRESHOLD_BYTES: u64 = 1024 * 1024; fn default_large_file_threshold() -> u64 {
974 LARGE_FILE_THRESHOLD_BYTES
975}
976
977pub const INDENT_FOLD_MAX_SCAN_LINES: usize = 10_000;
980
981pub const INDENT_FOLD_INDICATOR_MAX_SCAN: usize = 50;
984
985pub const INDENT_FOLD_MAX_UPWARD_SCAN: usize = 200;
988
989fn default_read_concurrency() -> usize {
990 64
991}
992
993fn default_true() -> bool {
994 true
995}
996
997fn default_false() -> bool {
998 false
999}
1000
1001fn default_quick_suggestions_delay() -> u64 {
1002 150 }
1004
1005fn default_accept_suggestion_on_enter() -> AcceptSuggestionOnEnter {
1006 AcceptSuggestionOnEnter::On
1007}
1008
1009fn default_scroll_offset() -> usize {
1010 3
1011}
1012
1013fn default_highlight_timeout() -> u64 {
1014 5
1015}
1016
1017fn default_snapshot_interval() -> usize {
1018 100
1019}
1020
1021fn default_estimated_line_length() -> usize {
1022 80
1023}
1024
1025fn default_auto_save_interval() -> u32 {
1026 30 }
1028
1029fn default_auto_recovery_save_interval() -> u32 {
1030 2 }
1032
1033fn default_highlight_context_bytes() -> usize {
1034 10_000 }
1036
1037fn default_mouse_hover_delay() -> u64 {
1038 500 }
1040
1041fn default_double_click_time() -> u64 {
1042 500 }
1044
1045fn default_auto_revert_poll_interval() -> u64 {
1046 2000 }
1048
1049fn default_file_tree_poll_interval() -> u64 {
1050 3000 }
1052
1053impl Default for EditorConfig {
1054 fn default() -> Self {
1055 Self {
1056 tab_size: default_tab_size(),
1057 auto_indent: true,
1058 auto_close: true,
1059 auto_surround: true,
1060 line_numbers: true,
1061 relative_line_numbers: false,
1062 scroll_offset: default_scroll_offset(),
1063 syntax_highlighting: true,
1064 line_wrap: true,
1065 wrap_indent: true,
1066 highlight_timeout_ms: default_highlight_timeout(),
1067 snapshot_interval: default_snapshot_interval(),
1068 large_file_threshold_bytes: default_large_file_threshold(),
1069 estimated_line_length: default_estimated_line_length(),
1070 enable_inlay_hints: true,
1071 enable_semantic_tokens_full: false,
1072 diagnostics_inline_text: false,
1073 auto_save_enabled: false,
1074 auto_save_interval_secs: default_auto_save_interval(),
1075 recovery_enabled: true,
1076 auto_recovery_save_interval_secs: default_auto_recovery_save_interval(),
1077 highlight_context_bytes: default_highlight_context_bytes(),
1078 mouse_hover_enabled: true,
1079 mouse_hover_delay_ms: default_mouse_hover_delay(),
1080 double_click_time_ms: default_double_click_time(),
1081 auto_revert_poll_interval_ms: default_auto_revert_poll_interval(),
1082 read_concurrency: default_read_concurrency(),
1083 file_tree_poll_interval_ms: default_file_tree_poll_interval(),
1084 default_line_ending: LineEndingOption::default(),
1085 trim_trailing_whitespace_on_save: false,
1086 ensure_final_newline_on_save: false,
1087 highlight_matching_brackets: true,
1088 rainbow_brackets: true,
1089 cursor_style: CursorStyle::default(),
1090 keyboard_disambiguate_escape_codes: true,
1091 keyboard_report_event_types: false,
1092 keyboard_report_alternate_keys: true,
1093 keyboard_report_all_keys_as_escape_codes: false,
1094 quick_suggestions: true,
1095 quick_suggestions_delay_ms: default_quick_suggestions_delay(),
1096 suggest_on_trigger_characters: true,
1097 accept_suggestion_on_enter: default_accept_suggestion_on_enter(),
1098 show_menu_bar: true,
1099 show_tab_bar: true,
1100 show_status_bar: true,
1101 show_vertical_scrollbar: true,
1102 show_horizontal_scrollbar: false,
1103 use_terminal_bg: false,
1104 rulers: Vec::new(),
1105 whitespace_show: true,
1106 whitespace_spaces_leading: false,
1107 whitespace_spaces_inner: false,
1108 whitespace_spaces_trailing: false,
1109 whitespace_tabs_leading: true,
1110 whitespace_tabs_inner: true,
1111 whitespace_tabs_trailing: true,
1112 }
1113 }
1114}
1115
1116#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1118pub struct FileExplorerConfig {
1119 #[serde(default = "default_true")]
1121 pub respect_gitignore: bool,
1122
1123 #[serde(default = "default_false")]
1125 pub show_hidden: bool,
1126
1127 #[serde(default = "default_false")]
1129 pub show_gitignored: bool,
1130
1131 #[serde(default)]
1133 pub custom_ignore_patterns: Vec<String>,
1134
1135 #[serde(default = "default_explorer_width")]
1137 pub width: f32,
1138}
1139
1140fn default_explorer_width() -> f32 {
1141 0.3 }
1143
1144#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1155pub struct ClipboardConfig {
1156 #[serde(default = "default_true")]
1159 pub use_osc52: bool,
1160
1161 #[serde(default = "default_true")]
1164 pub use_system_clipboard: bool,
1165}
1166
1167impl Default for ClipboardConfig {
1168 fn default() -> Self {
1169 Self {
1170 use_osc52: true,
1171 use_system_clipboard: true,
1172 }
1173 }
1174}
1175
1176#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1178pub struct TerminalConfig {
1179 #[serde(default = "default_true")]
1182 pub jump_to_end_on_output: bool,
1183}
1184
1185impl Default for TerminalConfig {
1186 fn default() -> Self {
1187 Self {
1188 jump_to_end_on_output: true,
1189 }
1190 }
1191}
1192
1193#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1195pub struct WarningsConfig {
1196 #[serde(default = "default_true")]
1199 pub show_status_indicator: bool,
1200}
1201
1202impl Default for WarningsConfig {
1203 fn default() -> Self {
1204 Self {
1205 show_status_indicator: true,
1206 }
1207 }
1208}
1209
1210#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1212pub struct PackagesConfig {
1213 #[serde(default = "default_package_sources")]
1216 pub sources: Vec<String>,
1217}
1218
1219fn default_package_sources() -> Vec<String> {
1220 vec!["https://github.com/sinelaw/fresh-plugins-registry".to_string()]
1221}
1222
1223impl Default for PackagesConfig {
1224 fn default() -> Self {
1225 Self {
1226 sources: default_package_sources(),
1227 }
1228 }
1229}
1230
1231pub use fresh_core::config::PluginConfig;
1233
1234impl Default for FileExplorerConfig {
1235 fn default() -> Self {
1236 Self {
1237 respect_gitignore: true,
1238 show_hidden: false,
1239 show_gitignored: false,
1240 custom_ignore_patterns: Vec::new(),
1241 width: default_explorer_width(),
1242 }
1243 }
1244}
1245
1246#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1248pub struct FileBrowserConfig {
1249 #[serde(default = "default_false")]
1251 pub show_hidden: bool,
1252}
1253
1254#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1256pub struct KeyPress {
1257 pub key: String,
1259 #[serde(default)]
1261 pub modifiers: Vec<String>,
1262}
1263
1264#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1266#[schemars(extend("x-display-field" = "/action"))]
1267pub struct Keybinding {
1268 #[serde(default, skip_serializing_if = "String::is_empty")]
1270 pub key: String,
1271
1272 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1274 pub modifiers: Vec<String>,
1275
1276 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1279 pub keys: Vec<KeyPress>,
1280
1281 pub action: String,
1283
1284 #[serde(default)]
1286 pub args: HashMap<String, serde_json::Value>,
1287
1288 #[serde(default)]
1290 pub when: Option<String>,
1291}
1292
1293#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1295#[schemars(extend("x-display-field" = "/inherits"))]
1296pub struct KeymapConfig {
1297 #[serde(default, skip_serializing_if = "Option::is_none")]
1299 pub inherits: Option<String>,
1300
1301 #[serde(default)]
1303 pub bindings: Vec<Keybinding>,
1304}
1305
1306#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1308#[schemars(extend("x-display-field" = "/command"))]
1309pub struct FormatterConfig {
1310 pub command: String,
1312
1313 #[serde(default)]
1316 pub args: Vec<String>,
1317
1318 #[serde(default = "default_true")]
1321 pub stdin: bool,
1322
1323 #[serde(default = "default_on_save_timeout")]
1325 pub timeout_ms: u64,
1326}
1327
1328#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1330#[schemars(extend("x-display-field" = "/command"))]
1331pub struct OnSaveAction {
1332 pub command: String,
1335
1336 #[serde(default)]
1339 pub args: Vec<String>,
1340
1341 #[serde(default)]
1343 pub working_dir: Option<String>,
1344
1345 #[serde(default)]
1347 pub stdin: bool,
1348
1349 #[serde(default = "default_on_save_timeout")]
1351 pub timeout_ms: u64,
1352
1353 #[serde(default = "default_true")]
1356 pub enabled: bool,
1357}
1358
1359fn default_on_save_timeout() -> u64 {
1360 10000
1361}
1362
1363#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1365#[schemars(extend("x-display-field" = "/grammar"))]
1366pub struct LanguageConfig {
1367 #[serde(default)]
1369 pub extensions: Vec<String>,
1370
1371 #[serde(default)]
1373 pub filenames: Vec<String>,
1374
1375 #[serde(default)]
1377 pub grammar: String,
1378
1379 #[serde(default)]
1381 pub comment_prefix: Option<String>,
1382
1383 #[serde(default = "default_true")]
1385 pub auto_indent: bool,
1386
1387 #[serde(default)]
1390 pub auto_close: Option<bool>,
1391
1392 #[serde(default)]
1395 pub auto_surround: Option<bool>,
1396
1397 #[serde(default)]
1399 pub highlighter: HighlighterPreference,
1400
1401 #[serde(default)]
1404 pub textmate_grammar: Option<std::path::PathBuf>,
1405
1406 #[serde(default = "default_true")]
1409 pub show_whitespace_tabs: bool,
1410
1411 #[serde(default = "default_false")]
1415 pub use_tabs: bool,
1416
1417 #[serde(default)]
1420 pub tab_size: Option<usize>,
1421
1422 #[serde(default)]
1424 pub formatter: Option<FormatterConfig>,
1425
1426 #[serde(default)]
1428 pub format_on_save: bool,
1429
1430 #[serde(default)]
1434 pub on_save: Vec<OnSaveAction>,
1435}
1436
1437#[derive(Debug, Clone)]
1444pub struct BufferConfig {
1445 pub tab_size: usize,
1447
1448 pub use_tabs: bool,
1450
1451 pub auto_indent: bool,
1453
1454 pub auto_close: bool,
1456
1457 pub auto_surround: bool,
1459
1460 pub whitespace: WhitespaceVisibility,
1462
1463 pub formatter: Option<FormatterConfig>,
1465
1466 pub format_on_save: bool,
1468
1469 pub on_save: Vec<OnSaveAction>,
1471
1472 pub highlighter: HighlighterPreference,
1474
1475 pub textmate_grammar: Option<std::path::PathBuf>,
1477}
1478
1479impl BufferConfig {
1480 pub fn resolve(global_config: &Config, language_id: Option<&str>) -> Self {
1489 let editor = &global_config.editor;
1490
1491 let mut whitespace = WhitespaceVisibility::from_editor_config(editor);
1493 let mut config = BufferConfig {
1494 tab_size: editor.tab_size,
1495 use_tabs: false, auto_indent: editor.auto_indent,
1497 auto_close: editor.auto_close,
1498 auto_surround: editor.auto_surround,
1499 whitespace,
1500 formatter: None,
1501 format_on_save: false,
1502 on_save: Vec::new(),
1503 highlighter: HighlighterPreference::Auto,
1504 textmate_grammar: None,
1505 };
1506
1507 if let Some(lang_id) = language_id {
1509 if let Some(lang_config) = global_config.languages.get(lang_id) {
1510 if let Some(ts) = lang_config.tab_size {
1512 config.tab_size = ts;
1513 }
1514
1515 config.use_tabs = lang_config.use_tabs;
1517
1518 config.auto_indent = lang_config.auto_indent;
1520
1521 if config.auto_close {
1523 if let Some(lang_auto_close) = lang_config.auto_close {
1524 config.auto_close = lang_auto_close;
1525 }
1526 }
1527
1528 if config.auto_surround {
1530 if let Some(lang_auto_surround) = lang_config.auto_surround {
1531 config.auto_surround = lang_auto_surround;
1532 }
1533 }
1534
1535 whitespace =
1537 whitespace.with_language_tab_override(lang_config.show_whitespace_tabs);
1538 config.whitespace = whitespace;
1539
1540 config.formatter = lang_config.formatter.clone();
1542
1543 config.format_on_save = lang_config.format_on_save;
1545
1546 config.on_save = lang_config.on_save.clone();
1548
1549 config.highlighter = lang_config.highlighter;
1551
1552 config.textmate_grammar = lang_config.textmate_grammar.clone();
1554 }
1555 }
1556
1557 config
1558 }
1559
1560 pub fn indent_string(&self) -> String {
1565 if self.use_tabs {
1566 "\t".to_string()
1567 } else {
1568 " ".repeat(self.tab_size)
1569 }
1570 }
1571}
1572
1573#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
1575#[serde(rename_all = "lowercase")]
1576pub enum HighlighterPreference {
1577 #[default]
1579 Auto,
1580 #[serde(rename = "tree-sitter")]
1582 TreeSitter,
1583 #[serde(rename = "textmate")]
1585 TextMate,
1586}
1587
1588#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1590pub struct MenuConfig {
1591 #[serde(default)]
1593 pub menus: Vec<Menu>,
1594}
1595
1596pub use fresh_core::menu::{Menu, MenuItem};
1598
1599pub trait MenuExt {
1601 fn match_id(&self) -> &str;
1604
1605 fn expand_dynamic_items(&mut self, themes_dir: &std::path::Path);
1608}
1609
1610impl MenuExt for Menu {
1611 fn match_id(&self) -> &str {
1612 self.id.as_deref().unwrap_or(&self.label)
1613 }
1614
1615 fn expand_dynamic_items(&mut self, themes_dir: &std::path::Path) {
1616 self.items = self
1617 .items
1618 .iter()
1619 .map(|item| item.expand_dynamic(themes_dir))
1620 .collect();
1621 }
1622}
1623
1624pub trait MenuItemExt {
1626 fn expand_dynamic(&self, themes_dir: &std::path::Path) -> MenuItem;
1629}
1630
1631impl MenuItemExt for MenuItem {
1632 fn expand_dynamic(&self, themes_dir: &std::path::Path) -> MenuItem {
1633 match self {
1634 MenuItem::DynamicSubmenu { label, source } => {
1635 let items = generate_dynamic_items(source, themes_dir);
1636 MenuItem::Submenu {
1637 label: label.clone(),
1638 items,
1639 }
1640 }
1641 other => other.clone(),
1642 }
1643 }
1644}
1645
1646#[cfg(feature = "runtime")]
1648pub fn generate_dynamic_items(source: &str, themes_dir: &std::path::Path) -> Vec<MenuItem> {
1649 match source {
1650 "copy_with_theme" => {
1651 let loader = crate::view::theme::ThemeLoader::new(themes_dir.to_path_buf());
1653 let registry = loader.load_all();
1654 registry
1655 .list()
1656 .iter()
1657 .map(|info| {
1658 let mut args = HashMap::new();
1659 args.insert("theme".to_string(), serde_json::json!(info.name));
1660 MenuItem::Action {
1661 label: info.name.clone(),
1662 action: "copy_with_theme".to_string(),
1663 args,
1664 when: Some(context_keys::HAS_SELECTION.to_string()),
1665 checkbox: None,
1666 }
1667 })
1668 .collect()
1669 }
1670 _ => vec![MenuItem::Label {
1671 info: format!("Unknown source: {}", source),
1672 }],
1673 }
1674}
1675
1676#[cfg(not(feature = "runtime"))]
1678pub fn generate_dynamic_items(_source: &str, _themes_dir: &std::path::Path) -> Vec<MenuItem> {
1679 vec![]
1681}
1682
1683impl Default for Config {
1684 fn default() -> Self {
1685 Self {
1686 version: 0,
1687 theme: default_theme_name(),
1688 locale: LocaleName::default(),
1689 check_for_updates: true,
1690 editor: EditorConfig::default(),
1691 file_explorer: FileExplorerConfig::default(),
1692 file_browser: FileBrowserConfig::default(),
1693 clipboard: ClipboardConfig::default(),
1694 terminal: TerminalConfig::default(),
1695 keybindings: vec![], keybinding_maps: HashMap::new(), active_keybinding_map: default_keybinding_map_name(),
1698 languages: Self::default_languages(),
1699 lsp: Self::default_lsp_config(),
1700 warnings: WarningsConfig::default(),
1701 plugins: HashMap::new(), packages: PackagesConfig::default(),
1703 }
1704 }
1705}
1706
1707impl MenuConfig {
1708 pub fn translated() -> Self {
1710 Self {
1711 menus: Self::translated_menus(),
1712 }
1713 }
1714
1715 pub fn translated_menus() -> Vec<Menu> {
1721 vec![
1722 Menu {
1724 id: Some("File".to_string()),
1725 label: t!("menu.file").to_string(),
1726 when: None,
1727 items: vec![
1728 MenuItem::Action {
1729 label: t!("menu.file.new_file").to_string(),
1730 action: "new".to_string(),
1731 args: HashMap::new(),
1732 when: None,
1733 checkbox: None,
1734 },
1735 MenuItem::Action {
1736 label: t!("menu.file.open_file").to_string(),
1737 action: "open".to_string(),
1738 args: HashMap::new(),
1739 when: None,
1740 checkbox: None,
1741 },
1742 MenuItem::Separator { separator: true },
1743 MenuItem::Action {
1744 label: t!("menu.file.save").to_string(),
1745 action: "save".to_string(),
1746 args: HashMap::new(),
1747 when: None,
1748 checkbox: None,
1749 },
1750 MenuItem::Action {
1751 label: t!("menu.file.save_as").to_string(),
1752 action: "save_as".to_string(),
1753 args: HashMap::new(),
1754 when: None,
1755 checkbox: None,
1756 },
1757 MenuItem::Action {
1758 label: t!("menu.file.revert").to_string(),
1759 action: "revert".to_string(),
1760 args: HashMap::new(),
1761 when: None,
1762 checkbox: None,
1763 },
1764 MenuItem::Action {
1765 label: t!("menu.file.reload_with_encoding").to_string(),
1766 action: "reload_with_encoding".to_string(),
1767 args: HashMap::new(),
1768 when: None,
1769 checkbox: None,
1770 },
1771 MenuItem::Separator { separator: true },
1772 MenuItem::Action {
1773 label: t!("menu.file.close_buffer").to_string(),
1774 action: "close".to_string(),
1775 args: HashMap::new(),
1776 when: None,
1777 checkbox: None,
1778 },
1779 MenuItem::Separator { separator: true },
1780 MenuItem::Action {
1781 label: t!("menu.file.switch_project").to_string(),
1782 action: "switch_project".to_string(),
1783 args: HashMap::new(),
1784 when: None,
1785 checkbox: None,
1786 },
1787 MenuItem::Separator { separator: true },
1788 MenuItem::Action {
1789 label: t!("menu.file.detach").to_string(),
1790 action: "detach".to_string(),
1791 args: HashMap::new(),
1792 when: Some(context_keys::SESSION_MODE.to_string()),
1793 checkbox: None,
1794 },
1795 MenuItem::Action {
1796 label: t!("menu.file.quit").to_string(),
1797 action: "quit".to_string(),
1798 args: HashMap::new(),
1799 when: None,
1800 checkbox: None,
1801 },
1802 ],
1803 },
1804 Menu {
1806 id: Some("Edit".to_string()),
1807 label: t!("menu.edit").to_string(),
1808 when: None,
1809 items: vec![
1810 MenuItem::Action {
1811 label: t!("menu.edit.undo").to_string(),
1812 action: "undo".to_string(),
1813 args: HashMap::new(),
1814 when: None,
1815 checkbox: None,
1816 },
1817 MenuItem::Action {
1818 label: t!("menu.edit.redo").to_string(),
1819 action: "redo".to_string(),
1820 args: HashMap::new(),
1821 when: None,
1822 checkbox: None,
1823 },
1824 MenuItem::Separator { separator: true },
1825 MenuItem::Action {
1826 label: t!("menu.edit.cut").to_string(),
1827 action: "cut".to_string(),
1828 args: HashMap::new(),
1829 when: Some(context_keys::HAS_SELECTION.to_string()),
1830 checkbox: None,
1831 },
1832 MenuItem::Action {
1833 label: t!("menu.edit.copy").to_string(),
1834 action: "copy".to_string(),
1835 args: HashMap::new(),
1836 when: Some(context_keys::HAS_SELECTION.to_string()),
1837 checkbox: None,
1838 },
1839 MenuItem::DynamicSubmenu {
1840 label: t!("menu.edit.copy_with_formatting").to_string(),
1841 source: "copy_with_theme".to_string(),
1842 },
1843 MenuItem::Action {
1844 label: t!("menu.edit.paste").to_string(),
1845 action: "paste".to_string(),
1846 args: HashMap::new(),
1847 when: None,
1848 checkbox: None,
1849 },
1850 MenuItem::Separator { separator: true },
1851 MenuItem::Action {
1852 label: t!("menu.edit.select_all").to_string(),
1853 action: "select_all".to_string(),
1854 args: HashMap::new(),
1855 when: None,
1856 checkbox: None,
1857 },
1858 MenuItem::Separator { separator: true },
1859 MenuItem::Action {
1860 label: t!("menu.edit.find").to_string(),
1861 action: "search".to_string(),
1862 args: HashMap::new(),
1863 when: None,
1864 checkbox: None,
1865 },
1866 MenuItem::Action {
1867 label: t!("menu.edit.find_in_selection").to_string(),
1868 action: "find_in_selection".to_string(),
1869 args: HashMap::new(),
1870 when: Some(context_keys::HAS_SELECTION.to_string()),
1871 checkbox: None,
1872 },
1873 MenuItem::Action {
1874 label: t!("menu.edit.find_next").to_string(),
1875 action: "find_next".to_string(),
1876 args: HashMap::new(),
1877 when: None,
1878 checkbox: None,
1879 },
1880 MenuItem::Action {
1881 label: t!("menu.edit.find_previous").to_string(),
1882 action: "find_previous".to_string(),
1883 args: HashMap::new(),
1884 when: None,
1885 checkbox: None,
1886 },
1887 MenuItem::Action {
1888 label: t!("menu.edit.replace").to_string(),
1889 action: "query_replace".to_string(),
1890 args: HashMap::new(),
1891 when: None,
1892 checkbox: None,
1893 },
1894 MenuItem::Separator { separator: true },
1895 MenuItem::Action {
1896 label: t!("menu.edit.delete_line").to_string(),
1897 action: "delete_line".to_string(),
1898 args: HashMap::new(),
1899 when: None,
1900 checkbox: None,
1901 },
1902 MenuItem::Action {
1903 label: t!("menu.edit.format_buffer").to_string(),
1904 action: "format_buffer".to_string(),
1905 args: HashMap::new(),
1906 when: Some(context_keys::FORMATTER_AVAILABLE.to_string()),
1907 checkbox: None,
1908 },
1909 MenuItem::Separator { separator: true },
1910 MenuItem::Action {
1911 label: t!("menu.edit.settings").to_string(),
1912 action: "open_settings".to_string(),
1913 args: HashMap::new(),
1914 when: None,
1915 checkbox: None,
1916 },
1917 MenuItem::Action {
1918 label: t!("menu.edit.keybinding_editor").to_string(),
1919 action: "open_keybinding_editor".to_string(),
1920 args: HashMap::new(),
1921 when: None,
1922 checkbox: None,
1923 },
1924 ],
1925 },
1926 Menu {
1928 id: Some("View".to_string()),
1929 label: t!("menu.view").to_string(),
1930 when: None,
1931 items: vec![
1932 MenuItem::Action {
1933 label: t!("menu.view.file_explorer").to_string(),
1934 action: "toggle_file_explorer".to_string(),
1935 args: HashMap::new(),
1936 when: None,
1937 checkbox: Some(context_keys::FILE_EXPLORER.to_string()),
1938 },
1939 MenuItem::Separator { separator: true },
1940 MenuItem::Action {
1941 label: t!("menu.view.line_numbers").to_string(),
1942 action: "toggle_line_numbers".to_string(),
1943 args: HashMap::new(),
1944 when: None,
1945 checkbox: Some(context_keys::LINE_NUMBERS.to_string()),
1946 },
1947 MenuItem::Action {
1948 label: t!("menu.view.line_wrap").to_string(),
1949 action: "toggle_line_wrap".to_string(),
1950 args: HashMap::new(),
1951 when: None,
1952 checkbox: Some(context_keys::LINE_WRAP.to_string()),
1953 },
1954 MenuItem::Action {
1955 label: t!("menu.view.mouse_support").to_string(),
1956 action: "toggle_mouse_capture".to_string(),
1957 args: HashMap::new(),
1958 when: None,
1959 checkbox: Some(context_keys::MOUSE_CAPTURE.to_string()),
1960 },
1961 MenuItem::Separator { separator: true },
1962 MenuItem::Action {
1963 label: t!("menu.view.vertical_scrollbar").to_string(),
1964 action: "toggle_vertical_scrollbar".to_string(),
1965 args: HashMap::new(),
1966 when: None,
1967 checkbox: Some(context_keys::VERTICAL_SCROLLBAR.to_string()),
1968 },
1969 MenuItem::Action {
1970 label: t!("menu.view.horizontal_scrollbar").to_string(),
1971 action: "toggle_horizontal_scrollbar".to_string(),
1972 args: HashMap::new(),
1973 when: None,
1974 checkbox: Some(context_keys::HORIZONTAL_SCROLLBAR.to_string()),
1975 },
1976 MenuItem::Separator { separator: true },
1977 MenuItem::Action {
1978 label: t!("menu.view.set_background").to_string(),
1979 action: "set_background".to_string(),
1980 args: HashMap::new(),
1981 when: None,
1982 checkbox: None,
1983 },
1984 MenuItem::Action {
1985 label: t!("menu.view.set_background_blend").to_string(),
1986 action: "set_background_blend".to_string(),
1987 args: HashMap::new(),
1988 when: None,
1989 checkbox: None,
1990 },
1991 MenuItem::Action {
1992 label: t!("menu.view.set_compose_width").to_string(),
1993 action: "set_compose_width".to_string(),
1994 args: HashMap::new(),
1995 when: None,
1996 checkbox: None,
1997 },
1998 MenuItem::Separator { separator: true },
1999 MenuItem::Action {
2000 label: t!("menu.view.select_theme").to_string(),
2001 action: "select_theme".to_string(),
2002 args: HashMap::new(),
2003 when: None,
2004 checkbox: None,
2005 },
2006 MenuItem::Action {
2007 label: t!("menu.view.select_locale").to_string(),
2008 action: "select_locale".to_string(),
2009 args: HashMap::new(),
2010 when: None,
2011 checkbox: None,
2012 },
2013 MenuItem::Action {
2014 label: t!("menu.view.settings").to_string(),
2015 action: "open_settings".to_string(),
2016 args: HashMap::new(),
2017 when: None,
2018 checkbox: None,
2019 },
2020 MenuItem::Action {
2021 label: t!("menu.view.calibrate_input").to_string(),
2022 action: "calibrate_input".to_string(),
2023 args: HashMap::new(),
2024 when: None,
2025 checkbox: None,
2026 },
2027 MenuItem::Separator { separator: true },
2028 MenuItem::Action {
2029 label: t!("menu.view.split_horizontal").to_string(),
2030 action: "split_horizontal".to_string(),
2031 args: HashMap::new(),
2032 when: None,
2033 checkbox: None,
2034 },
2035 MenuItem::Action {
2036 label: t!("menu.view.split_vertical").to_string(),
2037 action: "split_vertical".to_string(),
2038 args: HashMap::new(),
2039 when: None,
2040 checkbox: None,
2041 },
2042 MenuItem::Action {
2043 label: t!("menu.view.close_split").to_string(),
2044 action: "close_split".to_string(),
2045 args: HashMap::new(),
2046 when: None,
2047 checkbox: None,
2048 },
2049 MenuItem::Action {
2050 label: t!("menu.view.scroll_sync").to_string(),
2051 action: "toggle_scroll_sync".to_string(),
2052 args: HashMap::new(),
2053 when: Some(context_keys::HAS_SAME_BUFFER_SPLITS.to_string()),
2054 checkbox: Some(context_keys::SCROLL_SYNC.to_string()),
2055 },
2056 MenuItem::Action {
2057 label: t!("menu.view.focus_next_split").to_string(),
2058 action: "next_split".to_string(),
2059 args: HashMap::new(),
2060 when: None,
2061 checkbox: None,
2062 },
2063 MenuItem::Action {
2064 label: t!("menu.view.focus_prev_split").to_string(),
2065 action: "prev_split".to_string(),
2066 args: HashMap::new(),
2067 when: None,
2068 checkbox: None,
2069 },
2070 MenuItem::Action {
2071 label: t!("menu.view.toggle_maximize_split").to_string(),
2072 action: "toggle_maximize_split".to_string(),
2073 args: HashMap::new(),
2074 when: None,
2075 checkbox: None,
2076 },
2077 MenuItem::Separator { separator: true },
2078 MenuItem::Submenu {
2079 label: t!("menu.terminal").to_string(),
2080 items: vec![
2081 MenuItem::Action {
2082 label: t!("menu.terminal.open").to_string(),
2083 action: "open_terminal".to_string(),
2084 args: HashMap::new(),
2085 when: None,
2086 checkbox: None,
2087 },
2088 MenuItem::Action {
2089 label: t!("menu.terminal.close").to_string(),
2090 action: "close_terminal".to_string(),
2091 args: HashMap::new(),
2092 when: None,
2093 checkbox: None,
2094 },
2095 MenuItem::Separator { separator: true },
2096 MenuItem::Action {
2097 label: t!("menu.terminal.toggle_keyboard_capture").to_string(),
2098 action: "toggle_keyboard_capture".to_string(),
2099 args: HashMap::new(),
2100 when: None,
2101 checkbox: None,
2102 },
2103 ],
2104 },
2105 MenuItem::Separator { separator: true },
2106 MenuItem::Submenu {
2107 label: t!("menu.view.keybinding_style").to_string(),
2108 items: vec![
2109 MenuItem::Action {
2110 label: t!("menu.view.keybinding_default").to_string(),
2111 action: "switch_keybinding_map".to_string(),
2112 args: {
2113 let mut map = HashMap::new();
2114 map.insert("map".to_string(), serde_json::json!("default"));
2115 map
2116 },
2117 when: None,
2118 checkbox: None,
2119 },
2120 MenuItem::Action {
2121 label: t!("menu.view.keybinding_emacs").to_string(),
2122 action: "switch_keybinding_map".to_string(),
2123 args: {
2124 let mut map = HashMap::new();
2125 map.insert("map".to_string(), serde_json::json!("emacs"));
2126 map
2127 },
2128 when: None,
2129 checkbox: None,
2130 },
2131 MenuItem::Action {
2132 label: t!("menu.view.keybinding_vscode").to_string(),
2133 action: "switch_keybinding_map".to_string(),
2134 args: {
2135 let mut map = HashMap::new();
2136 map.insert("map".to_string(), serde_json::json!("vscode"));
2137 map
2138 },
2139 when: None,
2140 checkbox: None,
2141 },
2142 MenuItem::Action {
2143 label: "macOS GUI (⌘)".to_string(),
2144 action: "switch_keybinding_map".to_string(),
2145 args: {
2146 let mut map = HashMap::new();
2147 map.insert("map".to_string(), serde_json::json!("macos-gui"));
2148 map
2149 },
2150 when: None,
2151 checkbox: None,
2152 },
2153 ],
2154 },
2155 ],
2156 },
2157 Menu {
2159 id: Some("Selection".to_string()),
2160 label: t!("menu.selection").to_string(),
2161 when: None,
2162 items: vec![
2163 MenuItem::Action {
2164 label: t!("menu.selection.select_all").to_string(),
2165 action: "select_all".to_string(),
2166 args: HashMap::new(),
2167 when: None,
2168 checkbox: None,
2169 },
2170 MenuItem::Action {
2171 label: t!("menu.selection.select_word").to_string(),
2172 action: "select_word".to_string(),
2173 args: HashMap::new(),
2174 when: None,
2175 checkbox: None,
2176 },
2177 MenuItem::Action {
2178 label: t!("menu.selection.select_line").to_string(),
2179 action: "select_line".to_string(),
2180 args: HashMap::new(),
2181 when: None,
2182 checkbox: None,
2183 },
2184 MenuItem::Action {
2185 label: t!("menu.selection.expand_selection").to_string(),
2186 action: "expand_selection".to_string(),
2187 args: HashMap::new(),
2188 when: None,
2189 checkbox: None,
2190 },
2191 MenuItem::Separator { separator: true },
2192 MenuItem::Action {
2193 label: t!("menu.selection.add_cursor_above").to_string(),
2194 action: "add_cursor_above".to_string(),
2195 args: HashMap::new(),
2196 when: None,
2197 checkbox: None,
2198 },
2199 MenuItem::Action {
2200 label: t!("menu.selection.add_cursor_below").to_string(),
2201 action: "add_cursor_below".to_string(),
2202 args: HashMap::new(),
2203 when: None,
2204 checkbox: None,
2205 },
2206 MenuItem::Action {
2207 label: t!("menu.selection.add_cursor_next_match").to_string(),
2208 action: "add_cursor_next_match".to_string(),
2209 args: HashMap::new(),
2210 when: None,
2211 checkbox: None,
2212 },
2213 MenuItem::Action {
2214 label: t!("menu.selection.remove_secondary_cursors").to_string(),
2215 action: "remove_secondary_cursors".to_string(),
2216 args: HashMap::new(),
2217 when: None,
2218 checkbox: None,
2219 },
2220 ],
2221 },
2222 Menu {
2224 id: Some("Go".to_string()),
2225 label: t!("menu.go").to_string(),
2226 when: None,
2227 items: vec![
2228 MenuItem::Action {
2229 label: t!("menu.go.goto_line").to_string(),
2230 action: "goto_line".to_string(),
2231 args: HashMap::new(),
2232 when: None,
2233 checkbox: None,
2234 },
2235 MenuItem::Action {
2236 label: t!("menu.go.goto_definition").to_string(),
2237 action: "lsp_goto_definition".to_string(),
2238 args: HashMap::new(),
2239 when: None,
2240 checkbox: None,
2241 },
2242 MenuItem::Action {
2243 label: t!("menu.go.find_references").to_string(),
2244 action: "lsp_references".to_string(),
2245 args: HashMap::new(),
2246 when: None,
2247 checkbox: None,
2248 },
2249 MenuItem::Separator { separator: true },
2250 MenuItem::Action {
2251 label: t!("menu.go.next_buffer").to_string(),
2252 action: "next_buffer".to_string(),
2253 args: HashMap::new(),
2254 when: None,
2255 checkbox: None,
2256 },
2257 MenuItem::Action {
2258 label: t!("menu.go.prev_buffer").to_string(),
2259 action: "prev_buffer".to_string(),
2260 args: HashMap::new(),
2261 when: None,
2262 checkbox: None,
2263 },
2264 MenuItem::Separator { separator: true },
2265 MenuItem::Action {
2266 label: t!("menu.go.command_palette").to_string(),
2267 action: "command_palette".to_string(),
2268 args: HashMap::new(),
2269 when: None,
2270 checkbox: None,
2271 },
2272 ],
2273 },
2274 Menu {
2276 id: Some("LSP".to_string()),
2277 label: t!("menu.lsp").to_string(),
2278 when: None,
2279 items: vec![
2280 MenuItem::Action {
2281 label: t!("menu.lsp.show_hover").to_string(),
2282 action: "lsp_hover".to_string(),
2283 args: HashMap::new(),
2284 when: Some(context_keys::LSP_AVAILABLE.to_string()),
2285 checkbox: None,
2286 },
2287 MenuItem::Action {
2288 label: t!("menu.lsp.goto_definition").to_string(),
2289 action: "lsp_goto_definition".to_string(),
2290 args: HashMap::new(),
2291 when: Some(context_keys::LSP_AVAILABLE.to_string()),
2292 checkbox: None,
2293 },
2294 MenuItem::Action {
2295 label: t!("menu.lsp.find_references").to_string(),
2296 action: "lsp_references".to_string(),
2297 args: HashMap::new(),
2298 when: Some(context_keys::LSP_AVAILABLE.to_string()),
2299 checkbox: None,
2300 },
2301 MenuItem::Action {
2302 label: t!("menu.lsp.rename_symbol").to_string(),
2303 action: "lsp_rename".to_string(),
2304 args: HashMap::new(),
2305 when: Some(context_keys::LSP_AVAILABLE.to_string()),
2306 checkbox: None,
2307 },
2308 MenuItem::Separator { separator: true },
2309 MenuItem::Action {
2310 label: t!("menu.lsp.show_completions").to_string(),
2311 action: "lsp_completion".to_string(),
2312 args: HashMap::new(),
2313 when: Some(context_keys::LSP_AVAILABLE.to_string()),
2314 checkbox: None,
2315 },
2316 MenuItem::Action {
2317 label: t!("menu.lsp.show_signature").to_string(),
2318 action: "lsp_signature_help".to_string(),
2319 args: HashMap::new(),
2320 when: Some(context_keys::LSP_AVAILABLE.to_string()),
2321 checkbox: None,
2322 },
2323 MenuItem::Action {
2324 label: t!("menu.lsp.code_actions").to_string(),
2325 action: "lsp_code_actions".to_string(),
2326 args: HashMap::new(),
2327 when: Some(context_keys::LSP_AVAILABLE.to_string()),
2328 checkbox: None,
2329 },
2330 MenuItem::Separator { separator: true },
2331 MenuItem::Action {
2332 label: t!("menu.lsp.toggle_inlay_hints").to_string(),
2333 action: "toggle_inlay_hints".to_string(),
2334 args: HashMap::new(),
2335 when: Some(context_keys::LSP_AVAILABLE.to_string()),
2336 checkbox: Some(context_keys::INLAY_HINTS.to_string()),
2337 },
2338 MenuItem::Action {
2339 label: t!("menu.lsp.toggle_mouse_hover").to_string(),
2340 action: "toggle_mouse_hover".to_string(),
2341 args: HashMap::new(),
2342 when: None,
2343 checkbox: Some(context_keys::MOUSE_HOVER.to_string()),
2344 },
2345 MenuItem::Separator { separator: true },
2346 MenuItem::Action {
2347 label: t!("menu.lsp.restart_server").to_string(),
2348 action: "lsp_restart".to_string(),
2349 args: HashMap::new(),
2350 when: None,
2351 checkbox: None,
2352 },
2353 MenuItem::Action {
2354 label: t!("menu.lsp.stop_server").to_string(),
2355 action: "lsp_stop".to_string(),
2356 args: HashMap::new(),
2357 when: None,
2358 checkbox: None,
2359 },
2360 MenuItem::Separator { separator: true },
2361 MenuItem::Action {
2362 label: t!("menu.lsp.toggle_for_buffer").to_string(),
2363 action: "lsp_toggle_for_buffer".to_string(),
2364 args: HashMap::new(),
2365 when: None,
2366 checkbox: None,
2367 },
2368 ],
2369 },
2370 Menu {
2372 id: Some("Explorer".to_string()),
2373 label: t!("menu.explorer").to_string(),
2374 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2375 items: vec![
2376 MenuItem::Action {
2377 label: t!("menu.explorer.new_file").to_string(),
2378 action: "file_explorer_new_file".to_string(),
2379 args: HashMap::new(),
2380 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2381 checkbox: None,
2382 },
2383 MenuItem::Action {
2384 label: t!("menu.explorer.new_folder").to_string(),
2385 action: "file_explorer_new_directory".to_string(),
2386 args: HashMap::new(),
2387 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2388 checkbox: None,
2389 },
2390 MenuItem::Separator { separator: true },
2391 MenuItem::Action {
2392 label: t!("menu.explorer.open").to_string(),
2393 action: "file_explorer_open".to_string(),
2394 args: HashMap::new(),
2395 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2396 checkbox: None,
2397 },
2398 MenuItem::Action {
2399 label: t!("menu.explorer.rename").to_string(),
2400 action: "file_explorer_rename".to_string(),
2401 args: HashMap::new(),
2402 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2403 checkbox: None,
2404 },
2405 MenuItem::Action {
2406 label: t!("menu.explorer.delete").to_string(),
2407 action: "file_explorer_delete".to_string(),
2408 args: HashMap::new(),
2409 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2410 checkbox: None,
2411 },
2412 MenuItem::Separator { separator: true },
2413 MenuItem::Action {
2414 label: t!("menu.explorer.refresh").to_string(),
2415 action: "file_explorer_refresh".to_string(),
2416 args: HashMap::new(),
2417 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
2418 checkbox: None,
2419 },
2420 MenuItem::Separator { separator: true },
2421 MenuItem::Action {
2422 label: t!("menu.explorer.show_hidden").to_string(),
2423 action: "file_explorer_toggle_hidden".to_string(),
2424 args: HashMap::new(),
2425 when: Some(context_keys::FILE_EXPLORER.to_string()),
2426 checkbox: Some(context_keys::FILE_EXPLORER_SHOW_HIDDEN.to_string()),
2427 },
2428 MenuItem::Action {
2429 label: t!("menu.explorer.show_gitignored").to_string(),
2430 action: "file_explorer_toggle_gitignored".to_string(),
2431 args: HashMap::new(),
2432 when: Some(context_keys::FILE_EXPLORER.to_string()),
2433 checkbox: Some(context_keys::FILE_EXPLORER_SHOW_GITIGNORED.to_string()),
2434 },
2435 ],
2436 },
2437 Menu {
2439 id: Some("Help".to_string()),
2440 label: t!("menu.help").to_string(),
2441 when: None,
2442 items: vec![
2443 MenuItem::Label {
2444 info: format!("Fresh v{}", env!("CARGO_PKG_VERSION")),
2445 },
2446 MenuItem::Separator { separator: true },
2447 MenuItem::Action {
2448 label: t!("menu.help.show_manual").to_string(),
2449 action: "show_help".to_string(),
2450 args: HashMap::new(),
2451 when: None,
2452 checkbox: None,
2453 },
2454 MenuItem::Action {
2455 label: t!("menu.help.keyboard_shortcuts").to_string(),
2456 action: "keyboard_shortcuts".to_string(),
2457 args: HashMap::new(),
2458 when: None,
2459 checkbox: None,
2460 },
2461 MenuItem::Separator { separator: true },
2462 MenuItem::Action {
2463 label: t!("menu.help.event_debug").to_string(),
2464 action: "event_debug".to_string(),
2465 args: HashMap::new(),
2466 when: None,
2467 checkbox: None,
2468 },
2469 ],
2470 },
2471 ]
2472 }
2473}
2474
2475impl Config {
2476 pub(crate) const FILENAME: &'static str = "config.json";
2478
2479 pub(crate) fn local_config_path(working_dir: &Path) -> std::path::PathBuf {
2481 working_dir.join(Self::FILENAME)
2482 }
2483
2484 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
2490 let contents = std::fs::read_to_string(path.as_ref())
2491 .map_err(|e| ConfigError::IoError(e.to_string()))?;
2492
2493 let partial: crate::partial_config::PartialConfig =
2495 serde_json::from_str(&contents).map_err(|e| ConfigError::ParseError(e.to_string()))?;
2496
2497 Ok(partial.resolve())
2498 }
2499
2500 fn load_builtin_keymap(name: &str) -> Option<KeymapConfig> {
2502 let json_content = match name {
2503 "default" => include_str!("../keymaps/default.json"),
2504 "emacs" => include_str!("../keymaps/emacs.json"),
2505 "vscode" => include_str!("../keymaps/vscode.json"),
2506 "macos" => include_str!("../keymaps/macos.json"),
2507 "macos-gui" => include_str!("../keymaps/macos-gui.json"),
2508 _ => return None,
2509 };
2510
2511 match serde_json::from_str(json_content) {
2512 Ok(config) => Some(config),
2513 Err(e) => {
2514 eprintln!("Failed to parse builtin keymap '{}': {}", name, e);
2515 None
2516 }
2517 }
2518 }
2519
2520 pub fn resolve_keymap(&self, map_name: &str) -> Vec<Keybinding> {
2523 let mut visited = std::collections::HashSet::new();
2524 self.resolve_keymap_recursive(map_name, &mut visited)
2525 }
2526
2527 fn resolve_keymap_recursive(
2529 &self,
2530 map_name: &str,
2531 visited: &mut std::collections::HashSet<String>,
2532 ) -> Vec<Keybinding> {
2533 if visited.contains(map_name) {
2535 eprintln!(
2536 "Warning: Circular inheritance detected in keymap '{}'",
2537 map_name
2538 );
2539 return Vec::new();
2540 }
2541 visited.insert(map_name.to_string());
2542
2543 let keymap = self
2545 .keybinding_maps
2546 .get(map_name)
2547 .cloned()
2548 .or_else(|| Self::load_builtin_keymap(map_name));
2549
2550 let Some(keymap) = keymap else {
2551 return Vec::new();
2552 };
2553
2554 let mut all_bindings = if let Some(ref parent_name) = keymap.inherits {
2556 self.resolve_keymap_recursive(parent_name, visited)
2557 } else {
2558 Vec::new()
2559 };
2560
2561 all_bindings.extend(keymap.bindings);
2563
2564 all_bindings
2565 }
2566 fn default_languages() -> HashMap<String, LanguageConfig> {
2568 let mut languages = HashMap::new();
2569
2570 languages.insert(
2571 "rust".to_string(),
2572 LanguageConfig {
2573 extensions: vec!["rs".to_string()],
2574 filenames: vec![],
2575 grammar: "rust".to_string(),
2576 comment_prefix: Some("//".to_string()),
2577 auto_indent: true,
2578 auto_close: None,
2579 auto_surround: None,
2580 highlighter: HighlighterPreference::Auto,
2581 textmate_grammar: None,
2582 show_whitespace_tabs: true,
2583 use_tabs: false,
2584 tab_size: None,
2585 formatter: Some(FormatterConfig {
2586 command: "rustfmt".to_string(),
2587 args: vec!["--edition".to_string(), "2021".to_string()],
2588 stdin: true,
2589 timeout_ms: 10000,
2590 }),
2591 format_on_save: false,
2592 on_save: vec![],
2593 },
2594 );
2595
2596 languages.insert(
2597 "javascript".to_string(),
2598 LanguageConfig {
2599 extensions: vec!["js".to_string(), "jsx".to_string(), "mjs".to_string()],
2600 filenames: vec![],
2601 grammar: "javascript".to_string(),
2602 comment_prefix: Some("//".to_string()),
2603 auto_indent: true,
2604 auto_close: None,
2605 auto_surround: None,
2606 highlighter: HighlighterPreference::Auto,
2607 textmate_grammar: None,
2608 show_whitespace_tabs: true,
2609 use_tabs: false,
2610 tab_size: None,
2611 formatter: Some(FormatterConfig {
2612 command: "prettier".to_string(),
2613 args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2614 stdin: true,
2615 timeout_ms: 10000,
2616 }),
2617 format_on_save: false,
2618 on_save: vec![],
2619 },
2620 );
2621
2622 languages.insert(
2623 "typescript".to_string(),
2624 LanguageConfig {
2625 extensions: vec!["ts".to_string(), "tsx".to_string(), "mts".to_string()],
2626 filenames: vec![],
2627 grammar: "typescript".to_string(),
2628 comment_prefix: Some("//".to_string()),
2629 auto_indent: true,
2630 auto_close: None,
2631 auto_surround: None,
2632 highlighter: HighlighterPreference::Auto,
2633 textmate_grammar: None,
2634 show_whitespace_tabs: true,
2635 use_tabs: false,
2636 tab_size: None,
2637 formatter: Some(FormatterConfig {
2638 command: "prettier".to_string(),
2639 args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2640 stdin: true,
2641 timeout_ms: 10000,
2642 }),
2643 format_on_save: false,
2644 on_save: vec![],
2645 },
2646 );
2647
2648 languages.insert(
2649 "python".to_string(),
2650 LanguageConfig {
2651 extensions: vec!["py".to_string(), "pyi".to_string()],
2652 filenames: vec![],
2653 grammar: "python".to_string(),
2654 comment_prefix: Some("#".to_string()),
2655 auto_indent: true,
2656 auto_close: None,
2657 auto_surround: None,
2658 highlighter: HighlighterPreference::Auto,
2659 textmate_grammar: None,
2660 show_whitespace_tabs: true,
2661 use_tabs: false,
2662 tab_size: None,
2663 formatter: Some(FormatterConfig {
2664 command: "ruff".to_string(),
2665 args: vec![
2666 "format".to_string(),
2667 "--stdin-filename".to_string(),
2668 "$FILE".to_string(),
2669 ],
2670 stdin: true,
2671 timeout_ms: 10000,
2672 }),
2673 format_on_save: false,
2674 on_save: vec![],
2675 },
2676 );
2677
2678 languages.insert(
2679 "c".to_string(),
2680 LanguageConfig {
2681 extensions: vec!["c".to_string(), "h".to_string()],
2682 filenames: vec![],
2683 grammar: "c".to_string(),
2684 comment_prefix: Some("//".to_string()),
2685 auto_indent: true,
2686 auto_close: None,
2687 auto_surround: None,
2688 highlighter: HighlighterPreference::Auto,
2689 textmate_grammar: None,
2690 show_whitespace_tabs: true,
2691 use_tabs: false,
2692 tab_size: None,
2693 formatter: Some(FormatterConfig {
2694 command: "clang-format".to_string(),
2695 args: vec![],
2696 stdin: true,
2697 timeout_ms: 10000,
2698 }),
2699 format_on_save: false,
2700 on_save: vec![],
2701 },
2702 );
2703
2704 languages.insert(
2705 "cpp".to_string(),
2706 LanguageConfig {
2707 extensions: vec![
2708 "cpp".to_string(),
2709 "cc".to_string(),
2710 "cxx".to_string(),
2711 "hpp".to_string(),
2712 "hh".to_string(),
2713 "hxx".to_string(),
2714 ],
2715 filenames: vec![],
2716 grammar: "cpp".to_string(),
2717 comment_prefix: Some("//".to_string()),
2718 auto_indent: true,
2719 auto_close: None,
2720 auto_surround: None,
2721 highlighter: HighlighterPreference::Auto,
2722 textmate_grammar: None,
2723 show_whitespace_tabs: true,
2724 use_tabs: false,
2725 tab_size: None,
2726 formatter: Some(FormatterConfig {
2727 command: "clang-format".to_string(),
2728 args: vec![],
2729 stdin: true,
2730 timeout_ms: 10000,
2731 }),
2732 format_on_save: false,
2733 on_save: vec![],
2734 },
2735 );
2736
2737 languages.insert(
2738 "csharp".to_string(),
2739 LanguageConfig {
2740 extensions: vec!["cs".to_string()],
2741 filenames: vec![],
2742 grammar: "c_sharp".to_string(),
2743 comment_prefix: Some("//".to_string()),
2744 auto_indent: true,
2745 auto_close: None,
2746 auto_surround: None,
2747 highlighter: HighlighterPreference::Auto,
2748 textmate_grammar: None,
2749 show_whitespace_tabs: true,
2750 use_tabs: false,
2751 tab_size: None,
2752 formatter: None,
2753 format_on_save: false,
2754 on_save: vec![],
2755 },
2756 );
2757
2758 languages.insert(
2759 "bash".to_string(),
2760 LanguageConfig {
2761 extensions: vec!["sh".to_string(), "bash".to_string()],
2762 filenames: vec![
2763 ".bash_aliases".to_string(),
2764 ".bash_logout".to_string(),
2765 ".bash_profile".to_string(),
2766 ".bashrc".to_string(),
2767 ".env".to_string(),
2768 ".profile".to_string(),
2769 ".zlogin".to_string(),
2770 ".zlogout".to_string(),
2771 ".zprofile".to_string(),
2772 ".zshenv".to_string(),
2773 ".zshrc".to_string(),
2774 "PKGBUILD".to_string(),
2776 "APKBUILD".to_string(),
2777 ],
2778 grammar: "bash".to_string(),
2779 comment_prefix: Some("#".to_string()),
2780 auto_indent: true,
2781 auto_close: None,
2782 auto_surround: None,
2783 highlighter: HighlighterPreference::Auto,
2784 textmate_grammar: None,
2785 show_whitespace_tabs: true,
2786 use_tabs: false,
2787 tab_size: None,
2788 formatter: None,
2789 format_on_save: false,
2790 on_save: vec![],
2791 },
2792 );
2793
2794 languages.insert(
2795 "makefile".to_string(),
2796 LanguageConfig {
2797 extensions: vec!["mk".to_string()],
2798 filenames: vec![
2799 "Makefile".to_string(),
2800 "makefile".to_string(),
2801 "GNUmakefile".to_string(),
2802 ],
2803 grammar: "make".to_string(),
2804 comment_prefix: Some("#".to_string()),
2805 auto_indent: false,
2806 auto_close: None,
2807 auto_surround: None,
2808 highlighter: HighlighterPreference::Auto,
2809 textmate_grammar: None,
2810 show_whitespace_tabs: true,
2811 use_tabs: true, tab_size: Some(8), formatter: None,
2814 format_on_save: false,
2815 on_save: vec![],
2816 },
2817 );
2818
2819 languages.insert(
2820 "dockerfile".to_string(),
2821 LanguageConfig {
2822 extensions: vec!["dockerfile".to_string()],
2823 filenames: vec!["Dockerfile".to_string(), "Containerfile".to_string()],
2824 grammar: "dockerfile".to_string(),
2825 comment_prefix: Some("#".to_string()),
2826 auto_indent: true,
2827 auto_close: None,
2828 auto_surround: None,
2829 highlighter: HighlighterPreference::Auto,
2830 textmate_grammar: None,
2831 show_whitespace_tabs: true,
2832 use_tabs: false,
2833 tab_size: None,
2834 formatter: None,
2835 format_on_save: false,
2836 on_save: vec![],
2837 },
2838 );
2839
2840 languages.insert(
2841 "json".to_string(),
2842 LanguageConfig {
2843 extensions: vec!["json".to_string(), "jsonc".to_string()],
2844 filenames: vec![],
2845 grammar: "json".to_string(),
2846 comment_prefix: None,
2847 auto_indent: true,
2848 auto_close: None,
2849 auto_surround: None,
2850 highlighter: HighlighterPreference::Auto,
2851 textmate_grammar: None,
2852 show_whitespace_tabs: true,
2853 use_tabs: false,
2854 tab_size: None,
2855 formatter: Some(FormatterConfig {
2856 command: "prettier".to_string(),
2857 args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2858 stdin: true,
2859 timeout_ms: 10000,
2860 }),
2861 format_on_save: false,
2862 on_save: vec![],
2863 },
2864 );
2865
2866 languages.insert(
2867 "toml".to_string(),
2868 LanguageConfig {
2869 extensions: vec!["toml".to_string()],
2870 filenames: vec!["Cargo.lock".to_string()],
2871 grammar: "toml".to_string(),
2872 comment_prefix: Some("#".to_string()),
2873 auto_indent: true,
2874 auto_close: None,
2875 auto_surround: None,
2876 highlighter: HighlighterPreference::Auto,
2877 textmate_grammar: None,
2878 show_whitespace_tabs: true,
2879 use_tabs: false,
2880 tab_size: None,
2881 formatter: None,
2882 format_on_save: false,
2883 on_save: vec![],
2884 },
2885 );
2886
2887 languages.insert(
2888 "yaml".to_string(),
2889 LanguageConfig {
2890 extensions: vec!["yml".to_string(), "yaml".to_string()],
2891 filenames: vec![],
2892 grammar: "yaml".to_string(),
2893 comment_prefix: Some("#".to_string()),
2894 auto_indent: true,
2895 auto_close: None,
2896 auto_surround: None,
2897 highlighter: HighlighterPreference::Auto,
2898 textmate_grammar: None,
2899 show_whitespace_tabs: true,
2900 use_tabs: false,
2901 tab_size: None,
2902 formatter: Some(FormatterConfig {
2903 command: "prettier".to_string(),
2904 args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2905 stdin: true,
2906 timeout_ms: 10000,
2907 }),
2908 format_on_save: false,
2909 on_save: vec![],
2910 },
2911 );
2912
2913 languages.insert(
2914 "markdown".to_string(),
2915 LanguageConfig {
2916 extensions: vec!["md".to_string(), "markdown".to_string()],
2917 filenames: vec!["README".to_string()],
2918 grammar: "markdown".to_string(),
2919 comment_prefix: None,
2920 auto_indent: false,
2921 auto_close: None,
2922 auto_surround: None,
2923 highlighter: HighlighterPreference::Auto,
2924 textmate_grammar: None,
2925 show_whitespace_tabs: true,
2926 use_tabs: false,
2927 tab_size: None,
2928 formatter: None,
2929 format_on_save: false,
2930 on_save: vec![],
2931 },
2932 );
2933
2934 languages.insert(
2936 "go".to_string(),
2937 LanguageConfig {
2938 extensions: vec!["go".to_string()],
2939 filenames: vec![],
2940 grammar: "go".to_string(),
2941 comment_prefix: Some("//".to_string()),
2942 auto_indent: true,
2943 auto_close: None,
2944 auto_surround: None,
2945 highlighter: HighlighterPreference::Auto,
2946 textmate_grammar: None,
2947 show_whitespace_tabs: false,
2948 use_tabs: true, tab_size: Some(8), formatter: Some(FormatterConfig {
2951 command: "gofmt".to_string(),
2952 args: vec![],
2953 stdin: true,
2954 timeout_ms: 10000,
2955 }),
2956 format_on_save: false,
2957 on_save: vec![],
2958 },
2959 );
2960
2961 languages.insert(
2962 "odin".to_string(),
2963 LanguageConfig {
2964 extensions: vec!["odin".to_string()],
2965 filenames: vec![],
2966 grammar: "odin".to_string(),
2967 comment_prefix: Some("//".to_string()),
2968 auto_indent: true,
2969 auto_close: None,
2970 auto_surround: None,
2971 highlighter: HighlighterPreference::Auto,
2972 textmate_grammar: None,
2973 show_whitespace_tabs: false,
2974 use_tabs: true,
2975 tab_size: Some(8),
2976 formatter: None,
2977 format_on_save: false,
2978 on_save: vec![],
2979 },
2980 );
2981
2982 languages.insert(
2983 "zig".to_string(),
2984 LanguageConfig {
2985 extensions: vec!["zig".to_string(), "zon".to_string()],
2986 filenames: vec![],
2987 grammar: "zig".to_string(),
2988 comment_prefix: Some("//".to_string()),
2989 auto_indent: true,
2990 auto_close: None,
2991 auto_surround: None,
2992 highlighter: HighlighterPreference::Auto,
2993 textmate_grammar: None,
2994 show_whitespace_tabs: true,
2995 use_tabs: false,
2996 tab_size: None,
2997 formatter: None,
2998 format_on_save: false,
2999 on_save: vec![],
3000 },
3001 );
3002
3003 languages.insert(
3004 "java".to_string(),
3005 LanguageConfig {
3006 extensions: vec!["java".to_string()],
3007 filenames: vec![],
3008 grammar: "java".to_string(),
3009 comment_prefix: Some("//".to_string()),
3010 auto_indent: true,
3011 auto_close: None,
3012 auto_surround: None,
3013 highlighter: HighlighterPreference::Auto,
3014 textmate_grammar: None,
3015 show_whitespace_tabs: true,
3016 use_tabs: false,
3017 tab_size: None,
3018 formatter: None,
3019 format_on_save: false,
3020 on_save: vec![],
3021 },
3022 );
3023
3024 languages.insert(
3025 "latex".to_string(),
3026 LanguageConfig {
3027 extensions: vec![
3028 "tex".to_string(),
3029 "latex".to_string(),
3030 "ltx".to_string(),
3031 "sty".to_string(),
3032 "cls".to_string(),
3033 "bib".to_string(),
3034 ],
3035 filenames: vec![],
3036 grammar: "latex".to_string(),
3037 comment_prefix: Some("%".to_string()),
3038 auto_indent: true,
3039 auto_close: None,
3040 auto_surround: None,
3041 highlighter: HighlighterPreference::Auto,
3042 textmate_grammar: None,
3043 show_whitespace_tabs: true,
3044 use_tabs: false,
3045 tab_size: None,
3046 formatter: None,
3047 format_on_save: false,
3048 on_save: vec![],
3049 },
3050 );
3051
3052 languages.insert(
3053 "templ".to_string(),
3054 LanguageConfig {
3055 extensions: vec!["templ".to_string()],
3056 filenames: vec![],
3057 grammar: "go".to_string(), comment_prefix: Some("//".to_string()),
3059 auto_indent: true,
3060 auto_close: None,
3061 auto_surround: None,
3062 highlighter: HighlighterPreference::Auto,
3063 textmate_grammar: None,
3064 show_whitespace_tabs: true,
3065 use_tabs: false,
3066 tab_size: None,
3067 formatter: None,
3068 format_on_save: false,
3069 on_save: vec![],
3070 },
3071 );
3072
3073 languages.insert(
3075 "git-rebase".to_string(),
3076 LanguageConfig {
3077 extensions: vec![],
3078 filenames: vec!["git-rebase-todo".to_string()],
3079 grammar: "Git Rebase Todo".to_string(),
3080 comment_prefix: Some("#".to_string()),
3081 auto_indent: false,
3082 auto_close: None,
3083 auto_surround: None,
3084 highlighter: HighlighterPreference::Auto,
3085 textmate_grammar: None,
3086 show_whitespace_tabs: true,
3087 use_tabs: false,
3088 tab_size: None,
3089 formatter: None,
3090 format_on_save: false,
3091 on_save: vec![],
3092 },
3093 );
3094
3095 languages.insert(
3096 "git-commit".to_string(),
3097 LanguageConfig {
3098 extensions: vec![],
3099 filenames: vec![
3100 "COMMIT_EDITMSG".to_string(),
3101 "MERGE_MSG".to_string(),
3102 "SQUASH_MSG".to_string(),
3103 "TAG_EDITMSG".to_string(),
3104 ],
3105 grammar: "Git Commit Message".to_string(),
3106 comment_prefix: Some("#".to_string()),
3107 auto_indent: false,
3108 auto_close: None,
3109 auto_surround: None,
3110 highlighter: HighlighterPreference::Auto,
3111 textmate_grammar: None,
3112 show_whitespace_tabs: true,
3113 use_tabs: false,
3114 tab_size: None,
3115 formatter: None,
3116 format_on_save: false,
3117 on_save: vec![],
3118 },
3119 );
3120
3121 languages.insert(
3122 "gitignore".to_string(),
3123 LanguageConfig {
3124 extensions: vec!["gitignore".to_string()],
3125 filenames: vec![
3126 ".gitignore".to_string(),
3127 ".dockerignore".to_string(),
3128 ".npmignore".to_string(),
3129 ".hgignore".to_string(),
3130 ],
3131 grammar: "Gitignore".to_string(),
3132 comment_prefix: Some("#".to_string()),
3133 auto_indent: false,
3134 auto_close: None,
3135 auto_surround: None,
3136 highlighter: HighlighterPreference::Auto,
3137 textmate_grammar: None,
3138 show_whitespace_tabs: true,
3139 use_tabs: false,
3140 tab_size: None,
3141 formatter: None,
3142 format_on_save: false,
3143 on_save: vec![],
3144 },
3145 );
3146
3147 languages.insert(
3148 "gitconfig".to_string(),
3149 LanguageConfig {
3150 extensions: vec!["gitconfig".to_string()],
3151 filenames: vec![".gitconfig".to_string(), ".gitmodules".to_string()],
3152 grammar: "Git Config".to_string(),
3153 comment_prefix: Some("#".to_string()),
3154 auto_indent: true,
3155 auto_close: None,
3156 auto_surround: None,
3157 highlighter: HighlighterPreference::Auto,
3158 textmate_grammar: None,
3159 show_whitespace_tabs: true,
3160 use_tabs: false,
3161 tab_size: None,
3162 formatter: None,
3163 format_on_save: false,
3164 on_save: vec![],
3165 },
3166 );
3167
3168 languages.insert(
3169 "gitattributes".to_string(),
3170 LanguageConfig {
3171 extensions: vec!["gitattributes".to_string()],
3172 filenames: vec![".gitattributes".to_string()],
3173 grammar: "Git Attributes".to_string(),
3174 comment_prefix: Some("#".to_string()),
3175 auto_indent: false,
3176 auto_close: None,
3177 auto_surround: None,
3178 highlighter: HighlighterPreference::Auto,
3179 textmate_grammar: None,
3180 show_whitespace_tabs: true,
3181 use_tabs: false,
3182 tab_size: None,
3183 formatter: None,
3184 format_on_save: false,
3185 on_save: vec![],
3186 },
3187 );
3188
3189 languages.insert(
3190 "typst".to_string(),
3191 LanguageConfig {
3192 extensions: vec!["typ".to_string()],
3193 filenames: vec![],
3194 grammar: "Typst".to_string(),
3195 comment_prefix: Some("//".to_string()),
3196 auto_indent: true,
3197 auto_close: None,
3198 auto_surround: None,
3199 highlighter: HighlighterPreference::Auto,
3200 textmate_grammar: None,
3201 show_whitespace_tabs: true,
3202 use_tabs: false,
3203 tab_size: None,
3204 formatter: None,
3205 format_on_save: false,
3206 on_save: vec![],
3207 },
3208 );
3209
3210 languages
3211 }
3212
3213 #[cfg(feature = "runtime")]
3215 fn default_lsp_config() -> HashMap<String, LspServerConfig> {
3216 let mut lsp = HashMap::new();
3217
3218 let ra_log_path = crate::services::log_dirs::lsp_log_path("rust-analyzer")
3221 .to_string_lossy()
3222 .to_string();
3223
3224 Self::populate_lsp_config(&mut lsp, ra_log_path);
3225 lsp
3226 }
3227
3228 #[cfg(not(feature = "runtime"))]
3230 fn default_lsp_config() -> HashMap<String, LspServerConfig> {
3231 HashMap::new()
3233 }
3234
3235 #[cfg(feature = "runtime")]
3236 fn populate_lsp_config(lsp: &mut HashMap<String, LspServerConfig>, ra_log_path: String) {
3237 lsp.insert(
3241 "rust".to_string(),
3242 LspServerConfig {
3243 command: "rust-analyzer".to_string(),
3244 args: vec!["--log-file".to_string(), ra_log_path],
3245 enabled: true,
3246 auto_start: false,
3247 process_limits: ProcessLimits::unlimited(),
3248 initialization_options: None,
3249 env: Default::default(),
3250 language_id_overrides: Default::default(),
3251 },
3252 );
3253
3254 lsp.insert(
3256 "python".to_string(),
3257 LspServerConfig {
3258 command: "pylsp".to_string(),
3259 args: vec![],
3260 enabled: true,
3261 auto_start: false,
3262 process_limits: ProcessLimits::default(),
3263 initialization_options: None,
3264 env: Default::default(),
3265 language_id_overrides: Default::default(),
3266 },
3267 );
3268
3269 lsp.insert(
3272 "javascript".to_string(),
3273 LspServerConfig {
3274 command: "typescript-language-server".to_string(),
3275 args: vec!["--stdio".to_string()],
3276 enabled: true,
3277 auto_start: false,
3278 process_limits: ProcessLimits::default(),
3279 initialization_options: None,
3280 env: Default::default(),
3281 language_id_overrides: HashMap::from([(
3282 "jsx".to_string(),
3283 "javascriptreact".to_string(),
3284 )]),
3285 },
3286 );
3287 lsp.insert(
3288 "typescript".to_string(),
3289 LspServerConfig {
3290 command: "typescript-language-server".to_string(),
3291 args: vec!["--stdio".to_string()],
3292 enabled: true,
3293 auto_start: false,
3294 process_limits: ProcessLimits::default(),
3295 initialization_options: None,
3296 env: Default::default(),
3297 language_id_overrides: HashMap::from([(
3298 "tsx".to_string(),
3299 "typescriptreact".to_string(),
3300 )]),
3301 },
3302 );
3303
3304 lsp.insert(
3306 "html".to_string(),
3307 LspServerConfig {
3308 command: "vscode-html-language-server".to_string(),
3309 args: vec!["--stdio".to_string()],
3310 enabled: true,
3311 auto_start: false,
3312 process_limits: ProcessLimits::default(),
3313 initialization_options: None,
3314 env: Default::default(),
3315 language_id_overrides: Default::default(),
3316 },
3317 );
3318
3319 lsp.insert(
3321 "css".to_string(),
3322 LspServerConfig {
3323 command: "vscode-css-language-server".to_string(),
3324 args: vec!["--stdio".to_string()],
3325 enabled: true,
3326 auto_start: false,
3327 process_limits: ProcessLimits::default(),
3328 initialization_options: None,
3329 env: Default::default(),
3330 language_id_overrides: Default::default(),
3331 },
3332 );
3333
3334 lsp.insert(
3336 "c".to_string(),
3337 LspServerConfig {
3338 command: "clangd".to_string(),
3339 args: vec![],
3340 enabled: true,
3341 auto_start: false,
3342 process_limits: ProcessLimits::default(),
3343 initialization_options: None,
3344 env: Default::default(),
3345 language_id_overrides: Default::default(),
3346 },
3347 );
3348 lsp.insert(
3349 "cpp".to_string(),
3350 LspServerConfig {
3351 command: "clangd".to_string(),
3352 args: vec![],
3353 enabled: true,
3354 auto_start: false,
3355 process_limits: ProcessLimits::default(),
3356 initialization_options: None,
3357 env: Default::default(),
3358 language_id_overrides: Default::default(),
3359 },
3360 );
3361
3362 lsp.insert(
3364 "go".to_string(),
3365 LspServerConfig {
3366 command: "gopls".to_string(),
3367 args: vec![],
3368 enabled: true,
3369 auto_start: false,
3370 process_limits: ProcessLimits::default(),
3371 initialization_options: None,
3372 env: Default::default(),
3373 language_id_overrides: Default::default(),
3374 },
3375 );
3376
3377 lsp.insert(
3379 "json".to_string(),
3380 LspServerConfig {
3381 command: "vscode-json-language-server".to_string(),
3382 args: vec!["--stdio".to_string()],
3383 enabled: true,
3384 auto_start: false,
3385 process_limits: ProcessLimits::default(),
3386 initialization_options: None,
3387 env: Default::default(),
3388 language_id_overrides: Default::default(),
3389 },
3390 );
3391
3392 lsp.insert(
3394 "csharp".to_string(),
3395 LspServerConfig {
3396 command: "csharp-ls".to_string(),
3397 args: vec![],
3398 enabled: true,
3399 auto_start: false,
3400 process_limits: ProcessLimits::default(),
3401 initialization_options: None,
3402 env: Default::default(),
3403 language_id_overrides: Default::default(),
3404 },
3405 );
3406
3407 lsp.insert(
3410 "odin".to_string(),
3411 LspServerConfig {
3412 command: "ols".to_string(),
3413 args: vec![],
3414 enabled: true,
3415 auto_start: false,
3416 process_limits: ProcessLimits::default(),
3417 initialization_options: None,
3418 env: Default::default(),
3419 language_id_overrides: Default::default(),
3420 },
3421 );
3422
3423 lsp.insert(
3426 "zig".to_string(),
3427 LspServerConfig {
3428 command: "zls".to_string(),
3429 args: vec![],
3430 enabled: true,
3431 auto_start: false,
3432 process_limits: ProcessLimits::default(),
3433 initialization_options: None,
3434 env: Default::default(),
3435 language_id_overrides: Default::default(),
3436 },
3437 );
3438
3439 lsp.insert(
3442 "java".to_string(),
3443 LspServerConfig {
3444 command: "jdtls".to_string(),
3445 args: vec![],
3446 enabled: true,
3447 auto_start: false,
3448 process_limits: ProcessLimits::default(),
3449 initialization_options: None,
3450 env: Default::default(),
3451 language_id_overrides: Default::default(),
3452 },
3453 );
3454
3455 lsp.insert(
3458 "latex".to_string(),
3459 LspServerConfig {
3460 command: "texlab".to_string(),
3461 args: vec![],
3462 enabled: true,
3463 auto_start: false,
3464 process_limits: ProcessLimits::default(),
3465 initialization_options: None,
3466 env: Default::default(),
3467 language_id_overrides: Default::default(),
3468 },
3469 );
3470
3471 lsp.insert(
3474 "markdown".to_string(),
3475 LspServerConfig {
3476 command: "marksman".to_string(),
3477 args: vec!["server".to_string()],
3478 enabled: true,
3479 auto_start: false,
3480 process_limits: ProcessLimits::default(),
3481 initialization_options: None,
3482 env: Default::default(),
3483 language_id_overrides: Default::default(),
3484 },
3485 );
3486
3487 lsp.insert(
3490 "templ".to_string(),
3491 LspServerConfig {
3492 command: "templ".to_string(),
3493 args: vec!["lsp".to_string()],
3494 enabled: true,
3495 auto_start: false,
3496 process_limits: ProcessLimits::default(),
3497 initialization_options: None,
3498 env: Default::default(),
3499 language_id_overrides: Default::default(),
3500 },
3501 );
3502
3503 lsp.insert(
3506 "typst".to_string(),
3507 LspServerConfig {
3508 command: "tinymist".to_string(),
3509 args: vec![],
3510 enabled: true,
3511 auto_start: false,
3512 process_limits: ProcessLimits::default(),
3513 initialization_options: None,
3514 env: Default::default(),
3515 language_id_overrides: Default::default(),
3516 },
3517 );
3518
3519 lsp.insert(
3521 "bash".to_string(),
3522 LspServerConfig {
3523 command: "bash-language-server".to_string(),
3524 args: vec!["start".to_string()],
3525 enabled: true,
3526 auto_start: false,
3527 process_limits: ProcessLimits::default(),
3528 initialization_options: None,
3529 env: Default::default(),
3530 language_id_overrides: Default::default(),
3531 },
3532 );
3533
3534 lsp.insert(
3537 "lua".to_string(),
3538 LspServerConfig {
3539 command: "lua-language-server".to_string(),
3540 args: vec![],
3541 enabled: true,
3542 auto_start: false,
3543 process_limits: ProcessLimits::default(),
3544 initialization_options: None,
3545 env: Default::default(),
3546 language_id_overrides: Default::default(),
3547 },
3548 );
3549
3550 lsp.insert(
3552 "ruby".to_string(),
3553 LspServerConfig {
3554 command: "solargraph".to_string(),
3555 args: vec!["stdio".to_string()],
3556 enabled: true,
3557 auto_start: false,
3558 process_limits: ProcessLimits::default(),
3559 initialization_options: None,
3560 env: Default::default(),
3561 language_id_overrides: Default::default(),
3562 },
3563 );
3564
3565 lsp.insert(
3568 "php".to_string(),
3569 LspServerConfig {
3570 command: "phpactor".to_string(),
3571 args: vec!["language-server".to_string()],
3572 enabled: true,
3573 auto_start: false,
3574 process_limits: ProcessLimits::default(),
3575 initialization_options: None,
3576 env: Default::default(),
3577 language_id_overrides: Default::default(),
3578 },
3579 );
3580
3581 lsp.insert(
3583 "yaml".to_string(),
3584 LspServerConfig {
3585 command: "yaml-language-server".to_string(),
3586 args: vec!["--stdio".to_string()],
3587 enabled: true,
3588 auto_start: false,
3589 process_limits: ProcessLimits::default(),
3590 initialization_options: None,
3591 env: Default::default(),
3592 language_id_overrides: Default::default(),
3593 },
3594 );
3595
3596 lsp.insert(
3599 "toml".to_string(),
3600 LspServerConfig {
3601 command: "taplo".to_string(),
3602 args: vec!["lsp".to_string(), "stdio".to_string()],
3603 enabled: true,
3604 auto_start: false,
3605 process_limits: ProcessLimits::default(),
3606 initialization_options: None,
3607 env: Default::default(),
3608 language_id_overrides: Default::default(),
3609 },
3610 );
3611 }
3612
3613 pub fn validate(&self) -> Result<(), ConfigError> {
3615 if self.editor.tab_size == 0 {
3617 return Err(ConfigError::ValidationError(
3618 "tab_size must be greater than 0".to_string(),
3619 ));
3620 }
3621
3622 if self.editor.scroll_offset > 100 {
3624 return Err(ConfigError::ValidationError(
3625 "scroll_offset must be <= 100".to_string(),
3626 ));
3627 }
3628
3629 for binding in &self.keybindings {
3631 if binding.key.is_empty() {
3632 return Err(ConfigError::ValidationError(
3633 "keybinding key cannot be empty".to_string(),
3634 ));
3635 }
3636 if binding.action.is_empty() {
3637 return Err(ConfigError::ValidationError(
3638 "keybinding action cannot be empty".to_string(),
3639 ));
3640 }
3641 }
3642
3643 Ok(())
3644 }
3645}
3646
3647#[derive(Debug)]
3649pub enum ConfigError {
3650 IoError(String),
3651 ParseError(String),
3652 SerializeError(String),
3653 ValidationError(String),
3654}
3655
3656impl std::fmt::Display for ConfigError {
3657 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3658 match self {
3659 Self::IoError(msg) => write!(f, "IO error: {msg}"),
3660 Self::ParseError(msg) => write!(f, "Parse error: {msg}"),
3661 Self::SerializeError(msg) => write!(f, "Serialize error: {msg}"),
3662 Self::ValidationError(msg) => write!(f, "Validation error: {msg}"),
3663 }
3664 }
3665}
3666
3667impl std::error::Error for ConfigError {}
3668
3669#[cfg(test)]
3670mod tests {
3671 use super::*;
3672
3673 #[test]
3674 fn test_default_config() {
3675 let config = Config::default();
3676 assert_eq!(config.editor.tab_size, 4);
3677 assert!(config.editor.line_numbers);
3678 assert!(config.editor.syntax_highlighting);
3679 assert!(config.keybindings.is_empty());
3682 let resolved = config.resolve_keymap(&config.active_keybinding_map);
3684 assert!(!resolved.is_empty());
3685 }
3686
3687 #[test]
3688 fn test_all_builtin_keymaps_loadable() {
3689 for name in KeybindingMapName::BUILTIN_OPTIONS {
3690 let keymap = Config::load_builtin_keymap(name);
3691 assert!(keymap.is_some(), "Failed to load builtin keymap '{}'", name);
3692 }
3693 }
3694
3695 #[test]
3696 fn test_config_validation() {
3697 let mut config = Config::default();
3698 assert!(config.validate().is_ok());
3699
3700 config.editor.tab_size = 0;
3701 assert!(config.validate().is_err());
3702 }
3703
3704 #[test]
3705 fn test_macos_keymap_inherits_enter_bindings() {
3706 let config = Config::default();
3707 let bindings = config.resolve_keymap("macos");
3708
3709 let enter_bindings: Vec<_> = bindings.iter().filter(|b| b.key == "Enter").collect();
3710 assert!(
3711 !enter_bindings.is_empty(),
3712 "macos keymap should inherit Enter bindings from default, got {} Enter bindings",
3713 enter_bindings.len()
3714 );
3715 let has_insert_newline = enter_bindings.iter().any(|b| b.action == "insert_newline");
3717 assert!(
3718 has_insert_newline,
3719 "macos keymap should have insert_newline action for Enter key"
3720 );
3721 }
3722
3723 #[test]
3724 fn test_config_serialize_deserialize() {
3725 let config = Config::default();
3727
3728 let json = serde_json::to_string_pretty(&config).unwrap();
3730
3731 let loaded: Config = serde_json::from_str(&json).unwrap();
3733
3734 assert_eq!(config.editor.tab_size, loaded.editor.tab_size);
3735 assert_eq!(config.theme, loaded.theme);
3736 }
3737
3738 #[test]
3739 fn test_config_with_custom_keybinding() {
3740 let json = r#"{
3741 "editor": {
3742 "tab_size": 2
3743 },
3744 "keybindings": [
3745 {
3746 "key": "x",
3747 "modifiers": ["ctrl", "shift"],
3748 "action": "custom_action",
3749 "args": {},
3750 "when": null
3751 }
3752 ]
3753 }"#;
3754
3755 let config: Config = serde_json::from_str(json).unwrap();
3756 assert_eq!(config.editor.tab_size, 2);
3757 assert_eq!(config.keybindings.len(), 1);
3758 assert_eq!(config.keybindings[0].key, "x");
3759 assert_eq!(config.keybindings[0].modifiers.len(), 2);
3760 }
3761
3762 #[test]
3763 fn test_sparse_config_merges_with_defaults() {
3764 let temp_dir = tempfile::tempdir().unwrap();
3766 let config_path = temp_dir.path().join("config.json");
3767
3768 let sparse_config = r#"{
3770 "lsp": {
3771 "rust": {
3772 "command": "custom-rust-analyzer",
3773 "args": ["--custom-arg"]
3774 }
3775 }
3776 }"#;
3777 std::fs::write(&config_path, sparse_config).unwrap();
3778
3779 let loaded = Config::load_from_file(&config_path).unwrap();
3781
3782 assert!(loaded.lsp.contains_key("rust"));
3784 assert_eq!(
3785 loaded.lsp["rust"].command,
3786 "custom-rust-analyzer".to_string()
3787 );
3788
3789 assert!(
3791 loaded.lsp.contains_key("python"),
3792 "python LSP should be merged from defaults"
3793 );
3794 assert!(
3795 loaded.lsp.contains_key("typescript"),
3796 "typescript LSP should be merged from defaults"
3797 );
3798 assert!(
3799 loaded.lsp.contains_key("javascript"),
3800 "javascript LSP should be merged from defaults"
3801 );
3802
3803 assert!(loaded.languages.contains_key("rust"));
3805 assert!(loaded.languages.contains_key("python"));
3806 assert!(loaded.languages.contains_key("typescript"));
3807 }
3808
3809 #[test]
3810 fn test_empty_config_gets_all_defaults() {
3811 let temp_dir = tempfile::tempdir().unwrap();
3812 let config_path = temp_dir.path().join("config.json");
3813
3814 std::fs::write(&config_path, "{}").unwrap();
3816
3817 let loaded = Config::load_from_file(&config_path).unwrap();
3818 let defaults = Config::default();
3819
3820 assert_eq!(loaded.lsp.len(), defaults.lsp.len());
3822
3823 assert_eq!(loaded.languages.len(), defaults.languages.len());
3825 }
3826
3827 #[test]
3828 fn test_dynamic_submenu_expansion() {
3829 let temp_dir = tempfile::tempdir().unwrap();
3831 let themes_dir = temp_dir.path().to_path_buf();
3832
3833 let dynamic = MenuItem::DynamicSubmenu {
3834 label: "Test".to_string(),
3835 source: "copy_with_theme".to_string(),
3836 };
3837
3838 let expanded = dynamic.expand_dynamic(&themes_dir);
3839
3840 match expanded {
3842 MenuItem::Submenu { label, items } => {
3843 assert_eq!(label, "Test");
3844 let loader = crate::view::theme::ThemeLoader::embedded_only();
3846 let registry = loader.load_all();
3847 assert_eq!(items.len(), registry.len());
3848
3849 for (item, theme_info) in items.iter().zip(registry.list().iter()) {
3851 match item {
3852 MenuItem::Action {
3853 label,
3854 action,
3855 args,
3856 ..
3857 } => {
3858 assert_eq!(label, &theme_info.name);
3859 assert_eq!(action, "copy_with_theme");
3860 assert_eq!(
3861 args.get("theme").and_then(|v| v.as_str()),
3862 Some(theme_info.name.as_str())
3863 );
3864 }
3865 _ => panic!("Expected Action item"),
3866 }
3867 }
3868 }
3869 _ => panic!("Expected Submenu after expansion"),
3870 }
3871 }
3872
3873 #[test]
3874 fn test_non_dynamic_item_unchanged() {
3875 let temp_dir = tempfile::tempdir().unwrap();
3877 let themes_dir = temp_dir.path();
3878
3879 let action = MenuItem::Action {
3880 label: "Test".to_string(),
3881 action: "test".to_string(),
3882 args: HashMap::new(),
3883 when: None,
3884 checkbox: None,
3885 };
3886
3887 let expanded = action.expand_dynamic(themes_dir);
3888 match expanded {
3889 MenuItem::Action { label, action, .. } => {
3890 assert_eq!(label, "Test");
3891 assert_eq!(action, "test");
3892 }
3893 _ => panic!("Action should remain Action after expand_dynamic"),
3894 }
3895 }
3896
3897 #[test]
3898 fn test_buffer_config_uses_global_defaults() {
3899 let config = Config::default();
3900 let buffer_config = BufferConfig::resolve(&config, None);
3901
3902 assert_eq!(buffer_config.tab_size, config.editor.tab_size);
3903 assert_eq!(buffer_config.auto_indent, config.editor.auto_indent);
3904 assert!(!buffer_config.use_tabs); assert!(buffer_config.whitespace.any_tabs()); assert!(buffer_config.formatter.is_none());
3907 assert!(!buffer_config.format_on_save);
3908 }
3909
3910 #[test]
3911 fn test_buffer_config_applies_language_overrides() {
3912 let mut config = Config::default();
3913
3914 config.languages.insert(
3916 "go".to_string(),
3917 LanguageConfig {
3918 extensions: vec!["go".to_string()],
3919 filenames: vec![],
3920 grammar: "go".to_string(),
3921 comment_prefix: Some("//".to_string()),
3922 auto_indent: true,
3923 auto_close: None,
3924 auto_surround: None,
3925 highlighter: HighlighterPreference::Auto,
3926 textmate_grammar: None,
3927 show_whitespace_tabs: false, use_tabs: true, tab_size: Some(8), formatter: Some(FormatterConfig {
3931 command: "gofmt".to_string(),
3932 args: vec![],
3933 stdin: true,
3934 timeout_ms: 10000,
3935 }),
3936 format_on_save: true,
3937 on_save: vec![],
3938 },
3939 );
3940
3941 let buffer_config = BufferConfig::resolve(&config, Some("go"));
3942
3943 assert_eq!(buffer_config.tab_size, 8);
3944 assert!(buffer_config.use_tabs);
3945 assert!(!buffer_config.whitespace.any_tabs()); assert!(buffer_config.format_on_save);
3947 assert!(buffer_config.formatter.is_some());
3948 assert_eq!(buffer_config.formatter.as_ref().unwrap().command, "gofmt");
3949 }
3950
3951 #[test]
3952 fn test_buffer_config_unknown_language_uses_global() {
3953 let config = Config::default();
3954 let buffer_config = BufferConfig::resolve(&config, Some("unknown_lang"));
3955
3956 assert_eq!(buffer_config.tab_size, config.editor.tab_size);
3958 assert!(!buffer_config.use_tabs);
3959 }
3960
3961 #[test]
3962 fn test_buffer_config_indent_string() {
3963 let config = Config::default();
3964
3965 let spaces_config = BufferConfig::resolve(&config, None);
3967 assert_eq!(spaces_config.indent_string(), " "); let mut config_with_tabs = Config::default();
3971 config_with_tabs.languages.insert(
3972 "makefile".to_string(),
3973 LanguageConfig {
3974 use_tabs: true,
3975 tab_size: Some(8),
3976 ..Default::default()
3977 },
3978 );
3979 let tabs_config = BufferConfig::resolve(&config_with_tabs, Some("makefile"));
3980 assert_eq!(tabs_config.indent_string(), "\t");
3981 }
3982}