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