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 Completion,
234 FileExplorer,
236 Menu,
238 Terminal,
240 Settings,
242 CompositeBuffer,
244 Mode(String),
246}
247
248impl KeyContext {
249 pub fn allows_normal_fallthrough(&self) -> bool {
256 matches!(self, Self::CompositeBuffer)
257 }
258
259 pub fn allows_ui_fallthrough(&self) -> bool {
274 matches!(self, Self::FileExplorer | Self::Mode(_))
275 }
276
277 pub fn allows_text_input(&self) -> bool {
279 matches!(self, Self::Normal | Self::Prompt | Self::FileExplorer)
280 }
281
282 pub fn from_when_clause(when: &str) -> Option<Self> {
284 let trimmed = when.trim();
285 if let Some(mode_name) = trimmed.strip_prefix("mode:") {
286 return Some(Self::Mode(mode_name.to_string()));
287 }
288 Some(match trimmed {
289 "global" => Self::Global,
290 "prompt" => Self::Prompt,
291 "popup" => Self::Popup,
292 "completion" => Self::Completion,
293 "fileExplorer" | "file_explorer" => Self::FileExplorer,
294 "normal" => Self::Normal,
295 "menu" => Self::Menu,
296 "terminal" => Self::Terminal,
297 "settings" => Self::Settings,
298 "compositeBuffer" | "composite_buffer" => Self::CompositeBuffer,
299 _ => return None,
300 })
301 }
302
303 pub fn to_when_clause(&self) -> String {
305 match self {
306 Self::Global => "global".to_string(),
307 Self::Normal => "normal".to_string(),
308 Self::Prompt => "prompt".to_string(),
309 Self::Popup => "popup".to_string(),
310 Self::Completion => "completion".to_string(),
311 Self::FileExplorer => "fileExplorer".to_string(),
312 Self::Menu => "menu".to_string(),
313 Self::Terminal => "terminal".to_string(),
314 Self::Settings => "settings".to_string(),
315 Self::CompositeBuffer => "compositeBuffer".to_string(),
316 Self::Mode(name) => format!("mode:{}", name),
317 }
318 }
319}
320
321#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
323pub enum Action {
324 InsertChar(char),
326 InsertNewline,
327 InsertTab,
328
329 MoveLeft,
331 MoveRight,
332 MoveUp,
333 MoveDown,
334 MoveWordLeft,
335 MoveWordRight,
336 MoveWordEnd, ViMoveWordEnd, MoveLeftInLine, MoveRightInLine, MoveLineStart,
341 MoveLineEnd,
342 MoveLineUp,
343 MoveLineDown,
344 MovePageUp,
345 MovePageDown,
346 MoveDocumentStart,
347 MoveDocumentEnd,
348
349 SelectLeft,
351 SelectRight,
352 SelectUp,
353 SelectDown,
354 SelectToParagraphUp, SelectToParagraphDown, SelectWordLeft,
357 SelectWordRight,
358 SelectWordEnd, ViSelectWordEnd, SelectLineStart,
361 SelectLineEnd,
362 SelectDocumentStart,
363 SelectDocumentEnd,
364 SelectPageUp,
365 SelectPageDown,
366 SelectAll,
367 SelectWord,
368 SelectLine,
369 ExpandSelection,
370
371 BlockSelectLeft,
373 BlockSelectRight,
374 BlockSelectUp,
375 BlockSelectDown,
376
377 DeleteBackward,
379 DeleteForward,
380 DeleteWordBackward,
381 DeleteWordForward,
382 DeleteLine,
383 DeleteToLineEnd,
384 DeleteToLineStart,
385 DeleteViWordEnd, TransposeChars,
387 OpenLine,
388 DuplicateLine,
389
390 Recenter,
392
393 SetMark,
395
396 Copy,
398 CopyWithTheme(String),
399 Cut,
400 Paste,
401 CopyFilePath,
403 CopyRelativeFilePath,
406
407 YankWordForward,
409 YankWordBackward,
410 YankToLineEnd,
411 YankToLineStart,
412 YankViWordEnd, AddCursorAbove,
416 AddCursorBelow,
417 AddCursorNextMatch,
418 AddCursorsToLineEnds,
419 RemoveSecondaryCursors,
420
421 Save,
423 SaveAs,
424 Open,
425 SwitchProject,
426 New,
427 Close,
428 CloseTab,
429 Quit,
430 ForceQuit,
431 Detach,
432 Revert,
433 ToggleAutoRevert,
434 FormatBuffer,
435 TrimTrailingWhitespace,
436 EnsureFinalNewline,
437
438 GotoLine,
440 ScanLineIndex,
441 GoToMatchingBracket,
442 JumpToNextError,
443 JumpToPreviousError,
444
445 SmartHome,
447 DedentSelection,
448 ToggleComment,
449 DabbrevExpand,
450 ToggleFold,
451
452 SetBookmark(char),
454 JumpToBookmark(char),
455 ClearBookmark(char),
456 ListBookmarks,
457
458 ToggleSearchCaseSensitive,
460 ToggleSearchWholeWord,
461 ToggleSearchRegex,
462 ToggleSearchConfirmEach,
463
464 StartMacroRecording,
466 StopMacroRecording,
467 PlayMacro(char),
468 ToggleMacroRecording(char),
469 ShowMacro(char),
470 ListMacros,
471 PromptRecordMacro,
472 PromptPlayMacro,
473 PlayLastMacro,
474
475 PromptSetBookmark,
477 PromptJumpToBookmark,
478
479 Undo,
481 Redo,
482
483 ScrollUp,
485 ScrollDown,
486 ShowHelp,
487 ShowKeyboardShortcuts,
488 ShowWarnings,
489 ShowStatusLog,
490 ShowLspStatus,
491 ShowRemoteIndicatorMenu,
492 ClearWarnings,
493 CommandPalette, QuickOpen,
496 QuickOpenBuffers,
498 QuickOpenFiles,
500 OpenLiveGrep,
502 ResumeLiveGrep,
504 LiveGrepExportQuickfix,
508 ToggleUtilityDock,
512 OpenTerminalInDock,
515 CycleLiveGrepProvider,
520 ToggleLineWrap,
521 ToggleCurrentLineHighlight,
522 ToggleReadOnly,
523 TogglePageView,
524 SetPageWidth,
525 InspectThemeAtCursor,
526 SelectTheme,
527 SelectKeybindingMap,
528 SelectCursorStyle,
529 SelectLocale,
530
531 NextBuffer,
533 PrevBuffer,
534 SwitchToPreviousTab,
535 SwitchToTabByName,
536
537 ScrollTabsLeft,
539 ScrollTabsRight,
540
541 NavigateBack,
543 NavigateForward,
544
545 SplitHorizontal,
547 SplitVertical,
548 CloseSplit,
549 NextSplit,
550 PrevSplit,
551 IncreaseSplitSize,
552 DecreaseSplitSize,
553 ToggleMaximizeSplit,
554
555 PromptConfirm,
557 PromptConfirmWithText(String),
559 PromptCancel,
560 PromptBackspace,
561 PromptDelete,
562 PromptMoveLeft,
563 PromptMoveRight,
564 PromptMoveStart,
565 PromptMoveEnd,
566 PromptSelectPrev,
567 PromptSelectNext,
568 PromptPageUp,
569 PromptPageDown,
570 PromptAcceptSuggestion,
571 PromptMoveWordLeft,
572 PromptMoveWordRight,
573 PromptDeleteWordForward,
575 PromptDeleteWordBackward,
576 PromptDeleteToLineEnd,
577 PromptCopy,
578 PromptCut,
579 PromptPaste,
580 PromptMoveLeftSelecting,
582 PromptMoveRightSelecting,
583 PromptMoveHomeSelecting,
584 PromptMoveEndSelecting,
585 PromptSelectWordLeft,
586 PromptSelectWordRight,
587 PromptSelectAll,
588
589 FileBrowserToggleHidden,
591 FileBrowserToggleDetectEncoding,
592
593 PopupSelectNext,
595 PopupSelectPrev,
596 PopupPageUp,
597 PopupPageDown,
598 PopupConfirm,
599 PopupCancel,
600 PopupFocus,
605
606 CompletionAccept,
608 CompletionDismiss,
609
610 ToggleFileExplorer,
612 ToggleMenuBar,
614 ToggleTabBar,
616 ToggleStatusBar,
618 TogglePromptLine,
620 ToggleVerticalScrollbar,
622 ToggleHorizontalScrollbar,
623 FocusFileExplorer,
624 FocusEditor,
625 FileExplorerUp,
626 FileExplorerDown,
627 FileExplorerPageUp,
628 FileExplorerPageDown,
629 FileExplorerExpand,
630 FileExplorerCollapse,
631 FileExplorerOpen,
632 FileExplorerRefresh,
633 FileExplorerNewFile,
634 FileExplorerNewDirectory,
635 FileExplorerDelete,
636 FileExplorerRename,
637 FileExplorerToggleHidden,
638 FileExplorerToggleGitignored,
639 FileExplorerSearchClear,
640 FileExplorerSearchBackspace,
641 FileExplorerCopy,
642 FileExplorerCut,
643 FileExplorerPaste,
644 FileExplorerDuplicate,
645 FileExplorerCopyFullPath,
646 FileExplorerCopyRelativePath,
647 FileExplorerExtendSelectionUp,
648 FileExplorerExtendSelectionDown,
649 FileExplorerToggleSelect,
650 FileExplorerSelectAll,
651
652 LspCompletion,
654 LspGotoDefinition,
655 LspReferences,
656 LspRename,
657 LspHover,
658 LspSignatureHelp,
659 LspCodeActions,
660 LspRestart,
661 LspStop,
662 LspToggleForBuffer,
663 ToggleInlayHints,
664 ToggleMouseHover,
665
666 ToggleLineNumbers,
668 ToggleScrollSync,
669 ToggleMouseCapture,
670 ToggleDebugHighlights, SetBackground,
672 SetBackgroundBlend,
673
674 SetTabSize,
676 SetLineEnding,
677 SetEncoding,
678 ReloadWithEncoding,
679 SetLanguage,
680 ToggleIndentationStyle,
681 ToggleTabIndicators,
682 ToggleWhitespaceIndicators,
683 ResetBufferSettings,
684 AddRuler,
685 RemoveRuler,
686
687 DumpConfig,
689
690 RedrawScreen,
692
693 Search,
695 FindInSelection,
696 FindNext,
697 FindPrevious,
698 FindSelectionNext, FindSelectionPrevious, Replace,
701 QueryReplace, MenuActivate, MenuClose, MenuLeft, MenuRight, MenuUp, MenuDown, MenuExecute, MenuOpen(String), SwitchKeybindingMap(String), PluginAction(String),
718
719 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, SuspendProcess, OpenKeybindingEditor, LoadPluginFromBuffer, InitReload, InitEdit, InitCheck, CompositeNextHunk, CompositePrevHunk, None,
776}
777
778macro_rules! define_action_str_mapping {
791 (
792 $args_name:ident;
793 simple { $($s_name:literal => $s_variant:ident),* $(,)? }
794 alias { $($a_name:literal => $a_variant:ident),* $(,)? }
795 with_char { $($c_name:literal => $c_variant:ident),* $(,)? }
796 custom { $($x_name:literal => $x_variant:ident : $x_body:expr),* $(,)? }
797 ) => {
798 pub fn from_str(s: &str, $args_name: &HashMap<String, serde_json::Value>) -> Option<Self> {
800 Some(match s {
801 $($s_name => Self::$s_variant,)*
802 $($a_name => Self::$a_variant,)*
803 $($c_name => return Self::with_char($args_name, Self::$c_variant),)*
804 $($x_name => $x_body,)*
805 _ => Self::PluginAction(s.to_string()),
808 })
809 }
810
811 pub fn to_action_str(&self) -> String {
814 match self {
815 $(Self::$s_variant => $s_name.to_string(),)*
816 $(Self::$c_variant(_) => $c_name.to_string(),)*
817 $(Self::$x_variant(_) => $x_name.to_string(),)*
818 Self::PluginAction(name) => name.clone(),
819 }
820 }
821
822 pub fn all_action_names() -> Vec<String> {
825 let mut names = vec![
826 $($s_name.to_string(),)*
827 $($a_name.to_string(),)*
828 $($c_name.to_string(),)*
829 $($x_name.to_string(),)*
830 ];
831 names.sort();
832 names
833 }
834 };
835}
836
837impl Action {
838 fn with_char(
839 args: &HashMap<String, serde_json::Value>,
840 make_action: impl FnOnce(char) -> Self,
841 ) -> Option<Self> {
842 if let Some(serde_json::Value::String(value)) = args.get("char") {
843 value.chars().next().map(make_action)
844 } else {
845 None
846 }
847 }
848
849 define_action_str_mapping! {
850 args;
851 simple {
852 "insert_newline" => InsertNewline,
853 "insert_tab" => InsertTab,
854
855 "move_left" => MoveLeft,
856 "move_right" => MoveRight,
857 "move_up" => MoveUp,
858 "move_down" => MoveDown,
859 "move_word_left" => MoveWordLeft,
860 "move_word_right" => MoveWordRight,
861 "move_word_end" => MoveWordEnd,
862 "vi_move_word_end" => ViMoveWordEnd,
863 "move_left_in_line" => MoveLeftInLine,
864 "move_right_in_line" => MoveRightInLine,
865 "move_line_start" => MoveLineStart,
866 "move_line_end" => MoveLineEnd,
867 "move_line_up" => MoveLineUp,
868 "move_line_down" => MoveLineDown,
869 "move_page_up" => MovePageUp,
870 "move_page_down" => MovePageDown,
871 "move_document_start" => MoveDocumentStart,
872 "move_document_end" => MoveDocumentEnd,
873
874 "select_left" => SelectLeft,
875 "select_right" => SelectRight,
876 "select_up" => SelectUp,
877 "select_down" => SelectDown,
878 "select_to_paragraph_up" => SelectToParagraphUp,
879 "select_to_paragraph_down" => SelectToParagraphDown,
880 "select_word_left" => SelectWordLeft,
881 "select_word_right" => SelectWordRight,
882 "select_word_end" => SelectWordEnd,
883 "vi_select_word_end" => ViSelectWordEnd,
884 "select_line_start" => SelectLineStart,
885 "select_line_end" => SelectLineEnd,
886 "select_document_start" => SelectDocumentStart,
887 "select_document_end" => SelectDocumentEnd,
888 "select_page_up" => SelectPageUp,
889 "select_page_down" => SelectPageDown,
890 "select_all" => SelectAll,
891 "select_word" => SelectWord,
892 "select_line" => SelectLine,
893 "expand_selection" => ExpandSelection,
894
895 "block_select_left" => BlockSelectLeft,
896 "block_select_right" => BlockSelectRight,
897 "block_select_up" => BlockSelectUp,
898 "block_select_down" => BlockSelectDown,
899
900 "delete_backward" => DeleteBackward,
901 "delete_forward" => DeleteForward,
902 "delete_word_backward" => DeleteWordBackward,
903 "delete_word_forward" => DeleteWordForward,
904 "delete_line" => DeleteLine,
905 "delete_to_line_end" => DeleteToLineEnd,
906 "delete_to_line_start" => DeleteToLineStart,
907 "delete_vi_word_end" => DeleteViWordEnd,
908 "transpose_chars" => TransposeChars,
909 "open_line" => OpenLine,
910 "duplicate_line" => DuplicateLine,
911 "recenter" => Recenter,
912 "set_mark" => SetMark,
913
914 "copy" => Copy,
915 "cut" => Cut,
916 "paste" => Paste,
917 "copy_file_path" => CopyFilePath,
918 "copy_relative_file_path" => CopyRelativeFilePath,
919
920 "yank_word_forward" => YankWordForward,
921 "yank_word_backward" => YankWordBackward,
922 "yank_to_line_end" => YankToLineEnd,
923 "yank_to_line_start" => YankToLineStart,
924 "yank_vi_word_end" => YankViWordEnd,
925
926 "add_cursor_above" => AddCursorAbove,
927 "add_cursor_below" => AddCursorBelow,
928 "add_cursor_next_match" => AddCursorNextMatch,
929 "add_cursors_to_line_ends" => AddCursorsToLineEnds,
930 "remove_secondary_cursors" => RemoveSecondaryCursors,
931
932 "save" => Save,
933 "save_as" => SaveAs,
934 "open" => Open,
935 "switch_project" => SwitchProject,
936 "new" => New,
937 "close" => Close,
938 "close_tab" => CloseTab,
939 "quit" => Quit,
940 "force_quit" => ForceQuit,
941 "detach" => Detach,
942 "revert" => Revert,
943 "toggle_auto_revert" => ToggleAutoRevert,
944 "format_buffer" => FormatBuffer,
945 "trim_trailing_whitespace" => TrimTrailingWhitespace,
946 "ensure_final_newline" => EnsureFinalNewline,
947 "goto_line" => GotoLine,
948 "scan_line_index" => ScanLineIndex,
949 "goto_matching_bracket" => GoToMatchingBracket,
950 "jump_to_next_error" => JumpToNextError,
951 "jump_to_previous_error" => JumpToPreviousError,
952
953 "smart_home" => SmartHome,
954 "dedent_selection" => DedentSelection,
955 "toggle_comment" => ToggleComment,
956 "dabbrev_expand" => DabbrevExpand,
957 "toggle_fold" => ToggleFold,
958
959 "list_bookmarks" => ListBookmarks,
960
961 "toggle_search_case_sensitive" => ToggleSearchCaseSensitive,
962 "toggle_search_whole_word" => ToggleSearchWholeWord,
963 "toggle_search_regex" => ToggleSearchRegex,
964 "toggle_search_confirm_each" => ToggleSearchConfirmEach,
965
966 "start_macro_recording" => StartMacroRecording,
967 "stop_macro_recording" => StopMacroRecording,
968
969 "list_macros" => ListMacros,
970 "prompt_record_macro" => PromptRecordMacro,
971 "prompt_play_macro" => PromptPlayMacro,
972 "play_last_macro" => PlayLastMacro,
973 "prompt_set_bookmark" => PromptSetBookmark,
974 "prompt_jump_to_bookmark" => PromptJumpToBookmark,
975
976 "undo" => Undo,
977 "redo" => Redo,
978
979 "scroll_up" => ScrollUp,
980 "scroll_down" => ScrollDown,
981 "show_help" => ShowHelp,
982 "keyboard_shortcuts" => ShowKeyboardShortcuts,
983 "show_warnings" => ShowWarnings,
984 "show_status_log" => ShowStatusLog,
985 "show_lsp_status" => ShowLspStatus,
986 "show_remote_indicator_menu" => ShowRemoteIndicatorMenu,
987 "clear_warnings" => ClearWarnings,
988 "command_palette" => CommandPalette,
989 "quick_open" => QuickOpen,
990 "quick_open_buffers" => QuickOpenBuffers,
991 "quick_open_files" => QuickOpenFiles,
992 "open_live_grep" => OpenLiveGrep,
993 "resume_live_grep" => ResumeLiveGrep,
994 "live_grep_export_quickfix" => LiveGrepExportQuickfix,
995 "toggle_utility_dock" => ToggleUtilityDock,
996 "open_terminal_in_dock" => OpenTerminalInDock,
997 "cycle_live_grep_provider" => CycleLiveGrepProvider,
998 "toggle_line_wrap" => ToggleLineWrap,
999 "toggle_current_line_highlight" => ToggleCurrentLineHighlight,
1000 "toggle_read_only" => ToggleReadOnly,
1001 "toggle_page_view" => TogglePageView,
1002 "set_page_width" => SetPageWidth,
1003
1004 "next_buffer" => NextBuffer,
1005 "prev_buffer" => PrevBuffer,
1006 "switch_to_previous_tab" => SwitchToPreviousTab,
1007 "switch_to_tab_by_name" => SwitchToTabByName,
1008 "scroll_tabs_left" => ScrollTabsLeft,
1009 "scroll_tabs_right" => ScrollTabsRight,
1010
1011 "navigate_back" => NavigateBack,
1012 "navigate_forward" => NavigateForward,
1013
1014 "split_horizontal" => SplitHorizontal,
1015 "split_vertical" => SplitVertical,
1016 "close_split" => CloseSplit,
1017 "next_split" => NextSplit,
1018 "prev_split" => PrevSplit,
1019 "increase_split_size" => IncreaseSplitSize,
1020 "decrease_split_size" => DecreaseSplitSize,
1021 "toggle_maximize_split" => ToggleMaximizeSplit,
1022
1023 "prompt_confirm" => PromptConfirm,
1024 "prompt_cancel" => PromptCancel,
1025 "prompt_backspace" => PromptBackspace,
1026 "prompt_move_left" => PromptMoveLeft,
1027 "prompt_move_right" => PromptMoveRight,
1028 "prompt_move_start" => PromptMoveStart,
1029 "prompt_move_end" => PromptMoveEnd,
1030 "prompt_select_prev" => PromptSelectPrev,
1031 "prompt_select_next" => PromptSelectNext,
1032 "prompt_page_up" => PromptPageUp,
1033 "prompt_page_down" => PromptPageDown,
1034 "prompt_accept_suggestion" => PromptAcceptSuggestion,
1035 "prompt_delete_word_forward" => PromptDeleteWordForward,
1036 "prompt_delete_word_backward" => PromptDeleteWordBackward,
1037 "prompt_delete_to_line_end" => PromptDeleteToLineEnd,
1038 "prompt_copy" => PromptCopy,
1039 "prompt_cut" => PromptCut,
1040 "prompt_paste" => PromptPaste,
1041 "prompt_move_left_selecting" => PromptMoveLeftSelecting,
1042 "prompt_move_right_selecting" => PromptMoveRightSelecting,
1043 "prompt_move_home_selecting" => PromptMoveHomeSelecting,
1044 "prompt_move_end_selecting" => PromptMoveEndSelecting,
1045 "prompt_select_word_left" => PromptSelectWordLeft,
1046 "prompt_select_word_right" => PromptSelectWordRight,
1047 "prompt_select_all" => PromptSelectAll,
1048 "file_browser_toggle_hidden" => FileBrowserToggleHidden,
1049 "file_browser_toggle_detect_encoding" => FileBrowserToggleDetectEncoding,
1050 "prompt_move_word_left" => PromptMoveWordLeft,
1051 "prompt_move_word_right" => PromptMoveWordRight,
1052 "prompt_delete" => PromptDelete,
1053
1054 "popup_select_next" => PopupSelectNext,
1055 "popup_select_prev" => PopupSelectPrev,
1056 "popup_page_up" => PopupPageUp,
1057 "popup_page_down" => PopupPageDown,
1058 "popup_confirm" => PopupConfirm,
1059 "popup_cancel" => PopupCancel,
1060 "popup_focus" => PopupFocus,
1061
1062 "completion_accept" => CompletionAccept,
1063 "completion_dismiss" => CompletionDismiss,
1064
1065 "toggle_file_explorer" => ToggleFileExplorer,
1066 "toggle_menu_bar" => ToggleMenuBar,
1067 "toggle_tab_bar" => ToggleTabBar,
1068 "toggle_status_bar" => ToggleStatusBar,
1069 "toggle_prompt_line" => TogglePromptLine,
1070 "toggle_vertical_scrollbar" => ToggleVerticalScrollbar,
1071 "toggle_horizontal_scrollbar" => ToggleHorizontalScrollbar,
1072 "focus_file_explorer" => FocusFileExplorer,
1073 "focus_editor" => FocusEditor,
1074 "file_explorer_up" => FileExplorerUp,
1075 "file_explorer_down" => FileExplorerDown,
1076 "file_explorer_page_up" => FileExplorerPageUp,
1077 "file_explorer_page_down" => FileExplorerPageDown,
1078 "file_explorer_expand" => FileExplorerExpand,
1079 "file_explorer_collapse" => FileExplorerCollapse,
1080 "file_explorer_open" => FileExplorerOpen,
1081 "file_explorer_refresh" => FileExplorerRefresh,
1082 "file_explorer_new_file" => FileExplorerNewFile,
1083 "file_explorer_new_directory" => FileExplorerNewDirectory,
1084 "file_explorer_delete" => FileExplorerDelete,
1085 "file_explorer_rename" => FileExplorerRename,
1086 "file_explorer_toggle_hidden" => FileExplorerToggleHidden,
1087 "file_explorer_toggle_gitignored" => FileExplorerToggleGitignored,
1088 "file_explorer_search_clear" => FileExplorerSearchClear,
1089 "file_explorer_search_backspace" => FileExplorerSearchBackspace,
1090 "file_explorer_copy" => FileExplorerCopy,
1091 "file_explorer_cut" => FileExplorerCut,
1092 "file_explorer_paste" => FileExplorerPaste,
1093 "file_explorer_duplicate" => FileExplorerDuplicate,
1094 "file_explorer_copy_full_path" => FileExplorerCopyFullPath,
1095 "file_explorer_copy_relative_path" => FileExplorerCopyRelativePath,
1096 "file_explorer_extend_selection_up" => FileExplorerExtendSelectionUp,
1097 "file_explorer_extend_selection_down" => FileExplorerExtendSelectionDown,
1098 "file_explorer_toggle_select" => FileExplorerToggleSelect,
1099 "file_explorer_select_all" => FileExplorerSelectAll,
1100
1101 "lsp_completion" => LspCompletion,
1102 "lsp_goto_definition" => LspGotoDefinition,
1103 "lsp_references" => LspReferences,
1104 "lsp_rename" => LspRename,
1105 "lsp_hover" => LspHover,
1106 "lsp_signature_help" => LspSignatureHelp,
1107 "lsp_code_actions" => LspCodeActions,
1108 "lsp_restart" => LspRestart,
1109 "lsp_stop" => LspStop,
1110 "lsp_toggle_for_buffer" => LspToggleForBuffer,
1111 "toggle_inlay_hints" => ToggleInlayHints,
1112 "toggle_mouse_hover" => ToggleMouseHover,
1113
1114 "toggle_line_numbers" => ToggleLineNumbers,
1115 "toggle_scroll_sync" => ToggleScrollSync,
1116 "toggle_mouse_capture" => ToggleMouseCapture,
1117 "toggle_debug_highlights" => ToggleDebugHighlights,
1118 "set_background" => SetBackground,
1119 "set_background_blend" => SetBackgroundBlend,
1120 "inspect_theme_at_cursor" => InspectThemeAtCursor,
1121 "select_theme" => SelectTheme,
1122 "select_keybinding_map" => SelectKeybindingMap,
1123 "select_cursor_style" => SelectCursorStyle,
1124 "select_locale" => SelectLocale,
1125
1126 "set_tab_size" => SetTabSize,
1127 "set_line_ending" => SetLineEnding,
1128 "set_encoding" => SetEncoding,
1129 "reload_with_encoding" => ReloadWithEncoding,
1130 "set_language" => SetLanguage,
1131 "toggle_indentation_style" => ToggleIndentationStyle,
1132 "toggle_tab_indicators" => ToggleTabIndicators,
1133 "toggle_whitespace_indicators" => ToggleWhitespaceIndicators,
1134 "reset_buffer_settings" => ResetBufferSettings,
1135 "add_ruler" => AddRuler,
1136 "remove_ruler" => RemoveRuler,
1137
1138 "dump_config" => DumpConfig,
1139 "redraw_screen" => RedrawScreen,
1140
1141 "search" => Search,
1142 "find_in_selection" => FindInSelection,
1143 "find_next" => FindNext,
1144 "find_previous" => FindPrevious,
1145 "find_selection_next" => FindSelectionNext,
1146 "find_selection_previous" => FindSelectionPrevious,
1147 "replace" => Replace,
1148 "query_replace" => QueryReplace,
1149
1150 "menu_activate" => MenuActivate,
1151 "menu_close" => MenuClose,
1152 "menu_left" => MenuLeft,
1153 "menu_right" => MenuRight,
1154 "menu_up" => MenuUp,
1155 "menu_down" => MenuDown,
1156 "menu_execute" => MenuExecute,
1157
1158 "open_terminal" => OpenTerminal,
1159 "close_terminal" => CloseTerminal,
1160 "focus_terminal" => FocusTerminal,
1161 "terminal_escape" => TerminalEscape,
1162 "toggle_keyboard_capture" => ToggleKeyboardCapture,
1163 "terminal_paste" => TerminalPaste,
1164
1165 "shell_command" => ShellCommand,
1166 "shell_command_replace" => ShellCommandReplace,
1167
1168 "to_upper_case" => ToUpperCase,
1169 "to_lower_case" => ToLowerCase,
1170 "toggle_case" => ToggleCase,
1171 "sort_lines" => SortLines,
1172
1173 "calibrate_input" => CalibrateInput,
1174 "event_debug" => EventDebug,
1175 "suspend_process" => SuspendProcess,
1176 "load_plugin_from_buffer" => LoadPluginFromBuffer,
1177 "init_reload" => InitReload,
1178 "init_edit" => InitEdit,
1179 "init_check" => InitCheck,
1180 "open_keybinding_editor" => OpenKeybindingEditor,
1181
1182 "composite_next_hunk" => CompositeNextHunk,
1183 "composite_prev_hunk" => CompositePrevHunk,
1184
1185 "noop" => None,
1186
1187 "open_settings" => OpenSettings,
1188 "close_settings" => CloseSettings,
1189 "settings_save" => SettingsSave,
1190 "settings_reset" => SettingsReset,
1191 "settings_toggle_focus" => SettingsToggleFocus,
1192 "settings_activate" => SettingsActivate,
1193 "settings_search" => SettingsSearch,
1194 "settings_help" => SettingsHelp,
1195 "settings_increment" => SettingsIncrement,
1196 "settings_decrement" => SettingsDecrement,
1197 "settings_inherit" => SettingsInherit,
1198 }
1199 alias {
1200 "toggle_compose_mode" => TogglePageView,
1201 "set_compose_width" => SetPageWidth,
1202 }
1203 with_char {
1204 "insert_char" => InsertChar,
1205 "set_bookmark" => SetBookmark,
1206 "jump_to_bookmark" => JumpToBookmark,
1207 "clear_bookmark" => ClearBookmark,
1208 "play_macro" => PlayMacro,
1209 "toggle_macro_recording" => ToggleMacroRecording,
1210 "show_macro" => ShowMacro,
1211 }
1212 custom {
1213 "copy_with_theme" => CopyWithTheme : {
1214 let theme = args.get("theme").and_then(|v| v.as_str()).unwrap_or("");
1216 Self::CopyWithTheme(theme.to_string())
1217 },
1218 "menu_open" => MenuOpen : {
1219 let name = args.get("name")?.as_str()?;
1220 Self::MenuOpen(name.to_string())
1221 },
1222 "switch_keybinding_map" => SwitchKeybindingMap : {
1223 let map_name = args.get("map")?.as_str()?;
1224 Self::SwitchKeybindingMap(map_name.to_string())
1225 },
1226 "prompt_confirm_with_text" => PromptConfirmWithText : {
1227 let text = args.get("text")?.as_str()?;
1228 Self::PromptConfirmWithText(text.to_string())
1229 },
1230 }
1231 }
1232
1233 pub fn variant_arg_key(bare_action: &str) -> Option<&'static str> {
1240 match bare_action {
1241 "menu_open" => Some("name"),
1242 "switch_keybinding_map" => Some("map"),
1243 _ => None,
1244 }
1245 }
1246
1247 pub fn qualify_action(bare_action: &str, args: &HashMap<String, serde_json::Value>) -> String {
1251 if let Some(key) = Self::variant_arg_key(bare_action) {
1252 if let Some(v) = args.get(key).and_then(|v| v.as_str()) {
1253 return format!("{}:{}", bare_action, v);
1254 }
1255 }
1256 bare_action.to_string()
1257 }
1258
1259 pub fn to_qualified_action_str(&self) -> String {
1264 match self {
1265 Self::MenuOpen(name) => format!("menu_open:{}", name),
1266 Self::SwitchKeybindingMap(map) => format!("switch_keybinding_map:{}", map),
1267 other => other.to_action_str(),
1268 }
1269 }
1270
1271 pub fn unqualify_action(qualified: &str) -> (String, HashMap<String, serde_json::Value>) {
1276 if let Some((bare, suffix)) = qualified.split_once(':') {
1277 if let Some(arg_key) = Self::variant_arg_key(bare) {
1278 let mut args = HashMap::new();
1279 args.insert(
1280 arg_key.to_string(),
1281 serde_json::Value::String(suffix.to_string()),
1282 );
1283 return (bare.to_string(), args);
1284 }
1285 }
1286 (qualified.to_string(), HashMap::new())
1287 }
1288
1289 pub fn is_movement_or_editing(&self) -> bool {
1292 matches!(
1293 self,
1294 Action::MoveLeft
1296 | Action::MoveRight
1297 | Action::MoveUp
1298 | Action::MoveDown
1299 | Action::MoveWordLeft
1300 | Action::MoveWordRight
1301 | Action::MoveWordEnd
1302 | Action::ViMoveWordEnd
1303 | Action::MoveLeftInLine
1304 | Action::MoveRightInLine
1305 | Action::MoveLineStart
1306 | Action::MoveLineEnd
1307 | Action::MovePageUp
1308 | Action::MovePageDown
1309 | Action::MoveDocumentStart
1310 | Action::MoveDocumentEnd
1311 | Action::SelectLeft
1313 | Action::SelectRight
1314 | Action::SelectUp
1315 | Action::SelectDown
1316 | Action::SelectToParagraphUp
1317 | Action::SelectToParagraphDown
1318 | Action::SelectWordLeft
1319 | Action::SelectWordRight
1320 | Action::SelectWordEnd
1321 | Action::ViSelectWordEnd
1322 | Action::SelectLineStart
1323 | Action::SelectLineEnd
1324 | Action::SelectDocumentStart
1325 | Action::SelectDocumentEnd
1326 | Action::SelectPageUp
1327 | Action::SelectPageDown
1328 | Action::SelectAll
1329 | Action::SelectWord
1330 | Action::SelectLine
1331 | Action::ExpandSelection
1332 | Action::BlockSelectLeft
1334 | Action::BlockSelectRight
1335 | Action::BlockSelectUp
1336 | Action::BlockSelectDown
1337 | Action::InsertChar(_)
1339 | Action::InsertNewline
1340 | Action::InsertTab
1341 | Action::DeleteBackward
1342 | Action::DeleteForward
1343 | Action::DeleteWordBackward
1344 | Action::DeleteWordForward
1345 | Action::DeleteLine
1346 | Action::DeleteToLineEnd
1347 | Action::DeleteToLineStart
1348 | Action::TransposeChars
1349 | Action::OpenLine
1350 | Action::DuplicateLine
1351 | Action::MoveLineUp
1352 | Action::MoveLineDown
1353 | Action::Cut
1355 | Action::Paste
1356 | Action::Undo
1358 | Action::Redo
1359 )
1360 }
1361
1362 pub fn is_editing(&self) -> bool {
1365 matches!(
1366 self,
1367 Action::InsertChar(_)
1368 | Action::InsertNewline
1369 | Action::InsertTab
1370 | Action::DeleteBackward
1371 | Action::DeleteForward
1372 | Action::DeleteWordBackward
1373 | Action::DeleteWordForward
1374 | Action::DeleteLine
1375 | Action::DeleteToLineEnd
1376 | Action::DeleteToLineStart
1377 | Action::DeleteViWordEnd
1378 | Action::TransposeChars
1379 | Action::OpenLine
1380 | Action::DuplicateLine
1381 | Action::MoveLineUp
1382 | Action::MoveLineDown
1383 | Action::Cut
1384 | Action::Paste
1385 )
1386 }
1387}
1388
1389#[derive(Debug, Clone, PartialEq)]
1391pub enum ChordResolution {
1392 Complete(Action),
1394 Partial,
1396 NoMatch,
1398}
1399
1400#[derive(Clone)]
1402pub struct KeybindingResolver {
1403 bindings: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1406
1407 default_bindings: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1409
1410 plugin_defaults: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1413
1414 chord_bindings: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1417
1418 default_chord_bindings: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1420
1421 plugin_chord_defaults: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1423
1424 inheriting_modes: std::collections::HashSet<String>,
1428}
1429
1430impl KeybindingResolver {
1431 pub fn new(config: &Config) -> Self {
1433 let mut resolver = Self {
1434 bindings: HashMap::new(),
1435 default_bindings: HashMap::new(),
1436 plugin_defaults: HashMap::new(),
1437 chord_bindings: HashMap::new(),
1438 default_chord_bindings: HashMap::new(),
1439 plugin_chord_defaults: HashMap::new(),
1440 inheriting_modes: std::collections::HashSet::new(),
1441 };
1442
1443 let map_bindings = config.resolve_keymap(&config.active_keybinding_map);
1445 resolver.load_default_bindings_from_vec(&map_bindings);
1446
1447 resolver.load_bindings_from_vec(&config.keybindings);
1449
1450 resolver
1451 }
1452
1453 fn load_default_bindings_from_vec(&mut self, bindings: &[crate::config::Keybinding]) {
1455 for binding in bindings {
1456 let context = if let Some(ref when) = binding.when {
1458 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
1459 } else {
1460 KeyContext::Normal
1461 };
1462
1463 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
1464 if !binding.keys.is_empty() {
1466 let mut sequence = Vec::new();
1468 for key_press in &binding.keys {
1469 if let Some(key_code) = Self::parse_key(&key_press.key) {
1470 let modifiers = Self::parse_modifiers(&key_press.modifiers);
1471 sequence.push((key_code, modifiers));
1472 } else {
1473 break;
1475 }
1476 }
1477
1478 if sequence.len() == binding.keys.len() && !sequence.is_empty() {
1480 self.default_chord_bindings
1481 .entry(context)
1482 .or_default()
1483 .insert(sequence, action);
1484 }
1485 } else if let Some(key_code) = Self::parse_key(&binding.key) {
1486 let modifiers = Self::parse_modifiers(&binding.modifiers);
1488
1489 self.insert_binding_with_equivalents(
1491 context,
1492 key_code,
1493 modifiers,
1494 action,
1495 &binding.key,
1496 );
1497 }
1498 }
1499 }
1500 }
1501
1502 fn insert_binding_with_equivalents(
1505 &mut self,
1506 context: KeyContext,
1507 key_code: KeyCode,
1508 modifiers: KeyModifiers,
1509 action: Action,
1510 key_name: &str,
1511 ) {
1512 let context_bindings = self.default_bindings.entry(context.clone()).or_default();
1513
1514 context_bindings.insert((key_code, modifiers), action.clone());
1516
1517 let equivalents = terminal_key_equivalents(key_code, modifiers);
1519 for (equiv_key, equiv_mods) in equivalents {
1520 if let Some(existing_action) = context_bindings.get(&(equiv_key, equiv_mods)) {
1522 if existing_action != &action {
1524 let equiv_name = format!("{:?}", equiv_key);
1525 tracing::warn!(
1526 "Terminal key equivalent conflict in {:?} context: {} (equivalent of {}) \
1527 is bound to {:?}, but {} is bound to {:?}. \
1528 The explicit binding takes precedence.",
1529 context,
1530 equiv_name,
1531 key_name,
1532 existing_action,
1533 key_name,
1534 action
1535 );
1536 }
1537 } else {
1539 context_bindings.insert((equiv_key, equiv_mods), action.clone());
1541 }
1542 }
1543 }
1544
1545 fn load_bindings_from_vec(&mut self, bindings: &[crate::config::Keybinding]) {
1547 for binding in bindings {
1548 let context = if let Some(ref when) = binding.when {
1550 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
1551 } else {
1552 KeyContext::Normal
1553 };
1554
1555 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
1556 if !binding.keys.is_empty() {
1558 let mut sequence = Vec::new();
1560 for key_press in &binding.keys {
1561 if let Some(key_code) = Self::parse_key(&key_press.key) {
1562 let modifiers = Self::parse_modifiers(&key_press.modifiers);
1563 sequence.push((key_code, modifiers));
1564 } else {
1565 break;
1567 }
1568 }
1569
1570 if sequence.len() == binding.keys.len() && !sequence.is_empty() {
1572 self.chord_bindings
1573 .entry(context)
1574 .or_default()
1575 .insert(sequence, action);
1576 }
1577 } else if let Some(key_code) = Self::parse_key(&binding.key) {
1578 let modifiers = Self::parse_modifiers(&binding.modifiers);
1580 self.bindings
1581 .entry(context)
1582 .or_default()
1583 .insert((key_code, modifiers), action);
1584 }
1585 }
1586 }
1587 }
1588
1589 pub fn load_plugin_default(
1591 &mut self,
1592 context: KeyContext,
1593 key_code: KeyCode,
1594 modifiers: KeyModifiers,
1595 action: Action,
1596 ) {
1597 self.plugin_defaults
1598 .entry(context)
1599 .or_default()
1600 .insert((key_code, modifiers), action);
1601 }
1602
1603 pub fn load_plugin_chord_default(
1605 &mut self,
1606 context: KeyContext,
1607 sequence: Vec<(KeyCode, KeyModifiers)>,
1608 action: Action,
1609 ) {
1610 self.plugin_chord_defaults
1611 .entry(context)
1612 .or_default()
1613 .insert(sequence, action);
1614 }
1615
1616 pub fn clear_plugin_defaults_for_mode(&mut self, mode_name: &str) {
1618 let context = KeyContext::Mode(mode_name.to_string());
1619 self.plugin_defaults.remove(&context);
1620 self.plugin_chord_defaults.remove(&context);
1621 self.inheriting_modes.remove(mode_name);
1622 }
1623
1624 pub fn set_mode_inherits_normal_bindings(&mut self, mode_name: &str, inherit: bool) {
1627 if inherit {
1628 self.inheriting_modes.insert(mode_name.to_string());
1629 } else {
1630 self.inheriting_modes.remove(mode_name);
1631 }
1632 }
1633
1634 pub fn get_plugin_defaults(
1636 &self,
1637 ) -> &HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>> {
1638 &self.plugin_defaults
1639 }
1640
1641 fn is_application_wide_action(action: &Action) -> bool {
1643 matches!(
1644 action,
1645 Action::Quit
1646 | Action::ForceQuit
1647 | Action::Save
1648 | Action::SaveAs
1649 | Action::ShowHelp
1650 | Action::ShowKeyboardShortcuts
1651 | Action::PromptCancel | Action::PopupCancel )
1654 }
1655
1656 pub fn is_terminal_ui_action(action: &Action) -> bool {
1660 matches!(
1661 action,
1662 Action::CommandPalette
1664 | Action::QuickOpen
1665 | Action::QuickOpenBuffers
1666 | Action::QuickOpenFiles
1667 | Action::OpenLiveGrep
1668 | Action::ResumeLiveGrep
1669 | Action::LiveGrepExportQuickfix
1670 | Action::ToggleUtilityDock
1671 | Action::OpenTerminalInDock
1672 | Action::CycleLiveGrepProvider
1673 | Action::OpenSettings
1674 | Action::MenuActivate
1675 | Action::MenuOpen(_)
1676 | Action::ShowHelp
1677 | Action::ShowKeyboardShortcuts
1678 | Action::Quit
1679 | Action::ForceQuit
1680 | Action::NextSplit
1682 | Action::PrevSplit
1683 | Action::SplitHorizontal
1684 | Action::SplitVertical
1685 | Action::CloseSplit
1686 | Action::ToggleMaximizeSplit
1687 | Action::NextBuffer
1689 | Action::PrevBuffer
1690 | Action::Close
1691 | Action::CloseTab
1692 | Action::ScrollTabsLeft
1693 | Action::ScrollTabsRight
1694 | Action::TerminalEscape
1696 | Action::ToggleKeyboardCapture
1697 | Action::OpenTerminal
1698 | Action::CloseTerminal
1699 | Action::TerminalPaste
1700 | Action::ToggleFileExplorer
1702 | Action::ToggleMenuBar
1704 )
1705 }
1706
1707 pub fn resolve_chord(
1713 &self,
1714 chord_state: &[(KeyCode, KeyModifiers)],
1715 event: &KeyEvent,
1716 context: KeyContext,
1717 ) -> ChordResolution {
1718 let mut full_sequence: Vec<(KeyCode, KeyModifiers)> = chord_state
1720 .iter()
1721 .map(|(c, m)| normalize_key(*c, *m))
1722 .collect();
1723 let (norm_code, norm_mods) = normalize_key(event.code, event.modifiers);
1724 full_sequence.push((norm_code, norm_mods));
1725
1726 tracing::trace!(
1727 "KeybindingResolver.resolve_chord: sequence={:?}, context={:?}",
1728 full_sequence,
1729 context
1730 );
1731
1732 let search_order = vec![
1734 (&self.chord_bindings, &KeyContext::Global, "custom global"),
1735 (
1736 &self.default_chord_bindings,
1737 &KeyContext::Global,
1738 "default global",
1739 ),
1740 (&self.chord_bindings, &context, "custom context"),
1741 (&self.default_chord_bindings, &context, "default context"),
1742 (
1743 &self.plugin_chord_defaults,
1744 &context,
1745 "plugin default context",
1746 ),
1747 ];
1748
1749 let mut has_partial_match = false;
1750
1751 for (binding_map, bind_context, label) in search_order {
1752 if let Some(context_chords) = binding_map.get(bind_context) {
1753 if let Some(action) = context_chords.get(&full_sequence) {
1755 tracing::trace!(" -> Complete chord match in {}: {:?}", label, action);
1756 return ChordResolution::Complete(action.clone());
1757 }
1758
1759 for (chord_seq, _) in context_chords.iter() {
1761 if chord_seq.len() > full_sequence.len()
1762 && chord_seq[..full_sequence.len()] == full_sequence[..]
1763 {
1764 tracing::trace!(" -> Partial chord match in {}", label);
1765 has_partial_match = true;
1766 break;
1767 }
1768 }
1769 }
1770 }
1771
1772 if has_partial_match {
1773 ChordResolution::Partial
1774 } else {
1775 tracing::trace!(" -> No chord match");
1776 ChordResolution::NoMatch
1777 }
1778 }
1779
1780 pub fn resolve(&self, event: &KeyEvent, context: KeyContext) -> Action {
1782 let (norm_code, norm_mods) = normalize_key(event.code, event.modifiers);
1785 let norm = &(norm_code, norm_mods);
1786 tracing::trace!(
1787 "KeybindingResolver.resolve: code={:?}, modifiers={:?}, context={:?}",
1788 event.code,
1789 event.modifiers,
1790 context
1791 );
1792
1793 if let Some(global_bindings) = self.bindings.get(&KeyContext::Global) {
1795 if let Some(action) = global_bindings.get(norm) {
1796 tracing::trace!(" -> Found in custom global bindings: {:?}", action);
1797 return action.clone();
1798 }
1799 }
1800
1801 if let Some(global_bindings) = self.default_bindings.get(&KeyContext::Global) {
1802 if let Some(action) = global_bindings.get(norm) {
1803 tracing::trace!(" -> Found in default global bindings: {:?}", action);
1804 return action.clone();
1805 }
1806 }
1807
1808 if let Some(context_bindings) = self.bindings.get(&context) {
1810 if let Some(action) = context_bindings.get(norm) {
1811 tracing::trace!(
1812 " -> Found in custom {} bindings: {:?}",
1813 context.to_when_clause(),
1814 action
1815 );
1816 return action.clone();
1817 }
1818 }
1819
1820 if let Some(context_bindings) = self.default_bindings.get(&context) {
1822 if let Some(action) = context_bindings.get(norm) {
1823 tracing::trace!(
1824 " -> Found in default {} bindings: {:?}",
1825 context.to_when_clause(),
1826 action
1827 );
1828 return action.clone();
1829 }
1830 }
1831
1832 if let Some(plugin_bindings) = self.plugin_defaults.get(&context) {
1834 if let Some(action) = plugin_bindings.get(norm) {
1835 tracing::trace!(
1836 " -> Found in plugin default {} bindings: {:?}",
1837 context.to_when_clause(),
1838 action
1839 );
1840 return action.clone();
1841 }
1842 }
1843
1844 if context != KeyContext::Normal {
1848 let full_fallthrough = context.allows_normal_fallthrough()
1849 || matches!(&context, KeyContext::Mode(name) if self.inheriting_modes.contains(name));
1850
1851 let ui_fallthrough = context.allows_ui_fallthrough();
1852
1853 if let Some(normal_bindings) = self.bindings.get(&KeyContext::Normal) {
1854 if let Some(action) = normal_bindings.get(norm) {
1855 if full_fallthrough
1856 || Self::is_application_wide_action(action)
1857 || (ui_fallthrough && Self::is_terminal_ui_action(action))
1858 {
1859 tracing::trace!(
1860 " -> Found action in custom normal bindings (fallthrough): {:?}",
1861 action
1862 );
1863 return action.clone();
1864 }
1865 }
1866 }
1867
1868 if let Some(normal_bindings) = self.default_bindings.get(&KeyContext::Normal) {
1869 if let Some(action) = normal_bindings.get(norm) {
1870 if full_fallthrough
1871 || Self::is_application_wide_action(action)
1872 || (ui_fallthrough && Self::is_terminal_ui_action(action))
1873 {
1874 tracing::trace!(
1875 " -> Found action in default normal bindings (fallthrough): {:?}",
1876 action
1877 );
1878 return action.clone();
1879 }
1880 }
1881 }
1882 }
1883
1884 if context.allows_text_input() && is_text_input_modifier(event.modifiers) {
1886 if let KeyCode::Char(c) = event.code {
1887 tracing::trace!(" -> Character input: '{}'", c);
1888 return Action::InsertChar(c);
1889 }
1890 }
1891
1892 tracing::trace!(" -> No binding found, returning Action::None");
1893 Action::None
1894 }
1895
1896 pub fn resolve_in_context_only(&self, event: &KeyEvent, context: KeyContext) -> Option<Action> {
1901 let norm = normalize_key(event.code, event.modifiers);
1902 if let Some(context_bindings) = self.bindings.get(&context) {
1904 if let Some(action) = context_bindings.get(&norm) {
1905 return Some(action.clone());
1906 }
1907 }
1908
1909 if let Some(context_bindings) = self.default_bindings.get(&context) {
1911 if let Some(action) = context_bindings.get(&norm) {
1912 return Some(action.clone());
1913 }
1914 }
1915
1916 None
1917 }
1918
1919 pub fn has_explicit_binding(&self, event: &KeyEvent, context: &KeyContext) -> bool {
1927 let norm = normalize_key(event.code, event.modifiers);
1928 if let Some(bindings) = self.bindings.get(context) {
1929 if bindings.contains_key(&norm) {
1930 return true;
1931 }
1932 }
1933 if let Some(bindings) = self.default_bindings.get(context) {
1934 if bindings.contains_key(&norm) {
1935 return true;
1936 }
1937 }
1938 if let Some(bindings) = self.plugin_defaults.get(context) {
1939 if bindings.contains_key(&norm) {
1940 return true;
1941 }
1942 }
1943 false
1944 }
1945
1946 pub fn resolve_terminal_ui_action(&self, event: &KeyEvent) -> Action {
1950 let norm = normalize_key(event.code, event.modifiers);
1951 tracing::trace!(
1952 "KeybindingResolver.resolve_terminal_ui_action: code={:?}, modifiers={:?}",
1953 event.code,
1954 event.modifiers
1955 );
1956
1957 for bindings in [&self.bindings, &self.default_bindings] {
1959 if let Some(terminal_bindings) = bindings.get(&KeyContext::Terminal) {
1960 if let Some(action) = terminal_bindings.get(&norm) {
1961 if Self::is_terminal_ui_action(action) {
1962 tracing::trace!(" -> Found UI action in terminal bindings: {:?}", action);
1963 return action.clone();
1964 }
1965 }
1966 }
1967 }
1968
1969 for bindings in [&self.bindings, &self.default_bindings] {
1971 if let Some(global_bindings) = bindings.get(&KeyContext::Global) {
1972 if let Some(action) = global_bindings.get(&norm) {
1973 if Self::is_terminal_ui_action(action) {
1974 tracing::trace!(" -> Found UI action in global bindings: {:?}", action);
1975 return action.clone();
1976 }
1977 }
1978 }
1979 }
1980
1981 for bindings in [&self.bindings, &self.default_bindings] {
1983 if let Some(normal_bindings) = bindings.get(&KeyContext::Normal) {
1984 if let Some(action) = normal_bindings.get(&norm) {
1985 if Self::is_terminal_ui_action(action) {
1986 tracing::trace!(" -> Found UI action in normal bindings: {:?}", action);
1987 return action.clone();
1988 }
1989 }
1990 }
1991 }
1992
1993 tracing::trace!(" -> No UI action found");
1994 Action::None
1995 }
1996
1997 pub fn find_keybinding_for_action(
2000 &self,
2001 action_name: &str,
2002 context: KeyContext,
2003 ) -> Option<String> {
2004 let target_action = Action::from_str(action_name, &HashMap::new())?;
2006
2007 let search_maps = vec![
2009 self.bindings.get(&context),
2010 self.bindings.get(&KeyContext::Global),
2011 self.default_bindings.get(&context),
2012 self.default_bindings.get(&KeyContext::Global),
2013 ];
2014
2015 for map in search_maps.into_iter().flatten() {
2016 let mut matches: Vec<(KeyCode, KeyModifiers)> = map
2018 .iter()
2019 .filter(|(_, action)| {
2020 std::mem::discriminant(*action) == std::mem::discriminant(&target_action)
2021 })
2022 .map(|((key_code, modifiers), _)| (*key_code, *modifiers))
2023 .collect();
2024
2025 if !matches.is_empty() {
2026 matches.sort_by(|(key_a, mod_a), (key_b, mod_b)| {
2028 let mod_count_a = mod_a.bits().count_ones();
2030 let mod_count_b = mod_b.bits().count_ones();
2031 match mod_count_a.cmp(&mod_count_b) {
2032 std::cmp::Ordering::Equal => {
2033 match mod_a.bits().cmp(&mod_b.bits()) {
2035 std::cmp::Ordering::Equal => {
2036 Self::key_code_sort_key(key_a)
2038 .cmp(&Self::key_code_sort_key(key_b))
2039 }
2040 other => other,
2041 }
2042 }
2043 other => other,
2044 }
2045 });
2046
2047 let (key_code, modifiers) = matches[0];
2048 return Some(format_keybinding(&key_code, &modifiers));
2049 }
2050 }
2051
2052 None
2053 }
2054
2055 fn key_code_sort_key(key_code: &KeyCode) -> (u8, u32) {
2057 match key_code {
2058 KeyCode::Char(c) => (0, *c as u32),
2059 KeyCode::F(n) => (1, *n as u32),
2060 KeyCode::Enter => (2, 0),
2061 KeyCode::Tab => (2, 1),
2062 KeyCode::Backspace => (2, 2),
2063 KeyCode::Delete => (2, 3),
2064 KeyCode::Esc => (2, 4),
2065 KeyCode::Left => (3, 0),
2066 KeyCode::Right => (3, 1),
2067 KeyCode::Up => (3, 2),
2068 KeyCode::Down => (3, 3),
2069 KeyCode::Home => (3, 4),
2070 KeyCode::End => (3, 5),
2071 KeyCode::PageUp => (3, 6),
2072 KeyCode::PageDown => (3, 7),
2073 _ => (255, 0),
2074 }
2075 }
2076
2077 pub fn find_menu_mnemonic(&self, menu_name: &str) -> Option<char> {
2080 let search_maps = vec![
2082 self.bindings.get(&KeyContext::Normal),
2083 self.bindings.get(&KeyContext::Global),
2084 self.default_bindings.get(&KeyContext::Normal),
2085 self.default_bindings.get(&KeyContext::Global),
2086 ];
2087
2088 for map in search_maps.into_iter().flatten() {
2089 for ((key_code, modifiers), action) in map {
2090 if let Action::MenuOpen(name) = action {
2092 if name.eq_ignore_ascii_case(menu_name) && *modifiers == KeyModifiers::ALT {
2093 if let KeyCode::Char(c) = key_code {
2095 return Some(c.to_ascii_lowercase());
2096 }
2097 }
2098 }
2099 }
2100 }
2101
2102 None
2103 }
2104
2105 fn parse_key(key: &str) -> Option<KeyCode> {
2107 let lower = key.to_lowercase();
2108 match lower.as_str() {
2109 "enter" => Some(KeyCode::Enter),
2110 "backspace" => Some(KeyCode::Backspace),
2111 "delete" | "del" => Some(KeyCode::Delete),
2112 "tab" => Some(KeyCode::Tab),
2113 "backtab" => Some(KeyCode::BackTab),
2114 "esc" | "escape" => Some(KeyCode::Esc),
2115 "space" => Some(KeyCode::Char(' ')),
2116
2117 "left" => Some(KeyCode::Left),
2118 "right" => Some(KeyCode::Right),
2119 "up" => Some(KeyCode::Up),
2120 "down" => Some(KeyCode::Down),
2121 "home" => Some(KeyCode::Home),
2122 "end" => Some(KeyCode::End),
2123 "pageup" => Some(KeyCode::PageUp),
2124 "pagedown" => Some(KeyCode::PageDown),
2125
2126 s if s.len() == 1 => s.chars().next().map(KeyCode::Char),
2127 s if s.starts_with('f') && s.len() >= 2 => s[1..].parse::<u8>().ok().map(KeyCode::F),
2129 _ => None,
2130 }
2131 }
2132
2133 fn parse_modifiers(modifiers: &[String]) -> KeyModifiers {
2135 let mut result = KeyModifiers::empty();
2136 for m in modifiers {
2137 match m.to_lowercase().as_str() {
2138 "ctrl" | "control" => result |= KeyModifiers::CONTROL,
2139 "shift" => result |= KeyModifiers::SHIFT,
2140 "alt" => result |= KeyModifiers::ALT,
2141 "super" | "cmd" | "command" | "meta" => result |= KeyModifiers::SUPER,
2142 _ => {}
2143 }
2144 }
2145 result
2146 }
2147
2148 pub fn get_all_bindings(&self) -> Vec<(String, String)> {
2152 let mut bindings = Vec::new();
2153
2154 for context in &[
2156 KeyContext::Normal,
2157 KeyContext::Prompt,
2158 KeyContext::Popup,
2159 KeyContext::FileExplorer,
2160 KeyContext::Menu,
2161 KeyContext::CompositeBuffer,
2162 ] {
2163 let mut all_keys: HashMap<(KeyCode, KeyModifiers), Action> = HashMap::new();
2164
2165 if let Some(context_defaults) = self.default_bindings.get(context) {
2167 for (key, action) in context_defaults {
2168 all_keys.insert(*key, action.clone());
2169 }
2170 }
2171
2172 if let Some(context_bindings) = self.bindings.get(context) {
2174 for (key, action) in context_bindings {
2175 all_keys.insert(*key, action.clone());
2176 }
2177 }
2178
2179 let context_str = if *context != KeyContext::Normal {
2181 format!("[{}] ", context.to_when_clause())
2182 } else {
2183 String::new()
2184 };
2185
2186 for ((key_code, modifiers), action) in all_keys {
2187 let key_str = Self::format_key(key_code, modifiers);
2188 let action_str = format!("{}{}", context_str, Self::format_action(&action));
2189 bindings.push((key_str, action_str));
2190 }
2191 }
2192
2193 bindings.sort_by(|a, b| a.1.cmp(&b.1));
2195
2196 bindings
2197 }
2198
2199 fn format_key(key_code: KeyCode, modifiers: KeyModifiers) -> String {
2201 format_keybinding(&key_code, &modifiers)
2202 }
2203
2204 pub fn format_action(action: &Action) -> String {
2206 match action {
2207 Action::InsertChar(c) => t!("action.insert_char", char = c),
2208 Action::InsertNewline => t!("action.insert_newline"),
2209 Action::InsertTab => t!("action.insert_tab"),
2210 Action::MoveLeft => t!("action.move_left"),
2211 Action::MoveRight => t!("action.move_right"),
2212 Action::MoveUp => t!("action.move_up"),
2213 Action::MoveDown => t!("action.move_down"),
2214 Action::MoveWordLeft => t!("action.move_word_left"),
2215 Action::MoveWordRight => t!("action.move_word_right"),
2216 Action::MoveWordEnd => t!("action.move_word_end"),
2217 Action::ViMoveWordEnd => t!("action.move_word_end"),
2218 Action::MoveLeftInLine => t!("action.move_left"),
2219 Action::MoveRightInLine => t!("action.move_right"),
2220 Action::MoveLineStart => t!("action.move_line_start"),
2221 Action::MoveLineEnd => t!("action.move_line_end"),
2222 Action::MoveLineUp => t!("action.move_line_up"),
2223 Action::MoveLineDown => t!("action.move_line_down"),
2224 Action::MovePageUp => t!("action.move_page_up"),
2225 Action::MovePageDown => t!("action.move_page_down"),
2226 Action::MoveDocumentStart => t!("action.move_document_start"),
2227 Action::MoveDocumentEnd => t!("action.move_document_end"),
2228 Action::SelectLeft => t!("action.select_left"),
2229 Action::SelectRight => t!("action.select_right"),
2230 Action::SelectUp => t!("action.select_up"),
2231 Action::SelectDown => t!("action.select_down"),
2232 Action::SelectToParagraphUp => t!("action.select_to_paragraph_up"),
2233 Action::SelectToParagraphDown => t!("action.select_to_paragraph_down"),
2234 Action::SelectWordLeft => t!("action.select_word_left"),
2235 Action::SelectWordRight => t!("action.select_word_right"),
2236 Action::SelectWordEnd => t!("action.select_word_end"),
2237 Action::ViSelectWordEnd => t!("action.select_word_end"),
2238 Action::SelectLineStart => t!("action.select_line_start"),
2239 Action::SelectLineEnd => t!("action.select_line_end"),
2240 Action::SelectDocumentStart => t!("action.select_document_start"),
2241 Action::SelectDocumentEnd => t!("action.select_document_end"),
2242 Action::SelectPageUp => t!("action.select_page_up"),
2243 Action::SelectPageDown => t!("action.select_page_down"),
2244 Action::SelectAll => t!("action.select_all"),
2245 Action::SelectWord => t!("action.select_word"),
2246 Action::SelectLine => t!("action.select_line"),
2247 Action::ExpandSelection => t!("action.expand_selection"),
2248 Action::BlockSelectLeft => t!("action.block_select_left"),
2249 Action::BlockSelectRight => t!("action.block_select_right"),
2250 Action::BlockSelectUp => t!("action.block_select_up"),
2251 Action::BlockSelectDown => t!("action.block_select_down"),
2252 Action::DeleteBackward => t!("action.delete_backward"),
2253 Action::DeleteForward => t!("action.delete_forward"),
2254 Action::DeleteWordBackward => t!("action.delete_word_backward"),
2255 Action::DeleteWordForward => t!("action.delete_word_forward"),
2256 Action::DeleteLine => t!("action.delete_line"),
2257 Action::DeleteToLineEnd => t!("action.delete_to_line_end"),
2258 Action::DeleteToLineStart => t!("action.delete_to_line_start"),
2259 Action::DeleteViWordEnd => t!("action.delete_word_forward"),
2260 Action::TransposeChars => t!("action.transpose_chars"),
2261 Action::OpenLine => t!("action.open_line"),
2262 Action::DuplicateLine => t!("action.duplicate_line"),
2263 Action::Recenter => t!("action.recenter"),
2264 Action::SetMark => t!("action.set_mark"),
2265 Action::Copy => t!("action.copy"),
2266 Action::CopyWithTheme(theme) if theme.is_empty() => t!("action.copy_with_formatting"),
2267 Action::CopyWithTheme(theme) => t!("action.copy_with_theme", theme = theme),
2268 Action::Cut => t!("action.cut"),
2269 Action::Paste => t!("action.paste"),
2270 Action::CopyFilePath => t!("action.copy_file_path"),
2271 Action::CopyRelativeFilePath => t!("action.copy_relative_file_path"),
2272 Action::YankWordForward => t!("action.yank_word_forward"),
2273 Action::YankWordBackward => t!("action.yank_word_backward"),
2274 Action::YankToLineEnd => t!("action.yank_to_line_end"),
2275 Action::YankToLineStart => t!("action.yank_to_line_start"),
2276 Action::YankViWordEnd => t!("action.yank_word_forward"),
2277 Action::AddCursorAbove => t!("action.add_cursor_above"),
2278 Action::AddCursorBelow => t!("action.add_cursor_below"),
2279 Action::AddCursorNextMatch => t!("action.add_cursor_next_match"),
2280 Action::AddCursorsToLineEnds => t!("action.add_cursors_to_line_ends"),
2281 Action::RemoveSecondaryCursors => t!("action.remove_secondary_cursors"),
2282 Action::Save => t!("action.save"),
2283 Action::SaveAs => t!("action.save_as"),
2284 Action::Open => t!("action.open"),
2285 Action::SwitchProject => t!("action.switch_project"),
2286 Action::New => t!("action.new"),
2287 Action::Close => t!("action.close"),
2288 Action::CloseTab => t!("action.close_tab"),
2289 Action::Quit => t!("action.quit"),
2290 Action::ForceQuit => t!("action.force_quit"),
2291 Action::Detach => t!("action.detach"),
2292 Action::Revert => t!("action.revert"),
2293 Action::ToggleAutoRevert => t!("action.toggle_auto_revert"),
2294 Action::FormatBuffer => t!("action.format_buffer"),
2295 Action::TrimTrailingWhitespace => t!("action.trim_trailing_whitespace"),
2296 Action::EnsureFinalNewline => t!("action.ensure_final_newline"),
2297 Action::GotoLine => t!("action.goto_line"),
2298 Action::ScanLineIndex => t!("action.scan_line_index"),
2299 Action::GoToMatchingBracket => t!("action.goto_matching_bracket"),
2300 Action::JumpToNextError => t!("action.jump_to_next_error"),
2301 Action::JumpToPreviousError => t!("action.jump_to_previous_error"),
2302 Action::SmartHome => t!("action.smart_home"),
2303 Action::DedentSelection => t!("action.dedent_selection"),
2304 Action::ToggleComment => t!("action.toggle_comment"),
2305 Action::DabbrevExpand => std::borrow::Cow::Borrowed("Expand abbreviation (dabbrev)"),
2306 Action::ToggleFold => t!("action.toggle_fold"),
2307 Action::SetBookmark(c) => t!("action.set_bookmark", key = c),
2308 Action::JumpToBookmark(c) => t!("action.jump_to_bookmark", key = c),
2309 Action::ClearBookmark(c) => t!("action.clear_bookmark", key = c),
2310 Action::ListBookmarks => t!("action.list_bookmarks"),
2311 Action::ToggleSearchCaseSensitive => t!("action.toggle_search_case_sensitive"),
2312 Action::ToggleSearchWholeWord => t!("action.toggle_search_whole_word"),
2313 Action::ToggleSearchRegex => t!("action.toggle_search_regex"),
2314 Action::ToggleSearchConfirmEach => t!("action.toggle_search_confirm_each"),
2315 Action::StartMacroRecording => t!("action.start_macro_recording"),
2316 Action::StopMacroRecording => t!("action.stop_macro_recording"),
2317 Action::PlayMacro(c) => t!("action.play_macro", key = c),
2318 Action::ToggleMacroRecording(c) => t!("action.toggle_macro_recording", key = c),
2319 Action::ShowMacro(c) => t!("action.show_macro", key = c),
2320 Action::ListMacros => t!("action.list_macros"),
2321 Action::PromptRecordMacro => t!("action.prompt_record_macro"),
2322 Action::PromptPlayMacro => t!("action.prompt_play_macro"),
2323 Action::PlayLastMacro => t!("action.play_last_macro"),
2324 Action::PromptSetBookmark => t!("action.prompt_set_bookmark"),
2325 Action::PromptJumpToBookmark => t!("action.prompt_jump_to_bookmark"),
2326 Action::Undo => t!("action.undo"),
2327 Action::Redo => t!("action.redo"),
2328 Action::ScrollUp => t!("action.scroll_up"),
2329 Action::ScrollDown => t!("action.scroll_down"),
2330 Action::ShowHelp => t!("action.show_help"),
2331 Action::ShowKeyboardShortcuts => t!("action.show_keyboard_shortcuts"),
2332 Action::ShowWarnings => t!("action.show_warnings"),
2333 Action::ShowStatusLog => t!("action.show_status_log"),
2334 Action::ShowLspStatus => t!("action.show_lsp_status"),
2335 Action::ShowRemoteIndicatorMenu => t!("action.show_remote_indicator_menu"),
2336 Action::ClearWarnings => t!("action.clear_warnings"),
2337 Action::CommandPalette => t!("action.command_palette"),
2338 Action::QuickOpen => t!("action.quick_open"),
2339 Action::QuickOpenBuffers => t!("action.quick_open_buffers"),
2340 Action::QuickOpenFiles => t!("action.quick_open_files"),
2341 Action::OpenLiveGrep => t!("action.open_live_grep"),
2342 Action::ResumeLiveGrep => t!("action.resume_live_grep"),
2343 Action::LiveGrepExportQuickfix => t!("action.live_grep_export_quickfix"),
2344 Action::ToggleUtilityDock => t!("action.toggle_utility_dock"),
2345 Action::OpenTerminalInDock => t!("action.open_terminal_in_dock"),
2346 Action::CycleLiveGrepProvider => t!("action.cycle_live_grep_provider"),
2347 Action::InspectThemeAtCursor => t!("action.inspect_theme_at_cursor"),
2348 Action::ToggleLineWrap => t!("action.toggle_line_wrap"),
2349 Action::ToggleCurrentLineHighlight => t!("action.toggle_current_line_highlight"),
2350 Action::ToggleReadOnly => t!("action.toggle_read_only"),
2351 Action::TogglePageView => t!("action.toggle_page_view"),
2352 Action::SetPageWidth => t!("action.set_page_width"),
2353 Action::NextBuffer => t!("action.next_buffer"),
2354 Action::PrevBuffer => t!("action.prev_buffer"),
2355 Action::NavigateBack => t!("action.navigate_back"),
2356 Action::NavigateForward => t!("action.navigate_forward"),
2357 Action::SplitHorizontal => t!("action.split_horizontal"),
2358 Action::SplitVertical => t!("action.split_vertical"),
2359 Action::CloseSplit => t!("action.close_split"),
2360 Action::NextSplit => t!("action.next_split"),
2361 Action::PrevSplit => t!("action.prev_split"),
2362 Action::IncreaseSplitSize => t!("action.increase_split_size"),
2363 Action::DecreaseSplitSize => t!("action.decrease_split_size"),
2364 Action::ToggleMaximizeSplit => t!("action.toggle_maximize_split"),
2365 Action::PromptConfirm => t!("action.prompt_confirm"),
2366 Action::PromptConfirmWithText(ref text) => {
2367 format!("{} ({})", t!("action.prompt_confirm"), text).into()
2368 }
2369 Action::PromptCancel => t!("action.prompt_cancel"),
2370 Action::PromptBackspace => t!("action.prompt_backspace"),
2371 Action::PromptDelete => t!("action.prompt_delete"),
2372 Action::PromptMoveLeft => t!("action.prompt_move_left"),
2373 Action::PromptMoveRight => t!("action.prompt_move_right"),
2374 Action::PromptMoveStart => t!("action.prompt_move_start"),
2375 Action::PromptMoveEnd => t!("action.prompt_move_end"),
2376 Action::PromptSelectPrev => t!("action.prompt_select_prev"),
2377 Action::PromptSelectNext => t!("action.prompt_select_next"),
2378 Action::PromptPageUp => t!("action.prompt_page_up"),
2379 Action::PromptPageDown => t!("action.prompt_page_down"),
2380 Action::PromptAcceptSuggestion => t!("action.prompt_accept_suggestion"),
2381 Action::PromptMoveWordLeft => t!("action.prompt_move_word_left"),
2382 Action::PromptMoveWordRight => t!("action.prompt_move_word_right"),
2383 Action::PromptDeleteWordForward => t!("action.prompt_delete_word_forward"),
2384 Action::PromptDeleteWordBackward => t!("action.prompt_delete_word_backward"),
2385 Action::PromptDeleteToLineEnd => t!("action.prompt_delete_to_line_end"),
2386 Action::PromptCopy => t!("action.prompt_copy"),
2387 Action::PromptCut => t!("action.prompt_cut"),
2388 Action::PromptPaste => t!("action.prompt_paste"),
2389 Action::PromptMoveLeftSelecting => t!("action.prompt_move_left_selecting"),
2390 Action::PromptMoveRightSelecting => t!("action.prompt_move_right_selecting"),
2391 Action::PromptMoveHomeSelecting => t!("action.prompt_move_home_selecting"),
2392 Action::PromptMoveEndSelecting => t!("action.prompt_move_end_selecting"),
2393 Action::PromptSelectWordLeft => t!("action.prompt_select_word_left"),
2394 Action::PromptSelectWordRight => t!("action.prompt_select_word_right"),
2395 Action::PromptSelectAll => t!("action.prompt_select_all"),
2396 Action::FileBrowserToggleHidden => t!("action.file_browser_toggle_hidden"),
2397 Action::FileBrowserToggleDetectEncoding => {
2398 t!("action.file_browser_toggle_detect_encoding")
2399 }
2400 Action::PopupSelectNext => t!("action.popup_select_next"),
2401 Action::PopupSelectPrev => t!("action.popup_select_prev"),
2402 Action::PopupPageUp => t!("action.popup_page_up"),
2403 Action::PopupPageDown => t!("action.popup_page_down"),
2404 Action::PopupConfirm => t!("action.popup_confirm"),
2405 Action::PopupCancel => t!("action.popup_cancel"),
2406 Action::PopupFocus => t!("action.popup_focus"),
2407 Action::CompletionAccept => t!("action.completion_accept"),
2408 Action::CompletionDismiss => t!("action.completion_dismiss"),
2409 Action::ToggleFileExplorer => t!("action.toggle_file_explorer"),
2410 Action::ToggleMenuBar => t!("action.toggle_menu_bar"),
2411 Action::ToggleTabBar => t!("action.toggle_tab_bar"),
2412 Action::ToggleStatusBar => t!("action.toggle_status_bar"),
2413 Action::TogglePromptLine => t!("action.toggle_prompt_line"),
2414 Action::ToggleVerticalScrollbar => t!("action.toggle_vertical_scrollbar"),
2415 Action::ToggleHorizontalScrollbar => t!("action.toggle_horizontal_scrollbar"),
2416 Action::FocusFileExplorer => t!("action.focus_file_explorer"),
2417 Action::FocusEditor => t!("action.focus_editor"),
2418 Action::FileExplorerUp => t!("action.file_explorer_up"),
2419 Action::FileExplorerDown => t!("action.file_explorer_down"),
2420 Action::FileExplorerPageUp => t!("action.file_explorer_page_up"),
2421 Action::FileExplorerPageDown => t!("action.file_explorer_page_down"),
2422 Action::FileExplorerExpand => t!("action.file_explorer_expand"),
2423 Action::FileExplorerCollapse => t!("action.file_explorer_collapse"),
2424 Action::FileExplorerOpen => t!("action.file_explorer_open"),
2425 Action::FileExplorerRefresh => t!("action.file_explorer_refresh"),
2426 Action::FileExplorerNewFile => t!("action.file_explorer_new_file"),
2427 Action::FileExplorerNewDirectory => t!("action.file_explorer_new_directory"),
2428 Action::FileExplorerDelete => t!("action.file_explorer_delete"),
2429 Action::FileExplorerRename => t!("action.file_explorer_rename"),
2430 Action::FileExplorerToggleHidden => t!("action.file_explorer_toggle_hidden"),
2431 Action::FileExplorerToggleGitignored => t!("action.file_explorer_toggle_gitignored"),
2432 Action::FileExplorerSearchClear => t!("action.file_explorer_search_clear"),
2433 Action::FileExplorerSearchBackspace => t!("action.file_explorer_search_backspace"),
2434 Action::FileExplorerCopy => t!("action.file_explorer_copy"),
2435 Action::FileExplorerCut => t!("action.file_explorer_cut"),
2436 Action::FileExplorerPaste => t!("action.file_explorer_paste"),
2437 Action::FileExplorerDuplicate => t!("action.file_explorer_duplicate"),
2438 Action::FileExplorerCopyFullPath => t!("action.file_explorer_copy_full_path"),
2439 Action::FileExplorerCopyRelativePath => t!("action.file_explorer_copy_relative_path"),
2440 Action::FileExplorerExtendSelectionUp => t!("action.file_explorer_extend_selection_up"),
2441 Action::FileExplorerExtendSelectionDown => {
2442 t!("action.file_explorer_extend_selection_down")
2443 }
2444 Action::FileExplorerToggleSelect => t!("action.file_explorer_toggle_select"),
2445 Action::FileExplorerSelectAll => t!("action.file_explorer_select_all"),
2446 Action::LspCompletion => t!("action.lsp_completion"),
2447 Action::LspGotoDefinition => t!("action.lsp_goto_definition"),
2448 Action::LspReferences => t!("action.lsp_references"),
2449 Action::LspRename => t!("action.lsp_rename"),
2450 Action::LspHover => t!("action.lsp_hover"),
2451 Action::LspSignatureHelp => t!("action.lsp_signature_help"),
2452 Action::LspCodeActions => t!("action.lsp_code_actions"),
2453 Action::LspRestart => t!("action.lsp_restart"),
2454 Action::LspStop => t!("action.lsp_stop"),
2455 Action::LspToggleForBuffer => t!("action.lsp_toggle_for_buffer"),
2456 Action::ToggleInlayHints => t!("action.toggle_inlay_hints"),
2457 Action::ToggleMouseHover => t!("action.toggle_mouse_hover"),
2458 Action::ToggleLineNumbers => t!("action.toggle_line_numbers"),
2459 Action::ToggleScrollSync => t!("action.toggle_scroll_sync"),
2460 Action::ToggleMouseCapture => t!("action.toggle_mouse_capture"),
2461 Action::ToggleDebugHighlights => t!("action.toggle_debug_highlights"),
2462 Action::SetBackground => t!("action.set_background"),
2463 Action::SetBackgroundBlend => t!("action.set_background_blend"),
2464 Action::AddRuler => t!("action.add_ruler"),
2465 Action::RemoveRuler => t!("action.remove_ruler"),
2466 Action::SetTabSize => t!("action.set_tab_size"),
2467 Action::SetLineEnding => t!("action.set_line_ending"),
2468 Action::SetEncoding => t!("action.set_encoding"),
2469 Action::ReloadWithEncoding => t!("action.reload_with_encoding"),
2470 Action::SetLanguage => t!("action.set_language"),
2471 Action::ToggleIndentationStyle => t!("action.toggle_indentation_style"),
2472 Action::ToggleTabIndicators => t!("action.toggle_tab_indicators"),
2473 Action::ToggleWhitespaceIndicators => t!("action.toggle_whitespace_indicators"),
2474 Action::ResetBufferSettings => t!("action.reset_buffer_settings"),
2475 Action::DumpConfig => t!("action.dump_config"),
2476 Action::RedrawScreen => t!("action.redraw_screen"),
2477 Action::Search => t!("action.search"),
2478 Action::FindInSelection => t!("action.find_in_selection"),
2479 Action::FindNext => t!("action.find_next"),
2480 Action::FindPrevious => t!("action.find_previous"),
2481 Action::FindSelectionNext => t!("action.find_selection_next"),
2482 Action::FindSelectionPrevious => t!("action.find_selection_previous"),
2483 Action::Replace => t!("action.replace"),
2484 Action::QueryReplace => t!("action.query_replace"),
2485 Action::MenuActivate => t!("action.menu_activate"),
2486 Action::MenuClose => t!("action.menu_close"),
2487 Action::MenuLeft => t!("action.menu_left"),
2488 Action::MenuRight => t!("action.menu_right"),
2489 Action::MenuUp => t!("action.menu_up"),
2490 Action::MenuDown => t!("action.menu_down"),
2491 Action::MenuExecute => t!("action.menu_execute"),
2492 Action::MenuOpen(name) => t!("action.menu_open", name = name),
2493 Action::SwitchKeybindingMap(map) => t!("action.switch_keybinding_map", map = map),
2494 Action::PluginAction(name) => t!("action.plugin_action", name = name),
2495 Action::ScrollTabsLeft => t!("action.scroll_tabs_left"),
2496 Action::ScrollTabsRight => t!("action.scroll_tabs_right"),
2497 Action::SelectTheme => t!("action.select_theme"),
2498 Action::SelectKeybindingMap => t!("action.select_keybinding_map"),
2499 Action::SelectCursorStyle => t!("action.select_cursor_style"),
2500 Action::SelectLocale => t!("action.select_locale"),
2501 Action::SwitchToPreviousTab => t!("action.switch_to_previous_tab"),
2502 Action::SwitchToTabByName => t!("action.switch_to_tab_by_name"),
2503 Action::OpenTerminal => t!("action.open_terminal"),
2504 Action::CloseTerminal => t!("action.close_terminal"),
2505 Action::FocusTerminal => t!("action.focus_terminal"),
2506 Action::TerminalEscape => t!("action.terminal_escape"),
2507 Action::ToggleKeyboardCapture => t!("action.toggle_keyboard_capture"),
2508 Action::TerminalPaste => t!("action.terminal_paste"),
2509 Action::OpenSettings => t!("action.open_settings"),
2510 Action::CloseSettings => t!("action.close_settings"),
2511 Action::SettingsSave => t!("action.settings_save"),
2512 Action::SettingsReset => t!("action.settings_reset"),
2513 Action::SettingsToggleFocus => t!("action.settings_toggle_focus"),
2514 Action::SettingsActivate => t!("action.settings_activate"),
2515 Action::SettingsSearch => t!("action.settings_search"),
2516 Action::SettingsHelp => t!("action.settings_help"),
2517 Action::SettingsIncrement => t!("action.settings_increment"),
2518 Action::SettingsDecrement => t!("action.settings_decrement"),
2519 Action::SettingsInherit => t!("action.settings_inherit"),
2520 Action::ShellCommand => t!("action.shell_command"),
2521 Action::ShellCommandReplace => t!("action.shell_command_replace"),
2522 Action::ToUpperCase => t!("action.to_uppercase"),
2523 Action::ToLowerCase => t!("action.to_lowercase"),
2524 Action::ToggleCase => t!("action.to_uppercase"),
2525 Action::SortLines => t!("action.sort_lines"),
2526 Action::CalibrateInput => t!("action.calibrate_input"),
2527 Action::EventDebug => t!("action.event_debug"),
2528 Action::SuspendProcess => t!("action.suspend_process"),
2529 Action::LoadPluginFromBuffer => "Load Plugin from Buffer".into(),
2530 Action::InitReload => "Reload init.ts".into(),
2531 Action::InitEdit => "Edit init.ts".into(),
2532 Action::InitCheck => "Check init.ts".into(),
2533 Action::OpenKeybindingEditor => "Keybinding Editor".into(),
2534 Action::CompositeNextHunk => t!("action.composite_next_hunk"),
2535 Action::CompositePrevHunk => t!("action.composite_prev_hunk"),
2536 Action::None => t!("action.none"),
2537 }
2538 .to_string()
2539 }
2540
2541 pub fn parse_key_public(key: &str) -> Option<KeyCode> {
2543 Self::parse_key(key)
2544 }
2545
2546 pub fn parse_modifiers_public(modifiers: &[String]) -> KeyModifiers {
2548 Self::parse_modifiers(modifiers)
2549 }
2550
2551 pub fn format_action_from_str(action_name: &str) -> String {
2555 Self::format_action_from_str_with_args(action_name, &std::collections::HashMap::new())
2556 }
2557
2558 pub fn format_action_from_str_with_args(
2562 action_name: &str,
2563 args: &std::collections::HashMap<String, serde_json::Value>,
2564 ) -> String {
2565 if let Some(action) = Action::from_str(action_name, args) {
2567 Self::format_action(&action)
2568 } else {
2569 action_name
2571 .split('_')
2572 .map(|word| {
2573 let mut chars = word.chars();
2574 match chars.next() {
2575 Some(c) => {
2576 let upper: String = c.to_uppercase().collect();
2577 format!("{}{}", upper, chars.as_str())
2578 }
2579 None => String::new(),
2580 }
2581 })
2582 .collect::<Vec<_>>()
2583 .join(" ")
2584 }
2585 }
2586
2587 pub fn all_action_names() -> Vec<String> {
2591 Action::all_action_names()
2592 }
2593
2594 pub fn get_keybinding_for_action(
2600 &self,
2601 action: &Action,
2602 context: KeyContext,
2603 ) -> Option<String> {
2604 self.get_keybinding_event_for_action(action, context)
2605 .map(|(k, m)| format_keybinding(&k, &m))
2606 }
2607
2608 pub fn get_keybinding_event_for_action(
2615 &self,
2616 action: &Action,
2617 context: KeyContext,
2618 ) -> Option<(KeyCode, KeyModifiers)> {
2619 fn find_best_keybinding(
2621 bindings: &HashMap<(KeyCode, KeyModifiers), Action>,
2622 action: &Action,
2623 ) -> Option<(KeyCode, KeyModifiers)> {
2624 let matches: Vec<_> = bindings
2625 .iter()
2626 .filter(|(_, a)| *a == action)
2627 .map(|((k, m), _)| (*k, *m))
2628 .collect();
2629
2630 if matches.is_empty() {
2631 return None;
2632 }
2633
2634 let mut sorted = matches;
2637 sorted.sort_by(|(k1, m1), (k2, m2)| {
2638 let score1 = keybinding_priority_score(k1);
2639 let score2 = keybinding_priority_score(k2);
2640 match score1.cmp(&score2) {
2642 std::cmp::Ordering::Equal => {
2643 let s1 = format_keybinding(k1, m1);
2645 let s2 = format_keybinding(k2, m2);
2646 s1.cmp(&s2)
2647 }
2648 other => other,
2649 }
2650 });
2651
2652 sorted.into_iter().next()
2653 }
2654
2655 if let Some(context_bindings) = self.bindings.get(&context) {
2657 if let Some(hit) = find_best_keybinding(context_bindings, action) {
2658 return Some(hit);
2659 }
2660 }
2661
2662 if let Some(context_bindings) = self.default_bindings.get(&context) {
2664 if let Some(hit) = find_best_keybinding(context_bindings, action) {
2665 return Some(hit);
2666 }
2667 }
2668
2669 if context != KeyContext::Normal
2671 && (context.allows_normal_fallthrough()
2672 || Self::is_application_wide_action(action)
2673 || (context.allows_ui_fallthrough() && Self::is_terminal_ui_action(action)))
2674 {
2675 if let Some(normal_bindings) = self.bindings.get(&KeyContext::Normal) {
2677 if let Some(hit) = find_best_keybinding(normal_bindings, action) {
2678 return Some(hit);
2679 }
2680 }
2681
2682 if let Some(normal_bindings) = self.default_bindings.get(&KeyContext::Normal) {
2684 if let Some(hit) = find_best_keybinding(normal_bindings, action) {
2685 return Some(hit);
2686 }
2687 }
2688 }
2689
2690 None
2691 }
2692
2693 pub fn reload(&mut self, config: &Config) {
2695 self.bindings.clear();
2696 for binding in &config.keybindings {
2697 if let Some(key_code) = Self::parse_key(&binding.key) {
2698 let modifiers = Self::parse_modifiers(&binding.modifiers);
2699 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
2700 let context = if let Some(ref when) = binding.when {
2702 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
2703 } else {
2704 KeyContext::Normal
2705 };
2706
2707 self.bindings
2708 .entry(context)
2709 .or_default()
2710 .insert((key_code, modifiers), action);
2711 }
2712 }
2713 }
2714 }
2715}
2716
2717#[cfg(test)]
2718mod tests {
2719 use super::*;
2720
2721 #[test]
2722 fn test_parse_key() {
2723 assert_eq!(KeybindingResolver::parse_key("enter"), Some(KeyCode::Enter));
2724 assert_eq!(
2725 KeybindingResolver::parse_key("backspace"),
2726 Some(KeyCode::Backspace)
2727 );
2728 assert_eq!(KeybindingResolver::parse_key("tab"), Some(KeyCode::Tab));
2729 assert_eq!(
2730 KeybindingResolver::parse_key("backtab"),
2731 Some(KeyCode::BackTab)
2732 );
2733 assert_eq!(
2734 KeybindingResolver::parse_key("BackTab"),
2735 Some(KeyCode::BackTab)
2736 );
2737 assert_eq!(KeybindingResolver::parse_key("a"), Some(KeyCode::Char('a')));
2738 }
2739
2740 #[test]
2741 fn test_parse_modifiers() {
2742 let mods = vec!["ctrl".to_string()];
2743 assert_eq!(
2744 KeybindingResolver::parse_modifiers(&mods),
2745 KeyModifiers::CONTROL
2746 );
2747
2748 let mods = vec!["ctrl".to_string(), "shift".to_string()];
2749 assert_eq!(
2750 KeybindingResolver::parse_modifiers(&mods),
2751 KeyModifiers::CONTROL | KeyModifiers::SHIFT
2752 );
2753 }
2754
2755 #[test]
2756 fn test_format_action_from_str_distinguishes_menu_open_by_name() {
2757 let mut file_args = HashMap::new();
2760 file_args.insert(
2761 "name".to_string(),
2762 serde_json::Value::String("File".to_string()),
2763 );
2764 let mut edit_args = HashMap::new();
2765 edit_args.insert(
2766 "name".to_string(),
2767 serde_json::Value::String("Edit".to_string()),
2768 );
2769
2770 let file_display =
2771 KeybindingResolver::format_action_from_str_with_args("menu_open", &file_args);
2772 let edit_display =
2773 KeybindingResolver::format_action_from_str_with_args("menu_open", &edit_args);
2774 let no_args_display = KeybindingResolver::format_action_from_str("menu_open");
2775
2776 assert_ne!(
2777 file_display, edit_display,
2778 "menu_open with different names should produce different descriptions"
2779 );
2780 assert!(
2781 file_display.contains("File"),
2782 "expected the File menu description to contain \"File\", got {file_display:?}"
2783 );
2784 assert!(
2785 edit_display.contains("Edit"),
2786 "expected the Edit menu description to contain \"Edit\", got {edit_display:?}"
2787 );
2788 assert_eq!(no_args_display, "Menu Open");
2792 }
2793
2794 #[test]
2795 fn test_format_action_word_end_actions_are_localized() {
2796 crate::i18n::set_locale("en");
2801
2802 let move_desc = KeybindingResolver::format_action(&Action::MoveWordEnd);
2803 assert_ne!(
2804 move_desc, "action.move_word_end",
2805 "MoveWordEnd should resolve to a translated description"
2806 );
2807 let select_desc = KeybindingResolver::format_action(&Action::SelectWordEnd);
2808 assert_ne!(
2809 select_desc, "action.select_word_end",
2810 "SelectWordEnd should resolve to a translated description"
2811 );
2812
2813 assert_eq!(
2815 KeybindingResolver::format_action(&Action::ViMoveWordEnd),
2816 move_desc,
2817 );
2818 assert_eq!(
2819 KeybindingResolver::format_action(&Action::ViSelectWordEnd),
2820 select_desc,
2821 );
2822 }
2823
2824 #[test]
2825 fn test_qualify_and_unqualify_roundtrip_menu_open() {
2826 let mut args = HashMap::new();
2827 args.insert(
2828 "name".to_string(),
2829 serde_json::Value::String("File".to_string()),
2830 );
2831
2832 let qualified = Action::qualify_action("menu_open", &args);
2833 assert_eq!(qualified, "menu_open:File");
2834
2835 let (bare, parsed_args) = Action::unqualify_action(&qualified);
2836 assert_eq!(bare, "menu_open");
2837 assert_eq!(
2838 parsed_args.get("name").and_then(|v| v.as_str()),
2839 Some("File")
2840 );
2841 }
2842
2843 #[test]
2844 fn test_qualify_action_passthrough_for_unparameterised() {
2845 let args = HashMap::new();
2847 assert_eq!(Action::qualify_action("save", &args), "save");
2848 let (bare, parsed) = Action::unqualify_action("save");
2849 assert_eq!(bare, "save");
2850 assert!(parsed.is_empty());
2851 }
2852
2853 #[test]
2854 fn test_qualify_action_no_suffix_when_arg_missing() {
2855 let args = HashMap::new();
2858 assert_eq!(Action::qualify_action("menu_open", &args), "menu_open");
2859 }
2860
2861 #[test]
2862 fn test_unqualify_action_ignores_colon_on_unknown_action() {
2863 let (bare, parsed) = Action::unqualify_action("my_plugin:action_with:colons");
2866 assert_eq!(bare, "my_plugin:action_with:colons");
2867 assert!(parsed.is_empty());
2868 }
2869
2870 #[test]
2871 fn test_to_qualified_action_str_for_menu_open() {
2872 let action = Action::MenuOpen("Edit".to_string());
2873 assert_eq!(action.to_qualified_action_str(), "menu_open:Edit");
2874 }
2875
2876 #[test]
2877 fn test_resolve_basic() {
2878 let config = Config::default();
2879 let resolver = KeybindingResolver::new(&config);
2880
2881 let event = KeyEvent::new(KeyCode::Left, KeyModifiers::empty());
2882 assert_eq!(
2883 resolver.resolve(&event, KeyContext::Normal),
2884 Action::MoveLeft
2885 );
2886
2887 let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
2888 assert_eq!(
2889 resolver.resolve(&event, KeyContext::Normal),
2890 Action::InsertChar('a')
2891 );
2892 }
2893
2894 #[test]
2901 fn test_panel_mode_passthrough_for_ui_actions() {
2902 let config = Config::default();
2903 let resolver = KeybindingResolver::new(&config);
2904 let mode_ctx = KeyContext::Mode("search-replace-list".to_string());
2905
2906 let alt_close = KeyEvent::new(KeyCode::Char(']'), KeyModifiers::ALT);
2908 assert_eq!(
2909 resolver.resolve(&alt_close, mode_ctx.clone()),
2910 Action::NextSplit,
2911 "Alt+] should fall through to next_split inside a panel mode"
2912 );
2913
2914 let alt_open = KeyEvent::new(KeyCode::Char('['), KeyModifiers::ALT);
2916 assert_eq!(
2917 resolver.resolve(&alt_open, mode_ctx.clone()),
2918 Action::PrevSplit,
2919 "Alt+[ should fall through to prev_split inside a panel mode"
2920 );
2921
2922 let ctrl_s = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL);
2926 assert_eq!(
2927 resolver.resolve(&ctrl_s, mode_ctx.clone()),
2928 Action::Save,
2929 "Ctrl+S should still save while a panel mode is active"
2930 );
2931
2932 let ctrl_d = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL);
2936 assert_ne!(
2937 resolver.resolve(&ctrl_d, mode_ctx),
2938 Action::AddCursorNextMatch,
2939 "Ctrl+D (add cursor next match) must not pass through to a panel mode"
2940 );
2941 }
2942
2943 #[test]
2944 fn test_shift_backspace_matches_backspace() {
2945 let config = Config::default();
2949 let resolver = KeybindingResolver::new(&config);
2950
2951 let backspace = KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty());
2952 let shift_backspace = KeyEvent::new(KeyCode::Backspace, KeyModifiers::SHIFT);
2953
2954 assert_eq!(
2956 resolver.resolve(&backspace, KeyContext::Normal),
2957 Action::DeleteBackward,
2958 "Backspace should resolve to DeleteBackward in Normal context"
2959 );
2960 assert_eq!(
2961 resolver.resolve(&shift_backspace, KeyContext::Normal),
2962 Action::DeleteBackward,
2963 "Shift+Backspace should resolve to DeleteBackward (same as Backspace) in Normal context"
2964 );
2965
2966 assert_eq!(
2968 resolver.resolve(&backspace, KeyContext::Prompt),
2969 Action::PromptBackspace,
2970 "Backspace should resolve to PromptBackspace in Prompt context"
2971 );
2972 assert_eq!(
2973 resolver.resolve(&shift_backspace, KeyContext::Prompt),
2974 Action::PromptBackspace,
2975 "Shift+Backspace should resolve to PromptBackspace (same as Backspace) in Prompt context"
2976 );
2977
2978 assert_eq!(
2980 resolver.resolve(&backspace, KeyContext::FileExplorer),
2981 Action::FileExplorerSearchBackspace,
2982 "Backspace should resolve to FileExplorerSearchBackspace in FileExplorer context"
2983 );
2984 assert_eq!(
2985 resolver.resolve(&shift_backspace, KeyContext::FileExplorer),
2986 Action::FileExplorerSearchBackspace,
2987 "Shift+Backspace should resolve to FileExplorerSearchBackspace (same as Backspace) in FileExplorer context"
2988 );
2989 }
2990
2991 #[test]
2992 fn test_file_explorer_ui_fallthrough() {
2993 let config = Config::default();
3000 let resolver = KeybindingResolver::new(&config);
3001
3002 let cases = [
3003 (
3004 KeyCode::PageUp,
3005 KeyModifiers::CONTROL,
3006 Action::PrevBuffer,
3007 "Ctrl+PageUp -> prev_buffer",
3008 ),
3009 (
3010 KeyCode::PageDown,
3011 KeyModifiers::CONTROL,
3012 Action::NextBuffer,
3013 "Ctrl+PageDown -> next_buffer",
3014 ),
3015 (
3016 KeyCode::PageUp,
3017 KeyModifiers::ALT,
3018 Action::ScrollTabsLeft,
3019 "Alt+PageUp -> scroll_tabs_left",
3020 ),
3021 (
3022 KeyCode::PageDown,
3023 KeyModifiers::ALT,
3024 Action::ScrollTabsRight,
3025 "Alt+PageDown -> scroll_tabs_right",
3026 ),
3027 (
3028 KeyCode::Char('w'),
3029 KeyModifiers::ALT,
3030 Action::CloseTab,
3031 "Alt+W -> close_tab",
3032 ),
3033 ];
3034
3035 for (code, mods, expected, label) in cases {
3036 let event = KeyEvent::new(code, mods);
3037 assert_eq!(
3038 resolver.resolve(&event, KeyContext::FileExplorer),
3039 expected,
3040 "{label} should fall through from FileExplorer to Normal"
3041 );
3042 }
3043
3044 let up = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
3049 assert_eq!(
3050 resolver.resolve(&up, KeyContext::FileExplorer),
3051 Action::FileExplorerUp,
3052 "Up must continue to navigate the explorer, not move the cursor"
3053 );
3054
3055 let plain_d = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::empty());
3060 assert_eq!(
3061 resolver.resolve(&plain_d, KeyContext::FileExplorer),
3062 Action::InsertChar('d'),
3063 "Plain 'd' must remain text input for explorer search-as-you-type"
3064 );
3065 }
3066
3067 #[test]
3068 fn test_action_from_str() {
3069 let args = HashMap::new();
3070 assert_eq!(Action::from_str("move_left", &args), Some(Action::MoveLeft));
3071 assert_eq!(Action::from_str("save", &args), Some(Action::Save));
3072 assert_eq!(
3074 Action::from_str("unknown", &args),
3075 Some(Action::PluginAction("unknown".to_string()))
3076 );
3077
3078 assert_eq!(
3080 Action::from_str("keyboard_shortcuts", &args),
3081 Some(Action::ShowKeyboardShortcuts)
3082 );
3083 assert_eq!(
3084 Action::from_str("prompt_confirm", &args),
3085 Some(Action::PromptConfirm)
3086 );
3087 assert_eq!(
3088 Action::from_str("popup_cancel", &args),
3089 Some(Action::PopupCancel)
3090 );
3091
3092 assert_eq!(
3094 Action::from_str("calibrate_input", &args),
3095 Some(Action::CalibrateInput)
3096 );
3097 }
3098
3099 #[test]
3100 fn test_key_context_from_when_clause() {
3101 assert_eq!(
3102 KeyContext::from_when_clause("normal"),
3103 Some(KeyContext::Normal)
3104 );
3105 assert_eq!(
3106 KeyContext::from_when_clause("prompt"),
3107 Some(KeyContext::Prompt)
3108 );
3109 assert_eq!(
3110 KeyContext::from_when_clause("popup"),
3111 Some(KeyContext::Popup)
3112 );
3113 assert_eq!(KeyContext::from_when_clause("help"), None);
3114 assert_eq!(KeyContext::from_when_clause(" help "), None); assert_eq!(KeyContext::from_when_clause("unknown"), None);
3116 assert_eq!(KeyContext::from_when_clause(""), None);
3117 }
3118
3119 #[test]
3120 fn test_key_context_to_when_clause() {
3121 assert_eq!(KeyContext::Normal.to_when_clause(), "normal");
3122 assert_eq!(KeyContext::Prompt.to_when_clause(), "prompt");
3123 assert_eq!(KeyContext::Popup.to_when_clause(), "popup");
3124 }
3125
3126 #[test]
3127 fn test_context_specific_bindings() {
3128 let config = Config::default();
3129 let resolver = KeybindingResolver::new(&config);
3130
3131 let enter_event = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
3133 assert_eq!(
3134 resolver.resolve(&enter_event, KeyContext::Prompt),
3135 Action::PromptConfirm
3136 );
3137 assert_eq!(
3138 resolver.resolve(&enter_event, KeyContext::Normal),
3139 Action::InsertNewline
3140 );
3141
3142 let up_event = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
3144 assert_eq!(
3145 resolver.resolve(&up_event, KeyContext::Popup),
3146 Action::PopupSelectPrev
3147 );
3148 assert_eq!(
3149 resolver.resolve(&up_event, KeyContext::Normal),
3150 Action::MoveUp
3151 );
3152 }
3153
3154 #[test]
3155 fn test_context_fallback_to_normal() {
3156 let config = Config::default();
3157 let resolver = KeybindingResolver::new(&config);
3158
3159 let save_event = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL);
3161 assert_eq!(
3162 resolver.resolve(&save_event, KeyContext::Normal),
3163 Action::Save
3164 );
3165 assert_eq!(
3166 resolver.resolve(&save_event, KeyContext::Popup),
3167 Action::Save
3168 );
3169 }
3171
3172 #[test]
3173 fn test_context_priority_resolution() {
3174 use crate::config::Keybinding;
3175
3176 let mut config = Config::default();
3178 config.keybindings.push(Keybinding {
3179 key: "esc".to_string(),
3180 modifiers: vec![],
3181 keys: vec![],
3182 action: "quit".to_string(), args: HashMap::new(),
3184 when: Some("popup".to_string()),
3185 });
3186
3187 let resolver = KeybindingResolver::new(&config);
3188 let esc_event = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
3189
3190 assert_eq!(
3192 resolver.resolve(&esc_event, KeyContext::Popup),
3193 Action::Quit
3194 );
3195
3196 assert_eq!(
3198 resolver.resolve(&esc_event, KeyContext::Normal),
3199 Action::RemoveSecondaryCursors
3200 );
3201 }
3202
3203 #[test]
3204 fn test_character_input_in_contexts() {
3205 let config = Config::default();
3206 let resolver = KeybindingResolver::new(&config);
3207
3208 let char_event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
3209
3210 assert_eq!(
3212 resolver.resolve(&char_event, KeyContext::Normal),
3213 Action::InsertChar('a')
3214 );
3215 assert_eq!(
3216 resolver.resolve(&char_event, KeyContext::Prompt),
3217 Action::InsertChar('a')
3218 );
3219
3220 assert_eq!(
3222 resolver.resolve(&char_event, KeyContext::Popup),
3223 Action::None
3224 );
3225 }
3226
3227 #[test]
3228 fn test_custom_keybinding_loading() {
3229 use crate::config::Keybinding;
3230
3231 let mut config = Config::default();
3232
3233 config.keybindings.push(Keybinding {
3235 key: "f".to_string(),
3236 modifiers: vec!["ctrl".to_string()],
3237 keys: vec![],
3238 action: "command_palette".to_string(),
3239 args: HashMap::new(),
3240 when: None, });
3242
3243 let resolver = KeybindingResolver::new(&config);
3244
3245 let ctrl_f = KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL);
3247 assert_eq!(
3248 resolver.resolve(&ctrl_f, KeyContext::Normal),
3249 Action::CommandPalette
3250 );
3251
3252 let ctrl_k = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL);
3254 assert_eq!(
3255 resolver.resolve(&ctrl_k, KeyContext::Prompt),
3256 Action::PromptDeleteToLineEnd
3257 );
3258 assert_eq!(
3259 resolver.resolve(&ctrl_k, KeyContext::Normal),
3260 Action::DeleteToLineEnd
3261 );
3262 }
3263
3264 #[test]
3265 fn test_all_context_default_bindings_exist() {
3266 let config = Config::default();
3267 let resolver = KeybindingResolver::new(&config);
3268
3269 assert!(resolver.default_bindings.contains_key(&KeyContext::Normal));
3271 assert!(resolver.default_bindings.contains_key(&KeyContext::Prompt));
3272 assert!(resolver.default_bindings.contains_key(&KeyContext::Popup));
3273 assert!(resolver
3274 .default_bindings
3275 .contains_key(&KeyContext::FileExplorer));
3276 assert!(resolver.default_bindings.contains_key(&KeyContext::Menu));
3277
3278 assert!(!resolver.default_bindings[&KeyContext::Normal].is_empty());
3280 assert!(!resolver.default_bindings[&KeyContext::Prompt].is_empty());
3281 assert!(!resolver.default_bindings[&KeyContext::Popup].is_empty());
3282 assert!(!resolver.default_bindings[&KeyContext::FileExplorer].is_empty());
3283 assert!(!resolver.default_bindings[&KeyContext::Menu].is_empty());
3284 }
3285
3286 #[test]
3297 fn test_all_builtin_keymaps_have_valid_action_names() {
3298 let known_actions: std::collections::HashSet<String> =
3299 Action::all_action_names().into_iter().collect();
3300
3301 const ALLOWED_PLUGIN_ACTIONS_IN_DEFAULTS: &[&str] = &["start_search_replace"];
3302
3303 let config = Config::default();
3304
3305 for map_name in crate::config::KeybindingMapName::BUILTIN_OPTIONS {
3306 let bindings = config.resolve_keymap(map_name);
3307 for binding in &bindings {
3308 let is_known_builtin = known_actions.contains(&binding.action);
3309 let is_allowed_plugin =
3310 ALLOWED_PLUGIN_ACTIONS_IN_DEFAULTS.contains(&binding.action.as_str());
3311 assert!(
3312 is_known_builtin || is_allowed_plugin,
3313 "Keymap '{}' contains unknown action '{}' (key: '{}', when: {:?}). \
3314 This will be treated as a plugin action at runtime. \
3315 Check for typos in the keymap JSON file, or add the action to \
3316 ALLOWED_PLUGIN_ACTIONS_IN_DEFAULTS if it's an intentional \
3317 plugin-action binding.",
3318 map_name,
3319 binding.action,
3320 binding.key,
3321 binding.when,
3322 );
3323 }
3324 }
3325 }
3326
3327 #[test]
3328 fn test_resolve_determinism() {
3329 let config = Config::default();
3331 let resolver = KeybindingResolver::new(&config);
3332
3333 let test_cases = vec![
3334 (KeyCode::Left, KeyModifiers::empty(), KeyContext::Normal),
3335 (
3336 KeyCode::Esc,
3337 KeyModifiers::empty(),
3338 KeyContext::FileExplorer,
3339 ),
3340 (KeyCode::Enter, KeyModifiers::empty(), KeyContext::Prompt),
3341 (KeyCode::Down, KeyModifiers::empty(), KeyContext::Popup),
3342 ];
3343
3344 for (key_code, modifiers, context) in test_cases {
3345 let event = KeyEvent::new(key_code, modifiers);
3346 let action1 = resolver.resolve(&event, context.clone());
3347 let action2 = resolver.resolve(&event, context.clone());
3348 let action3 = resolver.resolve(&event, context);
3349
3350 assert_eq!(action1, action2, "Resolve should be deterministic");
3351 assert_eq!(action2, action3, "Resolve should be deterministic");
3352 }
3353 }
3354
3355 #[test]
3356 fn test_modifier_combinations() {
3357 let config = Config::default();
3358 let resolver = KeybindingResolver::new(&config);
3359
3360 let char_s = KeyCode::Char('s');
3362
3363 let no_mod = KeyEvent::new(char_s, KeyModifiers::empty());
3364 let ctrl = KeyEvent::new(char_s, KeyModifiers::CONTROL);
3365 let shift = KeyEvent::new(char_s, KeyModifiers::SHIFT);
3366 let ctrl_shift = KeyEvent::new(char_s, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
3367
3368 let action_no_mod = resolver.resolve(&no_mod, KeyContext::Normal);
3369 let action_ctrl = resolver.resolve(&ctrl, KeyContext::Normal);
3370 let action_shift = resolver.resolve(&shift, KeyContext::Normal);
3371 let action_ctrl_shift = resolver.resolve(&ctrl_shift, KeyContext::Normal);
3372
3373 assert_eq!(action_no_mod, Action::InsertChar('s'));
3375 assert_eq!(action_ctrl, Action::Save);
3376 assert_eq!(action_shift, Action::InsertChar('s')); assert_eq!(action_ctrl_shift, Action::None);
3379 }
3380
3381 #[test]
3382 fn test_scroll_keybindings() {
3383 let config = Config::default();
3384 let resolver = KeybindingResolver::new(&config);
3385
3386 let ctrl_up = KeyEvent::new(KeyCode::Up, KeyModifiers::CONTROL);
3388 assert_eq!(
3389 resolver.resolve(&ctrl_up, KeyContext::Normal),
3390 Action::ScrollUp,
3391 "Ctrl+Up should resolve to ScrollUp"
3392 );
3393
3394 let ctrl_down = KeyEvent::new(KeyCode::Down, KeyModifiers::CONTROL);
3396 assert_eq!(
3397 resolver.resolve(&ctrl_down, KeyContext::Normal),
3398 Action::ScrollDown,
3399 "Ctrl+Down should resolve to ScrollDown"
3400 );
3401 }
3402
3403 #[test]
3404 fn test_lsp_completion_keybinding() {
3405 let config = Config::default();
3406 let resolver = KeybindingResolver::new(&config);
3407
3408 let ctrl_space = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL);
3410 assert_eq!(
3411 resolver.resolve(&ctrl_space, KeyContext::Normal),
3412 Action::LspCompletion,
3413 "Ctrl+Space should resolve to LspCompletion"
3414 );
3415 }
3416
3417 #[test]
3418 fn test_terminal_key_equivalents() {
3419 let ctrl = KeyModifiers::CONTROL;
3421
3422 let slash_equivs = terminal_key_equivalents(KeyCode::Char('/'), ctrl);
3424 assert_eq!(slash_equivs, vec![(KeyCode::Char('7'), ctrl)]);
3425
3426 let seven_equivs = terminal_key_equivalents(KeyCode::Char('7'), ctrl);
3427 assert_eq!(seven_equivs, vec![(KeyCode::Char('/'), ctrl)]);
3428
3429 let backspace_equivs = terminal_key_equivalents(KeyCode::Backspace, ctrl);
3431 assert_eq!(backspace_equivs, vec![(KeyCode::Char('h'), ctrl)]);
3432
3433 let h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl);
3434 assert_eq!(h_equivs, vec![(KeyCode::Backspace, ctrl)]);
3435
3436 let a_equivs = terminal_key_equivalents(KeyCode::Char('a'), ctrl);
3438 assert!(a_equivs.is_empty());
3439
3440 let slash_no_ctrl = terminal_key_equivalents(KeyCode::Char('/'), KeyModifiers::empty());
3442 assert!(slash_no_ctrl.is_empty());
3443 }
3444
3445 #[test]
3446 fn test_terminal_key_equivalents_auto_binding() {
3447 let config = Config::default();
3448 let resolver = KeybindingResolver::new(&config);
3449
3450 let ctrl_slash = KeyEvent::new(KeyCode::Char('/'), KeyModifiers::CONTROL);
3452 let action_slash = resolver.resolve(&ctrl_slash, KeyContext::Normal);
3453 assert_eq!(
3454 action_slash,
3455 Action::ToggleComment,
3456 "Ctrl+/ should resolve to ToggleComment"
3457 );
3458
3459 let ctrl_7 = KeyEvent::new(KeyCode::Char('7'), KeyModifiers::CONTROL);
3461 let action_7 = resolver.resolve(&ctrl_7, KeyContext::Normal);
3462 assert_eq!(
3463 action_7,
3464 Action::ToggleComment,
3465 "Ctrl+7 should resolve to ToggleComment (terminal equivalent of Ctrl+/)"
3466 );
3467 }
3468
3469 #[test]
3470 fn test_terminal_key_equivalents_normalization() {
3471 let ctrl = KeyModifiers::CONTROL;
3476
3477 let slash_equivs = terminal_key_equivalents(KeyCode::Char('/'), ctrl);
3480 assert_eq!(
3481 slash_equivs,
3482 vec![(KeyCode::Char('7'), ctrl)],
3483 "Ctrl+/ should map to Ctrl+7"
3484 );
3485 let seven_equivs = terminal_key_equivalents(KeyCode::Char('7'), ctrl);
3486 assert_eq!(
3487 seven_equivs,
3488 vec![(KeyCode::Char('/'), ctrl)],
3489 "Ctrl+7 should map back to Ctrl+/"
3490 );
3491
3492 let backspace_equivs = terminal_key_equivalents(KeyCode::Backspace, ctrl);
3495 assert_eq!(
3496 backspace_equivs,
3497 vec![(KeyCode::Char('h'), ctrl)],
3498 "Ctrl+Backspace should map to Ctrl+H"
3499 );
3500 let h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl);
3501 assert_eq!(
3502 h_equivs,
3503 vec![(KeyCode::Backspace, ctrl)],
3504 "Ctrl+H should map back to Ctrl+Backspace"
3505 );
3506
3507 let space_equivs = terminal_key_equivalents(KeyCode::Char(' '), ctrl);
3510 assert_eq!(
3511 space_equivs,
3512 vec![(KeyCode::Char('@'), ctrl)],
3513 "Ctrl+Space should map to Ctrl+@"
3514 );
3515 let at_equivs = terminal_key_equivalents(KeyCode::Char('@'), ctrl);
3516 assert_eq!(
3517 at_equivs,
3518 vec![(KeyCode::Char(' '), ctrl)],
3519 "Ctrl+@ should map back to Ctrl+Space"
3520 );
3521
3522 let minus_equivs = terminal_key_equivalents(KeyCode::Char('-'), ctrl);
3525 assert_eq!(
3526 minus_equivs,
3527 vec![(KeyCode::Char('_'), ctrl)],
3528 "Ctrl+- should map to Ctrl+_"
3529 );
3530 let underscore_equivs = terminal_key_equivalents(KeyCode::Char('_'), ctrl);
3531 assert_eq!(
3532 underscore_equivs,
3533 vec![(KeyCode::Char('-'), ctrl)],
3534 "Ctrl+_ should map back to Ctrl+-"
3535 );
3536
3537 assert!(
3539 terminal_key_equivalents(KeyCode::Char('a'), ctrl).is_empty(),
3540 "Ctrl+A should have no terminal equivalents"
3541 );
3542 assert!(
3543 terminal_key_equivalents(KeyCode::Char('z'), ctrl).is_empty(),
3544 "Ctrl+Z should have no terminal equivalents"
3545 );
3546 assert!(
3547 terminal_key_equivalents(KeyCode::Enter, ctrl).is_empty(),
3548 "Ctrl+Enter should have no terminal equivalents"
3549 );
3550
3551 assert!(
3553 terminal_key_equivalents(KeyCode::Char('/'), KeyModifiers::empty()).is_empty(),
3554 "/ without Ctrl should have no equivalents"
3555 );
3556 assert!(
3557 terminal_key_equivalents(KeyCode::Char('7'), KeyModifiers::SHIFT).is_empty(),
3558 "Shift+7 should have no equivalents"
3559 );
3560 assert!(
3561 terminal_key_equivalents(KeyCode::Char('h'), KeyModifiers::ALT).is_empty(),
3562 "Alt+H should have no equivalents"
3563 );
3564
3565 let ctrl_shift = KeyModifiers::CONTROL | KeyModifiers::SHIFT;
3568 let ctrl_shift_h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl_shift);
3569 assert!(
3570 ctrl_shift_h_equivs.is_empty(),
3571 "Ctrl+Shift+H should NOT map to Ctrl+Shift+Backspace"
3572 );
3573 }
3574
3575 #[test]
3576 fn test_no_duplicate_keybindings_in_keymaps() {
3577 use std::collections::HashMap;
3580
3581 let keymaps: &[(&str, &str)] = &[
3582 ("default", include_str!("../../keymaps/default.json")),
3583 ("macos", include_str!("../../keymaps/macos.json")),
3584 ];
3585
3586 for (keymap_name, json_content) in keymaps {
3587 let keymap: crate::config::KeymapConfig = serde_json::from_str(json_content)
3588 .unwrap_or_else(|e| panic!("Failed to parse keymap '{}': {}", keymap_name, e));
3589
3590 let mut seen: HashMap<(String, Vec<String>, String), String> = HashMap::new();
3592 let mut duplicates: Vec<String> = Vec::new();
3593
3594 for binding in &keymap.bindings {
3595 let when = binding.when.clone().unwrap_or_default();
3596 let key_id = (binding.key.clone(), binding.modifiers.clone(), when.clone());
3597
3598 if let Some(existing_action) = seen.get(&key_id) {
3599 duplicates.push(format!(
3600 "Duplicate in '{}': key='{}', modifiers={:?}, when='{}' -> '{}' vs '{}'",
3601 keymap_name,
3602 binding.key,
3603 binding.modifiers,
3604 when,
3605 existing_action,
3606 binding.action
3607 ));
3608 } else {
3609 seen.insert(key_id, binding.action.clone());
3610 }
3611 }
3612
3613 assert!(
3614 duplicates.is_empty(),
3615 "Found duplicate keybindings:\n{}",
3616 duplicates.join("\n")
3617 );
3618 }
3619 }
3620}