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 to_crossterm_style(self) -> crossterm::cursor::SetCursorStyle {
159 use crossterm::cursor::SetCursorStyle;
160 match self {
161 Self::Default => SetCursorStyle::DefaultUserShape,
162 Self::BlinkingBlock => SetCursorStyle::BlinkingBlock,
163 Self::SteadyBlock => SetCursorStyle::SteadyBlock,
164 Self::BlinkingBar => SetCursorStyle::BlinkingBar,
165 Self::SteadyBar => SetCursorStyle::SteadyBar,
166 Self::BlinkingUnderline => SetCursorStyle::BlinkingUnderScore,
167 Self::SteadyUnderline => SetCursorStyle::SteadyUnderScore,
168 }
169 }
170
171 pub fn parse(s: &str) -> Option<Self> {
173 match s {
174 "default" => Some(CursorStyle::Default),
175 "blinking_block" => Some(CursorStyle::BlinkingBlock),
176 "steady_block" => Some(CursorStyle::SteadyBlock),
177 "blinking_bar" => Some(CursorStyle::BlinkingBar),
178 "steady_bar" => Some(CursorStyle::SteadyBar),
179 "blinking_underline" => Some(CursorStyle::BlinkingUnderline),
180 "steady_underline" => Some(CursorStyle::SteadyUnderline),
181 _ => None,
182 }
183 }
184
185 pub fn as_str(self) -> &'static str {
187 match self {
188 Self::Default => "default",
189 Self::BlinkingBlock => "blinking_block",
190 Self::SteadyBlock => "steady_block",
191 Self::BlinkingBar => "blinking_bar",
192 Self::SteadyBar => "steady_bar",
193 Self::BlinkingUnderline => "blinking_underline",
194 Self::SteadyUnderline => "steady_underline",
195 }
196 }
197}
198
199impl JsonSchema for CursorStyle {
200 fn schema_name() -> Cow<'static, str> {
201 Cow::Borrowed("CursorStyle")
202 }
203
204 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
205 schemars::json_schema!({
206 "description": "Terminal cursor style",
207 "type": "string",
208 "enum": Self::OPTIONS
209 })
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
215#[serde(transparent)]
216pub struct KeybindingMapName(pub String);
217
218impl KeybindingMapName {
219 pub const BUILTIN_OPTIONS: &'static [&'static str] = &["default", "emacs", "vscode", "macos"];
221}
222
223impl Deref for KeybindingMapName {
224 type Target = str;
225 fn deref(&self) -> &Self::Target {
226 &self.0
227 }
228}
229
230impl From<String> for KeybindingMapName {
231 fn from(s: String) -> Self {
232 Self(s)
233 }
234}
235
236impl From<&str> for KeybindingMapName {
237 fn from(s: &str) -> Self {
238 Self(s.to_string())
239 }
240}
241
242impl PartialEq<str> for KeybindingMapName {
243 fn eq(&self, other: &str) -> bool {
244 self.0 == other
245 }
246}
247
248#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
250#[serde(rename_all = "lowercase")]
251pub enum LineEndingOption {
252 #[default]
254 Lf,
255 Crlf,
257 Cr,
259}
260
261impl LineEndingOption {
262 pub fn to_line_ending(&self) -> crate::model::buffer::LineEnding {
264 match self {
265 Self::Lf => crate::model::buffer::LineEnding::LF,
266 Self::Crlf => crate::model::buffer::LineEnding::CRLF,
267 Self::Cr => crate::model::buffer::LineEnding::CR,
268 }
269 }
270}
271
272impl JsonSchema for LineEndingOption {
273 fn schema_name() -> Cow<'static, str> {
274 Cow::Borrowed("LineEndingOption")
275 }
276
277 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
278 schemars::json_schema!({
279 "description": "Default line ending format for new files",
280 "type": "string",
281 "enum": ["lf", "crlf", "cr"],
282 "default": "lf"
283 })
284 }
285}
286
287impl PartialEq<KeybindingMapName> for str {
288 fn eq(&self, other: &KeybindingMapName) -> bool {
289 self == other.0
290 }
291}
292
293impl JsonSchema for KeybindingMapName {
294 fn schema_name() -> Cow<'static, str> {
295 Cow::Borrowed("KeybindingMapOptions")
296 }
297
298 fn json_schema(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
299 schemars::json_schema!({
300 "description": "Available keybinding maps",
301 "type": "string",
302 "enum": Self::BUILTIN_OPTIONS
303 })
304 }
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
309pub struct Config {
310 #[serde(default)]
313 pub version: u32,
314
315 #[serde(default = "default_theme_name")]
317 pub theme: ThemeName,
318
319 #[serde(default)]
322 pub locale: LocaleName,
323
324 #[serde(default = "default_true")]
327 pub check_for_updates: bool,
328
329 #[serde(default)]
331 pub editor: EditorConfig,
332
333 #[serde(default)]
335 pub file_explorer: FileExplorerConfig,
336
337 #[serde(default)]
339 pub file_browser: FileBrowserConfig,
340
341 #[serde(default)]
343 pub terminal: TerminalConfig,
344
345 #[serde(default)]
347 pub keybindings: Vec<Keybinding>,
348
349 #[serde(default)]
352 pub keybinding_maps: HashMap<String, KeymapConfig>,
353
354 #[serde(default = "default_keybinding_map_name")]
356 pub active_keybinding_map: KeybindingMapName,
357
358 #[serde(default)]
360 pub languages: HashMap<String, LanguageConfig>,
361
362 #[serde(default)]
364 pub lsp: HashMap<String, LspServerConfig>,
365
366 #[serde(default)]
368 pub warnings: WarningsConfig,
369
370 #[serde(default)]
374 #[schemars(extend("x-standalone-category" = true, "x-no-add" = true))]
375 pub plugins: HashMap<String, PluginConfig>,
376}
377
378fn default_keybinding_map_name() -> KeybindingMapName {
379 if cfg!(target_os = "macos") {
382 KeybindingMapName("macos".to_string())
383 } else {
384 KeybindingMapName("default".to_string())
385 }
386}
387
388fn default_theme_name() -> ThemeName {
389 ThemeName("high-contrast".to_string())
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
394pub struct EditorConfig {
395 #[serde(default = "default_tab_size")]
397 pub tab_size: usize,
398
399 #[serde(default = "default_true")]
401 pub auto_indent: bool,
402
403 #[serde(default = "default_true")]
405 pub line_numbers: bool,
406
407 #[serde(default = "default_false")]
409 pub relative_line_numbers: bool,
410
411 #[serde(default = "default_scroll_offset")]
413 pub scroll_offset: usize,
414
415 #[serde(default = "default_true")]
417 pub syntax_highlighting: bool,
418
419 #[serde(default = "default_true")]
421 pub line_wrap: bool,
422
423 #[serde(default = "default_highlight_timeout")]
425 pub highlight_timeout_ms: u64,
426
427 #[serde(default = "default_snapshot_interval")]
429 pub snapshot_interval: usize,
430
431 #[serde(default = "default_large_file_threshold")]
438 pub large_file_threshold_bytes: u64,
439
440 #[serde(default = "default_estimated_line_length")]
444 pub estimated_line_length: usize,
445
446 #[serde(default = "default_true")]
448 pub enable_inlay_hints: bool,
449
450 #[serde(default = "default_false")]
454 pub enable_semantic_tokens_full: bool,
455
456 #[serde(default = "default_true")]
460 pub recovery_enabled: bool,
461
462 #[serde(default = "default_auto_save_interval")]
467 pub auto_save_interval_secs: u32,
468
469 #[serde(default = "default_highlight_context_bytes")]
474 pub highlight_context_bytes: usize,
475
476 #[serde(default = "default_true")]
480 pub mouse_hover_enabled: bool,
481
482 #[serde(default = "default_mouse_hover_delay")]
486 pub mouse_hover_delay_ms: u64,
487
488 #[serde(default = "default_double_click_time")]
492 pub double_click_time_ms: u64,
493
494 #[serde(default = "default_auto_revert_poll_interval")]
499 pub auto_revert_poll_interval_ms: u64,
500
501 #[serde(default = "default_file_tree_poll_interval")]
506 pub file_tree_poll_interval_ms: u64,
507
508 #[serde(default)]
513 pub default_line_ending: LineEndingOption,
514
515 #[serde(default)]
519 pub cursor_style: CursorStyle,
520
521 #[serde(default = "default_true")]
526 pub keyboard_disambiguate_escape_codes: bool,
527
528 #[serde(default = "default_false")]
533 pub keyboard_report_event_types: bool,
534
535 #[serde(default = "default_true")]
540 pub keyboard_report_alternate_keys: bool,
541
542 #[serde(default = "default_false")]
548 pub keyboard_report_all_keys_as_escape_codes: bool,
549
550 #[serde(default = "default_true")]
555 pub quick_suggestions: bool,
556
557 #[serde(default = "default_true")]
562 pub show_menu_bar: bool,
563
564 #[serde(default = "default_true")]
569 pub show_tab_bar: bool,
570
571 #[serde(default = "default_false")]
576 pub use_terminal_bg: bool,
577}
578
579fn default_tab_size() -> usize {
580 4
581}
582
583pub const LARGE_FILE_THRESHOLD_BYTES: u64 = 1024 * 1024; fn default_large_file_threshold() -> u64 {
589 LARGE_FILE_THRESHOLD_BYTES
590}
591
592fn default_true() -> bool {
593 true
594}
595
596fn default_false() -> bool {
597 false
598}
599
600fn default_scroll_offset() -> usize {
601 3
602}
603
604fn default_highlight_timeout() -> u64 {
605 5
606}
607
608fn default_snapshot_interval() -> usize {
609 100
610}
611
612fn default_estimated_line_length() -> usize {
613 80
614}
615
616fn default_auto_save_interval() -> u32 {
617 2 }
619
620fn default_highlight_context_bytes() -> usize {
621 10_000 }
623
624fn default_mouse_hover_delay() -> u64 {
625 500 }
627
628fn default_double_click_time() -> u64 {
629 500 }
631
632fn default_auto_revert_poll_interval() -> u64 {
633 2000 }
635
636fn default_file_tree_poll_interval() -> u64 {
637 3000 }
639
640impl Default for EditorConfig {
641 fn default() -> Self {
642 Self {
643 tab_size: default_tab_size(),
644 auto_indent: true,
645 line_numbers: true,
646 relative_line_numbers: false,
647 scroll_offset: default_scroll_offset(),
648 syntax_highlighting: true,
649 line_wrap: true,
650 highlight_timeout_ms: default_highlight_timeout(),
651 snapshot_interval: default_snapshot_interval(),
652 large_file_threshold_bytes: default_large_file_threshold(),
653 estimated_line_length: default_estimated_line_length(),
654 enable_inlay_hints: true,
655 enable_semantic_tokens_full: false,
656 recovery_enabled: true,
657 auto_save_interval_secs: default_auto_save_interval(),
658 highlight_context_bytes: default_highlight_context_bytes(),
659 mouse_hover_enabled: true,
660 mouse_hover_delay_ms: default_mouse_hover_delay(),
661 double_click_time_ms: default_double_click_time(),
662 auto_revert_poll_interval_ms: default_auto_revert_poll_interval(),
663 file_tree_poll_interval_ms: default_file_tree_poll_interval(),
664 default_line_ending: LineEndingOption::default(),
665 cursor_style: CursorStyle::default(),
666 keyboard_disambiguate_escape_codes: true,
667 keyboard_report_event_types: false,
668 keyboard_report_alternate_keys: true,
669 keyboard_report_all_keys_as_escape_codes: false,
670 quick_suggestions: true,
671 show_menu_bar: true,
672 show_tab_bar: true,
673 use_terminal_bg: false,
674 }
675 }
676}
677
678#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
680pub struct FileExplorerConfig {
681 #[serde(default = "default_true")]
683 pub respect_gitignore: bool,
684
685 #[serde(default = "default_false")]
687 pub show_hidden: bool,
688
689 #[serde(default = "default_false")]
691 pub show_gitignored: bool,
692
693 #[serde(default)]
695 pub custom_ignore_patterns: Vec<String>,
696
697 #[serde(default = "default_explorer_width")]
699 pub width: f32,
700}
701
702fn default_explorer_width() -> f32 {
703 0.3 }
705
706#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
708pub struct TerminalConfig {
709 #[serde(default = "default_true")]
712 pub jump_to_end_on_output: bool,
713}
714
715impl Default for TerminalConfig {
716 fn default() -> Self {
717 Self {
718 jump_to_end_on_output: true,
719 }
720 }
721}
722
723#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
725pub struct WarningsConfig {
726 #[serde(default = "default_true")]
729 pub show_status_indicator: bool,
730}
731
732impl Default for WarningsConfig {
733 fn default() -> Self {
734 Self {
735 show_status_indicator: true,
736 }
737 }
738}
739
740pub use fresh_core::config::PluginConfig;
742
743impl Default for FileExplorerConfig {
744 fn default() -> Self {
745 Self {
746 respect_gitignore: true,
747 show_hidden: false,
748 show_gitignored: false,
749 custom_ignore_patterns: Vec::new(),
750 width: default_explorer_width(),
751 }
752 }
753}
754
755#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
757pub struct FileBrowserConfig {
758 #[serde(default = "default_false")]
760 pub show_hidden: bool,
761}
762
763#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
765pub struct KeyPress {
766 pub key: String,
768 #[serde(default)]
770 pub modifiers: Vec<String>,
771}
772
773#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
775#[schemars(extend("x-display-field" = "/action"))]
776pub struct Keybinding {
777 #[serde(default, skip_serializing_if = "String::is_empty")]
779 pub key: String,
780
781 #[serde(default, skip_serializing_if = "Vec::is_empty")]
783 pub modifiers: Vec<String>,
784
785 #[serde(default, skip_serializing_if = "Vec::is_empty")]
788 pub keys: Vec<KeyPress>,
789
790 pub action: String,
792
793 #[serde(default)]
795 pub args: HashMap<String, serde_json::Value>,
796
797 #[serde(default)]
799 pub when: Option<String>,
800}
801
802#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
804#[schemars(extend("x-display-field" = "/inherits"))]
805pub struct KeymapConfig {
806 #[serde(default, skip_serializing_if = "Option::is_none")]
808 pub inherits: Option<String>,
809
810 #[serde(default)]
812 pub bindings: Vec<Keybinding>,
813}
814
815#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
817#[schemars(extend("x-display-field" = "/command"))]
818pub struct FormatterConfig {
819 pub command: String,
821
822 #[serde(default)]
825 pub args: Vec<String>,
826
827 #[serde(default = "default_true")]
830 pub stdin: bool,
831
832 #[serde(default = "default_on_save_timeout")]
834 pub timeout_ms: u64,
835}
836
837#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
839#[schemars(extend("x-display-field" = "/command"))]
840pub struct OnSaveAction {
841 pub command: String,
844
845 #[serde(default)]
848 pub args: Vec<String>,
849
850 #[serde(default)]
852 pub working_dir: Option<String>,
853
854 #[serde(default)]
856 pub stdin: bool,
857
858 #[serde(default = "default_on_save_timeout")]
860 pub timeout_ms: u64,
861
862 #[serde(default = "default_true")]
865 pub enabled: bool,
866}
867
868fn default_on_save_timeout() -> u64 {
869 10000
870}
871
872#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
874#[schemars(extend("x-display-field" = "/grammar"))]
875pub struct LanguageConfig {
876 #[serde(default)]
878 pub extensions: Vec<String>,
879
880 #[serde(default)]
882 pub filenames: Vec<String>,
883
884 #[serde(default)]
886 pub grammar: String,
887
888 #[serde(default)]
890 pub comment_prefix: Option<String>,
891
892 #[serde(default = "default_true")]
894 pub auto_indent: bool,
895
896 #[serde(default)]
898 pub highlighter: HighlighterPreference,
899
900 #[serde(default)]
903 pub textmate_grammar: Option<std::path::PathBuf>,
904
905 #[serde(default = "default_true")]
908 pub show_whitespace_tabs: bool,
909
910 #[serde(default = "default_false")]
914 pub use_tabs: bool,
915
916 #[serde(default)]
919 pub tab_size: Option<usize>,
920
921 #[serde(default)]
923 pub formatter: Option<FormatterConfig>,
924
925 #[serde(default)]
927 pub format_on_save: bool,
928
929 #[serde(default)]
933 pub on_save: Vec<OnSaveAction>,
934}
935
936#[derive(Debug, Clone)]
943pub struct BufferConfig {
944 pub tab_size: usize,
946
947 pub use_tabs: bool,
949
950 pub auto_indent: bool,
952
953 pub show_whitespace_tabs: bool,
955
956 pub formatter: Option<FormatterConfig>,
958
959 pub format_on_save: bool,
961
962 pub on_save: Vec<OnSaveAction>,
964
965 pub highlighter: HighlighterPreference,
967
968 pub textmate_grammar: Option<std::path::PathBuf>,
970}
971
972impl BufferConfig {
973 pub fn resolve(global_config: &Config, language_id: Option<&str>) -> Self {
982 let editor = &global_config.editor;
983
984 let mut config = BufferConfig {
986 tab_size: editor.tab_size,
987 use_tabs: false, auto_indent: editor.auto_indent,
989 show_whitespace_tabs: true, formatter: None,
991 format_on_save: false,
992 on_save: Vec::new(),
993 highlighter: HighlighterPreference::Auto,
994 textmate_grammar: None,
995 };
996
997 if let Some(lang_id) = language_id {
999 if let Some(lang_config) = global_config.languages.get(lang_id) {
1000 if let Some(ts) = lang_config.tab_size {
1002 config.tab_size = ts;
1003 }
1004
1005 config.use_tabs = lang_config.use_tabs;
1007
1008 config.auto_indent = lang_config.auto_indent;
1010
1011 config.show_whitespace_tabs = lang_config.show_whitespace_tabs;
1013
1014 config.formatter = lang_config.formatter.clone();
1016
1017 config.format_on_save = lang_config.format_on_save;
1019
1020 config.on_save = lang_config.on_save.clone();
1022
1023 config.highlighter = lang_config.highlighter;
1025
1026 config.textmate_grammar = lang_config.textmate_grammar.clone();
1028 }
1029 }
1030
1031 config
1032 }
1033
1034 pub fn indent_string(&self) -> String {
1039 if self.use_tabs {
1040 "\t".to_string()
1041 } else {
1042 " ".repeat(self.tab_size)
1043 }
1044 }
1045}
1046
1047#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
1049#[serde(rename_all = "lowercase")]
1050pub enum HighlighterPreference {
1051 #[default]
1053 Auto,
1054 #[serde(rename = "tree-sitter")]
1056 TreeSitter,
1057 #[serde(rename = "textmate")]
1059 TextMate,
1060}
1061
1062#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
1064pub struct MenuConfig {
1065 #[serde(default)]
1067 pub menus: Vec<Menu>,
1068}
1069
1070pub use fresh_core::menu::{Menu, MenuItem};
1072
1073pub trait MenuExt {
1075 fn match_id(&self) -> &str;
1078
1079 fn expand_dynamic_items(&mut self);
1082}
1083
1084impl MenuExt for Menu {
1085 fn match_id(&self) -> &str {
1086 self.id.as_deref().unwrap_or(&self.label)
1087 }
1088
1089 fn expand_dynamic_items(&mut self) {
1090 self.items = self
1091 .items
1092 .iter()
1093 .map(|item| item.expand_dynamic())
1094 .collect();
1095 }
1096}
1097
1098pub trait MenuItemExt {
1100 fn expand_dynamic(&self) -> MenuItem;
1103}
1104
1105impl MenuItemExt for MenuItem {
1106 fn expand_dynamic(&self) -> MenuItem {
1107 match self {
1108 MenuItem::DynamicSubmenu { label, source } => {
1109 let items = generate_dynamic_items(source);
1110 MenuItem::Submenu {
1111 label: label.clone(),
1112 items,
1113 }
1114 }
1115 other => other.clone(),
1116 }
1117 }
1118}
1119
1120pub fn generate_dynamic_items(source: &str) -> Vec<MenuItem> {
1122 match source {
1123 "copy_with_theme" => {
1124 let theme_loader = crate::view::theme::LocalThemeLoader::new();
1126 crate::view::theme::Theme::all_available(&theme_loader)
1127 .into_iter()
1128 .map(|theme_name| {
1129 let mut args = HashMap::new();
1130 args.insert("theme".to_string(), serde_json::json!(theme_name));
1131 MenuItem::Action {
1132 label: theme_name.to_string(),
1133 action: "copy_with_theme".to_string(),
1134 args,
1135 when: Some(context_keys::HAS_SELECTION.to_string()),
1136 checkbox: None,
1137 }
1138 })
1139 .collect()
1140 }
1141 _ => vec![MenuItem::Label {
1142 info: format!("Unknown source: {}", source),
1143 }],
1144 }
1145}
1146
1147impl Default for Config {
1148 fn default() -> Self {
1149 Self {
1150 version: 0,
1151 theme: default_theme_name(),
1152 locale: LocaleName::default(),
1153 check_for_updates: true,
1154 editor: EditorConfig::default(),
1155 file_explorer: FileExplorerConfig::default(),
1156 file_browser: FileBrowserConfig::default(),
1157 terminal: TerminalConfig::default(),
1158 keybindings: vec![], keybinding_maps: HashMap::new(), active_keybinding_map: default_keybinding_map_name(),
1161 languages: Self::default_languages(),
1162 lsp: Self::default_lsp_config(),
1163 warnings: WarningsConfig::default(),
1164 plugins: HashMap::new(), }
1166 }
1167}
1168
1169impl MenuConfig {
1170 pub fn translated() -> Self {
1172 Self {
1173 menus: Self::translated_menus(),
1174 }
1175 }
1176
1177 fn translated_menus() -> Vec<Menu> {
1179 vec![
1180 Menu {
1182 id: Some("File".to_string()),
1183 label: t!("menu.file").to_string(),
1184 items: vec![
1185 MenuItem::Action {
1186 label: t!("menu.file.new_file").to_string(),
1187 action: "new".to_string(),
1188 args: HashMap::new(),
1189 when: None,
1190 checkbox: None,
1191 },
1192 MenuItem::Action {
1193 label: t!("menu.file.open_file").to_string(),
1194 action: "open".to_string(),
1195 args: HashMap::new(),
1196 when: None,
1197 checkbox: None,
1198 },
1199 MenuItem::Separator { separator: true },
1200 MenuItem::Action {
1201 label: t!("menu.file.save").to_string(),
1202 action: "save".to_string(),
1203 args: HashMap::new(),
1204 when: None,
1205 checkbox: None,
1206 },
1207 MenuItem::Action {
1208 label: t!("menu.file.save_as").to_string(),
1209 action: "save_as".to_string(),
1210 args: HashMap::new(),
1211 when: None,
1212 checkbox: None,
1213 },
1214 MenuItem::Action {
1215 label: t!("menu.file.revert").to_string(),
1216 action: "revert".to_string(),
1217 args: HashMap::new(),
1218 when: None,
1219 checkbox: None,
1220 },
1221 MenuItem::Separator { separator: true },
1222 MenuItem::Action {
1223 label: t!("menu.file.close_buffer").to_string(),
1224 action: "close".to_string(),
1225 args: HashMap::new(),
1226 when: None,
1227 checkbox: None,
1228 },
1229 MenuItem::Separator { separator: true },
1230 MenuItem::Action {
1231 label: t!("menu.file.switch_project").to_string(),
1232 action: "switch_project".to_string(),
1233 args: HashMap::new(),
1234 when: None,
1235 checkbox: None,
1236 },
1237 MenuItem::Action {
1238 label: t!("menu.file.quit").to_string(),
1239 action: "quit".to_string(),
1240 args: HashMap::new(),
1241 when: None,
1242 checkbox: None,
1243 },
1244 ],
1245 },
1246 Menu {
1248 id: Some("Edit".to_string()),
1249 label: t!("menu.edit").to_string(),
1250 items: vec![
1251 MenuItem::Action {
1252 label: t!("menu.edit.undo").to_string(),
1253 action: "undo".to_string(),
1254 args: HashMap::new(),
1255 when: None,
1256 checkbox: None,
1257 },
1258 MenuItem::Action {
1259 label: t!("menu.edit.redo").to_string(),
1260 action: "redo".to_string(),
1261 args: HashMap::new(),
1262 when: None,
1263 checkbox: None,
1264 },
1265 MenuItem::Separator { separator: true },
1266 MenuItem::Action {
1267 label: t!("menu.edit.cut").to_string(),
1268 action: "cut".to_string(),
1269 args: HashMap::new(),
1270 when: Some(context_keys::HAS_SELECTION.to_string()),
1271 checkbox: None,
1272 },
1273 MenuItem::Action {
1274 label: t!("menu.edit.copy").to_string(),
1275 action: "copy".to_string(),
1276 args: HashMap::new(),
1277 when: Some(context_keys::HAS_SELECTION.to_string()),
1278 checkbox: None,
1279 },
1280 MenuItem::DynamicSubmenu {
1281 label: t!("menu.edit.copy_with_formatting").to_string(),
1282 source: "copy_with_theme".to_string(),
1283 },
1284 MenuItem::Action {
1285 label: t!("menu.edit.paste").to_string(),
1286 action: "paste".to_string(),
1287 args: HashMap::new(),
1288 when: None,
1289 checkbox: None,
1290 },
1291 MenuItem::Separator { separator: true },
1292 MenuItem::Action {
1293 label: t!("menu.edit.select_all").to_string(),
1294 action: "select_all".to_string(),
1295 args: HashMap::new(),
1296 when: None,
1297 checkbox: None,
1298 },
1299 MenuItem::Separator { separator: true },
1300 MenuItem::Action {
1301 label: t!("menu.edit.find").to_string(),
1302 action: "search".to_string(),
1303 args: HashMap::new(),
1304 when: None,
1305 checkbox: None,
1306 },
1307 MenuItem::Action {
1308 label: t!("menu.edit.find_in_selection").to_string(),
1309 action: "find_in_selection".to_string(),
1310 args: HashMap::new(),
1311 when: Some(context_keys::HAS_SELECTION.to_string()),
1312 checkbox: None,
1313 },
1314 MenuItem::Action {
1315 label: t!("menu.edit.find_next").to_string(),
1316 action: "find_next".to_string(),
1317 args: HashMap::new(),
1318 when: None,
1319 checkbox: None,
1320 },
1321 MenuItem::Action {
1322 label: t!("menu.edit.find_previous").to_string(),
1323 action: "find_previous".to_string(),
1324 args: HashMap::new(),
1325 when: None,
1326 checkbox: None,
1327 },
1328 MenuItem::Action {
1329 label: t!("menu.edit.replace").to_string(),
1330 action: "query_replace".to_string(),
1331 args: HashMap::new(),
1332 when: None,
1333 checkbox: None,
1334 },
1335 MenuItem::Separator { separator: true },
1336 MenuItem::Action {
1337 label: t!("menu.edit.delete_line").to_string(),
1338 action: "delete_line".to_string(),
1339 args: HashMap::new(),
1340 when: None,
1341 checkbox: None,
1342 },
1343 MenuItem::Action {
1344 label: t!("menu.edit.format_buffer").to_string(),
1345 action: "format_buffer".to_string(),
1346 args: HashMap::new(),
1347 when: Some(context_keys::FORMATTER_AVAILABLE.to_string()),
1348 checkbox: None,
1349 },
1350 ],
1351 },
1352 Menu {
1354 id: Some("View".to_string()),
1355 label: t!("menu.view").to_string(),
1356 items: vec![
1357 MenuItem::Action {
1358 label: t!("menu.view.file_explorer").to_string(),
1359 action: "toggle_file_explorer".to_string(),
1360 args: HashMap::new(),
1361 when: None,
1362 checkbox: Some(context_keys::FILE_EXPLORER.to_string()),
1363 },
1364 MenuItem::Separator { separator: true },
1365 MenuItem::Action {
1366 label: t!("menu.view.line_numbers").to_string(),
1367 action: "toggle_line_numbers".to_string(),
1368 args: HashMap::new(),
1369 when: None,
1370 checkbox: Some(context_keys::LINE_NUMBERS.to_string()),
1371 },
1372 MenuItem::Action {
1373 label: t!("menu.view.line_wrap").to_string(),
1374 action: "toggle_line_wrap".to_string(),
1375 args: HashMap::new(),
1376 when: None,
1377 checkbox: Some(context_keys::LINE_WRAP.to_string()),
1378 },
1379 MenuItem::Action {
1380 label: t!("menu.view.mouse_support").to_string(),
1381 action: "toggle_mouse_capture".to_string(),
1382 args: HashMap::new(),
1383 when: None,
1384 checkbox: Some(context_keys::MOUSE_CAPTURE.to_string()),
1385 },
1386 MenuItem::Separator { separator: true },
1387 MenuItem::Action {
1388 label: t!("menu.view.set_background").to_string(),
1389 action: "set_background".to_string(),
1390 args: HashMap::new(),
1391 when: None,
1392 checkbox: None,
1393 },
1394 MenuItem::Action {
1395 label: t!("menu.view.set_background_blend").to_string(),
1396 action: "set_background_blend".to_string(),
1397 args: HashMap::new(),
1398 when: None,
1399 checkbox: None,
1400 },
1401 MenuItem::Action {
1402 label: t!("menu.view.set_compose_width").to_string(),
1403 action: "set_compose_width".to_string(),
1404 args: HashMap::new(),
1405 when: None,
1406 checkbox: None,
1407 },
1408 MenuItem::Separator { separator: true },
1409 MenuItem::Action {
1410 label: t!("menu.view.select_theme").to_string(),
1411 action: "select_theme".to_string(),
1412 args: HashMap::new(),
1413 when: None,
1414 checkbox: None,
1415 },
1416 MenuItem::Action {
1417 label: t!("menu.view.select_locale").to_string(),
1418 action: "select_locale".to_string(),
1419 args: HashMap::new(),
1420 when: None,
1421 checkbox: None,
1422 },
1423 MenuItem::Action {
1424 label: t!("menu.view.settings").to_string(),
1425 action: "open_settings".to_string(),
1426 args: HashMap::new(),
1427 when: None,
1428 checkbox: None,
1429 },
1430 MenuItem::Action {
1431 label: t!("menu.view.calibrate_input").to_string(),
1432 action: "calibrate_input".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.view.split_horizontal").to_string(),
1440 action: "split_horizontal".to_string(),
1441 args: HashMap::new(),
1442 when: None,
1443 checkbox: None,
1444 },
1445 MenuItem::Action {
1446 label: t!("menu.view.split_vertical").to_string(),
1447 action: "split_vertical".to_string(),
1448 args: HashMap::new(),
1449 when: None,
1450 checkbox: None,
1451 },
1452 MenuItem::Action {
1453 label: t!("menu.view.close_split").to_string(),
1454 action: "close_split".to_string(),
1455 args: HashMap::new(),
1456 when: None,
1457 checkbox: None,
1458 },
1459 MenuItem::Action {
1460 label: t!("menu.view.focus_next_split").to_string(),
1461 action: "next_split".to_string(),
1462 args: HashMap::new(),
1463 when: None,
1464 checkbox: None,
1465 },
1466 MenuItem::Action {
1467 label: t!("menu.view.focus_prev_split").to_string(),
1468 action: "prev_split".to_string(),
1469 args: HashMap::new(),
1470 when: None,
1471 checkbox: None,
1472 },
1473 MenuItem::Action {
1474 label: t!("menu.view.toggle_maximize_split").to_string(),
1475 action: "toggle_maximize_split".to_string(),
1476 args: HashMap::new(),
1477 when: None,
1478 checkbox: None,
1479 },
1480 MenuItem::Separator { separator: true },
1481 MenuItem::Submenu {
1482 label: t!("menu.terminal").to_string(),
1483 items: vec![
1484 MenuItem::Action {
1485 label: t!("menu.terminal.open").to_string(),
1486 action: "open_terminal".to_string(),
1487 args: HashMap::new(),
1488 when: None,
1489 checkbox: None,
1490 },
1491 MenuItem::Action {
1492 label: t!("menu.terminal.close").to_string(),
1493 action: "close_terminal".to_string(),
1494 args: HashMap::new(),
1495 when: None,
1496 checkbox: None,
1497 },
1498 MenuItem::Separator { separator: true },
1499 MenuItem::Action {
1500 label: t!("menu.terminal.toggle_keyboard_capture").to_string(),
1501 action: "toggle_keyboard_capture".to_string(),
1502 args: HashMap::new(),
1503 when: None,
1504 checkbox: None,
1505 },
1506 ],
1507 },
1508 MenuItem::Separator { separator: true },
1509 MenuItem::Submenu {
1510 label: t!("menu.view.keybinding_style").to_string(),
1511 items: vec![
1512 MenuItem::Action {
1513 label: t!("menu.view.keybinding_default").to_string(),
1514 action: "switch_keybinding_map".to_string(),
1515 args: {
1516 let mut map = HashMap::new();
1517 map.insert("map".to_string(), serde_json::json!("default"));
1518 map
1519 },
1520 when: None,
1521 checkbox: None,
1522 },
1523 MenuItem::Action {
1524 label: t!("menu.view.keybinding_emacs").to_string(),
1525 action: "switch_keybinding_map".to_string(),
1526 args: {
1527 let mut map = HashMap::new();
1528 map.insert("map".to_string(), serde_json::json!("emacs"));
1529 map
1530 },
1531 when: None,
1532 checkbox: None,
1533 },
1534 MenuItem::Action {
1535 label: t!("menu.view.keybinding_vscode").to_string(),
1536 action: "switch_keybinding_map".to_string(),
1537 args: {
1538 let mut map = HashMap::new();
1539 map.insert("map".to_string(), serde_json::json!("vscode"));
1540 map
1541 },
1542 when: None,
1543 checkbox: None,
1544 },
1545 ],
1546 },
1547 ],
1548 },
1549 Menu {
1551 id: Some("Selection".to_string()),
1552 label: t!("menu.selection").to_string(),
1553 items: vec![
1554 MenuItem::Action {
1555 label: t!("menu.selection.select_all").to_string(),
1556 action: "select_all".to_string(),
1557 args: HashMap::new(),
1558 when: None,
1559 checkbox: None,
1560 },
1561 MenuItem::Action {
1562 label: t!("menu.selection.select_word").to_string(),
1563 action: "select_word".to_string(),
1564 args: HashMap::new(),
1565 when: None,
1566 checkbox: None,
1567 },
1568 MenuItem::Action {
1569 label: t!("menu.selection.select_line").to_string(),
1570 action: "select_line".to_string(),
1571 args: HashMap::new(),
1572 when: None,
1573 checkbox: None,
1574 },
1575 MenuItem::Action {
1576 label: t!("menu.selection.expand_selection").to_string(),
1577 action: "expand_selection".to_string(),
1578 args: HashMap::new(),
1579 when: None,
1580 checkbox: None,
1581 },
1582 MenuItem::Separator { separator: true },
1583 MenuItem::Action {
1584 label: t!("menu.selection.add_cursor_above").to_string(),
1585 action: "add_cursor_above".to_string(),
1586 args: HashMap::new(),
1587 when: None,
1588 checkbox: None,
1589 },
1590 MenuItem::Action {
1591 label: t!("menu.selection.add_cursor_below").to_string(),
1592 action: "add_cursor_below".to_string(),
1593 args: HashMap::new(),
1594 when: None,
1595 checkbox: None,
1596 },
1597 MenuItem::Action {
1598 label: t!("menu.selection.add_cursor_next_match").to_string(),
1599 action: "add_cursor_next_match".to_string(),
1600 args: HashMap::new(),
1601 when: None,
1602 checkbox: None,
1603 },
1604 MenuItem::Action {
1605 label: t!("menu.selection.remove_secondary_cursors").to_string(),
1606 action: "remove_secondary_cursors".to_string(),
1607 args: HashMap::new(),
1608 when: None,
1609 checkbox: None,
1610 },
1611 ],
1612 },
1613 Menu {
1615 id: Some("Go".to_string()),
1616 label: t!("menu.go").to_string(),
1617 items: vec![
1618 MenuItem::Action {
1619 label: t!("menu.go.goto_line").to_string(),
1620 action: "goto_line".to_string(),
1621 args: HashMap::new(),
1622 when: None,
1623 checkbox: None,
1624 },
1625 MenuItem::Action {
1626 label: t!("menu.go.goto_definition").to_string(),
1627 action: "lsp_goto_definition".to_string(),
1628 args: HashMap::new(),
1629 when: None,
1630 checkbox: None,
1631 },
1632 MenuItem::Action {
1633 label: t!("menu.go.find_references").to_string(),
1634 action: "lsp_references".to_string(),
1635 args: HashMap::new(),
1636 when: None,
1637 checkbox: None,
1638 },
1639 MenuItem::Separator { separator: true },
1640 MenuItem::Action {
1641 label: t!("menu.go.next_buffer").to_string(),
1642 action: "next_buffer".to_string(),
1643 args: HashMap::new(),
1644 when: None,
1645 checkbox: None,
1646 },
1647 MenuItem::Action {
1648 label: t!("menu.go.prev_buffer").to_string(),
1649 action: "prev_buffer".to_string(),
1650 args: HashMap::new(),
1651 when: None,
1652 checkbox: None,
1653 },
1654 MenuItem::Separator { separator: true },
1655 MenuItem::Action {
1656 label: t!("menu.go.command_palette").to_string(),
1657 action: "command_palette".to_string(),
1658 args: HashMap::new(),
1659 when: None,
1660 checkbox: None,
1661 },
1662 ],
1663 },
1664 Menu {
1666 id: Some("LSP".to_string()),
1667 label: t!("menu.lsp").to_string(),
1668 items: vec![
1669 MenuItem::Action {
1670 label: t!("menu.lsp.show_hover").to_string(),
1671 action: "lsp_hover".to_string(),
1672 args: HashMap::new(),
1673 when: Some(context_keys::LSP_AVAILABLE.to_string()),
1674 checkbox: None,
1675 },
1676 MenuItem::Action {
1677 label: t!("menu.lsp.goto_definition").to_string(),
1678 action: "lsp_goto_definition".to_string(),
1679 args: HashMap::new(),
1680 when: Some(context_keys::LSP_AVAILABLE.to_string()),
1681 checkbox: None,
1682 },
1683 MenuItem::Action {
1684 label: t!("menu.lsp.find_references").to_string(),
1685 action: "lsp_references".to_string(),
1686 args: HashMap::new(),
1687 when: Some(context_keys::LSP_AVAILABLE.to_string()),
1688 checkbox: None,
1689 },
1690 MenuItem::Action {
1691 label: t!("menu.lsp.rename_symbol").to_string(),
1692 action: "lsp_rename".to_string(),
1693 args: HashMap::new(),
1694 when: Some(context_keys::LSP_AVAILABLE.to_string()),
1695 checkbox: None,
1696 },
1697 MenuItem::Separator { separator: true },
1698 MenuItem::Action {
1699 label: t!("menu.lsp.show_completions").to_string(),
1700 action: "lsp_completion".to_string(),
1701 args: HashMap::new(),
1702 when: Some(context_keys::LSP_AVAILABLE.to_string()),
1703 checkbox: None,
1704 },
1705 MenuItem::Action {
1706 label: t!("menu.lsp.show_signature").to_string(),
1707 action: "lsp_signature_help".to_string(),
1708 args: HashMap::new(),
1709 when: Some(context_keys::LSP_AVAILABLE.to_string()),
1710 checkbox: None,
1711 },
1712 MenuItem::Action {
1713 label: t!("menu.lsp.code_actions").to_string(),
1714 action: "lsp_code_actions".to_string(),
1715 args: HashMap::new(),
1716 when: Some(context_keys::LSP_AVAILABLE.to_string()),
1717 checkbox: None,
1718 },
1719 MenuItem::Separator { separator: true },
1720 MenuItem::Action {
1721 label: t!("menu.lsp.toggle_inlay_hints").to_string(),
1722 action: "toggle_inlay_hints".to_string(),
1723 args: HashMap::new(),
1724 when: Some(context_keys::LSP_AVAILABLE.to_string()),
1725 checkbox: Some(context_keys::INLAY_HINTS.to_string()),
1726 },
1727 MenuItem::Action {
1728 label: t!("menu.lsp.toggle_mouse_hover").to_string(),
1729 action: "toggle_mouse_hover".to_string(),
1730 args: HashMap::new(),
1731 when: None,
1732 checkbox: Some(context_keys::MOUSE_HOVER.to_string()),
1733 },
1734 MenuItem::Separator { separator: true },
1735 MenuItem::Action {
1736 label: t!("menu.lsp.restart_server").to_string(),
1737 action: "lsp_restart".to_string(),
1738 args: HashMap::new(),
1739 when: None,
1740 checkbox: None,
1741 },
1742 MenuItem::Action {
1743 label: t!("menu.lsp.stop_server").to_string(),
1744 action: "lsp_stop".to_string(),
1745 args: HashMap::new(),
1746 when: None,
1747 checkbox: None,
1748 },
1749 ],
1750 },
1751 Menu {
1753 id: Some("Explorer".to_string()),
1754 label: t!("menu.explorer").to_string(),
1755 items: vec![
1756 MenuItem::Action {
1757 label: t!("menu.explorer.new_file").to_string(),
1758 action: "file_explorer_new_file".to_string(),
1759 args: HashMap::new(),
1760 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1761 checkbox: None,
1762 },
1763 MenuItem::Action {
1764 label: t!("menu.explorer.new_folder").to_string(),
1765 action: "file_explorer_new_directory".to_string(),
1766 args: HashMap::new(),
1767 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1768 checkbox: None,
1769 },
1770 MenuItem::Separator { separator: true },
1771 MenuItem::Action {
1772 label: t!("menu.explorer.open").to_string(),
1773 action: "file_explorer_open".to_string(),
1774 args: HashMap::new(),
1775 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1776 checkbox: None,
1777 },
1778 MenuItem::Action {
1779 label: t!("menu.explorer.rename").to_string(),
1780 action: "file_explorer_rename".to_string(),
1781 args: HashMap::new(),
1782 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1783 checkbox: None,
1784 },
1785 MenuItem::Action {
1786 label: t!("menu.explorer.delete").to_string(),
1787 action: "file_explorer_delete".to_string(),
1788 args: HashMap::new(),
1789 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1790 checkbox: None,
1791 },
1792 MenuItem::Separator { separator: true },
1793 MenuItem::Action {
1794 label: t!("menu.explorer.refresh").to_string(),
1795 action: "file_explorer_refresh".to_string(),
1796 args: HashMap::new(),
1797 when: Some(context_keys::FILE_EXPLORER_FOCUSED.to_string()),
1798 checkbox: None,
1799 },
1800 MenuItem::Separator { separator: true },
1801 MenuItem::Action {
1802 label: t!("menu.explorer.show_hidden").to_string(),
1803 action: "file_explorer_toggle_hidden".to_string(),
1804 args: HashMap::new(),
1805 when: Some(context_keys::FILE_EXPLORER.to_string()),
1806 checkbox: Some(context_keys::FILE_EXPLORER_SHOW_HIDDEN.to_string()),
1807 },
1808 MenuItem::Action {
1809 label: t!("menu.explorer.show_gitignored").to_string(),
1810 action: "file_explorer_toggle_gitignored".to_string(),
1811 args: HashMap::new(),
1812 when: Some(context_keys::FILE_EXPLORER.to_string()),
1813 checkbox: Some(context_keys::FILE_EXPLORER_SHOW_GITIGNORED.to_string()),
1814 },
1815 ],
1816 },
1817 Menu {
1819 id: Some("Help".to_string()),
1820 label: t!("menu.help").to_string(),
1821 items: vec![
1822 MenuItem::Label {
1823 info: format!("Fresh v{}", env!("CARGO_PKG_VERSION")),
1824 },
1825 MenuItem::Separator { separator: true },
1826 MenuItem::Action {
1827 label: t!("menu.help.show_manual").to_string(),
1828 action: "show_help".to_string(),
1829 args: HashMap::new(),
1830 when: None,
1831 checkbox: None,
1832 },
1833 MenuItem::Action {
1834 label: t!("menu.help.keyboard_shortcuts").to_string(),
1835 action: "keyboard_shortcuts".to_string(),
1836 args: HashMap::new(),
1837 when: None,
1838 checkbox: None,
1839 },
1840 ],
1841 },
1842 ]
1843 }
1844}
1845
1846impl Config {
1847 pub(crate) const FILENAME: &'static str = "config.json";
1849
1850 pub(crate) fn local_config_path(working_dir: &Path) -> std::path::PathBuf {
1852 working_dir.join(Self::FILENAME)
1853 }
1854
1855 pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
1861 let contents = std::fs::read_to_string(path.as_ref())
1862 .map_err(|e| ConfigError::IoError(e.to_string()))?;
1863
1864 let partial: crate::partial_config::PartialConfig =
1866 serde_json::from_str(&contents).map_err(|e| ConfigError::ParseError(e.to_string()))?;
1867
1868 Ok(partial.resolve())
1869 }
1870
1871 fn load_builtin_keymap(name: &str) -> Option<KeymapConfig> {
1873 let json_content = match name {
1874 "default" => include_str!("../keymaps/default.json"),
1875 "emacs" => include_str!("../keymaps/emacs.json"),
1876 "vscode" => include_str!("../keymaps/vscode.json"),
1877 "macos" => include_str!("../keymaps/macos.json"),
1878 _ => return None,
1879 };
1880
1881 match serde_json::from_str(json_content) {
1882 Ok(config) => Some(config),
1883 Err(e) => {
1884 eprintln!("Failed to parse builtin keymap '{}': {}", name, e);
1885 None
1886 }
1887 }
1888 }
1889
1890 pub fn resolve_keymap(&self, map_name: &str) -> Vec<Keybinding> {
1893 let mut visited = std::collections::HashSet::new();
1894 self.resolve_keymap_recursive(map_name, &mut visited)
1895 }
1896
1897 fn resolve_keymap_recursive(
1899 &self,
1900 map_name: &str,
1901 visited: &mut std::collections::HashSet<String>,
1902 ) -> Vec<Keybinding> {
1903 if visited.contains(map_name) {
1905 eprintln!(
1906 "Warning: Circular inheritance detected in keymap '{}'",
1907 map_name
1908 );
1909 return Vec::new();
1910 }
1911 visited.insert(map_name.to_string());
1912
1913 let keymap = self
1915 .keybinding_maps
1916 .get(map_name)
1917 .cloned()
1918 .or_else(|| Self::load_builtin_keymap(map_name));
1919
1920 let Some(keymap) = keymap else {
1921 return Vec::new();
1922 };
1923
1924 let mut all_bindings = if let Some(ref parent_name) = keymap.inherits {
1926 self.resolve_keymap_recursive(parent_name, visited)
1927 } else {
1928 Vec::new()
1929 };
1930
1931 all_bindings.extend(keymap.bindings);
1933
1934 all_bindings
1935 }
1936 fn default_languages() -> HashMap<String, LanguageConfig> {
1938 let mut languages = HashMap::new();
1939
1940 languages.insert(
1941 "rust".to_string(),
1942 LanguageConfig {
1943 extensions: vec!["rs".to_string()],
1944 filenames: vec![],
1945 grammar: "rust".to_string(),
1946 comment_prefix: Some("//".to_string()),
1947 auto_indent: true,
1948 highlighter: HighlighterPreference::Auto,
1949 textmate_grammar: None,
1950 show_whitespace_tabs: true,
1951 use_tabs: false,
1952 tab_size: None,
1953 formatter: Some(FormatterConfig {
1954 command: "rustfmt".to_string(),
1955 args: vec!["--edition".to_string(), "2021".to_string()],
1956 stdin: true,
1957 timeout_ms: 10000,
1958 }),
1959 format_on_save: false,
1960 on_save: vec![],
1961 },
1962 );
1963
1964 languages.insert(
1965 "javascript".to_string(),
1966 LanguageConfig {
1967 extensions: vec!["js".to_string(), "jsx".to_string(), "mjs".to_string()],
1968 filenames: vec![],
1969 grammar: "javascript".to_string(),
1970 comment_prefix: Some("//".to_string()),
1971 auto_indent: true,
1972 highlighter: HighlighterPreference::Auto,
1973 textmate_grammar: None,
1974 show_whitespace_tabs: true,
1975 use_tabs: false,
1976 tab_size: None,
1977 formatter: Some(FormatterConfig {
1978 command: "prettier".to_string(),
1979 args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
1980 stdin: true,
1981 timeout_ms: 10000,
1982 }),
1983 format_on_save: false,
1984 on_save: vec![],
1985 },
1986 );
1987
1988 languages.insert(
1989 "typescript".to_string(),
1990 LanguageConfig {
1991 extensions: vec!["ts".to_string(), "tsx".to_string(), "mts".to_string()],
1992 filenames: vec![],
1993 grammar: "typescript".to_string(),
1994 comment_prefix: Some("//".to_string()),
1995 auto_indent: true,
1996 highlighter: HighlighterPreference::Auto,
1997 textmate_grammar: None,
1998 show_whitespace_tabs: true,
1999 use_tabs: false,
2000 tab_size: None,
2001 formatter: Some(FormatterConfig {
2002 command: "prettier".to_string(),
2003 args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2004 stdin: true,
2005 timeout_ms: 10000,
2006 }),
2007 format_on_save: false,
2008 on_save: vec![],
2009 },
2010 );
2011
2012 languages.insert(
2013 "python".to_string(),
2014 LanguageConfig {
2015 extensions: vec!["py".to_string(), "pyi".to_string()],
2016 filenames: vec![],
2017 grammar: "python".to_string(),
2018 comment_prefix: Some("#".to_string()),
2019 auto_indent: true,
2020 highlighter: HighlighterPreference::Auto,
2021 textmate_grammar: None,
2022 show_whitespace_tabs: true,
2023 use_tabs: false,
2024 tab_size: None,
2025 formatter: Some(FormatterConfig {
2026 command: "ruff".to_string(),
2027 args: vec![
2028 "format".to_string(),
2029 "--stdin-filename".to_string(),
2030 "$FILE".to_string(),
2031 ],
2032 stdin: true,
2033 timeout_ms: 10000,
2034 }),
2035 format_on_save: false,
2036 on_save: vec![],
2037 },
2038 );
2039
2040 languages.insert(
2041 "c".to_string(),
2042 LanguageConfig {
2043 extensions: vec!["c".to_string(), "h".to_string()],
2044 filenames: vec![],
2045 grammar: "c".to_string(),
2046 comment_prefix: Some("//".to_string()),
2047 auto_indent: true,
2048 highlighter: HighlighterPreference::Auto,
2049 textmate_grammar: None,
2050 show_whitespace_tabs: true,
2051 use_tabs: false,
2052 tab_size: None,
2053 formatter: Some(FormatterConfig {
2054 command: "clang-format".to_string(),
2055 args: vec![],
2056 stdin: true,
2057 timeout_ms: 10000,
2058 }),
2059 format_on_save: false,
2060 on_save: vec![],
2061 },
2062 );
2063
2064 languages.insert(
2065 "cpp".to_string(),
2066 LanguageConfig {
2067 extensions: vec![
2068 "cpp".to_string(),
2069 "cc".to_string(),
2070 "cxx".to_string(),
2071 "hpp".to_string(),
2072 "hh".to_string(),
2073 "hxx".to_string(),
2074 ],
2075 filenames: vec![],
2076 grammar: "cpp".to_string(),
2077 comment_prefix: Some("//".to_string()),
2078 auto_indent: true,
2079 highlighter: HighlighterPreference::Auto,
2080 textmate_grammar: None,
2081 show_whitespace_tabs: true,
2082 use_tabs: false,
2083 tab_size: None,
2084 formatter: Some(FormatterConfig {
2085 command: "clang-format".to_string(),
2086 args: vec![],
2087 stdin: true,
2088 timeout_ms: 10000,
2089 }),
2090 format_on_save: false,
2091 on_save: vec![],
2092 },
2093 );
2094
2095 languages.insert(
2096 "csharp".to_string(),
2097 LanguageConfig {
2098 extensions: vec!["cs".to_string()],
2099 filenames: vec![],
2100 grammar: "c_sharp".to_string(),
2101 comment_prefix: Some("//".to_string()),
2102 auto_indent: true,
2103 highlighter: HighlighterPreference::Auto,
2104 textmate_grammar: None,
2105 show_whitespace_tabs: true,
2106 use_tabs: false,
2107 tab_size: None,
2108 formatter: None,
2109 format_on_save: false,
2110 on_save: vec![],
2111 },
2112 );
2113
2114 languages.insert(
2115 "bash".to_string(),
2116 LanguageConfig {
2117 extensions: vec!["sh".to_string(), "bash".to_string()],
2118 filenames: vec![
2119 ".bash_aliases".to_string(),
2120 ".bash_logout".to_string(),
2121 ".bash_profile".to_string(),
2122 ".bashrc".to_string(),
2123 ".env".to_string(),
2124 ".profile".to_string(),
2125 ".zlogin".to_string(),
2126 ".zlogout".to_string(),
2127 ".zprofile".to_string(),
2128 ".zshenv".to_string(),
2129 ".zshrc".to_string(),
2130 "PKGBUILD".to_string(),
2132 "APKBUILD".to_string(),
2133 ],
2134 grammar: "bash".to_string(),
2135 comment_prefix: Some("#".to_string()),
2136 auto_indent: true,
2137 highlighter: HighlighterPreference::Auto,
2138 textmate_grammar: None,
2139 show_whitespace_tabs: true,
2140 use_tabs: false,
2141 tab_size: None,
2142 formatter: None,
2143 format_on_save: false,
2144 on_save: vec![],
2145 },
2146 );
2147
2148 languages.insert(
2149 "makefile".to_string(),
2150 LanguageConfig {
2151 extensions: vec!["mk".to_string()],
2152 filenames: vec![
2153 "Makefile".to_string(),
2154 "makefile".to_string(),
2155 "GNUmakefile".to_string(),
2156 ],
2157 grammar: "make".to_string(),
2158 comment_prefix: Some("#".to_string()),
2159 auto_indent: false,
2160 highlighter: HighlighterPreference::Auto,
2161 textmate_grammar: None,
2162 show_whitespace_tabs: true,
2163 use_tabs: true, tab_size: Some(8), formatter: None,
2166 format_on_save: false,
2167 on_save: vec![],
2168 },
2169 );
2170
2171 languages.insert(
2172 "dockerfile".to_string(),
2173 LanguageConfig {
2174 extensions: vec!["dockerfile".to_string()],
2175 filenames: vec!["Dockerfile".to_string(), "Containerfile".to_string()],
2176 grammar: "dockerfile".to_string(),
2177 comment_prefix: Some("#".to_string()),
2178 auto_indent: true,
2179 highlighter: HighlighterPreference::Auto,
2180 textmate_grammar: None,
2181 show_whitespace_tabs: true,
2182 use_tabs: false,
2183 tab_size: None,
2184 formatter: None,
2185 format_on_save: false,
2186 on_save: vec![],
2187 },
2188 );
2189
2190 languages.insert(
2191 "json".to_string(),
2192 LanguageConfig {
2193 extensions: vec!["json".to_string(), "jsonc".to_string()],
2194 filenames: vec![],
2195 grammar: "json".to_string(),
2196 comment_prefix: None,
2197 auto_indent: true,
2198 highlighter: HighlighterPreference::Auto,
2199 textmate_grammar: None,
2200 show_whitespace_tabs: true,
2201 use_tabs: false,
2202 tab_size: None,
2203 formatter: Some(FormatterConfig {
2204 command: "prettier".to_string(),
2205 args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2206 stdin: true,
2207 timeout_ms: 10000,
2208 }),
2209 format_on_save: false,
2210 on_save: vec![],
2211 },
2212 );
2213
2214 languages.insert(
2215 "toml".to_string(),
2216 LanguageConfig {
2217 extensions: vec!["toml".to_string()],
2218 filenames: vec!["Cargo.lock".to_string()],
2219 grammar: "toml".to_string(),
2220 comment_prefix: Some("#".to_string()),
2221 auto_indent: true,
2222 highlighter: HighlighterPreference::Auto,
2223 textmate_grammar: None,
2224 show_whitespace_tabs: true,
2225 use_tabs: false,
2226 tab_size: None,
2227 formatter: None,
2228 format_on_save: false,
2229 on_save: vec![],
2230 },
2231 );
2232
2233 languages.insert(
2234 "yaml".to_string(),
2235 LanguageConfig {
2236 extensions: vec!["yml".to_string(), "yaml".to_string()],
2237 filenames: vec![],
2238 grammar: "yaml".to_string(),
2239 comment_prefix: Some("#".to_string()),
2240 auto_indent: true,
2241 highlighter: HighlighterPreference::Auto,
2242 textmate_grammar: None,
2243 show_whitespace_tabs: true,
2244 use_tabs: false,
2245 tab_size: None,
2246 formatter: Some(FormatterConfig {
2247 command: "prettier".to_string(),
2248 args: vec!["--stdin-filepath".to_string(), "$FILE".to_string()],
2249 stdin: true,
2250 timeout_ms: 10000,
2251 }),
2252 format_on_save: false,
2253 on_save: vec![],
2254 },
2255 );
2256
2257 languages.insert(
2258 "markdown".to_string(),
2259 LanguageConfig {
2260 extensions: vec!["md".to_string(), "markdown".to_string()],
2261 filenames: vec!["README".to_string()],
2262 grammar: "markdown".to_string(),
2263 comment_prefix: None,
2264 auto_indent: false,
2265 highlighter: HighlighterPreference::Auto,
2266 textmate_grammar: None,
2267 show_whitespace_tabs: true,
2268 use_tabs: false,
2269 tab_size: None,
2270 formatter: None,
2271 format_on_save: false,
2272 on_save: vec![],
2273 },
2274 );
2275
2276 languages.insert(
2278 "go".to_string(),
2279 LanguageConfig {
2280 extensions: vec!["go".to_string()],
2281 filenames: vec![],
2282 grammar: "go".to_string(),
2283 comment_prefix: Some("//".to_string()),
2284 auto_indent: true,
2285 highlighter: HighlighterPreference::Auto,
2286 textmate_grammar: None,
2287 show_whitespace_tabs: false,
2288 use_tabs: true, tab_size: Some(8), formatter: Some(FormatterConfig {
2291 command: "gofmt".to_string(),
2292 args: vec![],
2293 stdin: true,
2294 timeout_ms: 10000,
2295 }),
2296 format_on_save: false,
2297 on_save: vec![],
2298 },
2299 );
2300
2301 languages.insert(
2302 "odin".to_string(),
2303 LanguageConfig {
2304 extensions: vec!["odin".to_string()],
2305 filenames: vec![],
2306 grammar: "odin".to_string(),
2307 comment_prefix: Some("//".to_string()),
2308 auto_indent: true,
2309 highlighter: HighlighterPreference::Auto,
2310 textmate_grammar: None,
2311 show_whitespace_tabs: false,
2312 use_tabs: true,
2313 tab_size: Some(8),
2314 formatter: None,
2315 format_on_save: false,
2316 on_save: vec![],
2317 },
2318 );
2319
2320 languages.insert(
2321 "zig".to_string(),
2322 LanguageConfig {
2323 extensions: vec!["zig".to_string(), "zon".to_string()],
2324 filenames: vec![],
2325 grammar: "zig".to_string(),
2326 comment_prefix: Some("//".to_string()),
2327 auto_indent: true,
2328 highlighter: HighlighterPreference::Auto,
2329 textmate_grammar: None,
2330 show_whitespace_tabs: true,
2331 use_tabs: false,
2332 tab_size: None,
2333 formatter: None,
2334 format_on_save: false,
2335 on_save: vec![],
2336 },
2337 );
2338
2339 languages.insert(
2340 "java".to_string(),
2341 LanguageConfig {
2342 extensions: vec!["java".to_string()],
2343 filenames: vec![],
2344 grammar: "java".to_string(),
2345 comment_prefix: Some("//".to_string()),
2346 auto_indent: true,
2347 highlighter: HighlighterPreference::Auto,
2348 textmate_grammar: None,
2349 show_whitespace_tabs: true,
2350 use_tabs: false,
2351 tab_size: None,
2352 formatter: None,
2353 format_on_save: false,
2354 on_save: vec![],
2355 },
2356 );
2357
2358 languages.insert(
2359 "latex".to_string(),
2360 LanguageConfig {
2361 extensions: vec![
2362 "tex".to_string(),
2363 "latex".to_string(),
2364 "ltx".to_string(),
2365 "sty".to_string(),
2366 "cls".to_string(),
2367 "bib".to_string(),
2368 ],
2369 filenames: vec![],
2370 grammar: "latex".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 "templ".to_string(),
2386 LanguageConfig {
2387 extensions: vec!["templ".to_string()],
2388 filenames: vec![],
2389 grammar: "go".to_string(), comment_prefix: Some("//".to_string()),
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: None,
2398 format_on_save: false,
2399 on_save: vec![],
2400 },
2401 );
2402
2403 languages.insert(
2405 "git-rebase".to_string(),
2406 LanguageConfig {
2407 extensions: vec![],
2408 filenames: vec!["git-rebase-todo".to_string()],
2409 grammar: "Git Rebase Todo".to_string(),
2410 comment_prefix: Some("#".to_string()),
2411 auto_indent: false,
2412 highlighter: HighlighterPreference::Auto,
2413 textmate_grammar: None,
2414 show_whitespace_tabs: true,
2415 use_tabs: false,
2416 tab_size: None,
2417 formatter: None,
2418 format_on_save: false,
2419 on_save: vec![],
2420 },
2421 );
2422
2423 languages.insert(
2424 "git-commit".to_string(),
2425 LanguageConfig {
2426 extensions: vec![],
2427 filenames: vec![
2428 "COMMIT_EDITMSG".to_string(),
2429 "MERGE_MSG".to_string(),
2430 "SQUASH_MSG".to_string(),
2431 "TAG_EDITMSG".to_string(),
2432 ],
2433 grammar: "Git Commit Message".to_string(),
2434 comment_prefix: Some("#".to_string()),
2435 auto_indent: false,
2436 highlighter: HighlighterPreference::Auto,
2437 textmate_grammar: None,
2438 show_whitespace_tabs: true,
2439 use_tabs: false,
2440 tab_size: None,
2441 formatter: None,
2442 format_on_save: false,
2443 on_save: vec![],
2444 },
2445 );
2446
2447 languages.insert(
2448 "gitignore".to_string(),
2449 LanguageConfig {
2450 extensions: vec!["gitignore".to_string()],
2451 filenames: vec![
2452 ".gitignore".to_string(),
2453 ".dockerignore".to_string(),
2454 ".npmignore".to_string(),
2455 ".hgignore".to_string(),
2456 ],
2457 grammar: "Gitignore".to_string(),
2458 comment_prefix: Some("#".to_string()),
2459 auto_indent: false,
2460 highlighter: HighlighterPreference::Auto,
2461 textmate_grammar: None,
2462 show_whitespace_tabs: true,
2463 use_tabs: false,
2464 tab_size: None,
2465 formatter: None,
2466 format_on_save: false,
2467 on_save: vec![],
2468 },
2469 );
2470
2471 languages.insert(
2472 "gitconfig".to_string(),
2473 LanguageConfig {
2474 extensions: vec!["gitconfig".to_string()],
2475 filenames: vec![".gitconfig".to_string(), ".gitmodules".to_string()],
2476 grammar: "Git Config".to_string(),
2477 comment_prefix: Some("#".to_string()),
2478 auto_indent: true,
2479 highlighter: HighlighterPreference::Auto,
2480 textmate_grammar: None,
2481 show_whitespace_tabs: true,
2482 use_tabs: false,
2483 tab_size: None,
2484 formatter: None,
2485 format_on_save: false,
2486 on_save: vec![],
2487 },
2488 );
2489
2490 languages.insert(
2491 "gitattributes".to_string(),
2492 LanguageConfig {
2493 extensions: vec!["gitattributes".to_string()],
2494 filenames: vec![".gitattributes".to_string()],
2495 grammar: "Git Attributes".to_string(),
2496 comment_prefix: Some("#".to_string()),
2497 auto_indent: false,
2498 highlighter: HighlighterPreference::Auto,
2499 textmate_grammar: None,
2500 show_whitespace_tabs: true,
2501 use_tabs: false,
2502 tab_size: None,
2503 formatter: None,
2504 format_on_save: false,
2505 on_save: vec![],
2506 },
2507 );
2508
2509 languages
2510 }
2511
2512 fn default_lsp_config() -> HashMap<String, LspServerConfig> {
2514 let mut lsp = HashMap::new();
2515
2516 let ra_log_path = crate::services::log_dirs::lsp_log_path("rust-analyzer")
2519 .to_string_lossy()
2520 .to_string();
2521
2522 let ra_init_options = serde_json::json!({
2531 "checkOnSave": false,
2532 "cachePriming": { "enable": false },
2533 "procMacro": { "enable": false },
2534 "cargo": {
2535 "buildScripts": { "enable": false },
2536 "autoreload": false
2537 },
2538 "diagnostics": { "enable": true },
2539 "files": { "watcher": "server" }
2540 });
2541
2542 lsp.insert(
2543 "rust".to_string(),
2544 LspServerConfig {
2545 command: "rust-analyzer".to_string(),
2546 args: vec!["--log-file".to_string(), ra_log_path],
2547 enabled: true,
2548 auto_start: false,
2549 process_limits: ProcessLimits::default(),
2550 initialization_options: Some(ra_init_options),
2551 },
2552 );
2553
2554 lsp.insert(
2556 "python".to_string(),
2557 LspServerConfig {
2558 command: "pylsp".to_string(),
2559 args: vec![],
2560 enabled: true,
2561 auto_start: false,
2562 process_limits: ProcessLimits::default(),
2563 initialization_options: None,
2564 },
2565 );
2566
2567 let ts_lsp = LspServerConfig {
2570 command: "typescript-language-server".to_string(),
2571 args: vec!["--stdio".to_string()],
2572 enabled: true,
2573 auto_start: false,
2574 process_limits: ProcessLimits::default(),
2575 initialization_options: None,
2576 };
2577 lsp.insert("javascript".to_string(), ts_lsp.clone());
2578 lsp.insert("typescript".to_string(), ts_lsp);
2579
2580 lsp.insert(
2582 "html".to_string(),
2583 LspServerConfig {
2584 command: "vscode-html-language-server".to_string(),
2585 args: vec!["--stdio".to_string()],
2586 enabled: true,
2587 auto_start: false,
2588 process_limits: ProcessLimits::default(),
2589 initialization_options: None,
2590 },
2591 );
2592
2593 lsp.insert(
2595 "css".to_string(),
2596 LspServerConfig {
2597 command: "vscode-css-language-server".to_string(),
2598 args: vec!["--stdio".to_string()],
2599 enabled: true,
2600 auto_start: false,
2601 process_limits: ProcessLimits::default(),
2602 initialization_options: None,
2603 },
2604 );
2605
2606 lsp.insert(
2608 "c".to_string(),
2609 LspServerConfig {
2610 command: "clangd".to_string(),
2611 args: vec![],
2612 enabled: true,
2613 auto_start: false,
2614 process_limits: ProcessLimits::default(),
2615 initialization_options: None,
2616 },
2617 );
2618 lsp.insert(
2619 "cpp".to_string(),
2620 LspServerConfig {
2621 command: "clangd".to_string(),
2622 args: vec![],
2623 enabled: true,
2624 auto_start: false,
2625 process_limits: ProcessLimits::default(),
2626 initialization_options: None,
2627 },
2628 );
2629
2630 lsp.insert(
2632 "go".to_string(),
2633 LspServerConfig {
2634 command: "gopls".to_string(),
2635 args: vec![],
2636 enabled: true,
2637 auto_start: false,
2638 process_limits: ProcessLimits::default(),
2639 initialization_options: None,
2640 },
2641 );
2642
2643 lsp.insert(
2645 "json".to_string(),
2646 LspServerConfig {
2647 command: "vscode-json-language-server".to_string(),
2648 args: vec!["--stdio".to_string()],
2649 enabled: true,
2650 auto_start: false,
2651 process_limits: ProcessLimits::default(),
2652 initialization_options: None,
2653 },
2654 );
2655
2656 lsp.insert(
2658 "csharp".to_string(),
2659 LspServerConfig {
2660 command: "csharp-ls".to_string(),
2661 args: vec![],
2662 enabled: true,
2663 auto_start: false,
2664 process_limits: ProcessLimits::default(),
2665 initialization_options: None,
2666 },
2667 );
2668
2669 lsp.insert(
2672 "odin".to_string(),
2673 LspServerConfig {
2674 command: "ols".to_string(),
2675 args: vec![],
2676 enabled: true,
2677 auto_start: false,
2678 process_limits: ProcessLimits::default(),
2679 initialization_options: None,
2680 },
2681 );
2682
2683 lsp.insert(
2686 "zig".to_string(),
2687 LspServerConfig {
2688 command: "zls".to_string(),
2689 args: vec![],
2690 enabled: true,
2691 auto_start: false,
2692 process_limits: ProcessLimits::default(),
2693 initialization_options: None,
2694 },
2695 );
2696
2697 lsp.insert(
2700 "java".to_string(),
2701 LspServerConfig {
2702 command: "jdtls".to_string(),
2703 args: vec![],
2704 enabled: true,
2705 auto_start: false,
2706 process_limits: ProcessLimits::default(),
2707 initialization_options: None,
2708 },
2709 );
2710
2711 lsp.insert(
2714 "latex".to_string(),
2715 LspServerConfig {
2716 command: "texlab".to_string(),
2717 args: vec![],
2718 enabled: true,
2719 auto_start: false,
2720 process_limits: ProcessLimits::default(),
2721 initialization_options: None,
2722 },
2723 );
2724
2725 lsp.insert(
2728 "markdown".to_string(),
2729 LspServerConfig {
2730 command: "marksman".to_string(),
2731 args: vec!["server".to_string()],
2732 enabled: true,
2733 auto_start: false,
2734 process_limits: ProcessLimits::default(),
2735 initialization_options: None,
2736 },
2737 );
2738
2739 lsp.insert(
2742 "templ".to_string(),
2743 LspServerConfig {
2744 command: "templ".to_string(),
2745 args: vec!["lsp".to_string()],
2746 enabled: true,
2747 auto_start: false,
2748 process_limits: ProcessLimits::default(),
2749 initialization_options: None,
2750 },
2751 );
2752
2753 lsp
2754 }
2755
2756 pub fn validate(&self) -> Result<(), ConfigError> {
2758 if self.editor.tab_size == 0 {
2760 return Err(ConfigError::ValidationError(
2761 "tab_size must be greater than 0".to_string(),
2762 ));
2763 }
2764
2765 if self.editor.scroll_offset > 100 {
2767 return Err(ConfigError::ValidationError(
2768 "scroll_offset must be <= 100".to_string(),
2769 ));
2770 }
2771
2772 for binding in &self.keybindings {
2774 if binding.key.is_empty() {
2775 return Err(ConfigError::ValidationError(
2776 "keybinding key cannot be empty".to_string(),
2777 ));
2778 }
2779 if binding.action.is_empty() {
2780 return Err(ConfigError::ValidationError(
2781 "keybinding action cannot be empty".to_string(),
2782 ));
2783 }
2784 }
2785
2786 Ok(())
2787 }
2788}
2789
2790#[derive(Debug)]
2792pub enum ConfigError {
2793 IoError(String),
2794 ParseError(String),
2795 SerializeError(String),
2796 ValidationError(String),
2797}
2798
2799impl std::fmt::Display for ConfigError {
2800 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2801 match self {
2802 Self::IoError(msg) => write!(f, "IO error: {msg}"),
2803 Self::ParseError(msg) => write!(f, "Parse error: {msg}"),
2804 Self::SerializeError(msg) => write!(f, "Serialize error: {msg}"),
2805 Self::ValidationError(msg) => write!(f, "Validation error: {msg}"),
2806 }
2807 }
2808}
2809
2810impl std::error::Error for ConfigError {}
2811
2812#[cfg(test)]
2813mod tests {
2814 use super::*;
2815
2816 #[test]
2817 fn test_default_config() {
2818 let config = Config::default();
2819 assert_eq!(config.editor.tab_size, 4);
2820 assert!(config.editor.line_numbers);
2821 assert!(config.editor.syntax_highlighting);
2822 assert!(config.keybindings.is_empty());
2825 let resolved = config.resolve_keymap(&config.active_keybinding_map);
2827 assert!(!resolved.is_empty());
2828 }
2829
2830 #[test]
2831 fn test_all_builtin_keymaps_loadable() {
2832 for name in KeybindingMapName::BUILTIN_OPTIONS {
2833 let keymap = Config::load_builtin_keymap(name);
2834 assert!(keymap.is_some(), "Failed to load builtin keymap '{}'", name);
2835 }
2836 }
2837
2838 #[test]
2839 fn test_config_validation() {
2840 let mut config = Config::default();
2841 assert!(config.validate().is_ok());
2842
2843 config.editor.tab_size = 0;
2844 assert!(config.validate().is_err());
2845 }
2846
2847 #[test]
2848 fn test_macos_keymap_inherits_enter_bindings() {
2849 let config = Config::default();
2850 let bindings = config.resolve_keymap("macos");
2851
2852 let enter_bindings: Vec<_> = bindings.iter().filter(|b| b.key == "Enter").collect();
2853 assert!(
2854 !enter_bindings.is_empty(),
2855 "macos keymap should inherit Enter bindings from default, got {} Enter bindings",
2856 enter_bindings.len()
2857 );
2858 let has_insert_newline = enter_bindings.iter().any(|b| b.action == "insert_newline");
2860 assert!(
2861 has_insert_newline,
2862 "macos keymap should have insert_newline action for Enter key"
2863 );
2864 }
2865
2866 #[test]
2867 fn test_config_serialize_deserialize() {
2868 let config = Config::default();
2870
2871 let json = serde_json::to_string_pretty(&config).unwrap();
2873
2874 let loaded: Config = serde_json::from_str(&json).unwrap();
2876
2877 assert_eq!(config.editor.tab_size, loaded.editor.tab_size);
2878 assert_eq!(config.theme, loaded.theme);
2879 }
2880
2881 #[test]
2882 fn test_config_with_custom_keybinding() {
2883 let json = r#"{
2884 "editor": {
2885 "tab_size": 2
2886 },
2887 "keybindings": [
2888 {
2889 "key": "x",
2890 "modifiers": ["ctrl", "shift"],
2891 "action": "custom_action",
2892 "args": {},
2893 "when": null
2894 }
2895 ]
2896 }"#;
2897
2898 let config: Config = serde_json::from_str(json).unwrap();
2899 assert_eq!(config.editor.tab_size, 2);
2900 assert_eq!(config.keybindings.len(), 1);
2901 assert_eq!(config.keybindings[0].key, "x");
2902 assert_eq!(config.keybindings[0].modifiers.len(), 2);
2903 }
2904
2905 #[test]
2906 fn test_sparse_config_merges_with_defaults() {
2907 let temp_dir = tempfile::tempdir().unwrap();
2909 let config_path = temp_dir.path().join("config.json");
2910
2911 let sparse_config = r#"{
2913 "lsp": {
2914 "rust": {
2915 "command": "custom-rust-analyzer",
2916 "args": ["--custom-arg"]
2917 }
2918 }
2919 }"#;
2920 std::fs::write(&config_path, sparse_config).unwrap();
2921
2922 let loaded = Config::load_from_file(&config_path).unwrap();
2924
2925 assert!(loaded.lsp.contains_key("rust"));
2927 assert_eq!(
2928 loaded.lsp["rust"].command,
2929 "custom-rust-analyzer".to_string()
2930 );
2931
2932 assert!(
2934 loaded.lsp.contains_key("python"),
2935 "python LSP should be merged from defaults"
2936 );
2937 assert!(
2938 loaded.lsp.contains_key("typescript"),
2939 "typescript LSP should be merged from defaults"
2940 );
2941 assert!(
2942 loaded.lsp.contains_key("javascript"),
2943 "javascript LSP should be merged from defaults"
2944 );
2945
2946 assert!(loaded.languages.contains_key("rust"));
2948 assert!(loaded.languages.contains_key("python"));
2949 assert!(loaded.languages.contains_key("typescript"));
2950 }
2951
2952 #[test]
2953 fn test_empty_config_gets_all_defaults() {
2954 let temp_dir = tempfile::tempdir().unwrap();
2955 let config_path = temp_dir.path().join("config.json");
2956
2957 std::fs::write(&config_path, "{}").unwrap();
2959
2960 let loaded = Config::load_from_file(&config_path).unwrap();
2961 let defaults = Config::default();
2962
2963 assert_eq!(loaded.lsp.len(), defaults.lsp.len());
2965
2966 assert_eq!(loaded.languages.len(), defaults.languages.len());
2968 }
2969
2970 #[test]
2971 fn test_dynamic_submenu_expansion() {
2972 let dynamic = MenuItem::DynamicSubmenu {
2974 label: "Test".to_string(),
2975 source: "copy_with_theme".to_string(),
2976 };
2977
2978 let expanded = dynamic.expand_dynamic();
2979
2980 match expanded {
2982 MenuItem::Submenu { label, items } => {
2983 assert_eq!(label, "Test");
2984 let theme_loader = crate::view::theme::LocalThemeLoader::new();
2986 let themes = crate::view::theme::Theme::all_available(&theme_loader);
2987 assert_eq!(items.len(), themes.len());
2988
2989 for (item, theme_name) in items.iter().zip(themes.iter()) {
2991 match item {
2992 MenuItem::Action {
2993 label,
2994 action,
2995 args,
2996 ..
2997 } => {
2998 assert_eq!(label, theme_name);
2999 assert_eq!(action, "copy_with_theme");
3000 assert_eq!(
3001 args.get("theme").and_then(|v| v.as_str()),
3002 Some(theme_name.as_str())
3003 );
3004 }
3005 _ => panic!("Expected Action item"),
3006 }
3007 }
3008 }
3009 _ => panic!("Expected Submenu after expansion"),
3010 }
3011 }
3012
3013 #[test]
3014 fn test_non_dynamic_item_unchanged() {
3015 let action = MenuItem::Action {
3017 label: "Test".to_string(),
3018 action: "test".to_string(),
3019 args: HashMap::new(),
3020 when: None,
3021 checkbox: None,
3022 };
3023
3024 let expanded = action.expand_dynamic();
3025 match expanded {
3026 MenuItem::Action { label, action, .. } => {
3027 assert_eq!(label, "Test");
3028 assert_eq!(action, "test");
3029 }
3030 _ => panic!("Action should remain Action after expand_dynamic"),
3031 }
3032 }
3033
3034 #[test]
3035 fn test_buffer_config_uses_global_defaults() {
3036 let config = Config::default();
3037 let buffer_config = BufferConfig::resolve(&config, None);
3038
3039 assert_eq!(buffer_config.tab_size, config.editor.tab_size);
3040 assert_eq!(buffer_config.auto_indent, config.editor.auto_indent);
3041 assert!(!buffer_config.use_tabs); assert!(buffer_config.show_whitespace_tabs);
3043 assert!(buffer_config.formatter.is_none());
3044 assert!(!buffer_config.format_on_save);
3045 }
3046
3047 #[test]
3048 fn test_buffer_config_applies_language_overrides() {
3049 let mut config = Config::default();
3050
3051 config.languages.insert(
3053 "go".to_string(),
3054 LanguageConfig {
3055 extensions: vec!["go".to_string()],
3056 filenames: vec![],
3057 grammar: "go".to_string(),
3058 comment_prefix: Some("//".to_string()),
3059 auto_indent: true,
3060 highlighter: HighlighterPreference::Auto,
3061 textmate_grammar: None,
3062 show_whitespace_tabs: false, use_tabs: true, tab_size: Some(8), formatter: Some(FormatterConfig {
3066 command: "gofmt".to_string(),
3067 args: vec![],
3068 stdin: true,
3069 timeout_ms: 10000,
3070 }),
3071 format_on_save: true,
3072 on_save: vec![],
3073 },
3074 );
3075
3076 let buffer_config = BufferConfig::resolve(&config, Some("go"));
3077
3078 assert_eq!(buffer_config.tab_size, 8);
3079 assert!(buffer_config.use_tabs);
3080 assert!(!buffer_config.show_whitespace_tabs);
3081 assert!(buffer_config.format_on_save);
3082 assert!(buffer_config.formatter.is_some());
3083 assert_eq!(buffer_config.formatter.as_ref().unwrap().command, "gofmt");
3084 }
3085
3086 #[test]
3087 fn test_buffer_config_unknown_language_uses_global() {
3088 let config = Config::default();
3089 let buffer_config = BufferConfig::resolve(&config, Some("unknown_lang"));
3090
3091 assert_eq!(buffer_config.tab_size, config.editor.tab_size);
3093 assert!(!buffer_config.use_tabs);
3094 }
3095
3096 #[test]
3097 fn test_buffer_config_indent_string() {
3098 let config = Config::default();
3099
3100 let spaces_config = BufferConfig::resolve(&config, None);
3102 assert_eq!(spaces_config.indent_string(), " "); let mut config_with_tabs = Config::default();
3106 config_with_tabs.languages.insert(
3107 "makefile".to_string(),
3108 LanguageConfig {
3109 use_tabs: true,
3110 tab_size: Some(8),
3111 ..Default::default()
3112 },
3113 );
3114 let tabs_config = BufferConfig::resolve(&config_with_tabs, Some("makefile"));
3115 assert_eq!(tabs_config.indent_string(), "\t");
3116 }
3117}