1use crate::config::Config;
2use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
3use rust_i18n::t;
4use std::collections::HashMap;
5use std::sync::atomic::{AtomicBool, Ordering};
6
7fn normalize_key(code: KeyCode, modifiers: KeyModifiers) -> (KeyCode, KeyModifiers) {
18 if code == KeyCode::BackTab {
19 return (code, modifiers.difference(KeyModifiers::SHIFT));
20 }
21 if let KeyCode::Char(c) = code {
22 if c.is_ascii_uppercase() {
23 return (KeyCode::Char(c.to_ascii_lowercase()), modifiers);
24 }
25 }
26 (code, modifiers)
27}
28
29static FORCE_LINUX_KEYBINDINGS: AtomicBool = AtomicBool::new(false);
32
33pub fn set_force_linux_keybindings(force: bool) {
36 FORCE_LINUX_KEYBINDINGS.store(force, Ordering::SeqCst);
37}
38
39fn use_macos_symbols() -> bool {
41 if FORCE_LINUX_KEYBINDINGS.load(Ordering::SeqCst) {
42 return false;
43 }
44 cfg!(target_os = "macos")
45}
46
47fn is_text_input_modifier(modifiers: KeyModifiers) -> bool {
58 if modifiers.is_empty() || modifiers == KeyModifiers::SHIFT {
59 return true;
60 }
61
62 #[cfg(windows)]
66 if modifiers == (KeyModifiers::CONTROL | KeyModifiers::ALT)
67 || modifiers == (KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT)
68 {
69 return true;
70 }
71
72 false
73}
74
75pub fn format_keybinding(keycode: &KeyCode, modifiers: &KeyModifiers) -> String {
79 let mut result = String::new();
80
81 let (ctrl_label, alt_label, shift_label, super_label) = if use_macos_symbols() {
83 ("⌃", "⌥", "⇧", "⌘")
84 } else {
85 ("Ctrl", "Alt", "Shift", "Super")
86 };
87
88 let use_plus = !use_macos_symbols();
89
90 if modifiers.contains(KeyModifiers::SUPER) {
91 result.push_str(super_label);
92 if use_plus {
93 result.push('+');
94 }
95 }
96 if modifiers.contains(KeyModifiers::CONTROL) {
97 result.push_str(ctrl_label);
98 if use_plus {
99 result.push('+');
100 }
101 }
102 if modifiers.contains(KeyModifiers::ALT) {
103 result.push_str(alt_label);
104 if use_plus {
105 result.push('+');
106 }
107 }
108 if modifiers.contains(KeyModifiers::SHIFT) {
109 result.push_str(shift_label);
110 if use_plus {
111 result.push('+');
112 }
113 }
114
115 match keycode {
116 KeyCode::Enter => result.push_str("Enter"),
117 KeyCode::Backspace => result.push_str("Backspace"),
118 KeyCode::Delete => result.push_str("Del"),
119 KeyCode::Tab => result.push_str("Tab"),
120 KeyCode::Esc => result.push_str("Esc"),
121 KeyCode::Left => result.push('←'),
122 KeyCode::Right => result.push('→'),
123 KeyCode::Up => result.push('↑'),
124 KeyCode::Down => result.push('↓'),
125 KeyCode::Home => result.push_str("Home"),
126 KeyCode::End => result.push_str("End"),
127 KeyCode::PageUp => result.push_str("PgUp"),
128 KeyCode::PageDown => result.push_str("PgDn"),
129 KeyCode::Char(' ') => result.push_str("Space"),
130 KeyCode::Char(c) => result.push_str(&c.to_uppercase().to_string()),
131 KeyCode::F(n) => result.push_str(&format!("F{}", n)),
132 _ => return String::new(),
133 }
134
135 result
136}
137
138fn keybinding_priority_score(key: &KeyCode) -> u32 {
142 match key {
143 KeyCode::Char('@') => 100, KeyCode::Char('7') => 100, KeyCode::Char('_') => 100, _ => 0,
150 }
151}
152
153pub fn terminal_key_equivalents(
164 key: KeyCode,
165 modifiers: KeyModifiers,
166) -> Vec<(KeyCode, KeyModifiers)> {
167 let mut equivalents = Vec::new();
168
169 if modifiers.contains(KeyModifiers::CONTROL) {
171 let base_modifiers = modifiers; match key {
174 KeyCode::Char('/') => {
176 equivalents.push((KeyCode::Char('7'), base_modifiers));
177 }
178 KeyCode::Char('7') => {
179 equivalents.push((KeyCode::Char('/'), base_modifiers));
180 }
181
182 KeyCode::Backspace => {
184 equivalents.push((KeyCode::Char('h'), base_modifiers));
185 }
186 KeyCode::Char('h') if modifiers == KeyModifiers::CONTROL => {
187 equivalents.push((KeyCode::Backspace, base_modifiers));
189 }
190
191 KeyCode::Char(' ') => {
193 equivalents.push((KeyCode::Char('@'), base_modifiers));
194 }
195 KeyCode::Char('@') => {
196 equivalents.push((KeyCode::Char(' '), base_modifiers));
197 }
198
199 KeyCode::Char('-') => {
201 equivalents.push((KeyCode::Char('_'), base_modifiers));
202 }
203 KeyCode::Char('_') => {
204 equivalents.push((KeyCode::Char('-'), base_modifiers));
205 }
206
207 _ => {}
208 }
209 }
210
211 equivalents
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, Hash)]
216pub enum KeyContext {
217 Global,
219 Normal,
221 Prompt,
223 Popup,
225 FileExplorer,
227 Menu,
229 Terminal,
231 Settings,
233 CompositeBuffer,
235 Mode(String),
237}
238
239impl KeyContext {
240 pub fn allows_normal_fallthrough(&self) -> bool {
247 matches!(self, Self::CompositeBuffer)
248 }
249
250 pub fn allows_text_input(&self) -> bool {
252 matches!(self, Self::Normal | Self::Prompt | Self::FileExplorer)
253 }
254
255 pub fn from_when_clause(when: &str) -> Option<Self> {
257 let trimmed = when.trim();
258 if let Some(mode_name) = trimmed.strip_prefix("mode:") {
259 return Some(Self::Mode(mode_name.to_string()));
260 }
261 Some(match trimmed {
262 "global" => Self::Global,
263 "prompt" => Self::Prompt,
264 "popup" => Self::Popup,
265 "fileExplorer" | "file_explorer" => Self::FileExplorer,
266 "normal" => Self::Normal,
267 "menu" => Self::Menu,
268 "terminal" => Self::Terminal,
269 "settings" => Self::Settings,
270 "compositeBuffer" | "composite_buffer" => Self::CompositeBuffer,
271 _ => return None,
272 })
273 }
274
275 pub fn to_when_clause(&self) -> String {
277 match self {
278 Self::Global => "global".to_string(),
279 Self::Normal => "normal".to_string(),
280 Self::Prompt => "prompt".to_string(),
281 Self::Popup => "popup".to_string(),
282 Self::FileExplorer => "fileExplorer".to_string(),
283 Self::Menu => "menu".to_string(),
284 Self::Terminal => "terminal".to_string(),
285 Self::Settings => "settings".to_string(),
286 Self::CompositeBuffer => "compositeBuffer".to_string(),
287 Self::Mode(name) => format!("mode:{}", name),
288 }
289 }
290}
291
292#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
294pub enum Action {
295 InsertChar(char),
297 InsertNewline,
298 InsertTab,
299
300 MoveLeft,
302 MoveRight,
303 MoveUp,
304 MoveDown,
305 MoveWordLeft,
306 MoveWordRight,
307 MoveWordEnd, ViMoveWordEnd, MoveLeftInLine, MoveRightInLine, MoveLineStart,
312 MoveLineEnd,
313 MoveLineUp,
314 MoveLineDown,
315 MovePageUp,
316 MovePageDown,
317 MoveDocumentStart,
318 MoveDocumentEnd,
319
320 SelectLeft,
322 SelectRight,
323 SelectUp,
324 SelectDown,
325 SelectToParagraphUp, SelectToParagraphDown, SelectWordLeft,
328 SelectWordRight,
329 SelectWordEnd, ViSelectWordEnd, SelectLineStart,
332 SelectLineEnd,
333 SelectDocumentStart,
334 SelectDocumentEnd,
335 SelectPageUp,
336 SelectPageDown,
337 SelectAll,
338 SelectWord,
339 SelectLine,
340 ExpandSelection,
341
342 BlockSelectLeft,
344 BlockSelectRight,
345 BlockSelectUp,
346 BlockSelectDown,
347
348 DeleteBackward,
350 DeleteForward,
351 DeleteWordBackward,
352 DeleteWordForward,
353 DeleteLine,
354 DeleteToLineEnd,
355 DeleteToLineStart,
356 DeleteViWordEnd, TransposeChars,
358 OpenLine,
359 DuplicateLine,
360
361 Recenter,
363
364 SetMark,
366
367 Copy,
369 CopyWithTheme(String),
370 Cut,
371 Paste,
372
373 YankWordForward,
375 YankWordBackward,
376 YankToLineEnd,
377 YankToLineStart,
378 YankViWordEnd, AddCursorAbove,
382 AddCursorBelow,
383 AddCursorNextMatch,
384 RemoveSecondaryCursors,
385
386 Save,
388 SaveAs,
389 Open,
390 SwitchProject,
391 New,
392 Close,
393 CloseTab,
394 Quit,
395 ForceQuit,
396 Detach,
397 Revert,
398 ToggleAutoRevert,
399 FormatBuffer,
400 TrimTrailingWhitespace,
401 EnsureFinalNewline,
402
403 GotoLine,
405 ScanLineIndex,
406 GoToMatchingBracket,
407 JumpToNextError,
408 JumpToPreviousError,
409
410 SmartHome,
412 DedentSelection,
413 ToggleComment,
414 DabbrevExpand,
415 ToggleFold,
416
417 SetBookmark(char),
419 JumpToBookmark(char),
420 ClearBookmark(char),
421 ListBookmarks,
422
423 ToggleSearchCaseSensitive,
425 ToggleSearchWholeWord,
426 ToggleSearchRegex,
427 ToggleSearchConfirmEach,
428
429 StartMacroRecording,
431 StopMacroRecording,
432 PlayMacro(char),
433 ToggleMacroRecording(char),
434 ShowMacro(char),
435 ListMacros,
436 PromptRecordMacro,
437 PromptPlayMacro,
438 PlayLastMacro,
439
440 PromptSetBookmark,
442 PromptJumpToBookmark,
443
444 Undo,
446 Redo,
447
448 ScrollUp,
450 ScrollDown,
451 ShowHelp,
452 ShowKeyboardShortcuts,
453 ShowWarnings,
454 ShowStatusLog,
455 ShowLspStatus,
456 ClearWarnings,
457 CommandPalette, QuickOpen,
460 ToggleLineWrap,
461 ToggleCurrentLineHighlight,
462 ToggleReadOnly,
463 TogglePageView,
464 SetPageWidth,
465 InspectThemeAtCursor,
466 SelectTheme,
467 SelectKeybindingMap,
468 SelectCursorStyle,
469 SelectLocale,
470
471 NextBuffer,
473 PrevBuffer,
474 SwitchToPreviousTab,
475 SwitchToTabByName,
476
477 ScrollTabsLeft,
479 ScrollTabsRight,
480
481 NavigateBack,
483 NavigateForward,
484
485 SplitHorizontal,
487 SplitVertical,
488 CloseSplit,
489 NextSplit,
490 PrevSplit,
491 IncreaseSplitSize,
492 DecreaseSplitSize,
493 ToggleMaximizeSplit,
494
495 PromptConfirm,
497 PromptConfirmWithText(String),
499 PromptCancel,
500 PromptBackspace,
501 PromptDelete,
502 PromptMoveLeft,
503 PromptMoveRight,
504 PromptMoveStart,
505 PromptMoveEnd,
506 PromptSelectPrev,
507 PromptSelectNext,
508 PromptPageUp,
509 PromptPageDown,
510 PromptAcceptSuggestion,
511 PromptMoveWordLeft,
512 PromptMoveWordRight,
513 PromptDeleteWordForward,
515 PromptDeleteWordBackward,
516 PromptDeleteToLineEnd,
517 PromptCopy,
518 PromptCut,
519 PromptPaste,
520 PromptMoveLeftSelecting,
522 PromptMoveRightSelecting,
523 PromptMoveHomeSelecting,
524 PromptMoveEndSelecting,
525 PromptSelectWordLeft,
526 PromptSelectWordRight,
527 PromptSelectAll,
528
529 FileBrowserToggleHidden,
531 FileBrowserToggleDetectEncoding,
532
533 PopupSelectNext,
535 PopupSelectPrev,
536 PopupPageUp,
537 PopupPageDown,
538 PopupConfirm,
539 PopupCancel,
540
541 ToggleFileExplorer,
543 ToggleMenuBar,
545 ToggleTabBar,
547 ToggleStatusBar,
549 TogglePromptLine,
551 ToggleVerticalScrollbar,
553 ToggleHorizontalScrollbar,
554 FocusFileExplorer,
555 FocusEditor,
556 FileExplorerUp,
557 FileExplorerDown,
558 FileExplorerPageUp,
559 FileExplorerPageDown,
560 FileExplorerExpand,
561 FileExplorerCollapse,
562 FileExplorerOpen,
563 FileExplorerRefresh,
564 FileExplorerNewFile,
565 FileExplorerNewDirectory,
566 FileExplorerDelete,
567 FileExplorerRename,
568 FileExplorerToggleHidden,
569 FileExplorerToggleGitignored,
570 FileExplorerSearchClear,
571 FileExplorerSearchBackspace,
572
573 LspCompletion,
575 LspGotoDefinition,
576 LspReferences,
577 LspRename,
578 LspHover,
579 LspSignatureHelp,
580 LspCodeActions,
581 LspRestart,
582 LspStop,
583 LspToggleForBuffer,
584 ToggleInlayHints,
585 ToggleMouseHover,
586
587 ToggleLineNumbers,
589 ToggleScrollSync,
590 ToggleMouseCapture,
591 ToggleDebugHighlights, SetBackground,
593 SetBackgroundBlend,
594
595 SetTabSize,
597 SetLineEnding,
598 SetEncoding,
599 ReloadWithEncoding,
600 SetLanguage,
601 ToggleIndentationStyle,
602 ToggleTabIndicators,
603 ToggleWhitespaceIndicators,
604 ResetBufferSettings,
605 AddRuler,
606 RemoveRuler,
607
608 DumpConfig,
610
611 Search,
613 FindInSelection,
614 FindNext,
615 FindPrevious,
616 FindSelectionNext, FindSelectionPrevious, Replace,
619 QueryReplace, MenuActivate, MenuClose, MenuLeft, MenuRight, MenuUp, MenuDown, MenuExecute, MenuOpen(String), SwitchKeybindingMap(String), PluginAction(String),
636
637 OpenSettings, CloseSettings, SettingsSave, SettingsReset, SettingsToggleFocus, SettingsActivate, SettingsSearch, SettingsHelp, SettingsIncrement, SettingsDecrement, SettingsInherit, OpenTerminal, CloseTerminal, FocusTerminal, TerminalEscape, ToggleKeyboardCapture, TerminalPaste, ShellCommand, ShellCommandReplace, ToUpperCase, ToLowerCase, ToggleCase, SortLines, CalibrateInput, EventDebug, OpenKeybindingEditor, LoadPluginFromBuffer, CompositeNextHunk, CompositePrevHunk, None,
686}
687
688macro_rules! define_action_str_mapping {
701 (
702 $args_name:ident;
703 simple { $($s_name:literal => $s_variant:ident),* $(,)? }
704 alias { $($a_name:literal => $a_variant:ident),* $(,)? }
705 with_char { $($c_name:literal => $c_variant:ident),* $(,)? }
706 custom { $($x_name:literal => $x_variant:ident : $x_body:expr),* $(,)? }
707 ) => {
708 pub fn from_str(s: &str, $args_name: &HashMap<String, serde_json::Value>) -> Option<Self> {
710 Some(match s {
711 $($s_name => Self::$s_variant,)*
712 $($a_name => Self::$a_variant,)*
713 $($c_name => return Self::with_char($args_name, Self::$c_variant),)*
714 $($x_name => $x_body,)*
715 _ => Self::PluginAction(s.to_string()),
718 })
719 }
720
721 pub fn to_action_str(&self) -> String {
724 match self {
725 $(Self::$s_variant => $s_name.to_string(),)*
726 $(Self::$c_variant(_) => $c_name.to_string(),)*
727 $(Self::$x_variant(_) => $x_name.to_string(),)*
728 Self::PluginAction(name) => name.clone(),
729 }
730 }
731
732 pub fn all_action_names() -> Vec<String> {
735 let mut names = vec![
736 $($s_name.to_string(),)*
737 $($a_name.to_string(),)*
738 $($c_name.to_string(),)*
739 $($x_name.to_string(),)*
740 ];
741 names.sort();
742 names
743 }
744 };
745}
746
747impl Action {
748 fn with_char(
749 args: &HashMap<String, serde_json::Value>,
750 make_action: impl FnOnce(char) -> Self,
751 ) -> Option<Self> {
752 if let Some(serde_json::Value::String(value)) = args.get("char") {
753 value.chars().next().map(make_action)
754 } else {
755 None
756 }
757 }
758
759 define_action_str_mapping! {
760 args;
761 simple {
762 "insert_newline" => InsertNewline,
763 "insert_tab" => InsertTab,
764
765 "move_left" => MoveLeft,
766 "move_right" => MoveRight,
767 "move_up" => MoveUp,
768 "move_down" => MoveDown,
769 "move_word_left" => MoveWordLeft,
770 "move_word_right" => MoveWordRight,
771 "move_word_end" => MoveWordEnd,
772 "vi_move_word_end" => ViMoveWordEnd,
773 "move_left_in_line" => MoveLeftInLine,
774 "move_right_in_line" => MoveRightInLine,
775 "move_line_start" => MoveLineStart,
776 "move_line_end" => MoveLineEnd,
777 "move_line_up" => MoveLineUp,
778 "move_line_down" => MoveLineDown,
779 "move_page_up" => MovePageUp,
780 "move_page_down" => MovePageDown,
781 "move_document_start" => MoveDocumentStart,
782 "move_document_end" => MoveDocumentEnd,
783
784 "select_left" => SelectLeft,
785 "select_right" => SelectRight,
786 "select_up" => SelectUp,
787 "select_down" => SelectDown,
788 "select_to_paragraph_up" => SelectToParagraphUp,
789 "select_to_paragraph_down" => SelectToParagraphDown,
790 "select_word_left" => SelectWordLeft,
791 "select_word_right" => SelectWordRight,
792 "select_word_end" => SelectWordEnd,
793 "vi_select_word_end" => ViSelectWordEnd,
794 "select_line_start" => SelectLineStart,
795 "select_line_end" => SelectLineEnd,
796 "select_document_start" => SelectDocumentStart,
797 "select_document_end" => SelectDocumentEnd,
798 "select_page_up" => SelectPageUp,
799 "select_page_down" => SelectPageDown,
800 "select_all" => SelectAll,
801 "select_word" => SelectWord,
802 "select_line" => SelectLine,
803 "expand_selection" => ExpandSelection,
804
805 "block_select_left" => BlockSelectLeft,
806 "block_select_right" => BlockSelectRight,
807 "block_select_up" => BlockSelectUp,
808 "block_select_down" => BlockSelectDown,
809
810 "delete_backward" => DeleteBackward,
811 "delete_forward" => DeleteForward,
812 "delete_word_backward" => DeleteWordBackward,
813 "delete_word_forward" => DeleteWordForward,
814 "delete_line" => DeleteLine,
815 "delete_to_line_end" => DeleteToLineEnd,
816 "delete_to_line_start" => DeleteToLineStart,
817 "delete_vi_word_end" => DeleteViWordEnd,
818 "transpose_chars" => TransposeChars,
819 "open_line" => OpenLine,
820 "duplicate_line" => DuplicateLine,
821 "recenter" => Recenter,
822 "set_mark" => SetMark,
823
824 "copy" => Copy,
825 "cut" => Cut,
826 "paste" => Paste,
827
828 "yank_word_forward" => YankWordForward,
829 "yank_word_backward" => YankWordBackward,
830 "yank_to_line_end" => YankToLineEnd,
831 "yank_to_line_start" => YankToLineStart,
832 "yank_vi_word_end" => YankViWordEnd,
833
834 "add_cursor_above" => AddCursorAbove,
835 "add_cursor_below" => AddCursorBelow,
836 "add_cursor_next_match" => AddCursorNextMatch,
837 "remove_secondary_cursors" => RemoveSecondaryCursors,
838
839 "save" => Save,
840 "save_as" => SaveAs,
841 "open" => Open,
842 "switch_project" => SwitchProject,
843 "new" => New,
844 "close" => Close,
845 "close_tab" => CloseTab,
846 "quit" => Quit,
847 "force_quit" => ForceQuit,
848 "detach" => Detach,
849 "revert" => Revert,
850 "toggle_auto_revert" => ToggleAutoRevert,
851 "format_buffer" => FormatBuffer,
852 "trim_trailing_whitespace" => TrimTrailingWhitespace,
853 "ensure_final_newline" => EnsureFinalNewline,
854 "goto_line" => GotoLine,
855 "scan_line_index" => ScanLineIndex,
856 "goto_matching_bracket" => GoToMatchingBracket,
857 "jump_to_next_error" => JumpToNextError,
858 "jump_to_previous_error" => JumpToPreviousError,
859
860 "smart_home" => SmartHome,
861 "dedent_selection" => DedentSelection,
862 "toggle_comment" => ToggleComment,
863 "dabbrev_expand" => DabbrevExpand,
864 "toggle_fold" => ToggleFold,
865
866 "list_bookmarks" => ListBookmarks,
867
868 "toggle_search_case_sensitive" => ToggleSearchCaseSensitive,
869 "toggle_search_whole_word" => ToggleSearchWholeWord,
870 "toggle_search_regex" => ToggleSearchRegex,
871 "toggle_search_confirm_each" => ToggleSearchConfirmEach,
872
873 "start_macro_recording" => StartMacroRecording,
874 "stop_macro_recording" => StopMacroRecording,
875
876 "list_macros" => ListMacros,
877 "prompt_record_macro" => PromptRecordMacro,
878 "prompt_play_macro" => PromptPlayMacro,
879 "play_last_macro" => PlayLastMacro,
880 "prompt_set_bookmark" => PromptSetBookmark,
881 "prompt_jump_to_bookmark" => PromptJumpToBookmark,
882
883 "undo" => Undo,
884 "redo" => Redo,
885
886 "scroll_up" => ScrollUp,
887 "scroll_down" => ScrollDown,
888 "show_help" => ShowHelp,
889 "keyboard_shortcuts" => ShowKeyboardShortcuts,
890 "show_warnings" => ShowWarnings,
891 "show_status_log" => ShowStatusLog,
892 "show_lsp_status" => ShowLspStatus,
893 "clear_warnings" => ClearWarnings,
894 "command_palette" => CommandPalette,
895 "quick_open" => QuickOpen,
896 "toggle_line_wrap" => ToggleLineWrap,
897 "toggle_current_line_highlight" => ToggleCurrentLineHighlight,
898 "toggle_read_only" => ToggleReadOnly,
899 "toggle_page_view" => TogglePageView,
900 "set_page_width" => SetPageWidth,
901
902 "next_buffer" => NextBuffer,
903 "prev_buffer" => PrevBuffer,
904 "switch_to_previous_tab" => SwitchToPreviousTab,
905 "switch_to_tab_by_name" => SwitchToTabByName,
906 "scroll_tabs_left" => ScrollTabsLeft,
907 "scroll_tabs_right" => ScrollTabsRight,
908
909 "navigate_back" => NavigateBack,
910 "navigate_forward" => NavigateForward,
911
912 "split_horizontal" => SplitHorizontal,
913 "split_vertical" => SplitVertical,
914 "close_split" => CloseSplit,
915 "next_split" => NextSplit,
916 "prev_split" => PrevSplit,
917 "increase_split_size" => IncreaseSplitSize,
918 "decrease_split_size" => DecreaseSplitSize,
919 "toggle_maximize_split" => ToggleMaximizeSplit,
920
921 "prompt_confirm" => PromptConfirm,
922 "prompt_cancel" => PromptCancel,
923 "prompt_backspace" => PromptBackspace,
924 "prompt_move_left" => PromptMoveLeft,
925 "prompt_move_right" => PromptMoveRight,
926 "prompt_move_start" => PromptMoveStart,
927 "prompt_move_end" => PromptMoveEnd,
928 "prompt_select_prev" => PromptSelectPrev,
929 "prompt_select_next" => PromptSelectNext,
930 "prompt_page_up" => PromptPageUp,
931 "prompt_page_down" => PromptPageDown,
932 "prompt_accept_suggestion" => PromptAcceptSuggestion,
933 "prompt_delete_word_forward" => PromptDeleteWordForward,
934 "prompt_delete_word_backward" => PromptDeleteWordBackward,
935 "prompt_delete_to_line_end" => PromptDeleteToLineEnd,
936 "prompt_copy" => PromptCopy,
937 "prompt_cut" => PromptCut,
938 "prompt_paste" => PromptPaste,
939 "prompt_move_left_selecting" => PromptMoveLeftSelecting,
940 "prompt_move_right_selecting" => PromptMoveRightSelecting,
941 "prompt_move_home_selecting" => PromptMoveHomeSelecting,
942 "prompt_move_end_selecting" => PromptMoveEndSelecting,
943 "prompt_select_word_left" => PromptSelectWordLeft,
944 "prompt_select_word_right" => PromptSelectWordRight,
945 "prompt_select_all" => PromptSelectAll,
946 "file_browser_toggle_hidden" => FileBrowserToggleHidden,
947 "file_browser_toggle_detect_encoding" => FileBrowserToggleDetectEncoding,
948 "prompt_move_word_left" => PromptMoveWordLeft,
949 "prompt_move_word_right" => PromptMoveWordRight,
950 "prompt_delete" => PromptDelete,
951
952 "popup_select_next" => PopupSelectNext,
953 "popup_select_prev" => PopupSelectPrev,
954 "popup_page_up" => PopupPageUp,
955 "popup_page_down" => PopupPageDown,
956 "popup_confirm" => PopupConfirm,
957 "popup_cancel" => PopupCancel,
958
959 "toggle_file_explorer" => ToggleFileExplorer,
960 "toggle_menu_bar" => ToggleMenuBar,
961 "toggle_tab_bar" => ToggleTabBar,
962 "toggle_status_bar" => ToggleStatusBar,
963 "toggle_prompt_line" => TogglePromptLine,
964 "toggle_vertical_scrollbar" => ToggleVerticalScrollbar,
965 "toggle_horizontal_scrollbar" => ToggleHorizontalScrollbar,
966 "focus_file_explorer" => FocusFileExplorer,
967 "focus_editor" => FocusEditor,
968 "file_explorer_up" => FileExplorerUp,
969 "file_explorer_down" => FileExplorerDown,
970 "file_explorer_page_up" => FileExplorerPageUp,
971 "file_explorer_page_down" => FileExplorerPageDown,
972 "file_explorer_expand" => FileExplorerExpand,
973 "file_explorer_collapse" => FileExplorerCollapse,
974 "file_explorer_open" => FileExplorerOpen,
975 "file_explorer_refresh" => FileExplorerRefresh,
976 "file_explorer_new_file" => FileExplorerNewFile,
977 "file_explorer_new_directory" => FileExplorerNewDirectory,
978 "file_explorer_delete" => FileExplorerDelete,
979 "file_explorer_rename" => FileExplorerRename,
980 "file_explorer_toggle_hidden" => FileExplorerToggleHidden,
981 "file_explorer_toggle_gitignored" => FileExplorerToggleGitignored,
982 "file_explorer_search_clear" => FileExplorerSearchClear,
983 "file_explorer_search_backspace" => FileExplorerSearchBackspace,
984
985 "lsp_completion" => LspCompletion,
986 "lsp_goto_definition" => LspGotoDefinition,
987 "lsp_references" => LspReferences,
988 "lsp_rename" => LspRename,
989 "lsp_hover" => LspHover,
990 "lsp_signature_help" => LspSignatureHelp,
991 "lsp_code_actions" => LspCodeActions,
992 "lsp_restart" => LspRestart,
993 "lsp_stop" => LspStop,
994 "lsp_toggle_for_buffer" => LspToggleForBuffer,
995 "toggle_inlay_hints" => ToggleInlayHints,
996 "toggle_mouse_hover" => ToggleMouseHover,
997
998 "toggle_line_numbers" => ToggleLineNumbers,
999 "toggle_scroll_sync" => ToggleScrollSync,
1000 "toggle_mouse_capture" => ToggleMouseCapture,
1001 "toggle_debug_highlights" => ToggleDebugHighlights,
1002 "set_background" => SetBackground,
1003 "set_background_blend" => SetBackgroundBlend,
1004 "inspect_theme_at_cursor" => InspectThemeAtCursor,
1005 "select_theme" => SelectTheme,
1006 "select_keybinding_map" => SelectKeybindingMap,
1007 "select_cursor_style" => SelectCursorStyle,
1008 "select_locale" => SelectLocale,
1009
1010 "set_tab_size" => SetTabSize,
1011 "set_line_ending" => SetLineEnding,
1012 "set_encoding" => SetEncoding,
1013 "reload_with_encoding" => ReloadWithEncoding,
1014 "set_language" => SetLanguage,
1015 "toggle_indentation_style" => ToggleIndentationStyle,
1016 "toggle_tab_indicators" => ToggleTabIndicators,
1017 "toggle_whitespace_indicators" => ToggleWhitespaceIndicators,
1018 "reset_buffer_settings" => ResetBufferSettings,
1019 "add_ruler" => AddRuler,
1020 "remove_ruler" => RemoveRuler,
1021
1022 "dump_config" => DumpConfig,
1023
1024 "search" => Search,
1025 "find_in_selection" => FindInSelection,
1026 "find_next" => FindNext,
1027 "find_previous" => FindPrevious,
1028 "find_selection_next" => FindSelectionNext,
1029 "find_selection_previous" => FindSelectionPrevious,
1030 "replace" => Replace,
1031 "query_replace" => QueryReplace,
1032
1033 "menu_activate" => MenuActivate,
1034 "menu_close" => MenuClose,
1035 "menu_left" => MenuLeft,
1036 "menu_right" => MenuRight,
1037 "menu_up" => MenuUp,
1038 "menu_down" => MenuDown,
1039 "menu_execute" => MenuExecute,
1040
1041 "open_terminal" => OpenTerminal,
1042 "close_terminal" => CloseTerminal,
1043 "focus_terminal" => FocusTerminal,
1044 "terminal_escape" => TerminalEscape,
1045 "toggle_keyboard_capture" => ToggleKeyboardCapture,
1046 "terminal_paste" => TerminalPaste,
1047
1048 "shell_command" => ShellCommand,
1049 "shell_command_replace" => ShellCommandReplace,
1050
1051 "to_upper_case" => ToUpperCase,
1052 "to_lower_case" => ToLowerCase,
1053 "toggle_case" => ToggleCase,
1054 "sort_lines" => SortLines,
1055
1056 "calibrate_input" => CalibrateInput,
1057 "event_debug" => EventDebug,
1058 "load_plugin_from_buffer" => LoadPluginFromBuffer,
1059 "open_keybinding_editor" => OpenKeybindingEditor,
1060
1061 "composite_next_hunk" => CompositeNextHunk,
1062 "composite_prev_hunk" => CompositePrevHunk,
1063
1064 "noop" => None,
1065
1066 "open_settings" => OpenSettings,
1067 "close_settings" => CloseSettings,
1068 "settings_save" => SettingsSave,
1069 "settings_reset" => SettingsReset,
1070 "settings_toggle_focus" => SettingsToggleFocus,
1071 "settings_activate" => SettingsActivate,
1072 "settings_search" => SettingsSearch,
1073 "settings_help" => SettingsHelp,
1074 "settings_increment" => SettingsIncrement,
1075 "settings_decrement" => SettingsDecrement,
1076 "settings_inherit" => SettingsInherit,
1077 }
1078 alias {
1079 "toggle_compose_mode" => TogglePageView,
1080 "set_compose_width" => SetPageWidth,
1081 }
1082 with_char {
1083 "insert_char" => InsertChar,
1084 "set_bookmark" => SetBookmark,
1085 "jump_to_bookmark" => JumpToBookmark,
1086 "clear_bookmark" => ClearBookmark,
1087 "play_macro" => PlayMacro,
1088 "toggle_macro_recording" => ToggleMacroRecording,
1089 "show_macro" => ShowMacro,
1090 }
1091 custom {
1092 "copy_with_theme" => CopyWithTheme : {
1093 let theme = args.get("theme").and_then(|v| v.as_str()).unwrap_or("");
1095 Self::CopyWithTheme(theme.to_string())
1096 },
1097 "menu_open" => MenuOpen : {
1098 let name = args.get("name")?.as_str()?;
1099 Self::MenuOpen(name.to_string())
1100 },
1101 "switch_keybinding_map" => SwitchKeybindingMap : {
1102 let map_name = args.get("map")?.as_str()?;
1103 Self::SwitchKeybindingMap(map_name.to_string())
1104 },
1105 "prompt_confirm_with_text" => PromptConfirmWithText : {
1106 let text = args.get("text")?.as_str()?;
1107 Self::PromptConfirmWithText(text.to_string())
1108 },
1109 }
1110 }
1111
1112 pub fn is_movement_or_editing(&self) -> bool {
1115 matches!(
1116 self,
1117 Action::MoveLeft
1119 | Action::MoveRight
1120 | Action::MoveUp
1121 | Action::MoveDown
1122 | Action::MoveWordLeft
1123 | Action::MoveWordRight
1124 | Action::MoveWordEnd
1125 | Action::ViMoveWordEnd
1126 | Action::MoveLeftInLine
1127 | Action::MoveRightInLine
1128 | Action::MoveLineStart
1129 | Action::MoveLineEnd
1130 | Action::MovePageUp
1131 | Action::MovePageDown
1132 | Action::MoveDocumentStart
1133 | Action::MoveDocumentEnd
1134 | Action::SelectLeft
1136 | Action::SelectRight
1137 | Action::SelectUp
1138 | Action::SelectDown
1139 | Action::SelectToParagraphUp
1140 | Action::SelectToParagraphDown
1141 | Action::SelectWordLeft
1142 | Action::SelectWordRight
1143 | Action::SelectWordEnd
1144 | Action::ViSelectWordEnd
1145 | Action::SelectLineStart
1146 | Action::SelectLineEnd
1147 | Action::SelectDocumentStart
1148 | Action::SelectDocumentEnd
1149 | Action::SelectPageUp
1150 | Action::SelectPageDown
1151 | Action::SelectAll
1152 | Action::SelectWord
1153 | Action::SelectLine
1154 | Action::ExpandSelection
1155 | Action::BlockSelectLeft
1157 | Action::BlockSelectRight
1158 | Action::BlockSelectUp
1159 | Action::BlockSelectDown
1160 | Action::InsertChar(_)
1162 | Action::InsertNewline
1163 | Action::InsertTab
1164 | Action::DeleteBackward
1165 | Action::DeleteForward
1166 | Action::DeleteWordBackward
1167 | Action::DeleteWordForward
1168 | Action::DeleteLine
1169 | Action::DeleteToLineEnd
1170 | Action::DeleteToLineStart
1171 | Action::TransposeChars
1172 | Action::OpenLine
1173 | Action::DuplicateLine
1174 | Action::MoveLineUp
1175 | Action::MoveLineDown
1176 | Action::Cut
1178 | Action::Paste
1179 | Action::Undo
1181 | Action::Redo
1182 )
1183 }
1184
1185 pub fn is_editing(&self) -> bool {
1188 matches!(
1189 self,
1190 Action::InsertChar(_)
1191 | Action::InsertNewline
1192 | Action::InsertTab
1193 | Action::DeleteBackward
1194 | Action::DeleteForward
1195 | Action::DeleteWordBackward
1196 | Action::DeleteWordForward
1197 | Action::DeleteLine
1198 | Action::DeleteToLineEnd
1199 | Action::DeleteToLineStart
1200 | Action::DeleteViWordEnd
1201 | Action::TransposeChars
1202 | Action::OpenLine
1203 | Action::DuplicateLine
1204 | Action::MoveLineUp
1205 | Action::MoveLineDown
1206 | Action::Cut
1207 | Action::Paste
1208 )
1209 }
1210}
1211
1212#[derive(Debug, Clone, PartialEq)]
1214pub enum ChordResolution {
1215 Complete(Action),
1217 Partial,
1219 NoMatch,
1221}
1222
1223#[derive(Clone)]
1225pub struct KeybindingResolver {
1226 bindings: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1229
1230 default_bindings: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1232
1233 plugin_defaults: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1236
1237 chord_bindings: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1240
1241 default_chord_bindings: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1243
1244 plugin_chord_defaults: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1246}
1247
1248impl KeybindingResolver {
1249 pub fn new(config: &Config) -> Self {
1251 let mut resolver = Self {
1252 bindings: HashMap::new(),
1253 default_bindings: HashMap::new(),
1254 plugin_defaults: HashMap::new(),
1255 chord_bindings: HashMap::new(),
1256 default_chord_bindings: HashMap::new(),
1257 plugin_chord_defaults: HashMap::new(),
1258 };
1259
1260 let map_bindings = config.resolve_keymap(&config.active_keybinding_map);
1262 resolver.load_default_bindings_from_vec(&map_bindings);
1263
1264 resolver.load_bindings_from_vec(&config.keybindings);
1266
1267 resolver
1268 }
1269
1270 fn load_default_bindings_from_vec(&mut self, bindings: &[crate::config::Keybinding]) {
1272 for binding in bindings {
1273 let context = if let Some(ref when) = binding.when {
1275 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
1276 } else {
1277 KeyContext::Normal
1278 };
1279
1280 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
1281 if !binding.keys.is_empty() {
1283 let mut sequence = Vec::new();
1285 for key_press in &binding.keys {
1286 if let Some(key_code) = Self::parse_key(&key_press.key) {
1287 let modifiers = Self::parse_modifiers(&key_press.modifiers);
1288 sequence.push((key_code, modifiers));
1289 } else {
1290 break;
1292 }
1293 }
1294
1295 if sequence.len() == binding.keys.len() && !sequence.is_empty() {
1297 self.default_chord_bindings
1298 .entry(context)
1299 .or_default()
1300 .insert(sequence, action);
1301 }
1302 } else if let Some(key_code) = Self::parse_key(&binding.key) {
1303 let modifiers = Self::parse_modifiers(&binding.modifiers);
1305
1306 self.insert_binding_with_equivalents(
1308 context,
1309 key_code,
1310 modifiers,
1311 action,
1312 &binding.key,
1313 );
1314 }
1315 }
1316 }
1317 }
1318
1319 fn insert_binding_with_equivalents(
1322 &mut self,
1323 context: KeyContext,
1324 key_code: KeyCode,
1325 modifiers: KeyModifiers,
1326 action: Action,
1327 key_name: &str,
1328 ) {
1329 let context_bindings = self.default_bindings.entry(context.clone()).or_default();
1330
1331 context_bindings.insert((key_code, modifiers), action.clone());
1333
1334 let equivalents = terminal_key_equivalents(key_code, modifiers);
1336 for (equiv_key, equiv_mods) in equivalents {
1337 if let Some(existing_action) = context_bindings.get(&(equiv_key, equiv_mods)) {
1339 if existing_action != &action {
1341 let equiv_name = format!("{:?}", equiv_key);
1342 tracing::warn!(
1343 "Terminal key equivalent conflict in {:?} context: {} (equivalent of {}) \
1344 is bound to {:?}, but {} is bound to {:?}. \
1345 The explicit binding takes precedence.",
1346 context,
1347 equiv_name,
1348 key_name,
1349 existing_action,
1350 key_name,
1351 action
1352 );
1353 }
1354 } else {
1356 context_bindings.insert((equiv_key, equiv_mods), action.clone());
1358 }
1359 }
1360 }
1361
1362 fn load_bindings_from_vec(&mut self, bindings: &[crate::config::Keybinding]) {
1364 for binding in bindings {
1365 let context = if let Some(ref when) = binding.when {
1367 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
1368 } else {
1369 KeyContext::Normal
1370 };
1371
1372 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
1373 if !binding.keys.is_empty() {
1375 let mut sequence = Vec::new();
1377 for key_press in &binding.keys {
1378 if let Some(key_code) = Self::parse_key(&key_press.key) {
1379 let modifiers = Self::parse_modifiers(&key_press.modifiers);
1380 sequence.push((key_code, modifiers));
1381 } else {
1382 break;
1384 }
1385 }
1386
1387 if sequence.len() == binding.keys.len() && !sequence.is_empty() {
1389 self.chord_bindings
1390 .entry(context)
1391 .or_default()
1392 .insert(sequence, action);
1393 }
1394 } else if let Some(key_code) = Self::parse_key(&binding.key) {
1395 let modifiers = Self::parse_modifiers(&binding.modifiers);
1397 self.bindings
1398 .entry(context)
1399 .or_default()
1400 .insert((key_code, modifiers), action);
1401 }
1402 }
1403 }
1404 }
1405
1406 pub fn load_plugin_default(
1408 &mut self,
1409 context: KeyContext,
1410 key_code: KeyCode,
1411 modifiers: KeyModifiers,
1412 action: Action,
1413 ) {
1414 self.plugin_defaults
1415 .entry(context)
1416 .or_default()
1417 .insert((key_code, modifiers), action);
1418 }
1419
1420 pub fn load_plugin_chord_default(
1422 &mut self,
1423 context: KeyContext,
1424 sequence: Vec<(KeyCode, KeyModifiers)>,
1425 action: Action,
1426 ) {
1427 self.plugin_chord_defaults
1428 .entry(context)
1429 .or_default()
1430 .insert(sequence, action);
1431 }
1432
1433 pub fn clear_plugin_defaults_for_mode(&mut self, mode_name: &str) {
1435 let context = KeyContext::Mode(mode_name.to_string());
1436 self.plugin_defaults.remove(&context);
1437 self.plugin_chord_defaults.remove(&context);
1438 }
1439
1440 pub fn get_plugin_defaults(
1442 &self,
1443 ) -> &HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>> {
1444 &self.plugin_defaults
1445 }
1446
1447 fn is_application_wide_action(action: &Action) -> bool {
1449 matches!(
1450 action,
1451 Action::Quit
1452 | Action::ForceQuit
1453 | Action::Save
1454 | Action::SaveAs
1455 | Action::ShowHelp
1456 | Action::ShowKeyboardShortcuts
1457 | Action::PromptCancel | Action::PopupCancel )
1460 }
1461
1462 pub fn is_terminal_ui_action(action: &Action) -> bool {
1466 matches!(
1467 action,
1468 Action::CommandPalette
1470 | Action::QuickOpen
1471 | Action::OpenSettings
1472 | Action::MenuActivate
1473 | Action::MenuOpen(_)
1474 | Action::ShowHelp
1475 | Action::ShowKeyboardShortcuts
1476 | Action::Quit
1477 | Action::ForceQuit
1478 | Action::NextSplit
1480 | Action::PrevSplit
1481 | Action::SplitHorizontal
1482 | Action::SplitVertical
1483 | Action::CloseSplit
1484 | Action::ToggleMaximizeSplit
1485 | Action::NextBuffer
1487 | Action::PrevBuffer
1488 | Action::Close
1489 | Action::ScrollTabsLeft
1490 | Action::ScrollTabsRight
1491 | Action::TerminalEscape
1493 | Action::ToggleKeyboardCapture
1494 | Action::OpenTerminal
1495 | Action::CloseTerminal
1496 | Action::TerminalPaste
1497 | Action::ToggleFileExplorer
1499 | Action::ToggleMenuBar
1501 )
1502 }
1503
1504 pub fn resolve_chord(
1510 &self,
1511 chord_state: &[(KeyCode, KeyModifiers)],
1512 event: &KeyEvent,
1513 context: KeyContext,
1514 ) -> ChordResolution {
1515 let mut full_sequence: Vec<(KeyCode, KeyModifiers)> = chord_state
1517 .iter()
1518 .map(|(c, m)| normalize_key(*c, *m))
1519 .collect();
1520 let (norm_code, norm_mods) = normalize_key(event.code, event.modifiers);
1521 full_sequence.push((norm_code, norm_mods));
1522
1523 tracing::trace!(
1524 "KeybindingResolver.resolve_chord: sequence={:?}, context={:?}",
1525 full_sequence,
1526 context
1527 );
1528
1529 let search_order = vec![
1531 (&self.chord_bindings, &KeyContext::Global, "custom global"),
1532 (
1533 &self.default_chord_bindings,
1534 &KeyContext::Global,
1535 "default global",
1536 ),
1537 (&self.chord_bindings, &context, "custom context"),
1538 (&self.default_chord_bindings, &context, "default context"),
1539 (
1540 &self.plugin_chord_defaults,
1541 &context,
1542 "plugin default context",
1543 ),
1544 ];
1545
1546 let mut has_partial_match = false;
1547
1548 for (binding_map, bind_context, label) in search_order {
1549 if let Some(context_chords) = binding_map.get(bind_context) {
1550 if let Some(action) = context_chords.get(&full_sequence) {
1552 tracing::trace!(" -> Complete chord match in {}: {:?}", label, action);
1553 return ChordResolution::Complete(action.clone());
1554 }
1555
1556 for (chord_seq, _) in context_chords.iter() {
1558 if chord_seq.len() > full_sequence.len()
1559 && chord_seq[..full_sequence.len()] == full_sequence[..]
1560 {
1561 tracing::trace!(" -> Partial chord match in {}", label);
1562 has_partial_match = true;
1563 break;
1564 }
1565 }
1566 }
1567 }
1568
1569 if has_partial_match {
1570 ChordResolution::Partial
1571 } else {
1572 tracing::trace!(" -> No chord match");
1573 ChordResolution::NoMatch
1574 }
1575 }
1576
1577 pub fn resolve(&self, event: &KeyEvent, context: KeyContext) -> Action {
1579 let (norm_code, norm_mods) = normalize_key(event.code, event.modifiers);
1582 let norm = &(norm_code, norm_mods);
1583 tracing::trace!(
1584 "KeybindingResolver.resolve: code={:?}, modifiers={:?}, context={:?}",
1585 event.code,
1586 event.modifiers,
1587 context
1588 );
1589
1590 if let Some(global_bindings) = self.bindings.get(&KeyContext::Global) {
1592 if let Some(action) = global_bindings.get(norm) {
1593 tracing::trace!(" -> Found in custom global bindings: {:?}", action);
1594 return action.clone();
1595 }
1596 }
1597
1598 if let Some(global_bindings) = self.default_bindings.get(&KeyContext::Global) {
1599 if let Some(action) = global_bindings.get(norm) {
1600 tracing::trace!(" -> Found in default global bindings: {:?}", action);
1601 return action.clone();
1602 }
1603 }
1604
1605 if let Some(context_bindings) = self.bindings.get(&context) {
1607 if let Some(action) = context_bindings.get(norm) {
1608 tracing::trace!(
1609 " -> Found in custom {} bindings: {:?}",
1610 context.to_when_clause(),
1611 action
1612 );
1613 return action.clone();
1614 }
1615 }
1616
1617 if let Some(context_bindings) = self.default_bindings.get(&context) {
1619 if let Some(action) = context_bindings.get(norm) {
1620 tracing::trace!(
1621 " -> Found in default {} bindings: {:?}",
1622 context.to_when_clause(),
1623 action
1624 );
1625 return action.clone();
1626 }
1627 }
1628
1629 if let Some(plugin_bindings) = self.plugin_defaults.get(&context) {
1631 if let Some(action) = plugin_bindings.get(norm) {
1632 tracing::trace!(
1633 " -> Found in plugin default {} bindings: {:?}",
1634 context.to_when_clause(),
1635 action
1636 );
1637 return action.clone();
1638 }
1639 }
1640
1641 if context != KeyContext::Normal {
1645 let full_fallthrough = context.allows_normal_fallthrough();
1646
1647 if let Some(normal_bindings) = self.bindings.get(&KeyContext::Normal) {
1648 if let Some(action) = normal_bindings.get(norm) {
1649 if full_fallthrough || Self::is_application_wide_action(action) {
1650 tracing::trace!(
1651 " -> Found action in custom normal bindings (fallthrough): {:?}",
1652 action
1653 );
1654 return action.clone();
1655 }
1656 }
1657 }
1658
1659 if let Some(normal_bindings) = self.default_bindings.get(&KeyContext::Normal) {
1660 if let Some(action) = normal_bindings.get(norm) {
1661 if full_fallthrough || Self::is_application_wide_action(action) {
1662 tracing::trace!(
1663 " -> Found action in default normal bindings (fallthrough): {:?}",
1664 action
1665 );
1666 return action.clone();
1667 }
1668 }
1669 }
1670 }
1671
1672 if context.allows_text_input() && is_text_input_modifier(event.modifiers) {
1674 if let KeyCode::Char(c) = event.code {
1675 tracing::trace!(" -> Character input: '{}'", c);
1676 return Action::InsertChar(c);
1677 }
1678 }
1679
1680 tracing::trace!(" -> No binding found, returning Action::None");
1681 Action::None
1682 }
1683
1684 pub fn resolve_in_context_only(&self, event: &KeyEvent, context: KeyContext) -> Option<Action> {
1689 let norm = normalize_key(event.code, event.modifiers);
1690 if let Some(context_bindings) = self.bindings.get(&context) {
1692 if let Some(action) = context_bindings.get(&norm) {
1693 return Some(action.clone());
1694 }
1695 }
1696
1697 if let Some(context_bindings) = self.default_bindings.get(&context) {
1699 if let Some(action) = context_bindings.get(&norm) {
1700 return Some(action.clone());
1701 }
1702 }
1703
1704 None
1705 }
1706
1707 pub fn resolve_terminal_ui_action(&self, event: &KeyEvent) -> Action {
1711 let norm = normalize_key(event.code, event.modifiers);
1712 tracing::trace!(
1713 "KeybindingResolver.resolve_terminal_ui_action: code={:?}, modifiers={:?}",
1714 event.code,
1715 event.modifiers
1716 );
1717
1718 for bindings in [&self.bindings, &self.default_bindings] {
1720 if let Some(terminal_bindings) = bindings.get(&KeyContext::Terminal) {
1721 if let Some(action) = terminal_bindings.get(&norm) {
1722 if Self::is_terminal_ui_action(action) {
1723 tracing::trace!(" -> Found UI action in terminal bindings: {:?}", action);
1724 return action.clone();
1725 }
1726 }
1727 }
1728 }
1729
1730 for bindings in [&self.bindings, &self.default_bindings] {
1732 if let Some(global_bindings) = bindings.get(&KeyContext::Global) {
1733 if let Some(action) = global_bindings.get(&norm) {
1734 if Self::is_terminal_ui_action(action) {
1735 tracing::trace!(" -> Found UI action in global bindings: {:?}", action);
1736 return action.clone();
1737 }
1738 }
1739 }
1740 }
1741
1742 for bindings in [&self.bindings, &self.default_bindings] {
1744 if let Some(normal_bindings) = bindings.get(&KeyContext::Normal) {
1745 if let Some(action) = normal_bindings.get(&norm) {
1746 if Self::is_terminal_ui_action(action) {
1747 tracing::trace!(" -> Found UI action in normal bindings: {:?}", action);
1748 return action.clone();
1749 }
1750 }
1751 }
1752 }
1753
1754 tracing::trace!(" -> No UI action found");
1755 Action::None
1756 }
1757
1758 pub fn find_keybinding_for_action(
1761 &self,
1762 action_name: &str,
1763 context: KeyContext,
1764 ) -> Option<String> {
1765 let target_action = Action::from_str(action_name, &HashMap::new())?;
1767
1768 let search_maps = vec![
1770 self.bindings.get(&context),
1771 self.bindings.get(&KeyContext::Global),
1772 self.default_bindings.get(&context),
1773 self.default_bindings.get(&KeyContext::Global),
1774 ];
1775
1776 for map in search_maps.into_iter().flatten() {
1777 let mut matches: Vec<(KeyCode, KeyModifiers)> = map
1779 .iter()
1780 .filter(|(_, action)| {
1781 std::mem::discriminant(*action) == std::mem::discriminant(&target_action)
1782 })
1783 .map(|((key_code, modifiers), _)| (*key_code, *modifiers))
1784 .collect();
1785
1786 if !matches.is_empty() {
1787 matches.sort_by(|(key_a, mod_a), (key_b, mod_b)| {
1789 let mod_count_a = mod_a.bits().count_ones();
1791 let mod_count_b = mod_b.bits().count_ones();
1792 match mod_count_a.cmp(&mod_count_b) {
1793 std::cmp::Ordering::Equal => {
1794 match mod_a.bits().cmp(&mod_b.bits()) {
1796 std::cmp::Ordering::Equal => {
1797 Self::key_code_sort_key(key_a)
1799 .cmp(&Self::key_code_sort_key(key_b))
1800 }
1801 other => other,
1802 }
1803 }
1804 other => other,
1805 }
1806 });
1807
1808 let (key_code, modifiers) = matches[0];
1809 return Some(format_keybinding(&key_code, &modifiers));
1810 }
1811 }
1812
1813 None
1814 }
1815
1816 fn key_code_sort_key(key_code: &KeyCode) -> (u8, u32) {
1818 match key_code {
1819 KeyCode::Char(c) => (0, *c as u32),
1820 KeyCode::F(n) => (1, *n as u32),
1821 KeyCode::Enter => (2, 0),
1822 KeyCode::Tab => (2, 1),
1823 KeyCode::Backspace => (2, 2),
1824 KeyCode::Delete => (2, 3),
1825 KeyCode::Esc => (2, 4),
1826 KeyCode::Left => (3, 0),
1827 KeyCode::Right => (3, 1),
1828 KeyCode::Up => (3, 2),
1829 KeyCode::Down => (3, 3),
1830 KeyCode::Home => (3, 4),
1831 KeyCode::End => (3, 5),
1832 KeyCode::PageUp => (3, 6),
1833 KeyCode::PageDown => (3, 7),
1834 _ => (255, 0),
1835 }
1836 }
1837
1838 pub fn find_menu_mnemonic(&self, menu_name: &str) -> Option<char> {
1841 let search_maps = vec![
1843 self.bindings.get(&KeyContext::Normal),
1844 self.bindings.get(&KeyContext::Global),
1845 self.default_bindings.get(&KeyContext::Normal),
1846 self.default_bindings.get(&KeyContext::Global),
1847 ];
1848
1849 for map in search_maps.into_iter().flatten() {
1850 for ((key_code, modifiers), action) in map {
1851 if let Action::MenuOpen(name) = action {
1853 if name.eq_ignore_ascii_case(menu_name) && *modifiers == KeyModifiers::ALT {
1854 if let KeyCode::Char(c) = key_code {
1856 return Some(c.to_ascii_lowercase());
1857 }
1858 }
1859 }
1860 }
1861 }
1862
1863 None
1864 }
1865
1866 fn parse_key(key: &str) -> Option<KeyCode> {
1868 let lower = key.to_lowercase();
1869 match lower.as_str() {
1870 "enter" => Some(KeyCode::Enter),
1871 "backspace" => Some(KeyCode::Backspace),
1872 "delete" | "del" => Some(KeyCode::Delete),
1873 "tab" => Some(KeyCode::Tab),
1874 "backtab" => Some(KeyCode::BackTab),
1875 "esc" | "escape" => Some(KeyCode::Esc),
1876 "space" => Some(KeyCode::Char(' ')),
1877
1878 "left" => Some(KeyCode::Left),
1879 "right" => Some(KeyCode::Right),
1880 "up" => Some(KeyCode::Up),
1881 "down" => Some(KeyCode::Down),
1882 "home" => Some(KeyCode::Home),
1883 "end" => Some(KeyCode::End),
1884 "pageup" => Some(KeyCode::PageUp),
1885 "pagedown" => Some(KeyCode::PageDown),
1886
1887 s if s.len() == 1 => s.chars().next().map(KeyCode::Char),
1888 s if s.starts_with('f') && s.len() >= 2 => s[1..].parse::<u8>().ok().map(KeyCode::F),
1890 _ => None,
1891 }
1892 }
1893
1894 fn parse_modifiers(modifiers: &[String]) -> KeyModifiers {
1896 let mut result = KeyModifiers::empty();
1897 for m in modifiers {
1898 match m.to_lowercase().as_str() {
1899 "ctrl" | "control" => result |= KeyModifiers::CONTROL,
1900 "shift" => result |= KeyModifiers::SHIFT,
1901 "alt" => result |= KeyModifiers::ALT,
1902 "super" | "cmd" | "command" | "meta" => result |= KeyModifiers::SUPER,
1903 _ => {}
1904 }
1905 }
1906 result
1907 }
1908
1909 pub fn get_all_bindings(&self) -> Vec<(String, String)> {
1913 let mut bindings = Vec::new();
1914
1915 for context in &[
1917 KeyContext::Normal,
1918 KeyContext::Prompt,
1919 KeyContext::Popup,
1920 KeyContext::FileExplorer,
1921 KeyContext::Menu,
1922 KeyContext::CompositeBuffer,
1923 ] {
1924 let mut all_keys: HashMap<(KeyCode, KeyModifiers), Action> = HashMap::new();
1925
1926 if let Some(context_defaults) = self.default_bindings.get(context) {
1928 for (key, action) in context_defaults {
1929 all_keys.insert(*key, action.clone());
1930 }
1931 }
1932
1933 if let Some(context_bindings) = self.bindings.get(context) {
1935 for (key, action) in context_bindings {
1936 all_keys.insert(*key, action.clone());
1937 }
1938 }
1939
1940 let context_str = if *context != KeyContext::Normal {
1942 format!("[{}] ", context.to_when_clause())
1943 } else {
1944 String::new()
1945 };
1946
1947 for ((key_code, modifiers), action) in all_keys {
1948 let key_str = Self::format_key(key_code, modifiers);
1949 let action_str = format!("{}{}", context_str, Self::format_action(&action));
1950 bindings.push((key_str, action_str));
1951 }
1952 }
1953
1954 bindings.sort_by(|a, b| a.1.cmp(&b.1));
1956
1957 bindings
1958 }
1959
1960 fn format_key(key_code: KeyCode, modifiers: KeyModifiers) -> String {
1962 format_keybinding(&key_code, &modifiers)
1963 }
1964
1965 fn format_action(action: &Action) -> String {
1967 match action {
1968 Action::InsertChar(c) => t!("action.insert_char", char = c),
1969 Action::InsertNewline => t!("action.insert_newline"),
1970 Action::InsertTab => t!("action.insert_tab"),
1971 Action::MoveLeft => t!("action.move_left"),
1972 Action::MoveRight => t!("action.move_right"),
1973 Action::MoveUp => t!("action.move_up"),
1974 Action::MoveDown => t!("action.move_down"),
1975 Action::MoveWordLeft => t!("action.move_word_left"),
1976 Action::MoveWordRight => t!("action.move_word_right"),
1977 Action::MoveWordEnd => t!("action.move_word_end"),
1978 Action::ViMoveWordEnd => t!("action.move_word_end"),
1979 Action::MoveLeftInLine => t!("action.move_left"),
1980 Action::MoveRightInLine => t!("action.move_right"),
1981 Action::MoveLineStart => t!("action.move_line_start"),
1982 Action::MoveLineEnd => t!("action.move_line_end"),
1983 Action::MoveLineUp => t!("action.move_line_up"),
1984 Action::MoveLineDown => t!("action.move_line_down"),
1985 Action::MovePageUp => t!("action.move_page_up"),
1986 Action::MovePageDown => t!("action.move_page_down"),
1987 Action::MoveDocumentStart => t!("action.move_document_start"),
1988 Action::MoveDocumentEnd => t!("action.move_document_end"),
1989 Action::SelectLeft => t!("action.select_left"),
1990 Action::SelectRight => t!("action.select_right"),
1991 Action::SelectUp => t!("action.select_up"),
1992 Action::SelectDown => t!("action.select_down"),
1993 Action::SelectToParagraphUp => t!("action.select_to_paragraph_up"),
1994 Action::SelectToParagraphDown => t!("action.select_to_paragraph_down"),
1995 Action::SelectWordLeft => t!("action.select_word_left"),
1996 Action::SelectWordRight => t!("action.select_word_right"),
1997 Action::SelectWordEnd => t!("action.select_word_end"),
1998 Action::ViSelectWordEnd => t!("action.select_word_end"),
1999 Action::SelectLineStart => t!("action.select_line_start"),
2000 Action::SelectLineEnd => t!("action.select_line_end"),
2001 Action::SelectDocumentStart => t!("action.select_document_start"),
2002 Action::SelectDocumentEnd => t!("action.select_document_end"),
2003 Action::SelectPageUp => t!("action.select_page_up"),
2004 Action::SelectPageDown => t!("action.select_page_down"),
2005 Action::SelectAll => t!("action.select_all"),
2006 Action::SelectWord => t!("action.select_word"),
2007 Action::SelectLine => t!("action.select_line"),
2008 Action::ExpandSelection => t!("action.expand_selection"),
2009 Action::BlockSelectLeft => t!("action.block_select_left"),
2010 Action::BlockSelectRight => t!("action.block_select_right"),
2011 Action::BlockSelectUp => t!("action.block_select_up"),
2012 Action::BlockSelectDown => t!("action.block_select_down"),
2013 Action::DeleteBackward => t!("action.delete_backward"),
2014 Action::DeleteForward => t!("action.delete_forward"),
2015 Action::DeleteWordBackward => t!("action.delete_word_backward"),
2016 Action::DeleteWordForward => t!("action.delete_word_forward"),
2017 Action::DeleteLine => t!("action.delete_line"),
2018 Action::DeleteToLineEnd => t!("action.delete_to_line_end"),
2019 Action::DeleteToLineStart => t!("action.delete_to_line_start"),
2020 Action::DeleteViWordEnd => t!("action.delete_word_forward"),
2021 Action::TransposeChars => t!("action.transpose_chars"),
2022 Action::OpenLine => t!("action.open_line"),
2023 Action::DuplicateLine => t!("action.duplicate_line"),
2024 Action::Recenter => t!("action.recenter"),
2025 Action::SetMark => t!("action.set_mark"),
2026 Action::Copy => t!("action.copy"),
2027 Action::CopyWithTheme(theme) if theme.is_empty() => t!("action.copy_with_formatting"),
2028 Action::CopyWithTheme(theme) => t!("action.copy_with_theme", theme = theme),
2029 Action::Cut => t!("action.cut"),
2030 Action::Paste => t!("action.paste"),
2031 Action::YankWordForward => t!("action.yank_word_forward"),
2032 Action::YankWordBackward => t!("action.yank_word_backward"),
2033 Action::YankToLineEnd => t!("action.yank_to_line_end"),
2034 Action::YankToLineStart => t!("action.yank_to_line_start"),
2035 Action::YankViWordEnd => t!("action.yank_word_forward"),
2036 Action::AddCursorAbove => t!("action.add_cursor_above"),
2037 Action::AddCursorBelow => t!("action.add_cursor_below"),
2038 Action::AddCursorNextMatch => t!("action.add_cursor_next_match"),
2039 Action::RemoveSecondaryCursors => t!("action.remove_secondary_cursors"),
2040 Action::Save => t!("action.save"),
2041 Action::SaveAs => t!("action.save_as"),
2042 Action::Open => t!("action.open"),
2043 Action::SwitchProject => t!("action.switch_project"),
2044 Action::New => t!("action.new"),
2045 Action::Close => t!("action.close"),
2046 Action::CloseTab => t!("action.close_tab"),
2047 Action::Quit => t!("action.quit"),
2048 Action::ForceQuit => t!("action.force_quit"),
2049 Action::Detach => t!("action.detach"),
2050 Action::Revert => t!("action.revert"),
2051 Action::ToggleAutoRevert => t!("action.toggle_auto_revert"),
2052 Action::FormatBuffer => t!("action.format_buffer"),
2053 Action::TrimTrailingWhitespace => t!("action.trim_trailing_whitespace"),
2054 Action::EnsureFinalNewline => t!("action.ensure_final_newline"),
2055 Action::GotoLine => t!("action.goto_line"),
2056 Action::ScanLineIndex => t!("action.scan_line_index"),
2057 Action::GoToMatchingBracket => t!("action.goto_matching_bracket"),
2058 Action::JumpToNextError => t!("action.jump_to_next_error"),
2059 Action::JumpToPreviousError => t!("action.jump_to_previous_error"),
2060 Action::SmartHome => t!("action.smart_home"),
2061 Action::DedentSelection => t!("action.dedent_selection"),
2062 Action::ToggleComment => t!("action.toggle_comment"),
2063 Action::DabbrevExpand => std::borrow::Cow::Borrowed("Expand abbreviation (dabbrev)"),
2064 Action::ToggleFold => t!("action.toggle_fold"),
2065 Action::SetBookmark(c) => t!("action.set_bookmark", key = c),
2066 Action::JumpToBookmark(c) => t!("action.jump_to_bookmark", key = c),
2067 Action::ClearBookmark(c) => t!("action.clear_bookmark", key = c),
2068 Action::ListBookmarks => t!("action.list_bookmarks"),
2069 Action::ToggleSearchCaseSensitive => t!("action.toggle_search_case_sensitive"),
2070 Action::ToggleSearchWholeWord => t!("action.toggle_search_whole_word"),
2071 Action::ToggleSearchRegex => t!("action.toggle_search_regex"),
2072 Action::ToggleSearchConfirmEach => t!("action.toggle_search_confirm_each"),
2073 Action::StartMacroRecording => t!("action.start_macro_recording"),
2074 Action::StopMacroRecording => t!("action.stop_macro_recording"),
2075 Action::PlayMacro(c) => t!("action.play_macro", key = c),
2076 Action::ToggleMacroRecording(c) => t!("action.toggle_macro_recording", key = c),
2077 Action::ShowMacro(c) => t!("action.show_macro", key = c),
2078 Action::ListMacros => t!("action.list_macros"),
2079 Action::PromptRecordMacro => t!("action.prompt_record_macro"),
2080 Action::PromptPlayMacro => t!("action.prompt_play_macro"),
2081 Action::PlayLastMacro => t!("action.play_last_macro"),
2082 Action::PromptSetBookmark => t!("action.prompt_set_bookmark"),
2083 Action::PromptJumpToBookmark => t!("action.prompt_jump_to_bookmark"),
2084 Action::Undo => t!("action.undo"),
2085 Action::Redo => t!("action.redo"),
2086 Action::ScrollUp => t!("action.scroll_up"),
2087 Action::ScrollDown => t!("action.scroll_down"),
2088 Action::ShowHelp => t!("action.show_help"),
2089 Action::ShowKeyboardShortcuts => t!("action.show_keyboard_shortcuts"),
2090 Action::ShowWarnings => t!("action.show_warnings"),
2091 Action::ShowStatusLog => t!("action.show_status_log"),
2092 Action::ShowLspStatus => t!("action.show_lsp_status"),
2093 Action::ClearWarnings => t!("action.clear_warnings"),
2094 Action::CommandPalette => t!("action.command_palette"),
2095 Action::QuickOpen => t!("action.quick_open"),
2096 Action::InspectThemeAtCursor => t!("action.inspect_theme_at_cursor"),
2097 Action::ToggleLineWrap => t!("action.toggle_line_wrap"),
2098 Action::ToggleCurrentLineHighlight => t!("action.toggle_current_line_highlight"),
2099 Action::ToggleReadOnly => t!("action.toggle_read_only"),
2100 Action::TogglePageView => t!("action.toggle_page_view"),
2101 Action::SetPageWidth => t!("action.set_page_width"),
2102 Action::NextBuffer => t!("action.next_buffer"),
2103 Action::PrevBuffer => t!("action.prev_buffer"),
2104 Action::NavigateBack => t!("action.navigate_back"),
2105 Action::NavigateForward => t!("action.navigate_forward"),
2106 Action::SplitHorizontal => t!("action.split_horizontal"),
2107 Action::SplitVertical => t!("action.split_vertical"),
2108 Action::CloseSplit => t!("action.close_split"),
2109 Action::NextSplit => t!("action.next_split"),
2110 Action::PrevSplit => t!("action.prev_split"),
2111 Action::IncreaseSplitSize => t!("action.increase_split_size"),
2112 Action::DecreaseSplitSize => t!("action.decrease_split_size"),
2113 Action::ToggleMaximizeSplit => t!("action.toggle_maximize_split"),
2114 Action::PromptConfirm => t!("action.prompt_confirm"),
2115 Action::PromptConfirmWithText(ref text) => {
2116 format!("{} ({})", t!("action.prompt_confirm"), text).into()
2117 }
2118 Action::PromptCancel => t!("action.prompt_cancel"),
2119 Action::PromptBackspace => t!("action.prompt_backspace"),
2120 Action::PromptDelete => t!("action.prompt_delete"),
2121 Action::PromptMoveLeft => t!("action.prompt_move_left"),
2122 Action::PromptMoveRight => t!("action.prompt_move_right"),
2123 Action::PromptMoveStart => t!("action.prompt_move_start"),
2124 Action::PromptMoveEnd => t!("action.prompt_move_end"),
2125 Action::PromptSelectPrev => t!("action.prompt_select_prev"),
2126 Action::PromptSelectNext => t!("action.prompt_select_next"),
2127 Action::PromptPageUp => t!("action.prompt_page_up"),
2128 Action::PromptPageDown => t!("action.prompt_page_down"),
2129 Action::PromptAcceptSuggestion => t!("action.prompt_accept_suggestion"),
2130 Action::PromptMoveWordLeft => t!("action.prompt_move_word_left"),
2131 Action::PromptMoveWordRight => t!("action.prompt_move_word_right"),
2132 Action::PromptDeleteWordForward => t!("action.prompt_delete_word_forward"),
2133 Action::PromptDeleteWordBackward => t!("action.prompt_delete_word_backward"),
2134 Action::PromptDeleteToLineEnd => t!("action.prompt_delete_to_line_end"),
2135 Action::PromptCopy => t!("action.prompt_copy"),
2136 Action::PromptCut => t!("action.prompt_cut"),
2137 Action::PromptPaste => t!("action.prompt_paste"),
2138 Action::PromptMoveLeftSelecting => t!("action.prompt_move_left_selecting"),
2139 Action::PromptMoveRightSelecting => t!("action.prompt_move_right_selecting"),
2140 Action::PromptMoveHomeSelecting => t!("action.prompt_move_home_selecting"),
2141 Action::PromptMoveEndSelecting => t!("action.prompt_move_end_selecting"),
2142 Action::PromptSelectWordLeft => t!("action.prompt_select_word_left"),
2143 Action::PromptSelectWordRight => t!("action.prompt_select_word_right"),
2144 Action::PromptSelectAll => t!("action.prompt_select_all"),
2145 Action::FileBrowserToggleHidden => t!("action.file_browser_toggle_hidden"),
2146 Action::FileBrowserToggleDetectEncoding => {
2147 t!("action.file_browser_toggle_detect_encoding")
2148 }
2149 Action::PopupSelectNext => t!("action.popup_select_next"),
2150 Action::PopupSelectPrev => t!("action.popup_select_prev"),
2151 Action::PopupPageUp => t!("action.popup_page_up"),
2152 Action::PopupPageDown => t!("action.popup_page_down"),
2153 Action::PopupConfirm => t!("action.popup_confirm"),
2154 Action::PopupCancel => t!("action.popup_cancel"),
2155 Action::ToggleFileExplorer => t!("action.toggle_file_explorer"),
2156 Action::ToggleMenuBar => t!("action.toggle_menu_bar"),
2157 Action::ToggleTabBar => t!("action.toggle_tab_bar"),
2158 Action::ToggleStatusBar => t!("action.toggle_status_bar"),
2159 Action::TogglePromptLine => t!("action.toggle_prompt_line"),
2160 Action::ToggleVerticalScrollbar => t!("action.toggle_vertical_scrollbar"),
2161 Action::ToggleHorizontalScrollbar => t!("action.toggle_horizontal_scrollbar"),
2162 Action::FocusFileExplorer => t!("action.focus_file_explorer"),
2163 Action::FocusEditor => t!("action.focus_editor"),
2164 Action::FileExplorerUp => t!("action.file_explorer_up"),
2165 Action::FileExplorerDown => t!("action.file_explorer_down"),
2166 Action::FileExplorerPageUp => t!("action.file_explorer_page_up"),
2167 Action::FileExplorerPageDown => t!("action.file_explorer_page_down"),
2168 Action::FileExplorerExpand => t!("action.file_explorer_expand"),
2169 Action::FileExplorerCollapse => t!("action.file_explorer_collapse"),
2170 Action::FileExplorerOpen => t!("action.file_explorer_open"),
2171 Action::FileExplorerRefresh => t!("action.file_explorer_refresh"),
2172 Action::FileExplorerNewFile => t!("action.file_explorer_new_file"),
2173 Action::FileExplorerNewDirectory => t!("action.file_explorer_new_directory"),
2174 Action::FileExplorerDelete => t!("action.file_explorer_delete"),
2175 Action::FileExplorerRename => t!("action.file_explorer_rename"),
2176 Action::FileExplorerToggleHidden => t!("action.file_explorer_toggle_hidden"),
2177 Action::FileExplorerToggleGitignored => t!("action.file_explorer_toggle_gitignored"),
2178 Action::FileExplorerSearchClear => t!("action.file_explorer_search_clear"),
2179 Action::FileExplorerSearchBackspace => t!("action.file_explorer_search_backspace"),
2180 Action::LspCompletion => t!("action.lsp_completion"),
2181 Action::LspGotoDefinition => t!("action.lsp_goto_definition"),
2182 Action::LspReferences => t!("action.lsp_references"),
2183 Action::LspRename => t!("action.lsp_rename"),
2184 Action::LspHover => t!("action.lsp_hover"),
2185 Action::LspSignatureHelp => t!("action.lsp_signature_help"),
2186 Action::LspCodeActions => t!("action.lsp_code_actions"),
2187 Action::LspRestart => t!("action.lsp_restart"),
2188 Action::LspStop => t!("action.lsp_stop"),
2189 Action::LspToggleForBuffer => t!("action.lsp_toggle_for_buffer"),
2190 Action::ToggleInlayHints => t!("action.toggle_inlay_hints"),
2191 Action::ToggleMouseHover => t!("action.toggle_mouse_hover"),
2192 Action::ToggleLineNumbers => t!("action.toggle_line_numbers"),
2193 Action::ToggleScrollSync => t!("action.toggle_scroll_sync"),
2194 Action::ToggleMouseCapture => t!("action.toggle_mouse_capture"),
2195 Action::ToggleDebugHighlights => t!("action.toggle_debug_highlights"),
2196 Action::SetBackground => t!("action.set_background"),
2197 Action::SetBackgroundBlend => t!("action.set_background_blend"),
2198 Action::AddRuler => t!("action.add_ruler"),
2199 Action::RemoveRuler => t!("action.remove_ruler"),
2200 Action::SetTabSize => t!("action.set_tab_size"),
2201 Action::SetLineEnding => t!("action.set_line_ending"),
2202 Action::SetEncoding => t!("action.set_encoding"),
2203 Action::ReloadWithEncoding => t!("action.reload_with_encoding"),
2204 Action::SetLanguage => t!("action.set_language"),
2205 Action::ToggleIndentationStyle => t!("action.toggle_indentation_style"),
2206 Action::ToggleTabIndicators => t!("action.toggle_tab_indicators"),
2207 Action::ToggleWhitespaceIndicators => t!("action.toggle_whitespace_indicators"),
2208 Action::ResetBufferSettings => t!("action.reset_buffer_settings"),
2209 Action::DumpConfig => t!("action.dump_config"),
2210 Action::Search => t!("action.search"),
2211 Action::FindInSelection => t!("action.find_in_selection"),
2212 Action::FindNext => t!("action.find_next"),
2213 Action::FindPrevious => t!("action.find_previous"),
2214 Action::FindSelectionNext => t!("action.find_selection_next"),
2215 Action::FindSelectionPrevious => t!("action.find_selection_previous"),
2216 Action::Replace => t!("action.replace"),
2217 Action::QueryReplace => t!("action.query_replace"),
2218 Action::MenuActivate => t!("action.menu_activate"),
2219 Action::MenuClose => t!("action.menu_close"),
2220 Action::MenuLeft => t!("action.menu_left"),
2221 Action::MenuRight => t!("action.menu_right"),
2222 Action::MenuUp => t!("action.menu_up"),
2223 Action::MenuDown => t!("action.menu_down"),
2224 Action::MenuExecute => t!("action.menu_execute"),
2225 Action::MenuOpen(name) => t!("action.menu_open", name = name),
2226 Action::SwitchKeybindingMap(map) => t!("action.switch_keybinding_map", map = map),
2227 Action::PluginAction(name) => t!("action.plugin_action", name = name),
2228 Action::ScrollTabsLeft => t!("action.scroll_tabs_left"),
2229 Action::ScrollTabsRight => t!("action.scroll_tabs_right"),
2230 Action::SelectTheme => t!("action.select_theme"),
2231 Action::SelectKeybindingMap => t!("action.select_keybinding_map"),
2232 Action::SelectCursorStyle => t!("action.select_cursor_style"),
2233 Action::SelectLocale => t!("action.select_locale"),
2234 Action::SwitchToPreviousTab => t!("action.switch_to_previous_tab"),
2235 Action::SwitchToTabByName => t!("action.switch_to_tab_by_name"),
2236 Action::OpenTerminal => t!("action.open_terminal"),
2237 Action::CloseTerminal => t!("action.close_terminal"),
2238 Action::FocusTerminal => t!("action.focus_terminal"),
2239 Action::TerminalEscape => t!("action.terminal_escape"),
2240 Action::ToggleKeyboardCapture => t!("action.toggle_keyboard_capture"),
2241 Action::TerminalPaste => t!("action.terminal_paste"),
2242 Action::OpenSettings => t!("action.open_settings"),
2243 Action::CloseSettings => t!("action.close_settings"),
2244 Action::SettingsSave => t!("action.settings_save"),
2245 Action::SettingsReset => t!("action.settings_reset"),
2246 Action::SettingsToggleFocus => t!("action.settings_toggle_focus"),
2247 Action::SettingsActivate => t!("action.settings_activate"),
2248 Action::SettingsSearch => t!("action.settings_search"),
2249 Action::SettingsHelp => t!("action.settings_help"),
2250 Action::SettingsIncrement => t!("action.settings_increment"),
2251 Action::SettingsDecrement => t!("action.settings_decrement"),
2252 Action::SettingsInherit => t!("action.settings_inherit"),
2253 Action::ShellCommand => t!("action.shell_command"),
2254 Action::ShellCommandReplace => t!("action.shell_command_replace"),
2255 Action::ToUpperCase => t!("action.to_uppercase"),
2256 Action::ToLowerCase => t!("action.to_lowercase"),
2257 Action::ToggleCase => t!("action.to_uppercase"),
2258 Action::SortLines => t!("action.sort_lines"),
2259 Action::CalibrateInput => t!("action.calibrate_input"),
2260 Action::EventDebug => t!("action.event_debug"),
2261 Action::LoadPluginFromBuffer => "Load Plugin from Buffer".into(),
2262 Action::OpenKeybindingEditor => "Keybinding Editor".into(),
2263 Action::CompositeNextHunk => t!("action.composite_next_hunk"),
2264 Action::CompositePrevHunk => t!("action.composite_prev_hunk"),
2265 Action::None => t!("action.none"),
2266 }
2267 .to_string()
2268 }
2269
2270 pub fn parse_key_public(key: &str) -> Option<KeyCode> {
2272 Self::parse_key(key)
2273 }
2274
2275 pub fn parse_modifiers_public(modifiers: &[String]) -> KeyModifiers {
2277 Self::parse_modifiers(modifiers)
2278 }
2279
2280 pub fn format_action_from_str(action_name: &str) -> String {
2284 if let Some(action) = Action::from_str(action_name, &std::collections::HashMap::new()) {
2286 Self::format_action(&action)
2287 } else {
2288 action_name
2290 .split('_')
2291 .map(|word| {
2292 let mut chars = word.chars();
2293 match chars.next() {
2294 Some(c) => {
2295 let upper: String = c.to_uppercase().collect();
2296 format!("{}{}", upper, chars.as_str())
2297 }
2298 None => String::new(),
2299 }
2300 })
2301 .collect::<Vec<_>>()
2302 .join(" ")
2303 }
2304 }
2305
2306 pub fn all_action_names() -> Vec<String> {
2310 Action::all_action_names()
2311 }
2312
2313 pub fn get_keybinding_for_action(
2319 &self,
2320 action: &Action,
2321 context: KeyContext,
2322 ) -> Option<String> {
2323 fn find_best_keybinding(
2325 bindings: &HashMap<(KeyCode, KeyModifiers), Action>,
2326 action: &Action,
2327 ) -> Option<(KeyCode, KeyModifiers)> {
2328 let matches: Vec<_> = bindings
2329 .iter()
2330 .filter(|(_, a)| *a == action)
2331 .map(|((k, m), _)| (*k, *m))
2332 .collect();
2333
2334 if matches.is_empty() {
2335 return None;
2336 }
2337
2338 let mut sorted = matches;
2341 sorted.sort_by(|(k1, m1), (k2, m2)| {
2342 let score1 = keybinding_priority_score(k1);
2343 let score2 = keybinding_priority_score(k2);
2344 match score1.cmp(&score2) {
2346 std::cmp::Ordering::Equal => {
2347 let s1 = format_keybinding(k1, m1);
2349 let s2 = format_keybinding(k2, m2);
2350 s1.cmp(&s2)
2351 }
2352 other => other,
2353 }
2354 });
2355
2356 sorted.into_iter().next()
2357 }
2358
2359 if let Some(context_bindings) = self.bindings.get(&context) {
2361 if let Some((keycode, modifiers)) = find_best_keybinding(context_bindings, action) {
2362 return Some(format_keybinding(&keycode, &modifiers));
2363 }
2364 }
2365
2366 if let Some(context_bindings) = self.default_bindings.get(&context) {
2368 if let Some((keycode, modifiers)) = find_best_keybinding(context_bindings, action) {
2369 return Some(format_keybinding(&keycode, &modifiers));
2370 }
2371 }
2372
2373 if context != KeyContext::Normal
2375 && (context.allows_normal_fallthrough() || Self::is_application_wide_action(action))
2376 {
2377 if let Some(normal_bindings) = self.bindings.get(&KeyContext::Normal) {
2379 if let Some((keycode, modifiers)) = find_best_keybinding(normal_bindings, action) {
2380 return Some(format_keybinding(&keycode, &modifiers));
2381 }
2382 }
2383
2384 if let Some(normal_bindings) = self.default_bindings.get(&KeyContext::Normal) {
2386 if let Some((keycode, modifiers)) = find_best_keybinding(normal_bindings, action) {
2387 return Some(format_keybinding(&keycode, &modifiers));
2388 }
2389 }
2390 }
2391
2392 None
2393 }
2394
2395 pub fn reload(&mut self, config: &Config) {
2397 self.bindings.clear();
2398 for binding in &config.keybindings {
2399 if let Some(key_code) = Self::parse_key(&binding.key) {
2400 let modifiers = Self::parse_modifiers(&binding.modifiers);
2401 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
2402 let context = if let Some(ref when) = binding.when {
2404 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
2405 } else {
2406 KeyContext::Normal
2407 };
2408
2409 self.bindings
2410 .entry(context)
2411 .or_default()
2412 .insert((key_code, modifiers), action);
2413 }
2414 }
2415 }
2416 }
2417}
2418
2419#[cfg(test)]
2420mod tests {
2421 use super::*;
2422
2423 #[test]
2424 fn test_parse_key() {
2425 assert_eq!(KeybindingResolver::parse_key("enter"), Some(KeyCode::Enter));
2426 assert_eq!(
2427 KeybindingResolver::parse_key("backspace"),
2428 Some(KeyCode::Backspace)
2429 );
2430 assert_eq!(KeybindingResolver::parse_key("tab"), Some(KeyCode::Tab));
2431 assert_eq!(
2432 KeybindingResolver::parse_key("backtab"),
2433 Some(KeyCode::BackTab)
2434 );
2435 assert_eq!(
2436 KeybindingResolver::parse_key("BackTab"),
2437 Some(KeyCode::BackTab)
2438 );
2439 assert_eq!(KeybindingResolver::parse_key("a"), Some(KeyCode::Char('a')));
2440 }
2441
2442 #[test]
2443 fn test_parse_modifiers() {
2444 let mods = vec!["ctrl".to_string()];
2445 assert_eq!(
2446 KeybindingResolver::parse_modifiers(&mods),
2447 KeyModifiers::CONTROL
2448 );
2449
2450 let mods = vec!["ctrl".to_string(), "shift".to_string()];
2451 assert_eq!(
2452 KeybindingResolver::parse_modifiers(&mods),
2453 KeyModifiers::CONTROL | KeyModifiers::SHIFT
2454 );
2455 }
2456
2457 #[test]
2458 fn test_resolve_basic() {
2459 let config = Config::default();
2460 let resolver = KeybindingResolver::new(&config);
2461
2462 let event = KeyEvent::new(KeyCode::Left, KeyModifiers::empty());
2463 assert_eq!(
2464 resolver.resolve(&event, KeyContext::Normal),
2465 Action::MoveLeft
2466 );
2467
2468 let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
2469 assert_eq!(
2470 resolver.resolve(&event, KeyContext::Normal),
2471 Action::InsertChar('a')
2472 );
2473 }
2474
2475 #[test]
2476 fn test_action_from_str() {
2477 let args = HashMap::new();
2478 assert_eq!(Action::from_str("move_left", &args), Some(Action::MoveLeft));
2479 assert_eq!(Action::from_str("save", &args), Some(Action::Save));
2480 assert_eq!(
2482 Action::from_str("unknown", &args),
2483 Some(Action::PluginAction("unknown".to_string()))
2484 );
2485
2486 assert_eq!(
2488 Action::from_str("keyboard_shortcuts", &args),
2489 Some(Action::ShowKeyboardShortcuts)
2490 );
2491 assert_eq!(
2492 Action::from_str("prompt_confirm", &args),
2493 Some(Action::PromptConfirm)
2494 );
2495 assert_eq!(
2496 Action::from_str("popup_cancel", &args),
2497 Some(Action::PopupCancel)
2498 );
2499
2500 assert_eq!(
2502 Action::from_str("calibrate_input", &args),
2503 Some(Action::CalibrateInput)
2504 );
2505 }
2506
2507 #[test]
2508 fn test_key_context_from_when_clause() {
2509 assert_eq!(
2510 KeyContext::from_when_clause("normal"),
2511 Some(KeyContext::Normal)
2512 );
2513 assert_eq!(
2514 KeyContext::from_when_clause("prompt"),
2515 Some(KeyContext::Prompt)
2516 );
2517 assert_eq!(
2518 KeyContext::from_when_clause("popup"),
2519 Some(KeyContext::Popup)
2520 );
2521 assert_eq!(KeyContext::from_when_clause("help"), None);
2522 assert_eq!(KeyContext::from_when_clause(" help "), None); assert_eq!(KeyContext::from_when_clause("unknown"), None);
2524 assert_eq!(KeyContext::from_when_clause(""), None);
2525 }
2526
2527 #[test]
2528 fn test_key_context_to_when_clause() {
2529 assert_eq!(KeyContext::Normal.to_when_clause(), "normal");
2530 assert_eq!(KeyContext::Prompt.to_when_clause(), "prompt");
2531 assert_eq!(KeyContext::Popup.to_when_clause(), "popup");
2532 }
2533
2534 #[test]
2535 fn test_context_specific_bindings() {
2536 let config = Config::default();
2537 let resolver = KeybindingResolver::new(&config);
2538
2539 let enter_event = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
2541 assert_eq!(
2542 resolver.resolve(&enter_event, KeyContext::Prompt),
2543 Action::PromptConfirm
2544 );
2545 assert_eq!(
2546 resolver.resolve(&enter_event, KeyContext::Normal),
2547 Action::InsertNewline
2548 );
2549
2550 let up_event = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
2552 assert_eq!(
2553 resolver.resolve(&up_event, KeyContext::Popup),
2554 Action::PopupSelectPrev
2555 );
2556 assert_eq!(
2557 resolver.resolve(&up_event, KeyContext::Normal),
2558 Action::MoveUp
2559 );
2560 }
2561
2562 #[test]
2563 fn test_context_fallback_to_normal() {
2564 let config = Config::default();
2565 let resolver = KeybindingResolver::new(&config);
2566
2567 let save_event = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL);
2569 assert_eq!(
2570 resolver.resolve(&save_event, KeyContext::Normal),
2571 Action::Save
2572 );
2573 assert_eq!(
2574 resolver.resolve(&save_event, KeyContext::Popup),
2575 Action::Save
2576 );
2577 }
2579
2580 #[test]
2581 fn test_context_priority_resolution() {
2582 use crate::config::Keybinding;
2583
2584 let mut config = Config::default();
2586 config.keybindings.push(Keybinding {
2587 key: "esc".to_string(),
2588 modifiers: vec![],
2589 keys: vec![],
2590 action: "quit".to_string(), args: HashMap::new(),
2592 when: Some("popup".to_string()),
2593 });
2594
2595 let resolver = KeybindingResolver::new(&config);
2596 let esc_event = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
2597
2598 assert_eq!(
2600 resolver.resolve(&esc_event, KeyContext::Popup),
2601 Action::Quit
2602 );
2603
2604 assert_eq!(
2606 resolver.resolve(&esc_event, KeyContext::Normal),
2607 Action::RemoveSecondaryCursors
2608 );
2609 }
2610
2611 #[test]
2612 fn test_character_input_in_contexts() {
2613 let config = Config::default();
2614 let resolver = KeybindingResolver::new(&config);
2615
2616 let char_event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
2617
2618 assert_eq!(
2620 resolver.resolve(&char_event, KeyContext::Normal),
2621 Action::InsertChar('a')
2622 );
2623 assert_eq!(
2624 resolver.resolve(&char_event, KeyContext::Prompt),
2625 Action::InsertChar('a')
2626 );
2627
2628 assert_eq!(
2630 resolver.resolve(&char_event, KeyContext::Popup),
2631 Action::None
2632 );
2633 }
2634
2635 #[test]
2636 fn test_custom_keybinding_loading() {
2637 use crate::config::Keybinding;
2638
2639 let mut config = Config::default();
2640
2641 config.keybindings.push(Keybinding {
2643 key: "f".to_string(),
2644 modifiers: vec!["ctrl".to_string()],
2645 keys: vec![],
2646 action: "command_palette".to_string(),
2647 args: HashMap::new(),
2648 when: None, });
2650
2651 let resolver = KeybindingResolver::new(&config);
2652
2653 let ctrl_f = KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL);
2655 assert_eq!(
2656 resolver.resolve(&ctrl_f, KeyContext::Normal),
2657 Action::CommandPalette
2658 );
2659
2660 let ctrl_k = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL);
2662 assert_eq!(
2663 resolver.resolve(&ctrl_k, KeyContext::Prompt),
2664 Action::PromptDeleteToLineEnd
2665 );
2666 assert_eq!(
2667 resolver.resolve(&ctrl_k, KeyContext::Normal),
2668 Action::DeleteToLineEnd
2669 );
2670 }
2671
2672 #[test]
2673 fn test_all_context_default_bindings_exist() {
2674 let config = Config::default();
2675 let resolver = KeybindingResolver::new(&config);
2676
2677 assert!(resolver.default_bindings.contains_key(&KeyContext::Normal));
2679 assert!(resolver.default_bindings.contains_key(&KeyContext::Prompt));
2680 assert!(resolver.default_bindings.contains_key(&KeyContext::Popup));
2681 assert!(resolver
2682 .default_bindings
2683 .contains_key(&KeyContext::FileExplorer));
2684 assert!(resolver.default_bindings.contains_key(&KeyContext::Menu));
2685
2686 assert!(!resolver.default_bindings[&KeyContext::Normal].is_empty());
2688 assert!(!resolver.default_bindings[&KeyContext::Prompt].is_empty());
2689 assert!(!resolver.default_bindings[&KeyContext::Popup].is_empty());
2690 assert!(!resolver.default_bindings[&KeyContext::FileExplorer].is_empty());
2691 assert!(!resolver.default_bindings[&KeyContext::Menu].is_empty());
2692 }
2693
2694 #[test]
2698 fn test_all_builtin_keymaps_have_valid_action_names() {
2699 let known_actions: std::collections::HashSet<String> =
2700 Action::all_action_names().into_iter().collect();
2701
2702 let config = Config::default();
2703
2704 for map_name in crate::config::KeybindingMapName::BUILTIN_OPTIONS {
2705 let bindings = config.resolve_keymap(map_name);
2706 for binding in &bindings {
2707 assert!(
2708 known_actions.contains(&binding.action),
2709 "Keymap '{}' contains unknown action '{}' (key: '{}', when: {:?}). \
2710 This will be treated as a plugin action at runtime. \
2711 Check for typos in the keymap JSON file.",
2712 map_name,
2713 binding.action,
2714 binding.key,
2715 binding.when,
2716 );
2717 }
2718 }
2719 }
2720
2721 #[test]
2722 fn test_resolve_determinism() {
2723 let config = Config::default();
2725 let resolver = KeybindingResolver::new(&config);
2726
2727 let test_cases = vec![
2728 (KeyCode::Left, KeyModifiers::empty(), KeyContext::Normal),
2729 (
2730 KeyCode::Esc,
2731 KeyModifiers::empty(),
2732 KeyContext::FileExplorer,
2733 ),
2734 (KeyCode::Enter, KeyModifiers::empty(), KeyContext::Prompt),
2735 (KeyCode::Down, KeyModifiers::empty(), KeyContext::Popup),
2736 ];
2737
2738 for (key_code, modifiers, context) in test_cases {
2739 let event = KeyEvent::new(key_code, modifiers);
2740 let action1 = resolver.resolve(&event, context.clone());
2741 let action2 = resolver.resolve(&event, context.clone());
2742 let action3 = resolver.resolve(&event, context);
2743
2744 assert_eq!(action1, action2, "Resolve should be deterministic");
2745 assert_eq!(action2, action3, "Resolve should be deterministic");
2746 }
2747 }
2748
2749 #[test]
2750 fn test_modifier_combinations() {
2751 let config = Config::default();
2752 let resolver = KeybindingResolver::new(&config);
2753
2754 let char_s = KeyCode::Char('s');
2756
2757 let no_mod = KeyEvent::new(char_s, KeyModifiers::empty());
2758 let ctrl = KeyEvent::new(char_s, KeyModifiers::CONTROL);
2759 let shift = KeyEvent::new(char_s, KeyModifiers::SHIFT);
2760 let ctrl_shift = KeyEvent::new(char_s, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
2761
2762 let action_no_mod = resolver.resolve(&no_mod, KeyContext::Normal);
2763 let action_ctrl = resolver.resolve(&ctrl, KeyContext::Normal);
2764 let action_shift = resolver.resolve(&shift, KeyContext::Normal);
2765 let action_ctrl_shift = resolver.resolve(&ctrl_shift, KeyContext::Normal);
2766
2767 assert_eq!(action_no_mod, Action::InsertChar('s'));
2769 assert_eq!(action_ctrl, Action::Save);
2770 assert_eq!(action_shift, Action::InsertChar('s')); assert_eq!(action_ctrl_shift, Action::None);
2773 }
2774
2775 #[test]
2776 fn test_scroll_keybindings() {
2777 let config = Config::default();
2778 let resolver = KeybindingResolver::new(&config);
2779
2780 let ctrl_up = KeyEvent::new(KeyCode::Up, KeyModifiers::CONTROL);
2782 assert_eq!(
2783 resolver.resolve(&ctrl_up, KeyContext::Normal),
2784 Action::ScrollUp,
2785 "Ctrl+Up should resolve to ScrollUp"
2786 );
2787
2788 let ctrl_down = KeyEvent::new(KeyCode::Down, KeyModifiers::CONTROL);
2790 assert_eq!(
2791 resolver.resolve(&ctrl_down, KeyContext::Normal),
2792 Action::ScrollDown,
2793 "Ctrl+Down should resolve to ScrollDown"
2794 );
2795 }
2796
2797 #[test]
2798 fn test_lsp_completion_keybinding() {
2799 let config = Config::default();
2800 let resolver = KeybindingResolver::new(&config);
2801
2802 let ctrl_space = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL);
2804 assert_eq!(
2805 resolver.resolve(&ctrl_space, KeyContext::Normal),
2806 Action::LspCompletion,
2807 "Ctrl+Space should resolve to LspCompletion"
2808 );
2809 }
2810
2811 #[test]
2812 fn test_terminal_key_equivalents() {
2813 let ctrl = KeyModifiers::CONTROL;
2815
2816 let slash_equivs = terminal_key_equivalents(KeyCode::Char('/'), ctrl);
2818 assert_eq!(slash_equivs, vec![(KeyCode::Char('7'), ctrl)]);
2819
2820 let seven_equivs = terminal_key_equivalents(KeyCode::Char('7'), ctrl);
2821 assert_eq!(seven_equivs, vec![(KeyCode::Char('/'), ctrl)]);
2822
2823 let backspace_equivs = terminal_key_equivalents(KeyCode::Backspace, ctrl);
2825 assert_eq!(backspace_equivs, vec![(KeyCode::Char('h'), ctrl)]);
2826
2827 let h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl);
2828 assert_eq!(h_equivs, vec![(KeyCode::Backspace, ctrl)]);
2829
2830 let a_equivs = terminal_key_equivalents(KeyCode::Char('a'), ctrl);
2832 assert!(a_equivs.is_empty());
2833
2834 let slash_no_ctrl = terminal_key_equivalents(KeyCode::Char('/'), KeyModifiers::empty());
2836 assert!(slash_no_ctrl.is_empty());
2837 }
2838
2839 #[test]
2840 fn test_terminal_key_equivalents_auto_binding() {
2841 let config = Config::default();
2842 let resolver = KeybindingResolver::new(&config);
2843
2844 let ctrl_slash = KeyEvent::new(KeyCode::Char('/'), KeyModifiers::CONTROL);
2846 let action_slash = resolver.resolve(&ctrl_slash, KeyContext::Normal);
2847 assert_eq!(
2848 action_slash,
2849 Action::ToggleComment,
2850 "Ctrl+/ should resolve to ToggleComment"
2851 );
2852
2853 let ctrl_7 = KeyEvent::new(KeyCode::Char('7'), KeyModifiers::CONTROL);
2855 let action_7 = resolver.resolve(&ctrl_7, KeyContext::Normal);
2856 assert_eq!(
2857 action_7,
2858 Action::ToggleComment,
2859 "Ctrl+7 should resolve to ToggleComment (terminal equivalent of Ctrl+/)"
2860 );
2861 }
2862
2863 #[test]
2864 fn test_terminal_key_equivalents_normalization() {
2865 let ctrl = KeyModifiers::CONTROL;
2870
2871 let slash_equivs = terminal_key_equivalents(KeyCode::Char('/'), ctrl);
2874 assert_eq!(
2875 slash_equivs,
2876 vec![(KeyCode::Char('7'), ctrl)],
2877 "Ctrl+/ should map to Ctrl+7"
2878 );
2879 let seven_equivs = terminal_key_equivalents(KeyCode::Char('7'), ctrl);
2880 assert_eq!(
2881 seven_equivs,
2882 vec![(KeyCode::Char('/'), ctrl)],
2883 "Ctrl+7 should map back to Ctrl+/"
2884 );
2885
2886 let backspace_equivs = terminal_key_equivalents(KeyCode::Backspace, ctrl);
2889 assert_eq!(
2890 backspace_equivs,
2891 vec![(KeyCode::Char('h'), ctrl)],
2892 "Ctrl+Backspace should map to Ctrl+H"
2893 );
2894 let h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl);
2895 assert_eq!(
2896 h_equivs,
2897 vec![(KeyCode::Backspace, ctrl)],
2898 "Ctrl+H should map back to Ctrl+Backspace"
2899 );
2900
2901 let space_equivs = terminal_key_equivalents(KeyCode::Char(' '), ctrl);
2904 assert_eq!(
2905 space_equivs,
2906 vec![(KeyCode::Char('@'), ctrl)],
2907 "Ctrl+Space should map to Ctrl+@"
2908 );
2909 let at_equivs = terminal_key_equivalents(KeyCode::Char('@'), ctrl);
2910 assert_eq!(
2911 at_equivs,
2912 vec![(KeyCode::Char(' '), ctrl)],
2913 "Ctrl+@ should map back to Ctrl+Space"
2914 );
2915
2916 let minus_equivs = terminal_key_equivalents(KeyCode::Char('-'), ctrl);
2919 assert_eq!(
2920 minus_equivs,
2921 vec![(KeyCode::Char('_'), ctrl)],
2922 "Ctrl+- should map to Ctrl+_"
2923 );
2924 let underscore_equivs = terminal_key_equivalents(KeyCode::Char('_'), ctrl);
2925 assert_eq!(
2926 underscore_equivs,
2927 vec![(KeyCode::Char('-'), ctrl)],
2928 "Ctrl+_ should map back to Ctrl+-"
2929 );
2930
2931 assert!(
2933 terminal_key_equivalents(KeyCode::Char('a'), ctrl).is_empty(),
2934 "Ctrl+A should have no terminal equivalents"
2935 );
2936 assert!(
2937 terminal_key_equivalents(KeyCode::Char('z'), ctrl).is_empty(),
2938 "Ctrl+Z should have no terminal equivalents"
2939 );
2940 assert!(
2941 terminal_key_equivalents(KeyCode::Enter, ctrl).is_empty(),
2942 "Ctrl+Enter should have no terminal equivalents"
2943 );
2944
2945 assert!(
2947 terminal_key_equivalents(KeyCode::Char('/'), KeyModifiers::empty()).is_empty(),
2948 "/ without Ctrl should have no equivalents"
2949 );
2950 assert!(
2951 terminal_key_equivalents(KeyCode::Char('7'), KeyModifiers::SHIFT).is_empty(),
2952 "Shift+7 should have no equivalents"
2953 );
2954 assert!(
2955 terminal_key_equivalents(KeyCode::Char('h'), KeyModifiers::ALT).is_empty(),
2956 "Alt+H should have no equivalents"
2957 );
2958
2959 let ctrl_shift = KeyModifiers::CONTROL | KeyModifiers::SHIFT;
2962 let ctrl_shift_h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl_shift);
2963 assert!(
2964 ctrl_shift_h_equivs.is_empty(),
2965 "Ctrl+Shift+H should NOT map to Ctrl+Shift+Backspace"
2966 );
2967 }
2968
2969 #[test]
2970 fn test_no_duplicate_keybindings_in_keymaps() {
2971 use std::collections::HashMap;
2974
2975 let keymaps: &[(&str, &str)] = &[
2976 ("default", include_str!("../../keymaps/default.json")),
2977 ("macos", include_str!("../../keymaps/macos.json")),
2978 ];
2979
2980 for (keymap_name, json_content) in keymaps {
2981 let keymap: crate::config::KeymapConfig = serde_json::from_str(json_content)
2982 .unwrap_or_else(|e| panic!("Failed to parse keymap '{}': {}", keymap_name, e));
2983
2984 let mut seen: HashMap<(String, Vec<String>, String), String> = HashMap::new();
2986 let mut duplicates: Vec<String> = Vec::new();
2987
2988 for binding in &keymap.bindings {
2989 let when = binding.when.clone().unwrap_or_default();
2990 let key_id = (binding.key.clone(), binding.modifiers.clone(), when.clone());
2991
2992 if let Some(existing_action) = seen.get(&key_id) {
2993 duplicates.push(format!(
2994 "Duplicate in '{}': key='{}', modifiers={:?}, when='{}' -> '{}' vs '{}'",
2995 keymap_name,
2996 binding.key,
2997 binding.modifiers,
2998 when,
2999 existing_action,
3000 binding.action
3001 ));
3002 } else {
3003 seen.insert(key_id, binding.action.clone());
3004 }
3005 }
3006
3007 assert!(
3008 duplicates.is_empty(),
3009 "Found duplicate keybindings:\n{}",
3010 duplicates.join("\n")
3011 );
3012 }
3013 }
3014}