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 #[cfg(feature = "runtime")]
159 pub fn to_crossterm_style(self) -> crossterm::cursor::SetCursorStyle {
160 use crossterm::cursor::SetCursorStyle;
161 match self {
162 Self::Default => SetCursorStyle::DefaultUserShape,
163 Self::BlinkingBlock => SetCursorStyle::BlinkingBlock,
164 Self::SteadyBlock => SetCursorStyle::SteadyBlock,
165 Self::BlinkingBar => SetCursorStyle::BlinkingBar,
166 Self::SteadyBar => SetCursorStyle::SteadyBar,
167 Self::BlinkingUnderline => SetCursorStyle::BlinkingUnderScore,
168 Self::SteadyUnderline => SetCursorStyle::SteadyUnderScore,
169 }
170 }
171
172 pub fn parse(s: &str) -> Option<Self> {
174 match s {
175 "default" => Some(CursorStyle::Default),
176 "blinking_block" => Some(CursorStyle::BlinkingBlock),
177 "steady_block" => Some(CursorStyle::SteadyBlock),
178 "blinking_bar" => Some(CursorStyle::BlinkingBar),
179 "steady_bar" => Some(CursorStyle::SteadyBar),
180 "blinking_underline" => Some(CursorStyle::BlinkingUnderline),
181 "steady_underline" => Some(CursorStyle::SteadyUnderline),
182 _ => None,
183 }
184 }
185
186 pub fn as_str(self) -> &'static str {
188 match self {
189 Self::Default => "default",
190 Self::BlinkingBlock => "blinking_block",
191 Self::SteadyBlock => "steady_block",
192 Self::BlinkingBar => "blinking_bar",
193 Self::SteadyBar => "steady_bar",
194 Self::BlinkingUnderline => "blinking_underline",
195 Self::SteadyUnderline => "steady_underline",
196 }
197 }
198}
199
200impl JsonSchema for CursorStyle {
201 fn schema_name() -> Cow<'static, str> {
202 Cow::Borrowed("CursorStyle")
203 }
204
205 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
206 schemars::json_schema!({
207 "description": "Terminal cursor style",
208 "type": "string",
209 "enum": Self::OPTIONS
210 })
211 }
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(transparent)]
217pub struct KeybindingMapName(pub String);
218
219impl KeybindingMapName {
220 pub const BUILTIN_OPTIONS: &'static [&'static str] = &["default", "emacs", "vscode", "macos"];
222}
223
224impl Deref for KeybindingMapName {
225 type Target = str;
226 fn deref(&self) -> &Self::Target {
227 &self.0
228 }
229}
230
231impl From<String> for KeybindingMapName {
232 fn from(s: String) -> Self {
233 Self(s)
234 }
235}
236
237impl From<&str> for KeybindingMapName {
238 fn from(s: &str) -> Self {
239 Self(s.to_string())
240 }
241}
242
243impl PartialEq<str> for KeybindingMapName {
244 fn eq(&self, other: &str) -> bool {
245 self.0 == other
246 }
247}
248
249#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
251#[serde(rename_all = "lowercase")]
252pub enum LineEndingOption {
253 #[default]
255 Lf,
256 Crlf,
258 Cr,
260}
261
262impl LineEndingOption {
263 pub fn to_line_ending(&self) -> crate::model::buffer::LineEnding {
265 match self {
266 Self::Lf => crate::model::buffer::LineEnding::LF,
267 Self::Crlf => crate::model::buffer::LineEnding::CRLF,
268 Self::Cr => crate::model::buffer::LineEnding::CR,
269 }
270 }
271}
272
273impl JsonSchema for LineEndingOption {
274 fn schema_name() -> Cow<'static, str> {
275 Cow::Borrowed("LineEndingOption")
276 }
277
278 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
279 schemars::json_schema!({
280 "description": "Default line ending format for new files",
281 "type": "string",
282 "enum": ["lf", "crlf", "cr"],
283 "default": "lf"
284 })
285 }
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
290#[serde(rename_all = "lowercase")]
291pub enum AcceptSuggestionOnEnter {
292 #[default]
294 On,
295 Off,
297 Smart,
299}
300
301impl JsonSchema for AcceptSuggestionOnEnter {
302 fn schema_name() -> Cow<'static, str> {
303 Cow::Borrowed("AcceptSuggestionOnEnter")
304 }
305
306 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
307 schemars::json_schema!({
308 "description": "Controls whether Enter accepts a completion suggestion",
309 "type": "string",
310 "enum": ["on", "off", "smart"],
311 "default": "on"
312 })
313 }
314}
315
316impl PartialEq<KeybindingMapName> for str {
317 fn eq(&self, other: &KeybindingMapName) -> bool {
318 self == other.0
319 }
320}
321
322impl JsonSchema for KeybindingMapName {
323 fn schema_name() -> Cow<'static, str> {
324 Cow::Borrowed("KeybindingMapOptions")
325 }
326
327 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
328 schemars::json_schema!({
329 "description": "Available keybinding maps",
330 "type": "string",
331 "enum": Self::BUILTIN_OPTIONS
332 })
333 }
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
338pub struct Config {
339 #[serde(default)]
342 pub version: u32,
343
344 #[serde(default = "default_theme_name")]
346 pub theme: ThemeName,
347
348 #[serde(default)]
351 pub locale: LocaleName,
352
353 #[serde(default = "default_true")]
356 pub check_for_updates: bool,
357
358 #[serde(default)]
360 pub editor: EditorConfig,
361
362 #[serde(default)]
364 pub file_explorer: FileExplorerConfig,
365
366 #[serde(default)]
368 pub file_browser: FileBrowserConfig,
369
370 #[serde(default)]
372 pub terminal: TerminalConfig,
373
374 #[serde(default)]
376 pub keybindings: Vec<Keybinding>,
377
378 #[serde(default)]
381 pub keybinding_maps: HashMap<String, KeymapConfig>,
382
383 #[serde(default = "default_keybinding_map_name")]
385 pub active_keybinding_map: KeybindingMapName,
386
387 #[serde(default)]
389 pub languages: HashMap<String, LanguageConfig>,
390
391 #[serde(default)]
393 pub lsp: HashMap<String, LspServerConfig>,
394
395 #[serde(default)]
397 pub warnings: WarningsConfig,
398
399 #[serde(default)]
403 #[schemars(extend("x-standalone-category" = true, "x-no-add" = true))]
404 pub plugins: HashMap<String, PluginConfig>,
405
406 #[serde(default)]
408 pub packages: PackagesConfig,
409}
410
411fn default_keybinding_map_name() -> KeybindingMapName {
412 if cfg!(target_os = "macos") {
415 KeybindingMapName("macos".to_string())
416 } else {
417 KeybindingMapName("default".to_string())
418 }
419}
420
421fn default_theme_name() -> ThemeName {
422 ThemeName("high-contrast".to_string())
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
427pub struct EditorConfig {
428 #[serde(default = "default_true")]
431 #[schemars(extend("x-section" = "Display"))]
432 pub line_numbers: bool,
433
434 #[serde(default = "default_false")]
436 #[schemars(extend("x-section" = "Display"))]
437 pub relative_line_numbers: bool,
438
439 #[serde(default = "default_true")]
441 #[schemars(extend("x-section" = "Display"))]
442 pub line_wrap: bool,
443
444 #[serde(default = "default_true")]
446 #[schemars(extend("x-section" = "Display"))]
447 pub syntax_highlighting: bool,
448
449 #[serde(default = "default_true")]
454 #[schemars(extend("x-section" = "Display"))]
455 pub show_menu_bar: bool,
456
457 #[serde(default = "default_true")]
462 #[schemars(extend("x-section" = "Display"))]
463 pub show_tab_bar: bool,
464
465 #[serde(default = "default_false")]
470 #[schemars(extend("x-section" = "Display"))]
471 pub use_terminal_bg: bool,
472
473 #[serde(default)]
477 #[schemars(extend("x-section" = "Display"))]
478 pub cursor_style: CursorStyle,
479
480 #[serde(default = "default_tab_size")]
483 #[schemars(extend("x-section" = "Editing"))]
484 pub tab_size: usize,
485
486 #[serde(default = "default_true")]
488 #[schemars(extend("x-section" = "Editing"))]
489 pub auto_indent: bool,
490
491 #[serde(default = "default_scroll_offset")]
493 #[schemars(extend("x-section" = "Editing"))]
494 pub scroll_offset: usize,
495
496 #[serde(default)]
501 #[schemars(extend("x-section" = "Editing"))]
502 pub default_line_ending: LineEndingOption,
503
504 #[serde(default = "default_false")]
507 #[schemars(extend("x-section" = "Editing"))]
508 pub trim_trailing_whitespace_on_save: bool,
509
510 #[serde(default = "default_false")]
513 #[schemars(extend("x-section" = "Editing"))]
514 pub ensure_final_newline_on_save: bool,
515
516 #[serde(default = "default_true")]
520 #[schemars(extend("x-section" = "Bracket Matching"))]
521 pub highlight_matching_brackets: bool,
522
523 #[serde(default = "default_true")]
527 #[schemars(extend("x-section" = "Bracket Matching"))]
528 pub rainbow_brackets: bool,
529
530 #[serde(default = "default_true")]
536 #[schemars(extend("x-section" = "Completion"))]
537 pub quick_suggestions: bool,
538
539 #[serde(default = "default_quick_suggestions_delay")]
545 #[schemars(extend("x-section" = "Completion"))]
546 pub quick_suggestions_delay_ms: u64,
547
548 #[serde(default = "default_true")]
552 #[schemars(extend("x-section" = "Completion"))]
553 pub suggest_on_trigger_characters: bool,
554
555 #[serde(default = "default_accept_suggestion_on_enter")]
561 #[schemars(extend("x-section" = "Completion"))]
562 pub accept_suggestion_on_enter: AcceptSuggestionOnEnter,
563
564 #[serde(default = "default_true")]
567 #[schemars(extend("x-section" = "LSP"))]
568 pub enable_inlay_hints: bool,
569
570 #[serde(default = "default_false")]
574 #[schemars(extend("x-section" = "LSP"))]
575 pub enable_semantic_tokens_full: bool,
576
577 #[serde(default = "default_true")]
582 #[schemars(extend("x-section" = "Mouse"))]
583 pub mouse_hover_enabled: bool,
584
585 #[serde(default = "default_mouse_hover_delay")]
589 #[schemars(extend("x-section" = "Mouse"))]
590 pub mouse_hover_delay_ms: u64,
591
592 #[serde(default = "default_double_click_time")]
596 #[schemars(extend("x-section" = "Mouse"))]
597 pub double_click_time_ms: u64,
598
599 #[serde(default = "default_true")]
604 #[schemars(extend("x-section" = "Recovery"))]
605 pub recovery_enabled: bool,
606
607 #[serde(default = "default_auto_save_interval")]
612 #[schemars(extend("x-section" = "Recovery"))]
613 pub auto_save_interval_secs: u32,
614
615 #[serde(default = "default_auto_revert_poll_interval")]
620 #[schemars(extend("x-section" = "Recovery"))]
621 pub auto_revert_poll_interval_ms: u64,
622
623 #[serde(default = "default_true")]
629 #[schemars(extend("x-section" = "Keyboard"))]
630 pub keyboard_disambiguate_escape_codes: bool,
631
632 #[serde(default = "default_false")]
637 #[schemars(extend("x-section" = "Keyboard"))]
638 pub keyboard_report_event_types: bool,
639
640 #[serde(default = "default_true")]
645 #[schemars(extend("x-section" = "Keyboard"))]
646 pub keyboard_report_alternate_keys: bool,
647
648 #[serde(default = "default_false")]
654 #[schemars(extend("x-section" = "Keyboard"))]
655 pub keyboard_report_all_keys_as_escape_codes: bool,
656
657 #[serde(default = "default_highlight_timeout")]
660 #[schemars(extend("x-section" = "Performance"))]
661 pub highlight_timeout_ms: u64,
662
663 #[serde(default = "default_snapshot_interval")]
665 #[schemars(extend("x-section" = "Performance"))]
666 pub snapshot_interval: usize,
667
668 #[serde(default = "default_highlight_context_bytes")]
673 #[schemars(extend("x-section" = "Performance"))]
674 pub highlight_context_bytes: usize,
675
676 #[serde(default = "default_large_file_threshold")]
683 #[schemars(extend("x-section" = "Performance"))]
684 pub large_file_threshold_bytes: u64,
685
686 #[serde(default = "default_estimated_line_length")]
690 #[schemars(extend("x-section" = "Performance"))]
691 pub estimated_line_length: usize,
692
693 #[serde(default = "default_file_tree_poll_interval")]
698 #[schemars(extend("x-section" = "Performance"))]
699 pub file_tree_poll_interval_ms: u64,
700}
701
702fn default_tab_size() -> usize {
703 4
704}
705
706pub const LARGE_FILE_THRESHOLD_BYTES: u64 = 1024 * 1024; fn default_large_file_threshold() -> u64 {
712 LARGE_FILE_THRESHOLD_BYTES
713}
714
715fn default_true() -> bool {
716 true
717}
718
719fn default_false() -> bool {
720 false
721}
722
723fn default_quick_suggestions_delay() -> u64 {
724 10 }
726
727fn default_accept_suggestion_on_enter() -> AcceptSuggestionOnEnter {
728 AcceptSuggestionOnEnter::On
729}
730
731fn default_scroll_offset() -> usize {
732 3
733}
734
735fn default_highlight_timeout() -> u64 {
736 5
737}
738
739fn default_snapshot_interval() -> usize {
740 100
741}
742
743fn default_estimated_line_length() -> usize {
744 80
745}
746
747fn default_auto_save_interval() -> u32 {
748 2 }
750
751fn default_highlight_context_bytes() -> usize {
752 10_000 }
754
755fn default_mouse_hover_delay() -> u64 {
756 500 }
758
759fn default_double_click_time() -> u64 {
760 500 }
762
763fn default_auto_revert_poll_interval() -> u64 {
764 2000 }
766
767fn default_file_tree_poll_interval() -> u64 {
768 3000 }
770
771impl Default for EditorConfig {
772 fn default() -> Self {
773 Self {
774 tab_size: default_tab_size(),
775 auto_indent: true,
776 line_numbers: true,
777 relative_line_numbers: false,
778 scroll_offset: default_scroll_offset(),
779 syntax_highlighting: true,
780 line_wrap: true,
781 highlight_timeout_ms: default_highlight_timeout(),
782 snapshot_interval: default_snapshot_interval(),
783 large_file_threshold_bytes: default_large_file_threshold(),
784 estimated_line_length: default_estimated_line_length(),
785 enable_inlay_hints: true,
786 enable_semantic_tokens_full: false,
787 recovery_enabled: true,
788 auto_save_interval_secs: default_auto_save_interval(),
789 highlight_context_bytes: default_highlight_context_bytes(),
790 mouse_hover_enabled: true,
791 mouse_hover_delay_ms: default_mouse_hover_delay(),
792 double_click_time_ms: default_double_click_time(),
793 auto_revert_poll_interval_ms: default_auto_revert_poll_interval(),
794 file_tree_poll_interval_ms: default_file_tree_poll_interval(),
795 default_line_ending: LineEndingOption::default(),
796 trim_trailing_whitespace_on_save: false,
797 ensure_final_newline_on_save: false,
798 highlight_matching_brackets: true,
799 rainbow_brackets: true,
800 cursor_style: CursorStyle::default(),
801 keyboard_disambiguate_escape_codes: true,
802 keyboard_report_event_types: false,
803 keyboard_report_alternate_keys: true,
804 keyboard_report_all_keys_as_escape_codes: false,
805 quick_suggestions: true,
806 quick_suggestions_delay_ms: default_quick_suggestions_delay(),
807 suggest_on_trigger_characters: true,
808 accept_suggestion_on_enter: default_accept_suggestion_on_enter(),
809 show_menu_bar: true,
810 show_tab_bar: true,
811 use_terminal_bg: false,
812 }
813 }
814}
815
816#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
818pub struct FileExplorerConfig {
819 #[serde(default = "default_true")]
821 pub respect_gitignore: bool,
822
823 #[serde(default = "default_false")]
825 pub show_hidden: bool,
826
827 #[serde(default = "default_false")]
829 pub show_gitignored: bool,
830
831 #[serde(default)]
833 pub custom_ignore_patterns: Vec<String>,
834
835 #[serde(default = "default_explorer_width")]
837 pub width: f32,
838}
839
840fn default_explorer_width() -> f32 {
841 0.3 }
843
844#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
846pub struct TerminalConfig {
847 #[serde(default = "default_true")]
850 pub jump_to_end_on_output: bool,
851}
852
853impl Default for TerminalConfig {
854 fn default() -> Self {
855 Self {
856 jump_to_end_on_output: true,
857 }
858 }
859}
860
861#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
863pub struct WarningsConfig {
864 #[serde(default = "default_true")]
867 pub show_status_indicator: bool,
868}
869
870impl Default for WarningsConfig {
871 fn default() -> Self {
872 Self {
873 show_status_indicator: true,
874 }
875 }
876}
877
878#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
880pub struct PackagesConfig {
881 #[serde(default = "default_package_sources")]
884 pub sources: Vec<String>,
885}
886
887fn default_package_sources() -> Vec<String> {
888 vec!["https://github.com/sinelaw/fresh-plugins-registry".to_string()]
889}
890
891impl Default for PackagesConfig {
892 fn default() -> Self {
893 Self {
894 sources: default_package_sources(),
895 }
896 }
897}
898
899pub use fresh_core::config::PluginConfig;
901
902impl Default for FileExplorerConfig {
903 fn default() -> Self {
904 Self {
905 respect_gitignore: true,
906 show_hidden: false,
907 show_gitignored: false,
908 custom_ignore_patterns: Vec::new(),
909 width: default_explorer_width(),
910 }
911 }
912}
913
914#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
916pub struct FileBrowserConfig {
917 #[serde(default = "default_false")]
919 pub show_hidden: bool,
920}
921
922#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
924pub struct KeyPress {
925 pub key: String,
927 #[serde(default)]
929 pub modifiers: Vec<String>,
930}
931
932#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
934#[schemars(extend("x-display-field" = "/action"))]
935pub struct Keybinding {
936 #[serde(default, skip_serializing_if = "String::is_empty")]
938 pub key: String,
939
940 #[serde(default, skip_serializing_if = "Vec::is_empty")]
942 pub modifiers: Vec<String>,
943
944 #[serde(default, skip_serializing_if = "Vec::is_empty")]
947 pub keys: Vec<KeyPress>,
948
949 pub action: String,
951
952 #[serde(default)]
954 pub args: HashMap<String, serde_json::Value>,
955
956 #[serde(default)]
958 pub when: Option<String>,
959}
960
961#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
963#[schemars(extend("x-display-field" = "/inherits"))]
964pub struct KeymapConfig {
965 #[serde(default, skip_serializing_if = "Option::is_none")]
967 pub inherits: Option<String>,
968
969 #[serde(default)]
971 pub bindings: Vec<Keybinding>,
972}
973
974#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
976#[schemars(extend("x-display-field" = "/command"))]
977pub struct FormatterConfig {
978 pub command: String,
980
981 #[serde(default)]
984 pub args: Vec<String>,
985
986 #[serde(default = "default_true")]
989 pub stdin: bool,
990
991 #[serde(default = "default_on_save_timeout")]
993 pub timeout_ms: u64,
994}
995
996#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
998#[schemars(extend("x-display-field" = "/command"))]
999pub struct OnSaveAction {
1000 pub command: String,
1003
1004 #[serde(default)]
1007 pub args: Vec<String>,
1008
1009 #[serde(default)]
1011 pub working_dir: Option<String>,
1012
1013 #[serde(default)]
1015 pub stdin: bool,
1016
1017 #[serde(default = "default_on_save_timeout")]
1019 pub timeout_ms: u64,
1020
1021 #[serde(default = "default_true")]
1024 pub enabled: bool,
1025}
1026
1027fn default_on_save_timeout() -> u64 {
1028 10000
1029}
1030
1031#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
1033#[schemars(extend("x-display-field" = "/grammar"))]
1034pub struct LanguageConfig {
1035 #[serde(default)]
1037 pub extensions: Vec<String>,
1038
1039 #[serde(default)]
1041 pub filenames: Vec<String>,
1042
1043 #[serde(default)]
1045 pub grammar: String,
1046
1047 #[serde(default)]
1049 pub comment_prefix: Option<String>,
1050
1051 #[serde(default = "default_true")]
1053 pub auto_indent: bool,
1054
1055 #[serde(default)]
1057 pub highlighter: HighlighterPreference,
1058
1059 #[serde(default)]
1062 pub textmate_grammar: Option<std::path::PathBuf>,
1063
1064 #[serde(default = "default_true")]
1067 pub show_whitespace_tabs: bool,
1068
1069 #[serde(default = "default_false")]
1073 pub use_tabs: bool,
1074
1075 #[serde(default)]
1078 pub tab_size: Option<usize>,
1079
1080 #[serde(default)]
1082 pub formatter: Option<FormatterConfig>,
1083
1084 #[serde(default)]
1086 pub format_on_save: bool,
1087
1088 #[serde(default)]
1092 pub on_save: Vec<OnSaveAction>,
1093}
1094
1095#[derive(Debug, Clone)]
1102pub struct BufferConfig {
1103 pub tab_size: usize,
1105
1106 pub use_tabs: bool,
1108
1109 pub auto_indent: bool,
1111
1112 pub show_whitespace_tabs: bool,
1114
1115 pub formatter: Option<FormatterConfig>,
1117
1118 pub format_on_save: bool,
1120
1121 pub on_save: Vec<OnSaveAction>,
1123
1124 pub highlighter: HighlighterPreference,
1126
1127 pub textmate_grammar: Option<std::path::PathBuf>,
1129}
1130
1131impl BufferConfig {
1132 pub fn resolve(global_config: &Config, language_id: Option<&str>) -> Self {
1141 let editor = &global_config.editor;
1142
1143 let mut config = BufferConfig {
1145 tab_size: editor.tab_size,
1146 use_tabs: false, auto_indent: editor.auto_indent,
1148 show_whitespace_tabs: true, formatter: None,
1150 format_on_save: false,
1151 on_save: Vec::new(),
1152 highlighter: HighlighterPreference::Auto,
1153 textmate_grammar: None,
1154 };
1155
1156 if let Some(lang_id) = language_id {
1158 if let Some(lang_config) = global_config.languages.get(lang_id) {
1159 if let Some(ts) = lang_config.tab_size {
1161 config.tab_size = ts;
1162 }
1163
1164 config.use_tabs = lang_config.use_tabs;
1166
1167 config.auto_indent = lang_config.auto_indent;
1169
1170 config.show_whitespace_tabs = lang_config.show_whitespace_tabs;
1172
1173 config.formatter = lang_config.formatter.clone();
1175
1176 config.format_on_save = lang_config.format_on_save;
1178
1179 config.on_save = lang_config.on_save.clone();
1181
1182 config.highlighter = lang_config.highlighter;
1184
1185 config.textmate_grammar = lang_config.textmate_grammar.clone();
1187 }
1188 }
1189
1190 config
1191 }
1192
1193 pub fn indent_string(&self) -> String {
1198 if self.use_tabs {
1199 "\t".to_string()
1200 } else {
1201 " ".repeat(self.tab_size)
1202 }
1203 }
1204}
1205
1206#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
1208#[serde(rename_all = "lowercase")]
1209pub enum HighlighterPreference {
1210 #[default]
1212 Auto,
1213 #[serde(rename = "tree-sitter")]
1215 TreeSitter,
1216 #[serde(rename = "textmate")]
1218 TextMate,
1219}
1220
1221#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1223pub struct MenuConfig {
1224 #[serde(default)]
1226 pub menus: Vec<Menu>,
1227}
1228
1229pub use fresh_core::menu::{Menu, MenuItem};
1231
1232pub trait MenuExt {
1234 fn match_id(&self) -> &str;
1237
1238 fn expand_dynamic_items(&mut self);
1241}
1242
1243impl MenuExt for Menu {
1244 fn match_id(&self) -> &str {
1245 self.id.as_deref().unwrap_or(&self.label)
1246 }
1247
1248 fn expand_dynamic_items(&mut self) {
1249 self.items = self
1250 .items
1251 .iter()
1252 .map(|item| item.expand_dynamic())
1253 .collect();
1254 }
1255}
1256
1257pub trait MenuItemExt {
1259 fn expand_dynamic(&self) -> MenuItem;
1262}
1263
1264impl MenuItemExt for MenuItem {
1265 fn expand_dynamic(&self) -> MenuItem {
1266 match self {
1267 MenuItem::DynamicSubmenu { label, source } => {
1268 let items = generate_dynamic_items(source);
1269 MenuItem::Submenu {
1270 label: label.clone(),
1271 items,
1272 }
1273 }
1274 other => other.clone(),
1275 }
1276 }
1277}
1278
1279#[cfg(feature = "runtime")]
1281pub fn generate_dynamic_items(source: &str) -> Vec<MenuItem> {
1282 match source {
1283 "copy_with_theme" => {
1284 let loader = crate::view::theme::ThemeLoader::new();
1286 let registry = loader.load_all();
1287 registry
1288 .list()
1289 .iter()
1290 .map(|info| {
1291 let mut args = HashMap::new();
1292 args.insert("theme".to_string(), serde_json::json!(info.name));
1293 MenuItem::Action {
1294 label: info.name.clone(),
1295 action: "copy_with_theme".to_string(),
1296 args,
1297 when: Some(context_keys::HAS_SELECTION.to_string()),
1298 checkbox: None,
1299 }
1300 })
1301 .collect()
1302 }
1303 _ => vec![MenuItem::Label {
1304 info: format!("Unknown source: {}", source),
1305 }],
1306 }
1307}
1308
1309#[cfg(not(feature = "runtime"))]
1311pub fn generate_dynamic_items(_source: &str) -> Vec<MenuItem> {
1312 vec![]
1314}
1315
1316impl Default for Config {
1317 fn default() -> Self {
1318 Self {
1319 version: 0,
1320 theme: default_theme_name(),
1321 locale: LocaleName::default(),
1322 check_for_updates: true,
1323 editor: EditorConfig::default(),
1324 file_explorer: FileExplorerConfig::default(),
1325 file_browser: FileBrowserConfig::default(),
1326 terminal: TerminalConfig::default(),
1327 keybindings: vec![], keybinding_maps: HashMap::new(), active_keybinding_map: default_keybinding_map_name(),
1330 languages: Self::default_languages(),
1331 lsp: Self::default_lsp_config(),
1332 warnings: WarningsConfig::default(),
1333 plugins: HashMap::new(), packages: PackagesConfig::default(),
1335 }
1336 }
1337}
1338
1339impl MenuConfig {
1340 pub fn translated() -> Self {
1342 Self {
1343 menus: Self::translated_menus(),
1344 }
1345 }
1346
1347 fn translated_menus() -> Vec<Menu> {
1349 vec![
1350 Menu {
1352 id: Some("File".to_string()),
1353 label: t!("menu.file").to_string(),
1354 when: None,
1355 items: vec![
1356 MenuItem::Action {
1357 label: t!("menu.file.new_file").to_string(),
1358 action: "new".to_string(),
1359 args: HashMap::new(),
1360 when: None,
1361 checkbox: None,
1362 },
1363 MenuItem::Action {
1364 label: t!("menu.file.open_file").to_string(),
1365 action: "open".to_string(),
1366 args: HashMap::new(),
1367 when: None,
1368 checkbox: None,
1369 },
1370 MenuItem::Separator { separator: true },
1371 MenuItem::Action {
1372 label: t!("menu.file.save").to_string(),
1373 action: "save".to_string(),
1374 args: HashMap::new(),
1375 when: None,
1376 checkbox: None,
1377 },
1378 MenuItem::Action {
1379 label: t!("menu.file.save_as").to_string(),
1380 action: "save_as".to_string(),
1381 args: HashMap::new(),
1382 when: None,
1383 checkbox: None,
1384 },
1385 MenuItem::Action {
1386 label: t!("menu.file.revert").to_string(),
1387 action: "revert".to_string(),
1388 args: HashMap::new(),
1389 when: None,
1390 checkbox: None,
1391 },
1392 MenuItem::Separator { separator: true },
1393 MenuItem::Action {
1394 label: t!("menu.file.close_buffer").to_string(),
1395 action: "close".to_string(),
1396 args: HashMap::new(),
1397 when: None,
1398 checkbox: None,
1399 },
1400 MenuItem::Separator { separator: true },
1401 MenuItem::Action {
1402 label: t!("menu.file.switch_project").to_string(),
1403 action: "switch_project".to_string(),
1404 args: HashMap::new(),
1405 when: None,
1406 checkbox: None,
1407 },
1408 MenuItem::Action {
1409 label: t!("menu.file.quit").to_string(),
1410 action: "quit".to_string(),
1411 args: HashMap::new(),
1412 when: None,
1413 checkbox: None,
1414 },
1415 ],
1416 },
1417 Menu {
1419 id: Some("Edit".to_string()),
1420 label: t!("menu.edit").to_string(),
1421 when: None,
1422 items: vec![
1423 MenuItem::Action {
1424 label: t!("menu.edit.undo").to_string(),
1425 action: "undo".to_string(),
1426 args: HashMap::new(),
1427 when: None,
1428 checkbox: None,
1429 },
1430 MenuItem::Action {
1431 label: t!("menu.edit.redo").to_string(),
1432 action: "redo".to_string(),
1433 args: HashMap::new(),
1434 when: None,
1435 checkbox: None,
1436 },
1437 MenuItem::Separator { separator: true },
1438 MenuItem::Action {
1439 label: t!("menu.edit.cut").to_string(),
1440 action: "cut".to_string(),
1441 args: HashMap::new(),
1442 when: Some(context_keys::HAS_SELECTION.to_string()),
1443 checkbox: None,
1444 },
1445 MenuItem::Action {
1446 label: t!("menu.edit.copy").to_string(),
1447 action: "copy".to_string(),
1448 args: HashMap::new(),
1449 when: Some(context_keys::HAS_SELECTION.to_string()),
1450 checkbox: None,
1451 },
1452 MenuItem::DynamicSubmenu {
1453 label: t!("menu.edit.copy_with_formatting").to_string(),
1454 source: "copy_with_theme".to_string(),
1455 },
1456 MenuItem::Action {
1457 label: t!("menu.edit.paste").to_string(),
1458 action: "paste".to_string(),
1459 args: HashMap::new(),
1460 when: None,
1461 checkbox: None,
1462 },
1463 MenuItem::Separator { separator: true },
1464 MenuItem::Action {
1465 label: t!("menu.edit.select_all").to_string(),
1466 action: "select_all".to_string(),
1467 args: HashMap::new(),
1468 when: None,
1469 checkbox: None,
1470 },
1471 MenuItem::Separator { separator: true },
1472 MenuItem::Action {
1473 label: t!("menu.edit.find").to_string(),
1474 action: "search".to_string(),
1475 args: HashMap::new(),
1476 when: None,
1477 checkbox: None,
1478 },
1479 MenuItem::Action {
1480 label: t!("menu.edit.find_in_selection").to_string(),
1481 action: "find_in_selection".to_string(),
1482 args: HashMap::new(),
1483 when: Some(context_keys::HAS_SELECTION.to_string()),
1484 checkbox: None,
1485 },
1486 MenuItem::Action {
1487 label: t!("menu.edit.find_next").to_string(),
1488 action: "find_next".to_string(),
1489 args: HashMap::new(),
1490 when: None,
1491 checkbox: None,
1492 },
1493 MenuItem::Action {
1494 label: t!("menu.edit.find_previous").to_string(),
1495 action: "find_previous".to_string(),
1496 args: HashMap::new(),
1497 when: None,
1498 checkbox: None,
1499 },
1500 MenuItem::Action {
1501 label: t!("menu.edit.replace").to_string(),
1502 action: "query_replace".to_string(),
1503 args: HashMap::new(),
1504 when: None,
1505 checkbox: None,
1506 },
1507 MenuItem::Separator { separator: true },
1508 MenuItem::Action {
1509 label: t!("menu.edit.delete_line").to_string(),
1510 action: "delete_line".to_string(),
1511 args: HashMap::new(),
1512 when: None,
1513 checkbox: None,
1514 },
1515 MenuItem::Action {
1516 label: t!("menu.edit.format_buffer").to_string(),
1517 action: "format_buffer".to_string(),
1518 args: HashMap::new(),
1519 when: Some(context_keys::FORMATTER_AVAILABLE.to_string()),
1520 checkbox: None,
1521 },
1522 MenuItem::Separator { separator: true },
1523 MenuItem::Action {
1524 label: t!("menu.edit.settings").to_string(),
1525 action: "open_settings".to_string(),
1526 args: HashMap::new(),
1527 when: None,
1528 checkbox: None,
1529 },
1530 ],
1531 },
1532 Menu {
1534 id: Some("View".to_string()),
1535 label: t!("menu.view").to_string(),
1536 when: None,
1537 items: vec![
1538 MenuItem::Action {
1539 label: t!("menu.view.file_explorer").to_string(),
1540 action: "toggle_file_explorer".to_string(),
1541 args: HashMap::new(),
1542 when: None,
1543 checkbox: Some(context_keys::FILE_EXPLORER.to_string()),
1544 },
1545 MenuItem::Separator { separator: true },
1546 MenuItem::Action {
1547 label: t!("menu.view.line_numbers").to_string(),
1548 action: "toggle_line_numbers".to_string(),
1549 args: HashMap::new(),
1550 when: None,
1551 checkbox: Some(context_keys::LINE_NUMBERS.to_string()),
1552 },
1553 MenuItem::Action {
1554 label: t!("menu.view.line_wrap").to_string(),
1555 action: "toggle_line_wrap".to_string(),
1556 args: HashMap::new(),
1557 when: None,
1558 checkbox: Some(context_keys::LINE_WRAP.to_string()),
1559 },
1560 MenuItem::Action {
1561 label: t!("menu.view.mouse_support").to_string(),
1562 action: "toggle_mouse_capture".to_string(),
1563 args: HashMap::new(),
1564 when: None,
1565 checkbox: Some(context_keys::MOUSE_CAPTURE.to_string()),
1566 },
1567 MenuItem::Separator { separator: true },
1568 MenuItem::Action {
1569 label: t!("menu.view.set_background").to_string(),
1570 action: "set_background".to_string(),
1571 args: HashMap::new(),
1572 when: None,
1573 checkbox: None,
1574 },
1575 MenuItem::Action {
1576 label: t!("menu.view.set_background_blend").to_string(),
1577 action: "set_background_blend".to_string(),
1578 args: HashMap::new(),
1579 when: None,
1580 checkbox: None,
1581 },
1582 MenuItem::Action {
1583 label: t!("menu.view.set_compose_width").to_string(),
1584 action: "set_compose_width".to_string(),
1585 args: HashMap::new(),
1586 when: None,
1587 checkbox: None,
1588 },
1589 MenuItem::Separator { separator: true },
1590 MenuItem::Action {
1591 label: t!("menu.view.select_theme").to_string(),
1592 action: "select_theme".to_string(),
1593 args: HashMap::new(),
1594 when: None,
1595 checkbox: None,
1596 },
1597 MenuItem::Action {
1598 label: t!("menu.view.select_locale").to_string(),
1599 action: "select_locale".to_string(),
1600 args: HashMap::new(),
1601 when: None,
1602 checkbox: None,
1603 },
1604 MenuItem::Action {
1605 label: t!("menu.view.settings").to_string(),
1606 action: "open_settings".to_string(),
1607 args: HashMap::new(),
1608 when: None,
1609 checkbox: None,
1610 },
1611 MenuItem::Action {
1612 label: t!("menu.view.calibrate_input").to_string(),
1613 action: "calibrate_input".to_string(),
1614 args: HashMap::new(),
1615 when: None,
1616 checkbox: None,
1617 },
1618 MenuItem::Separator { separator: true },
1619 MenuItem::Action {
1620 label: t!("menu.view.split_horizontal").to_string(),
1621 action: "split_horizontal".to_string(),
1622 args: HashMap::new(),
1623 when: None,
1624 checkbox: None,
1625 },
1626 MenuItem::Action {
1627 label: t!("menu.view.split_vertical").to_string(),
1628 action: "split_vertical".to_string(),
1629 args: HashMap::new(),
1630 when: None,
1631 checkbox: None,
1632 },
1633 MenuItem::Action {
1634 label: t!("menu.view.close_split").to_string(),
1635 action: "close_split".to_string(),
1636 args: HashMap::new(),
1637 when: None,
1638 checkbox: None,
1639 },
1640 MenuItem::Action {
1641 label: t!("menu.view.focus_next_split").to_string(),
1642 action: "next_split".to_string(),
1643 args: HashMap::new(),
1644 when: None,
1645 checkbox: None,
1646 },
1647 MenuItem::Action {
1648 label: t!("menu.view.focus_prev_split").to_string(),
1649 action: "prev_split".to_string(),
1650 args: HashMap::new(),
1651 when: None,
1652 checkbox: None,
1653 },
1654 MenuItem::Action {
1655 label: t!("menu.view.toggle_maximize_split").to_string(),
1656 action: "toggle_maximize_split".to_string(),
1657 args: HashMap::new(),
1658 when: None,
1659 checkbox: None,
1660 },
1661 MenuItem::Separator { separator: true },
1662 MenuItem::Submenu {
1663 label: t!("menu.terminal").to_string(),
1664 items: vec![
1665 MenuItem::Action {
1666 label: t!("menu.terminal.open").to_string(),
1667 action: "open_terminal".to_string(),
1668 args: HashMap::new(),
1669 when: None,
1670 checkbox: None,
1671 },
1672 MenuItem::Action {
1673 label: t!("menu.terminal.close").to_string(),
1674 action: "close_terminal".to_string(),
1675 args: HashMap::new(),
1676 when: None,
1677 checkbox: None,
1678 },
1679 MenuItem::Separator { separator: true },
1680 MenuItem::Action {
1681 label: t!("menu.terminal.toggle_keyboard_capture").to_string(),
1682 action: "toggle_keyboard_capture".to_string(),
1683 args: HashMap::new(),
1684 when: None,
1685 checkbox: None,
1686 },
1687 ],
1688 },
1689 MenuItem::Separator { separator: true },
1690 MenuItem::Submenu {
1691 label: t!("menu.view.keybinding_style").to_string(),
1692 items: vec![
1693 MenuItem::Action {
1694 label: t!("menu.view.keybinding_default").to_string(),
1695 action: "switch_keybinding_map".to_string(),
1696 args: {
1697 let mut map = HashMap::new();
1698 map.insert("map".to_string(), serde_json::json!("default"));
1699 map
1700 },
1701 when: None,
1702 checkbox: None,
1703 },
1704 MenuItem::Action {
1705 label: t!("menu.view.keybinding_emacs").to_string(),
1706 action: "switch_keybinding_map".to_string(),
1707 args: {
1708 let mut map = HashMap::new();
1709 map.insert("map".to_string(), serde_json::json!("emacs"));
1710 map
1711 },
1712 when: None,
1713 checkbox: None,
1714 },
1715 MenuItem::Action {
1716 label: t!("menu.view.keybinding_vscode").to_string(),
1717 action: "switch_keybinding_map".to_string(),
1718 args: {
1719 let mut map = HashMap::new();
1720 map.insert("map".to_string(), serde_json::json!("vscode"));
1721 map
1722 },
1723 when: None,
1724 checkbox: None,
1725 },
1726 ],
1727 },
1728 ],
1729 },
1730 Menu {
1732 id: Some("Selection".to_string()),
1733 label: t!("menu.selection").to_string(),
1734 when: None,
1735 items: vec![
1736 MenuItem::Action {
1737 label: t!("menu.selection.select_all").to_string(),
1738 action: "select_all".to_string(),
1739 args: HashMap::new(),
1740 when: None,
1741 checkbox: None,
1742 },
1743 MenuItem::Action {
1744 label: t!("menu.selection.select_word").to_string(),
1745 action: "select_word".to_string(),
1746 args: HashMap::new(),
1747 when: None,
1748 checkbox: None,
1749 },
1750 MenuItem::Action {
1751 label: t!("menu.selection.select_line").to_string(),
1752 action: "select_line".to_string(),
1753 args: HashMap::new(),
1754 when: None,
1755 checkbox: None,
1756 },
1757 MenuItem::Action {
1758 label: t!("menu.selection.expand_selection").to_string(),
1759 action: "expand_selection".to_string(),
1760 args: HashMap::new(),
1761 when: None,
1762 checkbox: None,
1763 },
1764 MenuItem::Separator { separator: true },
1765 MenuItem::Action {
1766 label: t!("menu.selection.add_cursor_above").to_string(),
1767 action: "add_cursor_above".to_string(),
1768 args: HashMap::new(),
1769 when: None,
1770 checkbox: None,
1771 },
1772 MenuItem::Action {
1773 label: t!("menu.selection.add_cursor_below").to_string(),
1774 action: "add_cursor_below".to_string(),
1775 args: HashMap::new(),
1776 when: None,
1777 checkbox: None,
1778 },
1779 MenuItem::Action {
1780 label: t!("menu.selection.add_cursor_next_match").to_string(),
1781 action: "add_cursor_next_match".to_string(),
1782 args: HashMap::new(),
1783 when: None,
1784 checkbox: None,
1785 },
1786 MenuItem::Action {
1787 label: t!("menu.selection.remove_secondary_cursors").to_string(),
1788 action: "remove_secondary_cursors".to_string(),
1789 args: HashMap::new(),
1790 when: None,
1791 checkbox: None,
1792 },
1793 ],
1794 },
1795 Menu {
1797 id: Some("Go".to_string()),
1798 label: t!("menu.go").to_string(),
1799 when: None,
1800 items: vec![
1801 MenuItem::Action {
1802 label: t!("menu.go.goto_line").to_string(),
1803 action: "goto_line".to_string(),
1804 args: HashMap::new(),
1805 when: None,
1806 checkbox: None,
1807 },
1808 MenuItem::Action {
1809 label: t!("menu.go.goto_definition").to_string(),
1810 action: "lsp_goto_definition".to_string(),
1811 args: HashMap::new(),
1812 when: None,
1813 checkbox: None,
1814 },
1815 MenuItem::Action {
1816 label: t!("menu.go.find_references").to_string(),
1817 action: "lsp_references".to_string(),
1818 args: HashMap::new(),
1819 when: None,
1820 checkbox: None,
1821 },
1822 MenuItem::Separator { separator: true },
1823 MenuItem::Action {
1824 label: t!("menu.go.next_buffer").to_string(),
1825 action: "next_buffer".to_string(),
1826 args: HashMap::new(),
1827 when: None,
1828 checkbox: None,
1829 },
1830 MenuItem::Action {
1831 label: t!("menu.go.prev_buffer").to_string(),
1832 action: "prev_buffer".to_string(),
1833 args: HashMap::new(),
1834 when: None,
1835 checkbox: None,
1836 },
1837 MenuItem::Separator { separator: true },
1838 MenuItem::Action {
1839 label: t!("menu.go.command_palette").to_string(),
1840 action: "command_palette".to_string(),
1841 args: HashMap::new(),
1842 when: None,
1843 checkbox: None,
1844 },
1845 ],
1846 },
1847 Menu {
1849 id: Some("LSP".to_string()),
1850 label: t!("menu.lsp").to_string(),
1851 when: None,
1852 items: vec![
1853 MenuItem::Action {
1854 label: t!("menu.lsp.show_hover").to_string(),
1855 action: "lsp_hover".to_string(),
1856 args: HashMap::new(),
1857 when: Some(context_keys::LSP_AVAILABLE.to_string()),
1858 checkbox: None,
1859 },
1860 MenuItem::Action {
1861 label: t!("menu.lsp.goto_definition").to_string(),
1862 action: "lsp_goto_definition".to_string(),
1863 args: HashMap::new(),
1864 when: Some(context_keys::LSP_AVAILABLE.to_string()),
1865 checkbox: None,
1866 },
1867 MenuItem::Action {
1868 label: t!("menu.lsp.find_references").to_string(),
1869 action: "lsp_references".to_string(),
1870 args: HashMap::new(),
1871 when: Some(context_keys::LSP_AVAILABLE.to_string()),
1872 checkbox: None,
1873 },
1874 MenuItem::Action {
1875 label: t!("menu.lsp.rename_symbol").to_string(),
1876 action: "lsp_rename".to_string(),
1877 args: HashMap::new(),
1878 when: Some(context_keys::LSP_AVAILABLE.to_string()),
1879 checkbox: None,
1880 },
1881 MenuItem::Separator { separator: true },
1882 MenuItem::Action {
1883 label: t!("menu.lsp.show_completions").to_string(),
1884 action: "lsp_completion".to_string(),
1885 args: HashMap::new(),
1886 when: Some(context_keys::LSP_AVAILABLE.to_string()),
1887 checkbox: None,
1888 },
1889 MenuItem::Action {
1890 label: t!("menu.lsp.show_signature").to_string(),
1891 action: "lsp_signature_help".to_string(),
1892 args: HashMap::new(),
1893 when: Some(context_keys::LSP_AVAILABLE.to_string()),
1894 checkbox: None,
1895 },
1896 MenuItem::Action {
1897 label: t!("menu.lsp.code_actions").to_string(),
1898 action: "lsp_code_actions".to_string(),
1899 args: HashMap::new(),
1900 when: Some(context_keys::LSP_AVAILABLE.to_string()),
1901 checkbox: None,
1902 },
1903 MenuItem::Separator { separator: true },
1904 MenuItem::Action {
1905 label: t!("menu.lsp.toggle_inlay_hints").to_string(),
1906 action: "toggle_inlay_hints".to_string(),
1907 args: HashMap::new(),
1908 when: Some(context_keys::LSP_AVAILABLE.to_string()),
1909 checkbox: Some(context_keys::INLAY_HINTS.to_string()),
1910 },
1911 MenuItem::Action {
1912 label: t!("menu.lsp.toggle_mouse_hover").to_string(),
1913 action: "toggle_mouse_hover".to_string(),
1914 args: HashMap::new(),
1915 when: None,
1916 checkbox: Some(context_keys::MOUSE_HOVER.to_string()),
1917 },
1918 MenuItem::Separator { separator: true },
1919 MenuItem::Action {
1920 label: t!("menu.lsp.restart_server").to_string(),
1921 action: "lsp_restart".to_string(),
1922 args: HashMap::new(),
1923 when: None,
1924 checkbox: None,
1925 },
1926 MenuItem::Action {
1927 label: t!("menu.lsp.stop_server").to_string(),
1928 action: "lsp_stop".to_string(),
1929 args: HashMap::new(),
1930 when: None,
1931 checkbox: None,
1932 },
1933 ],
1934 },
1935 Menu {
1937 id: Some("Explorer".to_string()),
1938 label: t!("menu.explorer").to_string(),
1939 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1940 items: vec![
1941 MenuItem::Action {
1942 label: t!("menu.explorer.new_file").to_string(),
1943 action: "file_explorer_new_file".to_string(),
1944 args: HashMap::new(),
1945 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1946 checkbox: None,
1947 },
1948 MenuItem::Action {
1949 label: t!("menu.explorer.new_folder").to_string(),
1950 action: "file_explorer_new_directory".to_string(),
1951 args: HashMap::new(),
1952 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1953 checkbox: None,
1954 },
1955 MenuItem::Separator { separator: true },
1956 MenuItem::Action {
1957 label: t!("menu.explorer.open").to_string(),
1958 action: "file_explorer_open".to_string(),
1959 args: HashMap::new(),
1960 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1961 checkbox: None,
1962 },
1963 MenuItem::Action {
1964 label: t!("menu.explorer.rename").to_string(),
1965 action: "file_explorer_rename".to_string(),
1966 args: HashMap::new(),
1967 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1968 checkbox: None,
1969 },
1970 MenuItem::Action {
1971 label: t!("menu.explorer.delete").to_string(),
1972 action: "file_explorer_delete".to_string(),
1973 args: HashMap::new(),
1974 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1975 checkbox: None,
1976 },
1977 MenuItem::Separator { separator: true },
1978 MenuItem::Action {
1979 label: t!("menu.explorer.refresh").to_string(),
1980 action: "file_explorer_refresh".to_string(),
1981 args: HashMap::new(),
1982 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1983 checkbox: None,
1984 },
1985 MenuItem::Separator { separator: true },
1986 MenuItem::Action {
1987 label: t!("menu.explorer.show_hidden").to_string(),
1988 action: "file_explorer_toggle_hidden".to_string(),
1989 args: HashMap::new(),
1990 when: Some(context_keys::FILE_EXPLORER.to_string()),
1991 checkbox: Some(context_keys::FILE_EXPLORER_SHOW_HIDDEN.to_string()),
1992 },
1993 MenuItem::Action {
1994 label: t!("menu.explorer.show_gitignored").to_string(),
1995 action: "file_explorer_toggle_gitignored".to_string(),
1996 args: HashMap::new(),
1997 when: Some(context_keys::FILE_EXPLORER.to_string()),
1998 checkbox: Some(context_keys::FILE_EXPLORER_SHOW_GITIGNORED.to_string()),
1999 },
2000 ],
2001 },
2002 Menu {
2004 id: Some("Help".to_string()),
2005 label: t!("menu.help").to_string(),
2006 when: None,
2007 items: vec![
2008 MenuItem::Label {
2009 info: format!("Fresh v{}", env!("CARGO_PKG_VERSION")),
2010 },
2011 MenuItem::Separator { separator: true },
2012 MenuItem::Action {
2013 label: t!("menu.help.show_manual").to_string(),
2014 action: "show_help".to_string(),
2015 args: HashMap::new(),
2016 when: None,
2017 checkbox: None,
2018 },
2019 MenuItem::Action {
2020 label: t!("menu.help.keyboard_shortcuts").to_string(),
2021 action: "keyboard_shortcuts".to_string(),
2022 args: HashMap::new(),
2023 when: None,
2024 checkbox: None,
2025 },
2026 MenuItem::Separator { separator: true },
2027 MenuItem::Action {
2028 label: t!("menu.help.event_debug").to_string(),
2029 action: "event_debug".to_string(),
2030 args: HashMap::new(),
2031 when: None,
2032 checkbox: None,
2033 },
2034 ],
2035 },
2036 ]
2037 }
2038}
2039
2040impl Config {
2041 pub(crate) const FILENAME: &'static str = "config.json";
2043
2044 pub(crate) fn local_config_path(working_dir: &Path) -> std::path::PathBuf {
2046 working_dir.join(Self::FILENAME)
2047 }
2048
2049 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
2055 let contents = std::fs::read_to_string(path.as_ref())
2056 .map_err(|e| ConfigError::IoError(e.to_string()))?;
2057
2058 let partial: crate::partial_config::PartialConfig =
2060 serde_json::from_str(&contents).map_err(|e| ConfigError::ParseError(e.to_string()))?;
2061
2062 Ok(partial.resolve())
2063 }
2064
2065 fn load_builtin_keymap(name: &str) -> Option<KeymapConfig> {
2067 let json_content = match name {
2068 "default" => include_str!("../keymaps/default.json"),
2069 "emacs" => include_str!("../keymaps/emacs.json"),
2070 "vscode" => include_str!("../keymaps/vscode.json"),
2071 "macos" => include_str!("../keymaps/macos.json"),
2072 _ => return None,
2073 };
2074
2075 match serde_json::from_str(json_content) {
2076 Ok(config) => Some(config),
2077 Err(e) => {
2078 eprintln!("Failed to parse builtin keymap '{}': {}", name, e);
2079 None
2080 }
2081 }
2082 }
2083
2084 pub fn resolve_keymap(&self, map_name: &str) -> Vec<Keybinding> {
2087 let mut visited = std::collections::HashSet::new();
2088 self.resolve_keymap_recursive(map_name, &mut visited)
2089 }
2090
2091 fn resolve_keymap_recursive(
2093 &self,
2094 map_name: &str,
2095 visited: &mut std::collections::HashSet<String>,
2096 ) -> Vec<Keybinding> {
2097 if visited.contains(map_name) {
2099 eprintln!(
2100 "Warning: Circular inheritance detected in keymap '{}'",
2101 map_name
2102 );
2103 return Vec::new();
2104 }
2105 visited.insert(map_name.to_string());
2106
2107 let keymap = self
2109 .keybinding_maps
2110 .get(map_name)
2111 .cloned()
2112 .or_else(|| Self::load_builtin_keymap(map_name));
2113
2114 let Some(keymap) = keymap else {
2115 return Vec::new();
2116 };
2117
2118 let mut all_bindings = if let Some(ref parent_name) = keymap.inherits {
2120 self.resolve_keymap_recursive(parent_name, visited)
2121 } else {
2122 Vec::new()
2123 };
2124
2125 all_bindings.extend(keymap.bindings);
2127
2128 all_bindings
2129 }
2130 fn default_languages() -> HashMap<String, LanguageConfig> {
2132 let mut languages = HashMap::new();
2133
2134 languages.insert(
2135 "rust".to_string(),
2136 LanguageConfig {
2137 extensions: vec!["rs".to_string()],
2138 filenames: vec![],
2139 grammar: "rust".to_string(),
2140 comment_prefix: Some("//".to_string()),
2141 auto_indent: true,
2142 highlighter: HighlighterPreference::Auto,
2143 textmate_grammar: None,
2144 show_whitespace_tabs: true,
2145 use_tabs: false,
2146 tab_size: None,
2147 formatter: Some(FormatterConfig {
2148 command: "rustfmt".to_string(),
2149 args: vec!["--edition".to_string(), "2021".to_string()],
2150 stdin: true,
2151 timeout_ms: 10000,
2152 }),
2153 format_on_save: false,
2154 on_save: vec![],
2155 },
2156 );
2157
2158 languages.insert(
2159 "javascript".to_string(),
2160 LanguageConfig {
2161 extensions: vec!["js".to_string(), "jsx".to_string(), "mjs".to_string()],
2162 filenames: vec![],
2163 grammar: "javascript".to_string(),
2164 comment_prefix: Some("//".to_string()),
2165 auto_indent: true,
2166 highlighter: HighlighterPreference::Auto,
2167 textmate_grammar: None,
2168 show_whitespace_tabs: true,
2169 use_tabs: false,
2170 tab_size: None,
2171 formatter: Some(FormatterConfig {
2172 command: "prettier".to_string(),
2173 args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2174 stdin: true,
2175 timeout_ms: 10000,
2176 }),
2177 format_on_save: false,
2178 on_save: vec![],
2179 },
2180 );
2181
2182 languages.insert(
2183 "typescript".to_string(),
2184 LanguageConfig {
2185 extensions: vec!["ts".to_string(), "tsx".to_string(), "mts".to_string()],
2186 filenames: vec![],
2187 grammar: "typescript".to_string(),
2188 comment_prefix: Some("//".to_string()),
2189 auto_indent: true,
2190 highlighter: HighlighterPreference::Auto,
2191 textmate_grammar: None,
2192 show_whitespace_tabs: true,
2193 use_tabs: false,
2194 tab_size: None,
2195 formatter: Some(FormatterConfig {
2196 command: "prettier".to_string(),
2197 args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2198 stdin: true,
2199 timeout_ms: 10000,
2200 }),
2201 format_on_save: false,
2202 on_save: vec![],
2203 },
2204 );
2205
2206 languages.insert(
2207 "python".to_string(),
2208 LanguageConfig {
2209 extensions: vec!["py".to_string(), "pyi".to_string()],
2210 filenames: vec![],
2211 grammar: "python".to_string(),
2212 comment_prefix: Some("#".to_string()),
2213 auto_indent: true,
2214 highlighter: HighlighterPreference::Auto,
2215 textmate_grammar: None,
2216 show_whitespace_tabs: true,
2217 use_tabs: false,
2218 tab_size: None,
2219 formatter: Some(FormatterConfig {
2220 command: "ruff".to_string(),
2221 args: vec![
2222 "format".to_string(),
2223 "--stdin-filename".to_string(),
2224 "$FILE".to_string(),
2225 ],
2226 stdin: true,
2227 timeout_ms: 10000,
2228 }),
2229 format_on_save: false,
2230 on_save: vec![],
2231 },
2232 );
2233
2234 languages.insert(
2235 "c".to_string(),
2236 LanguageConfig {
2237 extensions: vec!["c".to_string(), "h".to_string()],
2238 filenames: vec![],
2239 grammar: "c".to_string(),
2240 comment_prefix: Some("//".to_string()),
2241 auto_indent: true,
2242 highlighter: HighlighterPreference::Auto,
2243 textmate_grammar: None,
2244 show_whitespace_tabs: true,
2245 use_tabs: false,
2246 tab_size: None,
2247 formatter: Some(FormatterConfig {
2248 command: "clang-format".to_string(),
2249 args: vec![],
2250 stdin: true,
2251 timeout_ms: 10000,
2252 }),
2253 format_on_save: false,
2254 on_save: vec![],
2255 },
2256 );
2257
2258 languages.insert(
2259 "cpp".to_string(),
2260 LanguageConfig {
2261 extensions: vec![
2262 "cpp".to_string(),
2263 "cc".to_string(),
2264 "cxx".to_string(),
2265 "hpp".to_string(),
2266 "hh".to_string(),
2267 "hxx".to_string(),
2268 ],
2269 filenames: vec![],
2270 grammar: "cpp".to_string(),
2271 comment_prefix: Some("//".to_string()),
2272 auto_indent: true,
2273 highlighter: HighlighterPreference::Auto,
2274 textmate_grammar: None,
2275 show_whitespace_tabs: true,
2276 use_tabs: false,
2277 tab_size: None,
2278 formatter: Some(FormatterConfig {
2279 command: "clang-format".to_string(),
2280 args: vec![],
2281 stdin: true,
2282 timeout_ms: 10000,
2283 }),
2284 format_on_save: false,
2285 on_save: vec![],
2286 },
2287 );
2288
2289 languages.insert(
2290 "csharp".to_string(),
2291 LanguageConfig {
2292 extensions: vec!["cs".to_string()],
2293 filenames: vec![],
2294 grammar: "c_sharp".to_string(),
2295 comment_prefix: Some("//".to_string()),
2296 auto_indent: true,
2297 highlighter: HighlighterPreference::Auto,
2298 textmate_grammar: None,
2299 show_whitespace_tabs: true,
2300 use_tabs: false,
2301 tab_size: None,
2302 formatter: None,
2303 format_on_save: false,
2304 on_save: vec![],
2305 },
2306 );
2307
2308 languages.insert(
2309 "bash".to_string(),
2310 LanguageConfig {
2311 extensions: vec!["sh".to_string(), "bash".to_string()],
2312 filenames: vec![
2313 ".bash_aliases".to_string(),
2314 ".bash_logout".to_string(),
2315 ".bash_profile".to_string(),
2316 ".bashrc".to_string(),
2317 ".env".to_string(),
2318 ".profile".to_string(),
2319 ".zlogin".to_string(),
2320 ".zlogout".to_string(),
2321 ".zprofile".to_string(),
2322 ".zshenv".to_string(),
2323 ".zshrc".to_string(),
2324 "PKGBUILD".to_string(),
2326 "APKBUILD".to_string(),
2327 ],
2328 grammar: "bash".to_string(),
2329 comment_prefix: Some("#".to_string()),
2330 auto_indent: true,
2331 highlighter: HighlighterPreference::Auto,
2332 textmate_grammar: None,
2333 show_whitespace_tabs: true,
2334 use_tabs: false,
2335 tab_size: None,
2336 formatter: None,
2337 format_on_save: false,
2338 on_save: vec![],
2339 },
2340 );
2341
2342 languages.insert(
2343 "makefile".to_string(),
2344 LanguageConfig {
2345 extensions: vec!["mk".to_string()],
2346 filenames: vec![
2347 "Makefile".to_string(),
2348 "makefile".to_string(),
2349 "GNUmakefile".to_string(),
2350 ],
2351 grammar: "make".to_string(),
2352 comment_prefix: Some("#".to_string()),
2353 auto_indent: false,
2354 highlighter: HighlighterPreference::Auto,
2355 textmate_grammar: None,
2356 show_whitespace_tabs: true,
2357 use_tabs: true, tab_size: Some(8), formatter: None,
2360 format_on_save: false,
2361 on_save: vec![],
2362 },
2363 );
2364
2365 languages.insert(
2366 "dockerfile".to_string(),
2367 LanguageConfig {
2368 extensions: vec!["dockerfile".to_string()],
2369 filenames: vec!["Dockerfile".to_string(), "Containerfile".to_string()],
2370 grammar: "dockerfile".to_string(),
2371 comment_prefix: Some("#".to_string()),
2372 auto_indent: true,
2373 highlighter: HighlighterPreference::Auto,
2374 textmate_grammar: None,
2375 show_whitespace_tabs: true,
2376 use_tabs: false,
2377 tab_size: None,
2378 formatter: None,
2379 format_on_save: false,
2380 on_save: vec![],
2381 },
2382 );
2383
2384 languages.insert(
2385 "json".to_string(),
2386 LanguageConfig {
2387 extensions: vec!["json".to_string(), "jsonc".to_string()],
2388 filenames: vec![],
2389 grammar: "json".to_string(),
2390 comment_prefix: None,
2391 auto_indent: true,
2392 highlighter: HighlighterPreference::Auto,
2393 textmate_grammar: None,
2394 show_whitespace_tabs: true,
2395 use_tabs: false,
2396 tab_size: None,
2397 formatter: Some(FormatterConfig {
2398 command: "prettier".to_string(),
2399 args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2400 stdin: true,
2401 timeout_ms: 10000,
2402 }),
2403 format_on_save: false,
2404 on_save: vec![],
2405 },
2406 );
2407
2408 languages.insert(
2409 "toml".to_string(),
2410 LanguageConfig {
2411 extensions: vec!["toml".to_string()],
2412 filenames: vec!["Cargo.lock".to_string()],
2413 grammar: "toml".to_string(),
2414 comment_prefix: Some("#".to_string()),
2415 auto_indent: true,
2416 highlighter: HighlighterPreference::Auto,
2417 textmate_grammar: None,
2418 show_whitespace_tabs: true,
2419 use_tabs: false,
2420 tab_size: None,
2421 formatter: None,
2422 format_on_save: false,
2423 on_save: vec![],
2424 },
2425 );
2426
2427 languages.insert(
2428 "yaml".to_string(),
2429 LanguageConfig {
2430 extensions: vec!["yml".to_string(), "yaml".to_string()],
2431 filenames: vec![],
2432 grammar: "yaml".to_string(),
2433 comment_prefix: Some("#".to_string()),
2434 auto_indent: true,
2435 highlighter: HighlighterPreference::Auto,
2436 textmate_grammar: None,
2437 show_whitespace_tabs: true,
2438 use_tabs: false,
2439 tab_size: None,
2440 formatter: Some(FormatterConfig {
2441 command: "prettier".to_string(),
2442 args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2443 stdin: true,
2444 timeout_ms: 10000,
2445 }),
2446 format_on_save: false,
2447 on_save: vec![],
2448 },
2449 );
2450
2451 languages.insert(
2452 "markdown".to_string(),
2453 LanguageConfig {
2454 extensions: vec!["md".to_string(), "markdown".to_string()],
2455 filenames: vec!["README".to_string()],
2456 grammar: "markdown".to_string(),
2457 comment_prefix: None,
2458 auto_indent: false,
2459 highlighter: HighlighterPreference::Auto,
2460 textmate_grammar: None,
2461 show_whitespace_tabs: true,
2462 use_tabs: false,
2463 tab_size: None,
2464 formatter: None,
2465 format_on_save: false,
2466 on_save: vec![],
2467 },
2468 );
2469
2470 languages.insert(
2472 "go".to_string(),
2473 LanguageConfig {
2474 extensions: vec!["go".to_string()],
2475 filenames: vec![],
2476 grammar: "go".to_string(),
2477 comment_prefix: Some("//".to_string()),
2478 auto_indent: true,
2479 highlighter: HighlighterPreference::Auto,
2480 textmate_grammar: None,
2481 show_whitespace_tabs: false,
2482 use_tabs: true, tab_size: Some(8), formatter: Some(FormatterConfig {
2485 command: "gofmt".to_string(),
2486 args: vec![],
2487 stdin: true,
2488 timeout_ms: 10000,
2489 }),
2490 format_on_save: false,
2491 on_save: vec![],
2492 },
2493 );
2494
2495 languages.insert(
2496 "odin".to_string(),
2497 LanguageConfig {
2498 extensions: vec!["odin".to_string()],
2499 filenames: vec![],
2500 grammar: "odin".to_string(),
2501 comment_prefix: Some("//".to_string()),
2502 auto_indent: true,
2503 highlighter: HighlighterPreference::Auto,
2504 textmate_grammar: None,
2505 show_whitespace_tabs: false,
2506 use_tabs: true,
2507 tab_size: Some(8),
2508 formatter: None,
2509 format_on_save: false,
2510 on_save: vec![],
2511 },
2512 );
2513
2514 languages.insert(
2515 "zig".to_string(),
2516 LanguageConfig {
2517 extensions: vec!["zig".to_string(), "zon".to_string()],
2518 filenames: vec![],
2519 grammar: "zig".to_string(),
2520 comment_prefix: Some("//".to_string()),
2521 auto_indent: true,
2522 highlighter: HighlighterPreference::Auto,
2523 textmate_grammar: None,
2524 show_whitespace_tabs: true,
2525 use_tabs: false,
2526 tab_size: None,
2527 formatter: None,
2528 format_on_save: false,
2529 on_save: vec![],
2530 },
2531 );
2532
2533 languages.insert(
2534 "java".to_string(),
2535 LanguageConfig {
2536 extensions: vec!["java".to_string()],
2537 filenames: vec![],
2538 grammar: "java".to_string(),
2539 comment_prefix: Some("//".to_string()),
2540 auto_indent: true,
2541 highlighter: HighlighterPreference::Auto,
2542 textmate_grammar: None,
2543 show_whitespace_tabs: true,
2544 use_tabs: false,
2545 tab_size: None,
2546 formatter: None,
2547 format_on_save: false,
2548 on_save: vec![],
2549 },
2550 );
2551
2552 languages.insert(
2553 "latex".to_string(),
2554 LanguageConfig {
2555 extensions: vec![
2556 "tex".to_string(),
2557 "latex".to_string(),
2558 "ltx".to_string(),
2559 "sty".to_string(),
2560 "cls".to_string(),
2561 "bib".to_string(),
2562 ],
2563 filenames: vec![],
2564 grammar: "latex".to_string(),
2565 comment_prefix: Some("%".to_string()),
2566 auto_indent: true,
2567 highlighter: HighlighterPreference::Auto,
2568 textmate_grammar: None,
2569 show_whitespace_tabs: true,
2570 use_tabs: false,
2571 tab_size: None,
2572 formatter: None,
2573 format_on_save: false,
2574 on_save: vec![],
2575 },
2576 );
2577
2578 languages.insert(
2579 "templ".to_string(),
2580 LanguageConfig {
2581 extensions: vec!["templ".to_string()],
2582 filenames: vec![],
2583 grammar: "go".to_string(), comment_prefix: Some("//".to_string()),
2585 auto_indent: true,
2586 highlighter: HighlighterPreference::Auto,
2587 textmate_grammar: None,
2588 show_whitespace_tabs: true,
2589 use_tabs: false,
2590 tab_size: None,
2591 formatter: None,
2592 format_on_save: false,
2593 on_save: vec![],
2594 },
2595 );
2596
2597 languages.insert(
2599 "git-rebase".to_string(),
2600 LanguageConfig {
2601 extensions: vec![],
2602 filenames: vec!["git-rebase-todo".to_string()],
2603 grammar: "Git Rebase Todo".to_string(),
2604 comment_prefix: Some("#".to_string()),
2605 auto_indent: false,
2606 highlighter: HighlighterPreference::Auto,
2607 textmate_grammar: None,
2608 show_whitespace_tabs: true,
2609 use_tabs: false,
2610 tab_size: None,
2611 formatter: None,
2612 format_on_save: false,
2613 on_save: vec![],
2614 },
2615 );
2616
2617 languages.insert(
2618 "git-commit".to_string(),
2619 LanguageConfig {
2620 extensions: vec![],
2621 filenames: vec![
2622 "COMMIT_EDITMSG".to_string(),
2623 "MERGE_MSG".to_string(),
2624 "SQUASH_MSG".to_string(),
2625 "TAG_EDITMSG".to_string(),
2626 ],
2627 grammar: "Git Commit Message".to_string(),
2628 comment_prefix: Some("#".to_string()),
2629 auto_indent: false,
2630 highlighter: HighlighterPreference::Auto,
2631 textmate_grammar: None,
2632 show_whitespace_tabs: true,
2633 use_tabs: false,
2634 tab_size: None,
2635 formatter: None,
2636 format_on_save: false,
2637 on_save: vec![],
2638 },
2639 );
2640
2641 languages.insert(
2642 "gitignore".to_string(),
2643 LanguageConfig {
2644 extensions: vec!["gitignore".to_string()],
2645 filenames: vec![
2646 ".gitignore".to_string(),
2647 ".dockerignore".to_string(),
2648 ".npmignore".to_string(),
2649 ".hgignore".to_string(),
2650 ],
2651 grammar: "Gitignore".to_string(),
2652 comment_prefix: Some("#".to_string()),
2653 auto_indent: false,
2654 highlighter: HighlighterPreference::Auto,
2655 textmate_grammar: None,
2656 show_whitespace_tabs: true,
2657 use_tabs: false,
2658 tab_size: None,
2659 formatter: None,
2660 format_on_save: false,
2661 on_save: vec![],
2662 },
2663 );
2664
2665 languages.insert(
2666 "gitconfig".to_string(),
2667 LanguageConfig {
2668 extensions: vec!["gitconfig".to_string()],
2669 filenames: vec![".gitconfig".to_string(), ".gitmodules".to_string()],
2670 grammar: "Git Config".to_string(),
2671 comment_prefix: Some("#".to_string()),
2672 auto_indent: true,
2673 highlighter: HighlighterPreference::Auto,
2674 textmate_grammar: None,
2675 show_whitespace_tabs: true,
2676 use_tabs: false,
2677 tab_size: None,
2678 formatter: None,
2679 format_on_save: false,
2680 on_save: vec![],
2681 },
2682 );
2683
2684 languages.insert(
2685 "gitattributes".to_string(),
2686 LanguageConfig {
2687 extensions: vec!["gitattributes".to_string()],
2688 filenames: vec![".gitattributes".to_string()],
2689 grammar: "Git Attributes".to_string(),
2690 comment_prefix: Some("#".to_string()),
2691 auto_indent: false,
2692 highlighter: HighlighterPreference::Auto,
2693 textmate_grammar: None,
2694 show_whitespace_tabs: true,
2695 use_tabs: false,
2696 tab_size: None,
2697 formatter: None,
2698 format_on_save: false,
2699 on_save: vec![],
2700 },
2701 );
2702
2703 languages
2704 }
2705
2706 #[cfg(feature = "runtime")]
2708 fn default_lsp_config() -> HashMap<String, LspServerConfig> {
2709 let mut lsp = HashMap::new();
2710
2711 let ra_log_path = crate::services::log_dirs::lsp_log_path("rust-analyzer")
2714 .to_string_lossy()
2715 .to_string();
2716
2717 Self::populate_lsp_config(&mut lsp, ra_log_path);
2718 lsp
2719 }
2720
2721 #[cfg(not(feature = "runtime"))]
2723 fn default_lsp_config() -> HashMap<String, LspServerConfig> {
2724 HashMap::new()
2726 }
2727
2728 #[cfg(feature = "runtime")]
2729 fn populate_lsp_config(lsp: &mut HashMap<String, LspServerConfig>, ra_log_path: String) {
2730 let ra_init_options = serde_json::json!({
2739 "checkOnSave": false,
2740 "cachePriming": { "enable": false },
2741 "procMacro": { "enable": false },
2742 "cargo": {
2743 "buildScripts": { "enable": false },
2744 "autoreload": false
2745 },
2746 "diagnostics": { "enable": true },
2747 "files": { "watcher": "server" }
2748 });
2749
2750 lsp.insert(
2751 "rust".to_string(),
2752 LspServerConfig {
2753 command: "rust-analyzer".to_string(),
2754 args: vec!["--log-file".to_string(), ra_log_path],
2755 enabled: true,
2756 auto_start: false,
2757 process_limits: ProcessLimits::default(),
2758 initialization_options: Some(ra_init_options),
2759 },
2760 );
2761
2762 lsp.insert(
2764 "python".to_string(),
2765 LspServerConfig {
2766 command: "pylsp".to_string(),
2767 args: vec![],
2768 enabled: true,
2769 auto_start: false,
2770 process_limits: ProcessLimits::default(),
2771 initialization_options: None,
2772 },
2773 );
2774
2775 let ts_lsp = LspServerConfig {
2778 command: "typescript-language-server".to_string(),
2779 args: vec!["--stdio".to_string()],
2780 enabled: true,
2781 auto_start: false,
2782 process_limits: ProcessLimits::default(),
2783 initialization_options: None,
2784 };
2785 lsp.insert("javascript".to_string(), ts_lsp.clone());
2786 lsp.insert("typescript".to_string(), ts_lsp);
2787
2788 lsp.insert(
2790 "html".to_string(),
2791 LspServerConfig {
2792 command: "vscode-html-language-server".to_string(),
2793 args: vec!["--stdio".to_string()],
2794 enabled: true,
2795 auto_start: false,
2796 process_limits: ProcessLimits::default(),
2797 initialization_options: None,
2798 },
2799 );
2800
2801 lsp.insert(
2803 "css".to_string(),
2804 LspServerConfig {
2805 command: "vscode-css-language-server".to_string(),
2806 args: vec!["--stdio".to_string()],
2807 enabled: true,
2808 auto_start: false,
2809 process_limits: ProcessLimits::default(),
2810 initialization_options: None,
2811 },
2812 );
2813
2814 lsp.insert(
2816 "c".to_string(),
2817 LspServerConfig {
2818 command: "clangd".to_string(),
2819 args: vec![],
2820 enabled: true,
2821 auto_start: false,
2822 process_limits: ProcessLimits::default(),
2823 initialization_options: None,
2824 },
2825 );
2826 lsp.insert(
2827 "cpp".to_string(),
2828 LspServerConfig {
2829 command: "clangd".to_string(),
2830 args: vec![],
2831 enabled: true,
2832 auto_start: false,
2833 process_limits: ProcessLimits::default(),
2834 initialization_options: None,
2835 },
2836 );
2837
2838 lsp.insert(
2840 "go".to_string(),
2841 LspServerConfig {
2842 command: "gopls".to_string(),
2843 args: vec![],
2844 enabled: true,
2845 auto_start: false,
2846 process_limits: ProcessLimits::default(),
2847 initialization_options: None,
2848 },
2849 );
2850
2851 lsp.insert(
2853 "json".to_string(),
2854 LspServerConfig {
2855 command: "vscode-json-language-server".to_string(),
2856 args: vec!["--stdio".to_string()],
2857 enabled: true,
2858 auto_start: false,
2859 process_limits: ProcessLimits::default(),
2860 initialization_options: None,
2861 },
2862 );
2863
2864 lsp.insert(
2866 "csharp".to_string(),
2867 LspServerConfig {
2868 command: "csharp-ls".to_string(),
2869 args: vec![],
2870 enabled: true,
2871 auto_start: false,
2872 process_limits: ProcessLimits::default(),
2873 initialization_options: None,
2874 },
2875 );
2876
2877 lsp.insert(
2880 "odin".to_string(),
2881 LspServerConfig {
2882 command: "ols".to_string(),
2883 args: vec![],
2884 enabled: true,
2885 auto_start: false,
2886 process_limits: ProcessLimits::default(),
2887 initialization_options: None,
2888 },
2889 );
2890
2891 lsp.insert(
2894 "zig".to_string(),
2895 LspServerConfig {
2896 command: "zls".to_string(),
2897 args: vec![],
2898 enabled: true,
2899 auto_start: false,
2900 process_limits: ProcessLimits::default(),
2901 initialization_options: None,
2902 },
2903 );
2904
2905 lsp.insert(
2908 "java".to_string(),
2909 LspServerConfig {
2910 command: "jdtls".to_string(),
2911 args: vec![],
2912 enabled: true,
2913 auto_start: false,
2914 process_limits: ProcessLimits::default(),
2915 initialization_options: None,
2916 },
2917 );
2918
2919 lsp.insert(
2922 "latex".to_string(),
2923 LspServerConfig {
2924 command: "texlab".to_string(),
2925 args: vec![],
2926 enabled: true,
2927 auto_start: false,
2928 process_limits: ProcessLimits::default(),
2929 initialization_options: None,
2930 },
2931 );
2932
2933 lsp.insert(
2936 "markdown".to_string(),
2937 LspServerConfig {
2938 command: "marksman".to_string(),
2939 args: vec!["server".to_string()],
2940 enabled: true,
2941 auto_start: false,
2942 process_limits: ProcessLimits::default(),
2943 initialization_options: None,
2944 },
2945 );
2946
2947 lsp.insert(
2950 "templ".to_string(),
2951 LspServerConfig {
2952 command: "templ".to_string(),
2953 args: vec!["lsp".to_string()],
2954 enabled: true,
2955 auto_start: false,
2956 process_limits: ProcessLimits::default(),
2957 initialization_options: None,
2958 },
2959 );
2960 }
2961
2962 pub fn validate(&self) -> Result<(), ConfigError> {
2964 if self.editor.tab_size == 0 {
2966 return Err(ConfigError::ValidationError(
2967 "tab_size must be greater than 0".to_string(),
2968 ));
2969 }
2970
2971 if self.editor.scroll_offset > 100 {
2973 return Err(ConfigError::ValidationError(
2974 "scroll_offset must be <= 100".to_string(),
2975 ));
2976 }
2977
2978 for binding in &self.keybindings {
2980 if binding.key.is_empty() {
2981 return Err(ConfigError::ValidationError(
2982 "keybinding key cannot be empty".to_string(),
2983 ));
2984 }
2985 if binding.action.is_empty() {
2986 return Err(ConfigError::ValidationError(
2987 "keybinding action cannot be empty".to_string(),
2988 ));
2989 }
2990 }
2991
2992 Ok(())
2993 }
2994}
2995
2996#[derive(Debug)]
2998pub enum ConfigError {
2999 IoError(String),
3000 ParseError(String),
3001 SerializeError(String),
3002 ValidationError(String),
3003}
3004
3005impl std::fmt::Display for ConfigError {
3006 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3007 match self {
3008 Self::IoError(msg) => write!(f, "IO error: {msg}"),
3009 Self::ParseError(msg) => write!(f, "Parse error: {msg}"),
3010 Self::SerializeError(msg) => write!(f, "Serialize error: {msg}"),
3011 Self::ValidationError(msg) => write!(f, "Validation error: {msg}"),
3012 }
3013 }
3014}
3015
3016impl std::error::Error for ConfigError {}
3017
3018#[cfg(test)]
3019mod tests {
3020 use super::*;
3021
3022 #[test]
3023 fn test_default_config() {
3024 let config = Config::default();
3025 assert_eq!(config.editor.tab_size, 4);
3026 assert!(config.editor.line_numbers);
3027 assert!(config.editor.syntax_highlighting);
3028 assert!(config.keybindings.is_empty());
3031 let resolved = config.resolve_keymap(&config.active_keybinding_map);
3033 assert!(!resolved.is_empty());
3034 }
3035
3036 #[test]
3037 fn test_all_builtin_keymaps_loadable() {
3038 for name in KeybindingMapName::BUILTIN_OPTIONS {
3039 let keymap = Config::load_builtin_keymap(name);
3040 assert!(keymap.is_some(), "Failed to load builtin keymap '{}'", name);
3041 }
3042 }
3043
3044 #[test]
3045 fn test_config_validation() {
3046 let mut config = Config::default();
3047 assert!(config.validate().is_ok());
3048
3049 config.editor.tab_size = 0;
3050 assert!(config.validate().is_err());
3051 }
3052
3053 #[test]
3054 fn test_macos_keymap_inherits_enter_bindings() {
3055 let config = Config::default();
3056 let bindings = config.resolve_keymap("macos");
3057
3058 let enter_bindings: Vec<_> = bindings.iter().filter(|b| b.key == "Enter").collect();
3059 assert!(
3060 !enter_bindings.is_empty(),
3061 "macos keymap should inherit Enter bindings from default, got {} Enter bindings",
3062 enter_bindings.len()
3063 );
3064 let has_insert_newline = enter_bindings.iter().any(|b| b.action == "insert_newline");
3066 assert!(
3067 has_insert_newline,
3068 "macos keymap should have insert_newline action for Enter key"
3069 );
3070 }
3071
3072 #[test]
3073 fn test_config_serialize_deserialize() {
3074 let config = Config::default();
3076
3077 let json = serde_json::to_string_pretty(&config).unwrap();
3079
3080 let loaded: Config = serde_json::from_str(&json).unwrap();
3082
3083 assert_eq!(config.editor.tab_size, loaded.editor.tab_size);
3084 assert_eq!(config.theme, loaded.theme);
3085 }
3086
3087 #[test]
3088 fn test_config_with_custom_keybinding() {
3089 let json = r#"{
3090 "editor": {
3091 "tab_size": 2
3092 },
3093 "keybindings": [
3094 {
3095 "key": "x",
3096 "modifiers": ["ctrl", "shift"],
3097 "action": "custom_action",
3098 "args": {},
3099 "when": null
3100 }
3101 ]
3102 }"#;
3103
3104 let config: Config = serde_json::from_str(json).unwrap();
3105 assert_eq!(config.editor.tab_size, 2);
3106 assert_eq!(config.keybindings.len(), 1);
3107 assert_eq!(config.keybindings[0].key, "x");
3108 assert_eq!(config.keybindings[0].modifiers.len(), 2);
3109 }
3110
3111 #[test]
3112 fn test_sparse_config_merges_with_defaults() {
3113 let temp_dir = tempfile::tempdir().unwrap();
3115 let config_path = temp_dir.path().join("config.json");
3116
3117 let sparse_config = r#"{
3119 "lsp": {
3120 "rust": {
3121 "command": "custom-rust-analyzer",
3122 "args": ["--custom-arg"]
3123 }
3124 }
3125 }"#;
3126 std::fs::write(&config_path, sparse_config).unwrap();
3127
3128 let loaded = Config::load_from_file(&config_path).unwrap();
3130
3131 assert!(loaded.lsp.contains_key("rust"));
3133 assert_eq!(
3134 loaded.lsp["rust"].command,
3135 "custom-rust-analyzer".to_string()
3136 );
3137
3138 assert!(
3140 loaded.lsp.contains_key("python"),
3141 "python LSP should be merged from defaults"
3142 );
3143 assert!(
3144 loaded.lsp.contains_key("typescript"),
3145 "typescript LSP should be merged from defaults"
3146 );
3147 assert!(
3148 loaded.lsp.contains_key("javascript"),
3149 "javascript LSP should be merged from defaults"
3150 );
3151
3152 assert!(loaded.languages.contains_key("rust"));
3154 assert!(loaded.languages.contains_key("python"));
3155 assert!(loaded.languages.contains_key("typescript"));
3156 }
3157
3158 #[test]
3159 fn test_empty_config_gets_all_defaults() {
3160 let temp_dir = tempfile::tempdir().unwrap();
3161 let config_path = temp_dir.path().join("config.json");
3162
3163 std::fs::write(&config_path, "{}").unwrap();
3165
3166 let loaded = Config::load_from_file(&config_path).unwrap();
3167 let defaults = Config::default();
3168
3169 assert_eq!(loaded.lsp.len(), defaults.lsp.len());
3171
3172 assert_eq!(loaded.languages.len(), defaults.languages.len());
3174 }
3175
3176 #[test]
3177 fn test_dynamic_submenu_expansion() {
3178 let dynamic = MenuItem::DynamicSubmenu {
3180 label: "Test".to_string(),
3181 source: "copy_with_theme".to_string(),
3182 };
3183
3184 let expanded = dynamic.expand_dynamic();
3185
3186 match expanded {
3188 MenuItem::Submenu { label, items } => {
3189 assert_eq!(label, "Test");
3190 let loader = crate::view::theme::ThemeLoader::new();
3192 let registry = loader.load_all();
3193 assert_eq!(items.len(), registry.len());
3194
3195 for (item, theme_info) in items.iter().zip(registry.list().iter()) {
3197 match item {
3198 MenuItem::Action {
3199 label,
3200 action,
3201 args,
3202 ..
3203 } => {
3204 assert_eq!(label, &theme_info.name);
3205 assert_eq!(action, "copy_with_theme");
3206 assert_eq!(
3207 args.get("theme").and_then(|v| v.as_str()),
3208 Some(theme_info.name.as_str())
3209 );
3210 }
3211 _ => panic!("Expected Action item"),
3212 }
3213 }
3214 }
3215 _ => panic!("Expected Submenu after expansion"),
3216 }
3217 }
3218
3219 #[test]
3220 fn test_non_dynamic_item_unchanged() {
3221 let action = MenuItem::Action {
3223 label: "Test".to_string(),
3224 action: "test".to_string(),
3225 args: HashMap::new(),
3226 when: None,
3227 checkbox: None,
3228 };
3229
3230 let expanded = action.expand_dynamic();
3231 match expanded {
3232 MenuItem::Action { label, action, .. } => {
3233 assert_eq!(label, "Test");
3234 assert_eq!(action, "test");
3235 }
3236 _ => panic!("Action should remain Action after expand_dynamic"),
3237 }
3238 }
3239
3240 #[test]
3241 fn test_buffer_config_uses_global_defaults() {
3242 let config = Config::default();
3243 let buffer_config = BufferConfig::resolve(&config, None);
3244
3245 assert_eq!(buffer_config.tab_size, config.editor.tab_size);
3246 assert_eq!(buffer_config.auto_indent, config.editor.auto_indent);
3247 assert!(!buffer_config.use_tabs); assert!(buffer_config.show_whitespace_tabs);
3249 assert!(buffer_config.formatter.is_none());
3250 assert!(!buffer_config.format_on_save);
3251 }
3252
3253 #[test]
3254 fn test_buffer_config_applies_language_overrides() {
3255 let mut config = Config::default();
3256
3257 config.languages.insert(
3259 "go".to_string(),
3260 LanguageConfig {
3261 extensions: vec!["go".to_string()],
3262 filenames: vec![],
3263 grammar: "go".to_string(),
3264 comment_prefix: Some("//".to_string()),
3265 auto_indent: true,
3266 highlighter: HighlighterPreference::Auto,
3267 textmate_grammar: None,
3268 show_whitespace_tabs: false, use_tabs: true, tab_size: Some(8), formatter: Some(FormatterConfig {
3272 command: "gofmt".to_string(),
3273 args: vec![],
3274 stdin: true,
3275 timeout_ms: 10000,
3276 }),
3277 format_on_save: true,
3278 on_save: vec![],
3279 },
3280 );
3281
3282 let buffer_config = BufferConfig::resolve(&config, Some("go"));
3283
3284 assert_eq!(buffer_config.tab_size, 8);
3285 assert!(buffer_config.use_tabs);
3286 assert!(!buffer_config.show_whitespace_tabs);
3287 assert!(buffer_config.format_on_save);
3288 assert!(buffer_config.formatter.is_some());
3289 assert_eq!(buffer_config.formatter.as_ref().unwrap().command, "gofmt");
3290 }
3291
3292 #[test]
3293 fn test_buffer_config_unknown_language_uses_global() {
3294 let config = Config::default();
3295 let buffer_config = BufferConfig::resolve(&config, Some("unknown_lang"));
3296
3297 assert_eq!(buffer_config.tab_size, config.editor.tab_size);
3299 assert!(!buffer_config.use_tabs);
3300 }
3301
3302 #[test]
3303 fn test_buffer_config_indent_string() {
3304 let config = Config::default();
3305
3306 let spaces_config = BufferConfig::resolve(&config, None);
3308 assert_eq!(spaces_config.indent_string(), " "); let mut config_with_tabs = Config::default();
3312 config_with_tabs.languages.insert(
3313 "makefile".to_string(),
3314 LanguageConfig {
3315 use_tabs: true,
3316 tab_size: Some(8),
3317 ..Default::default()
3318 },
3319 );
3320 let tabs_config = BufferConfig::resolve(&config_with_tabs, Some("makefile"));
3321 assert_eq!(tabs_config.indent_string(), "\t");
3322 }
3323}