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