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 MoveToParagraphUp,
345 MoveToParagraphDown,
346 MovePageUp,
347 MovePageDown,
348 MoveDocumentStart,
349 MoveDocumentEnd,
350
351 SelectLeft,
353 SelectRight,
354 SelectUp,
355 SelectDown,
356 SelectToParagraphUp, SelectToParagraphDown, SelectWordLeft,
359 SelectWordRight,
360 SelectWordEnd, ViSelectWordEnd, SelectLineStart,
363 SelectLineEnd,
364 SelectDocumentStart,
365 SelectDocumentEnd,
366 SelectPageUp,
367 SelectPageDown,
368 SelectAll,
369 SelectWord,
370 SelectLine,
371 ExpandSelection,
372
373 BlockSelectLeft,
375 BlockSelectRight,
376 BlockSelectUp,
377 BlockSelectDown,
378
379 DeleteBackward,
381 DeleteForward,
382 DeleteWordBackward,
383 DeleteWordForward,
384 DeleteLine,
385 DeleteToLineEnd,
386 DeleteToLineStart,
387 DeleteViWordEnd, TransposeChars,
389 OpenLine,
390 DuplicateLine,
391
392 Recenter,
394
395 SetMark,
397
398 Copy,
400 CopyWithTheme(String),
401 Cut,
402 Paste,
403 CopyFilePath,
405 CopyRelativeFilePath,
408
409 YankWordForward,
411 YankWordBackward,
412 YankToLineEnd,
413 YankToLineStart,
414 YankViWordEnd, AddCursorAbove,
418 AddCursorBelow,
419 AddCursorNextMatch,
420 AddCursorsToLineEnds,
421 RemoveSecondaryCursors,
422
423 Save,
425 SaveAs,
426 Open,
427 SwitchProject,
428 New,
429 Close,
430 CloseTab,
431 Quit,
432 ForceQuit,
433 Detach,
434 Revert,
435 ToggleAutoRevert,
436 FormatBuffer,
437 TrimTrailingWhitespace,
438 EnsureFinalNewline,
439
440 GotoLine,
442 ScanLineIndex,
443 GoToMatchingBracket,
444 JumpToNextError,
445 JumpToPreviousError,
446
447 SmartHome,
449 DedentSelection,
450 ToggleComment,
451 DabbrevExpand,
452 ToggleFold,
453
454 SetBookmark(char),
456 JumpToBookmark(char),
457 ClearBookmark(char),
458 ListBookmarks,
459
460 ToggleSearchCaseSensitive,
462 ToggleSearchWholeWord,
463 ToggleSearchRegex,
464 ToggleSearchConfirmEach,
465
466 StartMacroRecording,
468 StopMacroRecording,
469 PlayMacro(char),
470 ToggleMacroRecording(char),
471 ShowMacro(char),
472 ListMacros,
473 PromptRecordMacro,
474 PromptPlayMacro,
475 PlayLastMacro,
476
477 PromptSetBookmark,
479 PromptJumpToBookmark,
480
481 Undo,
483 Redo,
484
485 ScrollUp,
487 ScrollDown,
488 ShowHelp,
489 ShowKeyboardShortcuts,
490 ShowWarnings,
491 ShowStatusLog,
492 ShowLspStatus,
493 ShowRemoteIndicatorMenu,
494 ClearWarnings,
495 CommandPalette, QuickOpen,
498 QuickOpenBuffers,
500 QuickOpenFiles,
502 OpenLiveGrep,
504 ResumeLiveGrep,
506 ToggleUtilityDock,
510 OpenTerminalInDock,
513 CycleLiveGrepProvider,
518 ToggleLineWrap,
519 ToggleCurrentLineHighlight,
520 ToggleReadOnly,
521 TogglePageView,
522 SetPageWidth,
523 InspectThemeAtCursor,
524 SelectTheme,
525 SelectKeybindingMap,
526 SelectCursorStyle,
527 SelectLocale,
528
529 NextBuffer,
531 PrevBuffer,
532 SwitchToPreviousTab,
533 SwitchToTabByName,
534
535 ScrollTabsLeft,
537 ScrollTabsRight,
538
539 NavigateBack,
541 NavigateForward,
542
543 SplitHorizontal,
545 SplitVertical,
546 CloseSplit,
547 NextSplit,
548 PrevSplit,
549 NextWindow,
550 PrevWindow,
551 IncreaseSplitSize,
552 DecreaseSplitSize,
553 ToggleMaximizeSplit,
554
555 PromptConfirm,
557 PromptConfirmWithText(String),
559 PromptCancel,
560 PromptBackspace,
561 PromptDelete,
562 PromptMoveLeft,
563 PromptMoveRight,
564 PromptMoveStart,
565 PromptMoveEnd,
566 PromptSelectPrev,
567 PromptSelectNext,
568 PromptPageUp,
569 PromptPageDown,
570 PromptAcceptSuggestion,
571 PromptMoveWordLeft,
572 PromptMoveWordRight,
573 PromptDeleteWordForward,
575 PromptDeleteWordBackward,
576 PromptDeleteToLineEnd,
577 PromptCopy,
578 PromptCut,
579 PromptPaste,
580 PromptMoveLeftSelecting,
582 PromptMoveRightSelecting,
583 PromptMoveHomeSelecting,
584 PromptMoveEndSelecting,
585 PromptSelectWordLeft,
586 PromptSelectWordRight,
587 PromptSelectAll,
588
589 FileBrowserToggleHidden,
591 FileBrowserToggleDetectEncoding,
592
593 PopupSelectNext,
595 PopupSelectPrev,
596 PopupPageUp,
597 PopupPageDown,
598 PopupConfirm,
599 PopupCancel,
600 PopupFocus,
605
606 CompletionAccept,
608 CompletionDismiss,
609
610 ToggleFileExplorer,
612 ToggleFileExplorerSide,
614 ToggleMenuBar,
616 ToggleTabBar,
618 ToggleStatusBar,
620 TogglePromptLine,
622 ToggleVerticalScrollbar,
624 ToggleHorizontalScrollbar,
625 FocusFileExplorer,
626 FocusEditor,
627 FileExplorerUp,
628 FileExplorerDown,
629 FileExplorerPageUp,
630 FileExplorerPageDown,
631 FileExplorerExpand,
632 FileExplorerCollapse,
633 FileExplorerOpen,
634 FileExplorerRefresh,
635 FileExplorerNewFile,
636 FileExplorerNewDirectory,
637 FileExplorerDelete,
638 FileExplorerRename,
639 FileExplorerToggleHidden,
640 FileExplorerToggleGitignored,
641 FileExplorerSearchClear,
642 FileExplorerSearchBackspace,
643 FileExplorerCopy,
644 FileExplorerCut,
645 FileExplorerPaste,
646 FileExplorerDuplicate,
647 FileExplorerCopyFullPath,
648 FileExplorerCopyRelativePath,
649 FileExplorerExtendSelectionUp,
650 FileExplorerExtendSelectionDown,
651 FileExplorerToggleSelect,
652 FileExplorerSelectAll,
653
654 LspCompletion,
656 LspGotoDefinition,
657 LspReferences,
658 LspRename,
659 LspHover,
660 LspSignatureHelp,
661 LspCodeActions,
662 LspRestart,
663 LspStop,
664 LspToggleForBuffer,
665 ToggleInlayHints,
666 ToggleMouseHover,
667
668 ToggleLineNumbers,
670 ToggleScrollSync,
671 ToggleMouseCapture,
672 ToggleDebugHighlights, SetBackground,
674 SetBackgroundBlend,
675
676 SetTabSize,
678 SetLineEnding,
679 SetEncoding,
680 ReloadWithEncoding,
681 SetLanguage,
682 ToggleIndentationStyle,
683 ToggleTabIndicators,
684 ToggleWhitespaceIndicators,
685 ResetBufferSettings,
686 AddRuler,
687 RemoveRuler,
688
689 DumpConfig,
691
692 RedrawScreen,
694
695 Search,
697 FindInSelection,
698 FindNext,
699 FindPrevious,
700 FindSelectionNext, FindSelectionPrevious, Replace,
703 QueryReplace, MenuActivate, MenuClose, MenuLeft, MenuRight, MenuUp, MenuDown, MenuExecute, MenuOpen(String), SwitchKeybindingMap(String), PluginAction(String),
720
721 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,
784}
785
786macro_rules! define_action_str_mapping {
799 (
800 $args_name:ident;
801 simple { $($s_name:literal => $s_variant:ident),* $(,)? }
802 alias { $($a_name:literal => $a_variant:ident),* $(,)? }
803 with_char { $($c_name:literal => $c_variant:ident),* $(,)? }
804 custom { $($x_name:literal => $x_variant:ident : $x_body:expr),* $(,)? }
805 ) => {
806 pub fn from_str(s: &str, $args_name: &HashMap<String, serde_json::Value>) -> Option<Self> {
808 Some(match s {
809 $($s_name => Self::$s_variant,)*
810 $($a_name => Self::$a_variant,)*
811 $($c_name => return Self::with_char($args_name, Self::$c_variant),)*
812 $($x_name => $x_body,)*
813 _ => Self::PluginAction(s.to_string()),
816 })
817 }
818
819 pub fn to_action_str(&self) -> String {
822 match self {
823 $(Self::$s_variant => $s_name.to_string(),)*
824 $(Self::$c_variant(_) => $c_name.to_string(),)*
825 $(Self::$x_variant(_) => $x_name.to_string(),)*
826 Self::PluginAction(name) => name.clone(),
827 }
828 }
829
830 pub fn all_action_names() -> Vec<String> {
833 let mut names = vec![
834 $($s_name.to_string(),)*
835 $($a_name.to_string(),)*
836 $($c_name.to_string(),)*
837 $($x_name.to_string(),)*
838 ];
839 names.sort();
840 names
841 }
842 };
843}
844
845impl Action {
846 fn with_char(
847 args: &HashMap<String, serde_json::Value>,
848 make_action: impl FnOnce(char) -> Self,
849 ) -> Option<Self> {
850 if let Some(serde_json::Value::String(value)) = args.get("char") {
851 value.chars().next().map(make_action)
852 } else {
853 None
854 }
855 }
856
857 define_action_str_mapping! {
858 args;
859 simple {
860 "insert_newline" => InsertNewline,
861 "insert_tab" => InsertTab,
862
863 "move_left" => MoveLeft,
864 "move_right" => MoveRight,
865 "move_up" => MoveUp,
866 "move_down" => MoveDown,
867 "move_word_left" => MoveWordLeft,
868 "move_word_right" => MoveWordRight,
869 "move_word_end" => MoveWordEnd,
870 "vi_move_word_end" => ViMoveWordEnd,
871 "move_left_in_line" => MoveLeftInLine,
872 "move_right_in_line" => MoveRightInLine,
873 "move_line_start" => MoveLineStart,
874 "move_line_end" => MoveLineEnd,
875 "move_line_up" => MoveLineUp,
876 "move_line_down" => MoveLineDown,
877 "move_page_up" => MovePageUp,
878 "move_page_down" => MovePageDown,
879 "move_document_start" => MoveDocumentStart,
880 "move_document_end" => MoveDocumentEnd,
881 "move_to_paragraph_up" => MoveToParagraphUp,
882 "move_to_paragraph_down" => MoveToParagraphDown,
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 "toggle_utility_dock" => ToggleUtilityDock,
1005 "open_terminal_in_dock" => OpenTerminalInDock,
1006 "cycle_live_grep_provider" => CycleLiveGrepProvider,
1007 "toggle_line_wrap" => ToggleLineWrap,
1008 "toggle_current_line_highlight" => ToggleCurrentLineHighlight,
1009 "toggle_read_only" => ToggleReadOnly,
1010 "toggle_page_view" => TogglePageView,
1011 "set_page_width" => SetPageWidth,
1012
1013 "next_buffer" => NextBuffer,
1014 "prev_buffer" => PrevBuffer,
1015 "switch_to_previous_tab" => SwitchToPreviousTab,
1016 "switch_to_tab_by_name" => SwitchToTabByName,
1017 "scroll_tabs_left" => ScrollTabsLeft,
1018 "scroll_tabs_right" => ScrollTabsRight,
1019
1020 "navigate_back" => NavigateBack,
1021 "navigate_forward" => NavigateForward,
1022
1023 "split_horizontal" => SplitHorizontal,
1024 "split_vertical" => SplitVertical,
1025 "close_split" => CloseSplit,
1026 "next_split" => NextSplit,
1027 "prev_split" => PrevSplit,
1028 "next_window" => NextWindow,
1029 "prev_window" => PrevWindow,
1030 "increase_split_size" => IncreaseSplitSize,
1031 "decrease_split_size" => DecreaseSplitSize,
1032 "toggle_maximize_split" => ToggleMaximizeSplit,
1033
1034 "prompt_confirm" => PromptConfirm,
1035 "prompt_cancel" => PromptCancel,
1036 "prompt_backspace" => PromptBackspace,
1037 "prompt_move_left" => PromptMoveLeft,
1038 "prompt_move_right" => PromptMoveRight,
1039 "prompt_move_start" => PromptMoveStart,
1040 "prompt_move_end" => PromptMoveEnd,
1041 "prompt_select_prev" => PromptSelectPrev,
1042 "prompt_select_next" => PromptSelectNext,
1043 "prompt_page_up" => PromptPageUp,
1044 "prompt_page_down" => PromptPageDown,
1045 "prompt_accept_suggestion" => PromptAcceptSuggestion,
1046 "prompt_delete_word_forward" => PromptDeleteWordForward,
1047 "prompt_delete_word_backward" => PromptDeleteWordBackward,
1048 "prompt_delete_to_line_end" => PromptDeleteToLineEnd,
1049 "prompt_copy" => PromptCopy,
1050 "prompt_cut" => PromptCut,
1051 "prompt_paste" => PromptPaste,
1052 "prompt_move_left_selecting" => PromptMoveLeftSelecting,
1053 "prompt_move_right_selecting" => PromptMoveRightSelecting,
1054 "prompt_move_home_selecting" => PromptMoveHomeSelecting,
1055 "prompt_move_end_selecting" => PromptMoveEndSelecting,
1056 "prompt_select_word_left" => PromptSelectWordLeft,
1057 "prompt_select_word_right" => PromptSelectWordRight,
1058 "prompt_select_all" => PromptSelectAll,
1059 "file_browser_toggle_hidden" => FileBrowserToggleHidden,
1060 "file_browser_toggle_detect_encoding" => FileBrowserToggleDetectEncoding,
1061 "prompt_move_word_left" => PromptMoveWordLeft,
1062 "prompt_move_word_right" => PromptMoveWordRight,
1063 "prompt_delete" => PromptDelete,
1064
1065 "popup_select_next" => PopupSelectNext,
1066 "popup_select_prev" => PopupSelectPrev,
1067 "popup_page_up" => PopupPageUp,
1068 "popup_page_down" => PopupPageDown,
1069 "popup_confirm" => PopupConfirm,
1070 "popup_cancel" => PopupCancel,
1071 "popup_focus" => PopupFocus,
1072
1073 "completion_accept" => CompletionAccept,
1074 "completion_dismiss" => CompletionDismiss,
1075
1076 "toggle_file_explorer" => ToggleFileExplorer,
1077 "toggle_file_explorer_side" => ToggleFileExplorerSide,
1078 "toggle_menu_bar" => ToggleMenuBar,
1079 "toggle_tab_bar" => ToggleTabBar,
1080 "toggle_status_bar" => ToggleStatusBar,
1081 "toggle_prompt_line" => TogglePromptLine,
1082 "toggle_vertical_scrollbar" => ToggleVerticalScrollbar,
1083 "toggle_horizontal_scrollbar" => ToggleHorizontalScrollbar,
1084 "focus_file_explorer" => FocusFileExplorer,
1085 "focus_editor" => FocusEditor,
1086 "file_explorer_up" => FileExplorerUp,
1087 "file_explorer_down" => FileExplorerDown,
1088 "file_explorer_page_up" => FileExplorerPageUp,
1089 "file_explorer_page_down" => FileExplorerPageDown,
1090 "file_explorer_expand" => FileExplorerExpand,
1091 "file_explorer_collapse" => FileExplorerCollapse,
1092 "file_explorer_open" => FileExplorerOpen,
1093 "file_explorer_refresh" => FileExplorerRefresh,
1094 "file_explorer_new_file" => FileExplorerNewFile,
1095 "file_explorer_new_directory" => FileExplorerNewDirectory,
1096 "file_explorer_delete" => FileExplorerDelete,
1097 "file_explorer_rename" => FileExplorerRename,
1098 "file_explorer_toggle_hidden" => FileExplorerToggleHidden,
1099 "file_explorer_toggle_gitignored" => FileExplorerToggleGitignored,
1100 "file_explorer_search_clear" => FileExplorerSearchClear,
1101 "file_explorer_search_backspace" => FileExplorerSearchBackspace,
1102 "file_explorer_copy" => FileExplorerCopy,
1103 "file_explorer_cut" => FileExplorerCut,
1104 "file_explorer_paste" => FileExplorerPaste,
1105 "file_explorer_duplicate" => FileExplorerDuplicate,
1106 "file_explorer_copy_full_path" => FileExplorerCopyFullPath,
1107 "file_explorer_copy_relative_path" => FileExplorerCopyRelativePath,
1108 "file_explorer_extend_selection_up" => FileExplorerExtendSelectionUp,
1109 "file_explorer_extend_selection_down" => FileExplorerExtendSelectionDown,
1110 "file_explorer_toggle_select" => FileExplorerToggleSelect,
1111 "file_explorer_select_all" => FileExplorerSelectAll,
1112
1113 "lsp_completion" => LspCompletion,
1114 "lsp_goto_definition" => LspGotoDefinition,
1115 "lsp_references" => LspReferences,
1116 "lsp_rename" => LspRename,
1117 "lsp_hover" => LspHover,
1118 "lsp_signature_help" => LspSignatureHelp,
1119 "lsp_code_actions" => LspCodeActions,
1120 "lsp_restart" => LspRestart,
1121 "lsp_stop" => LspStop,
1122 "lsp_toggle_for_buffer" => LspToggleForBuffer,
1123 "toggle_inlay_hints" => ToggleInlayHints,
1124 "toggle_mouse_hover" => ToggleMouseHover,
1125
1126 "toggle_line_numbers" => ToggleLineNumbers,
1127 "toggle_scroll_sync" => ToggleScrollSync,
1128 "toggle_mouse_capture" => ToggleMouseCapture,
1129 "toggle_debug_highlights" => ToggleDebugHighlights,
1130 "set_background" => SetBackground,
1131 "set_background_blend" => SetBackgroundBlend,
1132 "inspect_theme_at_cursor" => InspectThemeAtCursor,
1133 "select_theme" => SelectTheme,
1134 "select_keybinding_map" => SelectKeybindingMap,
1135 "select_cursor_style" => SelectCursorStyle,
1136 "select_locale" => SelectLocale,
1137
1138 "set_tab_size" => SetTabSize,
1139 "set_line_ending" => SetLineEnding,
1140 "set_encoding" => SetEncoding,
1141 "reload_with_encoding" => ReloadWithEncoding,
1142 "set_language" => SetLanguage,
1143 "toggle_indentation_style" => ToggleIndentationStyle,
1144 "toggle_tab_indicators" => ToggleTabIndicators,
1145 "toggle_whitespace_indicators" => ToggleWhitespaceIndicators,
1146 "reset_buffer_settings" => ResetBufferSettings,
1147 "add_ruler" => AddRuler,
1148 "remove_ruler" => RemoveRuler,
1149
1150 "dump_config" => DumpConfig,
1151 "redraw_screen" => RedrawScreen,
1152
1153 "search" => Search,
1154 "find_in_selection" => FindInSelection,
1155 "find_next" => FindNext,
1156 "find_previous" => FindPrevious,
1157 "find_selection_next" => FindSelectionNext,
1158 "find_selection_previous" => FindSelectionPrevious,
1159 "replace" => Replace,
1160 "query_replace" => QueryReplace,
1161
1162 "menu_activate" => MenuActivate,
1163 "menu_close" => MenuClose,
1164 "menu_left" => MenuLeft,
1165 "menu_right" => MenuRight,
1166 "menu_up" => MenuUp,
1167 "menu_down" => MenuDown,
1168 "menu_execute" => MenuExecute,
1169
1170 "open_terminal" => OpenTerminal,
1171 "close_terminal" => CloseTerminal,
1172 "focus_terminal" => FocusTerminal,
1173 "terminal_escape" => TerminalEscape,
1174 "toggle_keyboard_capture" => ToggleKeyboardCapture,
1175 "terminal_paste" => TerminalPaste,
1176
1177 "shell_command" => ShellCommand,
1178 "shell_command_replace" => ShellCommandReplace,
1179
1180 "to_upper_case" => ToUpperCase,
1181 "to_lower_case" => ToLowerCase,
1182 "toggle_case" => ToggleCase,
1183 "sort_lines" => SortLines,
1184
1185 "calibrate_input" => CalibrateInput,
1186 "event_debug" => EventDebug,
1187 "suspend_process" => SuspendProcess,
1188 "load_plugin_from_buffer" => LoadPluginFromBuffer,
1189 "init_reload" => InitReload,
1190 "init_edit" => InitEdit,
1191 "init_check" => InitCheck,
1192 "open_keybinding_editor" => OpenKeybindingEditor,
1193
1194 "composite_next_hunk" => CompositeNextHunk,
1195 "composite_prev_hunk" => CompositePrevHunk,
1196
1197 "workspace_trust_trust" => WorkspaceTrustTrust,
1198 "workspace_trust_restrict" => WorkspaceTrustRestrict,
1199 "workspace_trust_block" => WorkspaceTrustBlock,
1200 "workspace_trust_prompt" => WorkspaceTrustPrompt,
1201
1202 "noop" => None,
1203
1204 "open_settings" => OpenSettings,
1205 "close_settings" => CloseSettings,
1206 "settings_save" => SettingsSave,
1207 "settings_reset" => SettingsReset,
1208 "settings_toggle_focus" => SettingsToggleFocus,
1209 "settings_activate" => SettingsActivate,
1210 "settings_search" => SettingsSearch,
1211 "settings_help" => SettingsHelp,
1212 "settings_increment" => SettingsIncrement,
1213 "settings_decrement" => SettingsDecrement,
1214 "settings_inherit" => SettingsInherit,
1215 }
1216 alias {
1217 "toggle_compose_mode" => TogglePageView,
1218 "set_compose_width" => SetPageWidth,
1219 "none" => None,
1224 }
1225 with_char {
1226 "insert_char" => InsertChar,
1227 "set_bookmark" => SetBookmark,
1228 "jump_to_bookmark" => JumpToBookmark,
1229 "clear_bookmark" => ClearBookmark,
1230 "play_macro" => PlayMacro,
1231 "toggle_macro_recording" => ToggleMacroRecording,
1232 "show_macro" => ShowMacro,
1233 }
1234 custom {
1235 "copy_with_theme" => CopyWithTheme : {
1236 let theme = args.get("theme").and_then(|v| v.as_str()).unwrap_or("");
1238 Self::CopyWithTheme(theme.to_string())
1239 },
1240 "menu_open" => MenuOpen : {
1241 let name = args.get("name")?.as_str()?;
1242 Self::MenuOpen(name.to_string())
1243 },
1244 "switch_keybinding_map" => SwitchKeybindingMap : {
1245 let map_name = args.get("map")?.as_str()?;
1246 Self::SwitchKeybindingMap(map_name.to_string())
1247 },
1248 "prompt_confirm_with_text" => PromptConfirmWithText : {
1249 let text = args.get("text")?.as_str()?;
1250 Self::PromptConfirmWithText(text.to_string())
1251 },
1252 }
1253 }
1254
1255 pub fn variant_arg_key(bare_action: &str) -> Option<&'static str> {
1262 match bare_action {
1263 "menu_open" => Some("name"),
1264 "switch_keybinding_map" => Some("map"),
1265 _ => None,
1266 }
1267 }
1268
1269 pub fn qualify_action(bare_action: &str, args: &HashMap<String, serde_json::Value>) -> String {
1273 if let Some(key) = Self::variant_arg_key(bare_action) {
1274 if let Some(v) = args.get(key).and_then(|v| v.as_str()) {
1275 return format!("{}:{}", bare_action, v);
1276 }
1277 }
1278 bare_action.to_string()
1279 }
1280
1281 pub fn to_qualified_action_str(&self) -> String {
1286 match self {
1287 Self::MenuOpen(name) => format!("menu_open:{}", name),
1288 Self::SwitchKeybindingMap(map) => format!("switch_keybinding_map:{}", map),
1289 other => other.to_action_str(),
1290 }
1291 }
1292
1293 pub fn unqualify_action(qualified: &str) -> (String, HashMap<String, serde_json::Value>) {
1298 if let Some((bare, suffix)) = qualified.split_once(':') {
1299 if let Some(arg_key) = Self::variant_arg_key(bare) {
1300 let mut args = HashMap::new();
1301 args.insert(
1302 arg_key.to_string(),
1303 serde_json::Value::String(suffix.to_string()),
1304 );
1305 return (bare.to_string(), args);
1306 }
1307 }
1308 (qualified.to_string(), HashMap::new())
1309 }
1310
1311 pub fn is_movement_or_editing(&self) -> bool {
1314 matches!(
1315 self,
1316 Action::MoveLeft
1318 | Action::MoveRight
1319 | Action::MoveUp
1320 | Action::MoveDown
1321 | Action::MoveWordLeft
1322 | Action::MoveWordRight
1323 | Action::MoveWordEnd
1324 | Action::ViMoveWordEnd
1325 | Action::MoveLeftInLine
1326 | Action::MoveRightInLine
1327 | Action::MoveLineStart
1328 | Action::MoveLineEnd
1329 | Action::MovePageUp
1330 | Action::MovePageDown
1331 | Action::MoveDocumentStart
1332 | Action::MoveDocumentEnd
1333 | Action::MoveToParagraphUp
1334 | Action::MoveToParagraphDown
1335 | Action::SelectLeft
1337 | Action::SelectRight
1338 | Action::SelectUp
1339 | Action::SelectDown
1340 | Action::SelectToParagraphUp
1341 | Action::SelectToParagraphDown
1342 | Action::SelectWordLeft
1343 | Action::SelectWordRight
1344 | Action::SelectWordEnd
1345 | Action::ViSelectWordEnd
1346 | Action::SelectLineStart
1347 | Action::SelectLineEnd
1348 | Action::SelectDocumentStart
1349 | Action::SelectDocumentEnd
1350 | Action::SelectPageUp
1351 | Action::SelectPageDown
1352 | Action::SelectAll
1353 | Action::SelectWord
1354 | Action::SelectLine
1355 | Action::ExpandSelection
1356 | Action::BlockSelectLeft
1358 | Action::BlockSelectRight
1359 | Action::BlockSelectUp
1360 | Action::BlockSelectDown
1361 | Action::InsertChar(_)
1363 | Action::InsertNewline
1364 | Action::InsertTab
1365 | Action::DeleteBackward
1366 | Action::DeleteForward
1367 | Action::DeleteWordBackward
1368 | Action::DeleteWordForward
1369 | Action::DeleteLine
1370 | Action::DeleteToLineEnd
1371 | Action::DeleteToLineStart
1372 | Action::TransposeChars
1373 | Action::OpenLine
1374 | Action::DuplicateLine
1375 | Action::MoveLineUp
1376 | Action::MoveLineDown
1377 | Action::Cut
1379 | Action::Paste
1380 | Action::Undo
1382 | Action::Redo
1383 )
1384 }
1385
1386 pub fn is_editing(&self) -> bool {
1389 matches!(
1390 self,
1391 Action::InsertChar(_)
1392 | Action::InsertNewline
1393 | Action::InsertTab
1394 | Action::DeleteBackward
1395 | Action::DeleteForward
1396 | Action::DeleteWordBackward
1397 | Action::DeleteWordForward
1398 | Action::DeleteLine
1399 | Action::DeleteToLineEnd
1400 | Action::DeleteToLineStart
1401 | Action::DeleteViWordEnd
1402 | Action::TransposeChars
1403 | Action::OpenLine
1404 | Action::DuplicateLine
1405 | Action::MoveLineUp
1406 | Action::MoveLineDown
1407 | Action::Cut
1408 | Action::Paste
1409 )
1410 }
1411}
1412
1413#[derive(Debug, Clone, PartialEq)]
1415pub enum ChordResolution {
1416 Complete(Action),
1418 Partial,
1420 NoMatch,
1422}
1423
1424#[derive(Clone)]
1426pub struct KeybindingResolver {
1427 bindings: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1430
1431 default_bindings: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1433
1434 plugin_defaults: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1437
1438 chord_bindings: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1441
1442 default_chord_bindings: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1444
1445 plugin_chord_defaults: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1447
1448 inheriting_modes: std::collections::HashSet<String>,
1452}
1453
1454impl KeybindingResolver {
1455 pub fn new(config: &Config) -> Self {
1457 let mut resolver = Self {
1458 bindings: HashMap::new(),
1459 default_bindings: HashMap::new(),
1460 plugin_defaults: HashMap::new(),
1461 chord_bindings: HashMap::new(),
1462 default_chord_bindings: HashMap::new(),
1463 plugin_chord_defaults: HashMap::new(),
1464 inheriting_modes: std::collections::HashSet::new(),
1465 };
1466
1467 let map_bindings = config.resolve_keymap(&config.active_keybinding_map);
1469 resolver.load_default_bindings_from_vec(&map_bindings);
1470
1471 resolver.load_bindings_from_vec(&config.keybindings);
1473
1474 resolver
1475 }
1476
1477 fn load_default_bindings_from_vec(&mut self, bindings: &[crate::config::Keybinding]) {
1479 for binding in bindings {
1480 let context = if let Some(ref when) = binding.when {
1482 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
1483 } else {
1484 KeyContext::Normal
1485 };
1486
1487 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
1488 if !binding.keys.is_empty() {
1490 let mut sequence = Vec::new();
1492 for key_press in &binding.keys {
1493 if let Some(key_code) = Self::parse_key(&key_press.key) {
1494 let modifiers = Self::parse_modifiers(&key_press.modifiers);
1495 sequence.push((key_code, modifiers));
1496 } else {
1497 break;
1499 }
1500 }
1501
1502 if sequence.len() == binding.keys.len() && !sequence.is_empty() {
1504 self.default_chord_bindings
1505 .entry(context)
1506 .or_default()
1507 .insert(sequence, action);
1508 }
1509 } else if let Some(key_code) = Self::parse_key(&binding.key) {
1510 let modifiers = Self::parse_modifiers(&binding.modifiers);
1512
1513 self.insert_binding_with_equivalents(
1515 context,
1516 key_code,
1517 modifiers,
1518 action,
1519 &binding.key,
1520 );
1521 }
1522 }
1523 }
1524 }
1525
1526 fn insert_binding_with_equivalents(
1529 &mut self,
1530 context: KeyContext,
1531 key_code: KeyCode,
1532 modifiers: KeyModifiers,
1533 action: Action,
1534 key_name: &str,
1535 ) {
1536 let context_bindings = self.default_bindings.entry(context.clone()).or_default();
1537
1538 context_bindings.insert((key_code, modifiers), action.clone());
1540
1541 let equivalents = terminal_key_equivalents(key_code, modifiers);
1543 for (equiv_key, equiv_mods) in equivalents {
1544 if let Some(existing_action) = context_bindings.get(&(equiv_key, equiv_mods)) {
1546 if existing_action != &action {
1548 let equiv_name = format!("{:?}", equiv_key);
1549 tracing::warn!(
1550 "Terminal key equivalent conflict in {:?} context: {} (equivalent of {}) \
1551 is bound to {:?}, but {} is bound to {:?}. \
1552 The explicit binding takes precedence.",
1553 context,
1554 equiv_name,
1555 key_name,
1556 existing_action,
1557 key_name,
1558 action
1559 );
1560 }
1561 } else {
1563 context_bindings.insert((equiv_key, equiv_mods), action.clone());
1565 }
1566 }
1567 }
1568
1569 fn load_bindings_from_vec(&mut self, bindings: &[crate::config::Keybinding]) {
1571 for binding in bindings {
1572 let context = if let Some(ref when) = binding.when {
1574 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
1575 } else {
1576 KeyContext::Normal
1577 };
1578
1579 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
1580 if !binding.keys.is_empty() {
1582 let mut sequence = Vec::new();
1584 for key_press in &binding.keys {
1585 if let Some(key_code) = Self::parse_key(&key_press.key) {
1586 let modifiers = Self::parse_modifiers(&key_press.modifiers);
1587 sequence.push((key_code, modifiers));
1588 } else {
1589 break;
1591 }
1592 }
1593
1594 if sequence.len() == binding.keys.len() && !sequence.is_empty() {
1596 self.chord_bindings
1597 .entry(context)
1598 .or_default()
1599 .insert(sequence, action);
1600 }
1601 } else if let Some(key_code) = Self::parse_key(&binding.key) {
1602 let modifiers = Self::parse_modifiers(&binding.modifiers);
1604 self.bindings
1605 .entry(context)
1606 .or_default()
1607 .insert((key_code, modifiers), action);
1608 }
1609 }
1610 }
1611 }
1612
1613 pub fn load_plugin_default(
1615 &mut self,
1616 context: KeyContext,
1617 key_code: KeyCode,
1618 modifiers: KeyModifiers,
1619 action: Action,
1620 ) {
1621 self.plugin_defaults
1622 .entry(context)
1623 .or_default()
1624 .insert((key_code, modifiers), action);
1625 }
1626
1627 pub fn load_plugin_chord_default(
1629 &mut self,
1630 context: KeyContext,
1631 sequence: Vec<(KeyCode, KeyModifiers)>,
1632 action: Action,
1633 ) {
1634 self.plugin_chord_defaults
1635 .entry(context)
1636 .or_default()
1637 .insert(sequence, action);
1638 }
1639
1640 pub fn clear_plugin_defaults_for_mode(&mut self, mode_name: &str) {
1642 let context = KeyContext::Mode(mode_name.to_string());
1643 self.plugin_defaults.remove(&context);
1644 self.plugin_chord_defaults.remove(&context);
1645 self.inheriting_modes.remove(mode_name);
1646 }
1647
1648 pub fn set_mode_inherits_normal_bindings(&mut self, mode_name: &str, inherit: bool) {
1651 if inherit {
1652 self.inheriting_modes.insert(mode_name.to_string());
1653 } else {
1654 self.inheriting_modes.remove(mode_name);
1655 }
1656 }
1657
1658 pub fn get_plugin_defaults(
1660 &self,
1661 ) -> &HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>> {
1662 &self.plugin_defaults
1663 }
1664
1665 fn is_application_wide_action(action: &Action) -> bool {
1667 matches!(
1668 action,
1669 Action::Quit
1670 | Action::ForceQuit
1671 | Action::Save
1672 | Action::SaveAs
1673 | Action::ShowHelp
1674 | Action::ShowKeyboardShortcuts
1675 | Action::PromptCancel | Action::PopupCancel )
1678 }
1679
1680 pub fn is_terminal_ui_action(action: &Action) -> bool {
1684 matches!(
1685 action,
1686 Action::CommandPalette
1688 | Action::QuickOpen
1689 | Action::QuickOpenBuffers
1690 | Action::QuickOpenFiles
1691 | Action::OpenLiveGrep
1692 | Action::ResumeLiveGrep
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 bound_plugin_action_names(&self) -> std::collections::HashSet<String> {
2046 let mut names = std::collections::HashSet::new();
2047 for map in self.bindings.values().chain(self.default_bindings.values()) {
2048 for action in map.values() {
2049 if let Action::PluginAction(name) = action {
2050 names.insert(name.clone());
2051 }
2052 }
2053 }
2054 names
2055 }
2056
2057 pub fn find_keybinding_for_action(
2058 &self,
2059 action_name: &str,
2060 context: KeyContext,
2061 ) -> Option<String> {
2062 let target_action = Action::from_str(action_name, &HashMap::new())?;
2064
2065 let search_maps = vec![
2067 self.bindings.get(&context),
2068 self.bindings.get(&KeyContext::Global),
2069 self.default_bindings.get(&context),
2070 self.default_bindings.get(&KeyContext::Global),
2071 ];
2072
2073 for map in search_maps.into_iter().flatten() {
2074 let mut matches: Vec<(KeyCode, KeyModifiers)> = map
2076 .iter()
2077 .filter(|(_, action)| {
2078 match (*action, &target_action) {
2083 (Action::PluginAction(a), Action::PluginAction(b)) => a == b,
2084 (a, b) => std::mem::discriminant(a) == std::mem::discriminant(b),
2085 }
2086 })
2087 .map(|((key_code, modifiers), _)| (*key_code, *modifiers))
2088 .collect();
2089
2090 if !matches.is_empty() {
2091 matches.sort_by(|(key_a, mod_a), (key_b, mod_b)| {
2093 let mod_count_a = mod_a.bits().count_ones();
2095 let mod_count_b = mod_b.bits().count_ones();
2096 match mod_count_a.cmp(&mod_count_b) {
2097 std::cmp::Ordering::Equal => {
2098 match mod_a.bits().cmp(&mod_b.bits()) {
2100 std::cmp::Ordering::Equal => {
2101 Self::key_code_sort_key(key_a)
2103 .cmp(&Self::key_code_sort_key(key_b))
2104 }
2105 other => other,
2106 }
2107 }
2108 other => other,
2109 }
2110 });
2111
2112 let (key_code, modifiers) = matches[0];
2113 return Some(format_keybinding(&key_code, &modifiers));
2114 }
2115 }
2116
2117 None
2118 }
2119
2120 fn key_code_sort_key(key_code: &KeyCode) -> (u8, u32) {
2122 match key_code {
2123 KeyCode::Char(c) => (0, *c as u32),
2124 KeyCode::F(n) => (1, *n as u32),
2125 KeyCode::Enter => (2, 0),
2126 KeyCode::Tab => (2, 1),
2127 KeyCode::Backspace => (2, 2),
2128 KeyCode::Delete => (2, 3),
2129 KeyCode::Esc => (2, 4),
2130 KeyCode::Left => (3, 0),
2131 KeyCode::Right => (3, 1),
2132 KeyCode::Up => (3, 2),
2133 KeyCode::Down => (3, 3),
2134 KeyCode::Home => (3, 4),
2135 KeyCode::End => (3, 5),
2136 KeyCode::PageUp => (3, 6),
2137 KeyCode::PageDown => (3, 7),
2138 _ => (255, 0),
2139 }
2140 }
2141
2142 pub fn find_menu_mnemonic(&self, menu_name: &str) -> Option<char> {
2145 let search_maps = vec![
2147 self.bindings.get(&KeyContext::Normal),
2148 self.bindings.get(&KeyContext::Global),
2149 self.default_bindings.get(&KeyContext::Normal),
2150 self.default_bindings.get(&KeyContext::Global),
2151 ];
2152
2153 for map in search_maps.into_iter().flatten() {
2154 for ((key_code, modifiers), action) in map {
2155 if let Action::MenuOpen(name) = action {
2157 if name.eq_ignore_ascii_case(menu_name) && *modifiers == KeyModifiers::ALT {
2158 if let KeyCode::Char(c) = key_code {
2160 return Some(c.to_ascii_lowercase());
2161 }
2162 }
2163 }
2164 }
2165 }
2166
2167 None
2168 }
2169
2170 fn parse_key(key: &str) -> Option<KeyCode> {
2172 let lower = key.to_lowercase();
2173 match lower.as_str() {
2174 "enter" => Some(KeyCode::Enter),
2175 "backspace" => Some(KeyCode::Backspace),
2176 "delete" | "del" => Some(KeyCode::Delete),
2177 "tab" => Some(KeyCode::Tab),
2178 "backtab" => Some(KeyCode::BackTab),
2179 "esc" | "escape" => Some(KeyCode::Esc),
2180 "space" => Some(KeyCode::Char(' ')),
2181
2182 "left" => Some(KeyCode::Left),
2183 "right" => Some(KeyCode::Right),
2184 "up" => Some(KeyCode::Up),
2185 "down" => Some(KeyCode::Down),
2186 "home" => Some(KeyCode::Home),
2187 "end" => Some(KeyCode::End),
2188 "pageup" => Some(KeyCode::PageUp),
2189 "pagedown" => Some(KeyCode::PageDown),
2190
2191 s if s.len() == 1 => s.chars().next().map(KeyCode::Char),
2192 s if s.starts_with('f') && s.len() >= 2 => s[1..].parse::<u8>().ok().map(KeyCode::F),
2194 _ => None,
2195 }
2196 }
2197
2198 fn parse_modifiers(modifiers: &[String]) -> KeyModifiers {
2200 let mut result = KeyModifiers::empty();
2201 for m in modifiers {
2202 match m.to_lowercase().as_str() {
2203 "ctrl" | "control" => result |= KeyModifiers::CONTROL,
2204 "shift" => result |= KeyModifiers::SHIFT,
2205 "alt" => result |= KeyModifiers::ALT,
2206 "super" | "cmd" | "command" | "meta" => result |= KeyModifiers::SUPER,
2207 _ => {}
2208 }
2209 }
2210 result
2211 }
2212
2213 pub fn get_all_bindings(&self) -> Vec<(String, String)> {
2217 let mut bindings = Vec::new();
2218
2219 for context in &[
2221 KeyContext::Normal,
2222 KeyContext::Prompt,
2223 KeyContext::Popup,
2224 KeyContext::FileExplorer,
2225 KeyContext::Menu,
2226 KeyContext::CompositeBuffer,
2227 ] {
2228 let mut all_keys: HashMap<(KeyCode, KeyModifiers), Action> = HashMap::new();
2229
2230 if let Some(context_defaults) = self.default_bindings.get(context) {
2232 for (key, action) in context_defaults {
2233 all_keys.insert(*key, action.clone());
2234 }
2235 }
2236
2237 if let Some(context_bindings) = self.bindings.get(context) {
2239 for (key, action) in context_bindings {
2240 all_keys.insert(*key, action.clone());
2241 }
2242 }
2243
2244 let context_str = if *context != KeyContext::Normal {
2246 format!("[{}] ", context.to_when_clause())
2247 } else {
2248 String::new()
2249 };
2250
2251 for ((key_code, modifiers), action) in all_keys {
2252 let key_str = Self::format_key(key_code, modifiers);
2253 let action_str = format!("{}{}", context_str, Self::format_action(&action));
2254 bindings.push((key_str, action_str));
2255 }
2256 }
2257
2258 bindings.sort_by(|a, b| a.1.cmp(&b.1));
2260
2261 bindings
2262 }
2263
2264 fn format_key(key_code: KeyCode, modifiers: KeyModifiers) -> String {
2266 format_keybinding(&key_code, &modifiers)
2267 }
2268
2269 pub fn format_action(action: &Action) -> String {
2271 match action {
2272 Action::InsertChar(c) => t!("action.insert_char", char = c),
2273 Action::InsertNewline => t!("action.insert_newline"),
2274 Action::InsertTab => t!("action.insert_tab"),
2275 Action::MoveLeft => t!("action.move_left"),
2276 Action::MoveRight => t!("action.move_right"),
2277 Action::MoveUp => t!("action.move_up"),
2278 Action::MoveDown => t!("action.move_down"),
2279 Action::MoveWordLeft => t!("action.move_word_left"),
2280 Action::MoveWordRight => t!("action.move_word_right"),
2281 Action::MoveWordEnd => t!("action.move_word_end"),
2282 Action::ViMoveWordEnd => t!("action.move_word_end"),
2283 Action::MoveLeftInLine => t!("action.move_left"),
2284 Action::MoveRightInLine => t!("action.move_right"),
2285 Action::MoveLineStart => t!("action.move_line_start"),
2286 Action::MoveLineEnd => t!("action.move_line_end"),
2287 Action::MoveLineUp => t!("action.move_line_up"),
2288 Action::MoveLineDown => t!("action.move_line_down"),
2289 Action::MovePageUp => t!("action.move_page_up"),
2290 Action::MovePageDown => t!("action.move_page_down"),
2291 Action::MoveDocumentStart => t!("action.move_document_start"),
2292 Action::MoveDocumentEnd => t!("action.move_document_end"),
2293 Action::SelectLeft => t!("action.select_left"),
2294 Action::SelectRight => t!("action.select_right"),
2295 Action::SelectUp => t!("action.select_up"),
2296 Action::SelectDown => t!("action.select_down"),
2297 Action::SelectToParagraphUp => t!("action.select_to_paragraph_up"),
2298 Action::SelectToParagraphDown => t!("action.select_to_paragraph_down"),
2299 Action::MoveToParagraphUp => t!("action.move_to_paragraph_up"),
2300 Action::MoveToParagraphDown => t!("action.move_to_paragraph_down"),
2301 Action::SelectWordLeft => t!("action.select_word_left"),
2302 Action::SelectWordRight => t!("action.select_word_right"),
2303 Action::SelectWordEnd => t!("action.select_word_end"),
2304 Action::ViSelectWordEnd => t!("action.select_word_end"),
2305 Action::SelectLineStart => t!("action.select_line_start"),
2306 Action::SelectLineEnd => t!("action.select_line_end"),
2307 Action::SelectDocumentStart => t!("action.select_document_start"),
2308 Action::SelectDocumentEnd => t!("action.select_document_end"),
2309 Action::SelectPageUp => t!("action.select_page_up"),
2310 Action::SelectPageDown => t!("action.select_page_down"),
2311 Action::SelectAll => t!("action.select_all"),
2312 Action::SelectWord => t!("action.select_word"),
2313 Action::SelectLine => t!("action.select_line"),
2314 Action::ExpandSelection => t!("action.expand_selection"),
2315 Action::BlockSelectLeft => t!("action.block_select_left"),
2316 Action::BlockSelectRight => t!("action.block_select_right"),
2317 Action::BlockSelectUp => t!("action.block_select_up"),
2318 Action::BlockSelectDown => t!("action.block_select_down"),
2319 Action::DeleteBackward => t!("action.delete_backward"),
2320 Action::DeleteForward => t!("action.delete_forward"),
2321 Action::DeleteWordBackward => t!("action.delete_word_backward"),
2322 Action::DeleteWordForward => t!("action.delete_word_forward"),
2323 Action::DeleteLine => t!("action.delete_line"),
2324 Action::DeleteToLineEnd => t!("action.delete_to_line_end"),
2325 Action::DeleteToLineStart => t!("action.delete_to_line_start"),
2326 Action::DeleteViWordEnd => t!("action.delete_word_forward"),
2327 Action::TransposeChars => t!("action.transpose_chars"),
2328 Action::OpenLine => t!("action.open_line"),
2329 Action::DuplicateLine => t!("action.duplicate_line"),
2330 Action::Recenter => t!("action.recenter"),
2331 Action::SetMark => t!("action.set_mark"),
2332 Action::Copy => t!("action.copy"),
2333 Action::CopyWithTheme(theme) if theme.is_empty() => t!("action.copy_with_formatting"),
2334 Action::CopyWithTheme(theme) => t!("action.copy_with_theme", theme = theme),
2335 Action::Cut => t!("action.cut"),
2336 Action::Paste => t!("action.paste"),
2337 Action::CopyFilePath => t!("action.copy_file_path"),
2338 Action::CopyRelativeFilePath => t!("action.copy_relative_file_path"),
2339 Action::YankWordForward => t!("action.yank_word_forward"),
2340 Action::YankWordBackward => t!("action.yank_word_backward"),
2341 Action::YankToLineEnd => t!("action.yank_to_line_end"),
2342 Action::YankToLineStart => t!("action.yank_to_line_start"),
2343 Action::YankViWordEnd => t!("action.yank_word_forward"),
2344 Action::AddCursorAbove => t!("action.add_cursor_above"),
2345 Action::AddCursorBelow => t!("action.add_cursor_below"),
2346 Action::AddCursorNextMatch => t!("action.add_cursor_next_match"),
2347 Action::AddCursorsToLineEnds => t!("action.add_cursors_to_line_ends"),
2348 Action::RemoveSecondaryCursors => t!("action.remove_secondary_cursors"),
2349 Action::Save => t!("action.save"),
2350 Action::SaveAs => t!("action.save_as"),
2351 Action::Open => t!("action.open"),
2352 Action::SwitchProject => t!("action.switch_project"),
2353 Action::New => t!("action.new"),
2354 Action::Close => t!("action.close"),
2355 Action::CloseTab => t!("action.close_tab"),
2356 Action::Quit => t!("action.quit"),
2357 Action::ForceQuit => t!("action.force_quit"),
2358 Action::Detach => t!("action.detach"),
2359 Action::Revert => t!("action.revert"),
2360 Action::ToggleAutoRevert => t!("action.toggle_auto_revert"),
2361 Action::FormatBuffer => t!("action.format_buffer"),
2362 Action::TrimTrailingWhitespace => t!("action.trim_trailing_whitespace"),
2363 Action::EnsureFinalNewline => t!("action.ensure_final_newline"),
2364 Action::GotoLine => t!("action.goto_line"),
2365 Action::ScanLineIndex => t!("action.scan_line_index"),
2366 Action::GoToMatchingBracket => t!("action.goto_matching_bracket"),
2367 Action::JumpToNextError => t!("action.jump_to_next_error"),
2368 Action::JumpToPreviousError => t!("action.jump_to_previous_error"),
2369 Action::SmartHome => t!("action.smart_home"),
2370 Action::DedentSelection => t!("action.dedent_selection"),
2371 Action::ToggleComment => t!("action.toggle_comment"),
2372 Action::DabbrevExpand => std::borrow::Cow::Borrowed("Expand abbreviation (dabbrev)"),
2373 Action::ToggleFold => t!("action.toggle_fold"),
2374 Action::SetBookmark(c) => t!("action.set_bookmark", key = c),
2375 Action::JumpToBookmark(c) => t!("action.jump_to_bookmark", key = c),
2376 Action::ClearBookmark(c) => t!("action.clear_bookmark", key = c),
2377 Action::ListBookmarks => t!("action.list_bookmarks"),
2378 Action::ToggleSearchCaseSensitive => t!("action.toggle_search_case_sensitive"),
2379 Action::ToggleSearchWholeWord => t!("action.toggle_search_whole_word"),
2380 Action::ToggleSearchRegex => t!("action.toggle_search_regex"),
2381 Action::ToggleSearchConfirmEach => t!("action.toggle_search_confirm_each"),
2382 Action::StartMacroRecording => t!("action.start_macro_recording"),
2383 Action::StopMacroRecording => t!("action.stop_macro_recording"),
2384 Action::PlayMacro(c) => t!("action.play_macro", key = c),
2385 Action::ToggleMacroRecording(c) => t!("action.toggle_macro_recording", key = c),
2386 Action::ShowMacro(c) => t!("action.show_macro", key = c),
2387 Action::ListMacros => t!("action.list_macros"),
2388 Action::PromptRecordMacro => t!("action.prompt_record_macro"),
2389 Action::PromptPlayMacro => t!("action.prompt_play_macro"),
2390 Action::PlayLastMacro => t!("action.play_last_macro"),
2391 Action::PromptSetBookmark => t!("action.prompt_set_bookmark"),
2392 Action::PromptJumpToBookmark => t!("action.prompt_jump_to_bookmark"),
2393 Action::Undo => t!("action.undo"),
2394 Action::Redo => t!("action.redo"),
2395 Action::ScrollUp => t!("action.scroll_up"),
2396 Action::ScrollDown => t!("action.scroll_down"),
2397 Action::ShowHelp => t!("action.show_help"),
2398 Action::ShowKeyboardShortcuts => t!("action.show_keyboard_shortcuts"),
2399 Action::ShowWarnings => t!("action.show_warnings"),
2400 Action::ShowStatusLog => t!("action.show_status_log"),
2401 Action::ShowLspStatus => t!("action.show_lsp_status"),
2402 Action::ShowRemoteIndicatorMenu => t!("action.show_remote_indicator_menu"),
2403 Action::ClearWarnings => t!("action.clear_warnings"),
2404 Action::CommandPalette => t!("action.command_palette"),
2405 Action::QuickOpen => t!("action.quick_open"),
2406 Action::QuickOpenBuffers => t!("action.quick_open_buffers"),
2407 Action::QuickOpenFiles => t!("action.quick_open_files"),
2408 Action::OpenLiveGrep => t!("action.open_live_grep"),
2409 Action::ResumeLiveGrep => t!("action.resume_live_grep"),
2410 Action::ToggleUtilityDock => t!("action.toggle_utility_dock"),
2411 Action::OpenTerminalInDock => t!("action.open_terminal_in_dock"),
2412 Action::CycleLiveGrepProvider => t!("action.cycle_live_grep_provider"),
2413 Action::InspectThemeAtCursor => t!("action.inspect_theme_at_cursor"),
2414 Action::ToggleLineWrap => t!("action.toggle_line_wrap"),
2415 Action::ToggleCurrentLineHighlight => t!("action.toggle_current_line_highlight"),
2416 Action::ToggleReadOnly => t!("action.toggle_read_only"),
2417 Action::TogglePageView => t!("action.toggle_page_view"),
2418 Action::SetPageWidth => t!("action.set_page_width"),
2419 Action::NextBuffer => t!("action.next_buffer"),
2420 Action::PrevBuffer => t!("action.prev_buffer"),
2421 Action::NavigateBack => t!("action.navigate_back"),
2422 Action::NavigateForward => t!("action.navigate_forward"),
2423 Action::SplitHorizontal => t!("action.split_horizontal"),
2424 Action::SplitVertical => t!("action.split_vertical"),
2425 Action::CloseSplit => t!("action.close_split"),
2426 Action::NextSplit => t!("action.next_split"),
2427 Action::PrevSplit => t!("action.prev_split"),
2428 Action::NextWindow => t!("action.next_window"),
2429 Action::PrevWindow => t!("action.prev_window"),
2430 Action::IncreaseSplitSize => t!("action.increase_split_size"),
2431 Action::DecreaseSplitSize => t!("action.decrease_split_size"),
2432 Action::ToggleMaximizeSplit => t!("action.toggle_maximize_split"),
2433 Action::PromptConfirm => t!("action.prompt_confirm"),
2434 Action::PromptConfirmWithText(ref text) => {
2435 format!("{} ({})", t!("action.prompt_confirm"), text).into()
2436 }
2437 Action::PromptCancel => t!("action.prompt_cancel"),
2438 Action::PromptBackspace => t!("action.prompt_backspace"),
2439 Action::PromptDelete => t!("action.prompt_delete"),
2440 Action::PromptMoveLeft => t!("action.prompt_move_left"),
2441 Action::PromptMoveRight => t!("action.prompt_move_right"),
2442 Action::PromptMoveStart => t!("action.prompt_move_start"),
2443 Action::PromptMoveEnd => t!("action.prompt_move_end"),
2444 Action::PromptSelectPrev => t!("action.prompt_select_prev"),
2445 Action::PromptSelectNext => t!("action.prompt_select_next"),
2446 Action::PromptPageUp => t!("action.prompt_page_up"),
2447 Action::PromptPageDown => t!("action.prompt_page_down"),
2448 Action::PromptAcceptSuggestion => t!("action.prompt_accept_suggestion"),
2449 Action::PromptMoveWordLeft => t!("action.prompt_move_word_left"),
2450 Action::PromptMoveWordRight => t!("action.prompt_move_word_right"),
2451 Action::PromptDeleteWordForward => t!("action.prompt_delete_word_forward"),
2452 Action::PromptDeleteWordBackward => t!("action.prompt_delete_word_backward"),
2453 Action::PromptDeleteToLineEnd => t!("action.prompt_delete_to_line_end"),
2454 Action::PromptCopy => t!("action.prompt_copy"),
2455 Action::PromptCut => t!("action.prompt_cut"),
2456 Action::PromptPaste => t!("action.prompt_paste"),
2457 Action::PromptMoveLeftSelecting => t!("action.prompt_move_left_selecting"),
2458 Action::PromptMoveRightSelecting => t!("action.prompt_move_right_selecting"),
2459 Action::PromptMoveHomeSelecting => t!("action.prompt_move_home_selecting"),
2460 Action::PromptMoveEndSelecting => t!("action.prompt_move_end_selecting"),
2461 Action::PromptSelectWordLeft => t!("action.prompt_select_word_left"),
2462 Action::PromptSelectWordRight => t!("action.prompt_select_word_right"),
2463 Action::PromptSelectAll => t!("action.prompt_select_all"),
2464 Action::FileBrowserToggleHidden => t!("action.file_browser_toggle_hidden"),
2465 Action::FileBrowserToggleDetectEncoding => {
2466 t!("action.file_browser_toggle_detect_encoding")
2467 }
2468 Action::PopupSelectNext => t!("action.popup_select_next"),
2469 Action::PopupSelectPrev => t!("action.popup_select_prev"),
2470 Action::PopupPageUp => t!("action.popup_page_up"),
2471 Action::PopupPageDown => t!("action.popup_page_down"),
2472 Action::PopupConfirm => t!("action.popup_confirm"),
2473 Action::PopupCancel => t!("action.popup_cancel"),
2474 Action::PopupFocus => t!("action.popup_focus"),
2475 Action::CompletionAccept => t!("action.completion_accept"),
2476 Action::CompletionDismiss => t!("action.completion_dismiss"),
2477 Action::ToggleFileExplorer => t!("action.toggle_file_explorer"),
2478 Action::ToggleFileExplorerSide => t!("action.toggle_file_explorer_side"),
2479 Action::ToggleMenuBar => t!("action.toggle_menu_bar"),
2480 Action::ToggleTabBar => t!("action.toggle_tab_bar"),
2481 Action::ToggleStatusBar => t!("action.toggle_status_bar"),
2482 Action::TogglePromptLine => t!("action.toggle_prompt_line"),
2483 Action::ToggleVerticalScrollbar => t!("action.toggle_vertical_scrollbar"),
2484 Action::ToggleHorizontalScrollbar => t!("action.toggle_horizontal_scrollbar"),
2485 Action::FocusFileExplorer => t!("action.focus_file_explorer"),
2486 Action::FocusEditor => t!("action.focus_editor"),
2487 Action::FileExplorerUp => t!("action.file_explorer_up"),
2488 Action::FileExplorerDown => t!("action.file_explorer_down"),
2489 Action::FileExplorerPageUp => t!("action.file_explorer_page_up"),
2490 Action::FileExplorerPageDown => t!("action.file_explorer_page_down"),
2491 Action::FileExplorerExpand => t!("action.file_explorer_expand"),
2492 Action::FileExplorerCollapse => t!("action.file_explorer_collapse"),
2493 Action::FileExplorerOpen => t!("action.file_explorer_open"),
2494 Action::FileExplorerRefresh => t!("action.file_explorer_refresh"),
2495 Action::FileExplorerNewFile => t!("action.file_explorer_new_file"),
2496 Action::FileExplorerNewDirectory => t!("action.file_explorer_new_directory"),
2497 Action::FileExplorerDelete => t!("action.file_explorer_delete"),
2498 Action::FileExplorerRename => t!("action.file_explorer_rename"),
2499 Action::FileExplorerToggleHidden => t!("action.file_explorer_toggle_hidden"),
2500 Action::FileExplorerToggleGitignored => t!("action.file_explorer_toggle_gitignored"),
2501 Action::FileExplorerSearchClear => t!("action.file_explorer_search_clear"),
2502 Action::FileExplorerSearchBackspace => t!("action.file_explorer_search_backspace"),
2503 Action::FileExplorerCopy => t!("action.file_explorer_copy"),
2504 Action::FileExplorerCut => t!("action.file_explorer_cut"),
2505 Action::FileExplorerPaste => t!("action.file_explorer_paste"),
2506 Action::FileExplorerDuplicate => t!("action.file_explorer_duplicate"),
2507 Action::FileExplorerCopyFullPath => t!("action.file_explorer_copy_full_path"),
2508 Action::FileExplorerCopyRelativePath => t!("action.file_explorer_copy_relative_path"),
2509 Action::FileExplorerExtendSelectionUp => t!("action.file_explorer_extend_selection_up"),
2510 Action::FileExplorerExtendSelectionDown => {
2511 t!("action.file_explorer_extend_selection_down")
2512 }
2513 Action::FileExplorerToggleSelect => t!("action.file_explorer_toggle_select"),
2514 Action::FileExplorerSelectAll => t!("action.file_explorer_select_all"),
2515 Action::LspCompletion => t!("action.lsp_completion"),
2516 Action::LspGotoDefinition => t!("action.lsp_goto_definition"),
2517 Action::LspReferences => t!("action.lsp_references"),
2518 Action::LspRename => t!("action.lsp_rename"),
2519 Action::LspHover => t!("action.lsp_hover"),
2520 Action::LspSignatureHelp => t!("action.lsp_signature_help"),
2521 Action::LspCodeActions => t!("action.lsp_code_actions"),
2522 Action::LspRestart => t!("action.lsp_restart"),
2523 Action::LspStop => t!("action.lsp_stop"),
2524 Action::LspToggleForBuffer => t!("action.lsp_toggle_for_buffer"),
2525 Action::ToggleInlayHints => t!("action.toggle_inlay_hints"),
2526 Action::ToggleMouseHover => t!("action.toggle_mouse_hover"),
2527 Action::ToggleLineNumbers => t!("action.toggle_line_numbers"),
2528 Action::ToggleScrollSync => t!("action.toggle_scroll_sync"),
2529 Action::ToggleMouseCapture => t!("action.toggle_mouse_capture"),
2530 Action::ToggleDebugHighlights => t!("action.toggle_debug_highlights"),
2531 Action::SetBackground => t!("action.set_background"),
2532 Action::SetBackgroundBlend => t!("action.set_background_blend"),
2533 Action::AddRuler => t!("action.add_ruler"),
2534 Action::RemoveRuler => t!("action.remove_ruler"),
2535 Action::SetTabSize => t!("action.set_tab_size"),
2536 Action::SetLineEnding => t!("action.set_line_ending"),
2537 Action::SetEncoding => t!("action.set_encoding"),
2538 Action::ReloadWithEncoding => t!("action.reload_with_encoding"),
2539 Action::SetLanguage => t!("action.set_language"),
2540 Action::ToggleIndentationStyle => t!("action.toggle_indentation_style"),
2541 Action::ToggleTabIndicators => t!("action.toggle_tab_indicators"),
2542 Action::ToggleWhitespaceIndicators => t!("action.toggle_whitespace_indicators"),
2543 Action::ResetBufferSettings => t!("action.reset_buffer_settings"),
2544 Action::DumpConfig => t!("action.dump_config"),
2545 Action::RedrawScreen => t!("action.redraw_screen"),
2546 Action::Search => t!("action.search"),
2547 Action::FindInSelection => t!("action.find_in_selection"),
2548 Action::FindNext => t!("action.find_next"),
2549 Action::FindPrevious => t!("action.find_previous"),
2550 Action::FindSelectionNext => t!("action.find_selection_next"),
2551 Action::FindSelectionPrevious => t!("action.find_selection_previous"),
2552 Action::Replace => t!("action.replace"),
2553 Action::QueryReplace => t!("action.query_replace"),
2554 Action::MenuActivate => t!("action.menu_activate"),
2555 Action::MenuClose => t!("action.menu_close"),
2556 Action::MenuLeft => t!("action.menu_left"),
2557 Action::MenuRight => t!("action.menu_right"),
2558 Action::MenuUp => t!("action.menu_up"),
2559 Action::MenuDown => t!("action.menu_down"),
2560 Action::MenuExecute => t!("action.menu_execute"),
2561 Action::MenuOpen(name) => t!("action.menu_open", name = name),
2562 Action::SwitchKeybindingMap(map) => t!("action.switch_keybinding_map", map = map),
2563 Action::PluginAction(name) => t!("action.plugin_action", name = name),
2564 Action::ScrollTabsLeft => t!("action.scroll_tabs_left"),
2565 Action::ScrollTabsRight => t!("action.scroll_tabs_right"),
2566 Action::SelectTheme => t!("action.select_theme"),
2567 Action::SelectKeybindingMap => t!("action.select_keybinding_map"),
2568 Action::SelectCursorStyle => t!("action.select_cursor_style"),
2569 Action::SelectLocale => t!("action.select_locale"),
2570 Action::SwitchToPreviousTab => t!("action.switch_to_previous_tab"),
2571 Action::SwitchToTabByName => t!("action.switch_to_tab_by_name"),
2572 Action::OpenTerminal => t!("action.open_terminal"),
2573 Action::CloseTerminal => t!("action.close_terminal"),
2574 Action::FocusTerminal => t!("action.focus_terminal"),
2575 Action::TerminalEscape => t!("action.terminal_escape"),
2576 Action::ToggleKeyboardCapture => t!("action.toggle_keyboard_capture"),
2577 Action::TerminalPaste => t!("action.terminal_paste"),
2578 Action::OpenSettings => t!("action.open_settings"),
2579 Action::CloseSettings => t!("action.close_settings"),
2580 Action::SettingsSave => t!("action.settings_save"),
2581 Action::SettingsReset => t!("action.settings_reset"),
2582 Action::SettingsToggleFocus => t!("action.settings_toggle_focus"),
2583 Action::SettingsActivate => t!("action.settings_activate"),
2584 Action::SettingsSearch => t!("action.settings_search"),
2585 Action::SettingsHelp => t!("action.settings_help"),
2586 Action::SettingsIncrement => t!("action.settings_increment"),
2587 Action::SettingsDecrement => t!("action.settings_decrement"),
2588 Action::SettingsInherit => t!("action.settings_inherit"),
2589 Action::ShellCommand => t!("action.shell_command"),
2590 Action::ShellCommandReplace => t!("action.shell_command_replace"),
2591 Action::ToUpperCase => t!("action.to_uppercase"),
2592 Action::ToLowerCase => t!("action.to_lowercase"),
2593 Action::ToggleCase => t!("action.to_uppercase"),
2594 Action::SortLines => t!("action.sort_lines"),
2595 Action::CalibrateInput => t!("action.calibrate_input"),
2596 Action::EventDebug => t!("action.event_debug"),
2597 Action::SuspendProcess => t!("action.suspend_process"),
2598 Action::LoadPluginFromBuffer => "Load Plugin from Buffer".into(),
2599 Action::InitReload => "Reload init.ts".into(),
2600 Action::InitEdit => "Edit init.ts".into(),
2601 Action::InitCheck => "Check init.ts".into(),
2602 Action::OpenKeybindingEditor => "Keybinding Editor".into(),
2603 Action::CompositeNextHunk => t!("action.composite_next_hunk"),
2604 Action::CompositePrevHunk => t!("action.composite_prev_hunk"),
2605 Action::WorkspaceTrustTrust => t!("action.workspace_trust_trust"),
2606 Action::WorkspaceTrustRestrict => t!("action.workspace_trust_restrict"),
2607 Action::WorkspaceTrustBlock => t!("action.workspace_trust_block"),
2608 Action::WorkspaceTrustPrompt => t!("action.workspace_trust_prompt"),
2609 Action::None => t!("action.none"),
2610 }
2611 .to_string()
2612 }
2613
2614 pub fn parse_key_public(key: &str) -> Option<KeyCode> {
2616 Self::parse_key(key)
2617 }
2618
2619 pub fn parse_modifiers_public(modifiers: &[String]) -> KeyModifiers {
2621 Self::parse_modifiers(modifiers)
2622 }
2623
2624 pub fn format_action_from_str(action_name: &str) -> String {
2628 Self::format_action_from_str_with_args(action_name, &std::collections::HashMap::new())
2629 }
2630
2631 pub fn format_action_from_str_with_args(
2635 action_name: &str,
2636 args: &std::collections::HashMap<String, serde_json::Value>,
2637 ) -> String {
2638 if let Some(action) = Action::from_str(action_name, args) {
2640 Self::format_action(&action)
2641 } else {
2642 action_name
2644 .split('_')
2645 .map(|word| {
2646 let mut chars = word.chars();
2647 match chars.next() {
2648 Some(c) => {
2649 let upper: String = c.to_uppercase().collect();
2650 format!("{}{}", upper, chars.as_str())
2651 }
2652 None => String::new(),
2653 }
2654 })
2655 .collect::<Vec<_>>()
2656 .join(" ")
2657 }
2658 }
2659
2660 pub fn all_action_names() -> Vec<String> {
2664 Action::all_action_names()
2665 }
2666
2667 pub fn get_keybinding_for_action(
2673 &self,
2674 action: &Action,
2675 context: KeyContext,
2676 ) -> Option<String> {
2677 self.get_keybinding_event_for_action(action, context)
2678 .map(|(k, m)| format_keybinding(&k, &m))
2679 }
2680
2681 pub fn get_keybinding_event_for_action(
2688 &self,
2689 action: &Action,
2690 context: KeyContext,
2691 ) -> Option<(KeyCode, KeyModifiers)> {
2692 fn find_best_keybinding(
2694 bindings: &HashMap<(KeyCode, KeyModifiers), Action>,
2695 action: &Action,
2696 ) -> Option<(KeyCode, KeyModifiers)> {
2697 let matches: Vec<_> = bindings
2698 .iter()
2699 .filter(|(_, a)| *a == action)
2700 .map(|((k, m), _)| (*k, *m))
2701 .collect();
2702
2703 if matches.is_empty() {
2704 return None;
2705 }
2706
2707 let mut sorted = matches;
2710 sorted.sort_by(|(k1, m1), (k2, m2)| {
2711 let score1 = keybinding_priority_score(k1);
2712 let score2 = keybinding_priority_score(k2);
2713 match score1.cmp(&score2) {
2715 std::cmp::Ordering::Equal => {
2716 let s1 = format_keybinding(k1, m1);
2718 let s2 = format_keybinding(k2, m2);
2719 s1.cmp(&s2)
2720 }
2721 other => other,
2722 }
2723 });
2724
2725 sorted.into_iter().next()
2726 }
2727
2728 if let Some(context_bindings) = self.bindings.get(&context) {
2730 if let Some(hit) = find_best_keybinding(context_bindings, action) {
2731 return Some(hit);
2732 }
2733 }
2734
2735 if let Some(context_bindings) = self.default_bindings.get(&context) {
2737 if let Some(hit) = find_best_keybinding(context_bindings, action) {
2738 return Some(hit);
2739 }
2740 }
2741
2742 if context != KeyContext::Normal
2744 && (context.allows_normal_fallthrough()
2745 || Self::is_application_wide_action(action)
2746 || (context.allows_ui_fallthrough() && Self::is_terminal_ui_action(action)))
2747 {
2748 if let Some(normal_bindings) = self.bindings.get(&KeyContext::Normal) {
2750 if let Some(hit) = find_best_keybinding(normal_bindings, action) {
2751 return Some(hit);
2752 }
2753 }
2754
2755 if let Some(normal_bindings) = self.default_bindings.get(&KeyContext::Normal) {
2757 if let Some(hit) = find_best_keybinding(normal_bindings, action) {
2758 return Some(hit);
2759 }
2760 }
2761 }
2762
2763 None
2764 }
2765
2766 pub fn reload(&mut self, config: &Config) {
2768 self.bindings.clear();
2769 for binding in &config.keybindings {
2770 if let Some(key_code) = Self::parse_key(&binding.key) {
2771 let modifiers = Self::parse_modifiers(&binding.modifiers);
2772 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
2773 let context = if let Some(ref when) = binding.when {
2775 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
2776 } else {
2777 KeyContext::Normal
2778 };
2779
2780 self.bindings
2781 .entry(context)
2782 .or_default()
2783 .insert((key_code, modifiers), action);
2784 }
2785 }
2786 }
2787 }
2788}
2789
2790#[cfg(test)]
2791mod tests {
2792 use super::*;
2793
2794 #[test]
2795 fn test_parse_key() {
2796 assert_eq!(KeybindingResolver::parse_key("enter"), Some(KeyCode::Enter));
2797 assert_eq!(
2798 KeybindingResolver::parse_key("backspace"),
2799 Some(KeyCode::Backspace)
2800 );
2801 assert_eq!(KeybindingResolver::parse_key("tab"), Some(KeyCode::Tab));
2802 assert_eq!(
2803 KeybindingResolver::parse_key("backtab"),
2804 Some(KeyCode::BackTab)
2805 );
2806 assert_eq!(
2807 KeybindingResolver::parse_key("BackTab"),
2808 Some(KeyCode::BackTab)
2809 );
2810 assert_eq!(KeybindingResolver::parse_key("a"), Some(KeyCode::Char('a')));
2811 }
2812
2813 #[test]
2814 fn test_parse_modifiers() {
2815 let mods = vec!["ctrl".to_string()];
2816 assert_eq!(
2817 KeybindingResolver::parse_modifiers(&mods),
2818 KeyModifiers::CONTROL
2819 );
2820
2821 let mods = vec!["ctrl".to_string(), "shift".to_string()];
2822 assert_eq!(
2823 KeybindingResolver::parse_modifiers(&mods),
2824 KeyModifiers::CONTROL | KeyModifiers::SHIFT
2825 );
2826 }
2827
2828 #[test]
2829 fn test_format_action_from_str_distinguishes_menu_open_by_name() {
2830 let mut file_args = HashMap::new();
2833 file_args.insert(
2834 "name".to_string(),
2835 serde_json::Value::String("File".to_string()),
2836 );
2837 let mut edit_args = HashMap::new();
2838 edit_args.insert(
2839 "name".to_string(),
2840 serde_json::Value::String("Edit".to_string()),
2841 );
2842
2843 let file_display =
2844 KeybindingResolver::format_action_from_str_with_args("menu_open", &file_args);
2845 let edit_display =
2846 KeybindingResolver::format_action_from_str_with_args("menu_open", &edit_args);
2847 let no_args_display = KeybindingResolver::format_action_from_str("menu_open");
2848
2849 assert_ne!(
2850 file_display, edit_display,
2851 "menu_open with different names should produce different descriptions"
2852 );
2853 assert!(
2854 file_display.contains("File"),
2855 "expected the File menu description to contain \"File\", got {file_display:?}"
2856 );
2857 assert!(
2858 edit_display.contains("Edit"),
2859 "expected the Edit menu description to contain \"Edit\", got {edit_display:?}"
2860 );
2861 assert_eq!(no_args_display, "Menu Open");
2865 }
2866
2867 #[test]
2868 fn test_format_action_word_end_actions_are_localized() {
2869 crate::i18n::set_locale("en");
2874
2875 let move_desc = KeybindingResolver::format_action(&Action::MoveWordEnd);
2876 assert_ne!(
2877 move_desc, "action.move_word_end",
2878 "MoveWordEnd should resolve to a translated description"
2879 );
2880 let select_desc = KeybindingResolver::format_action(&Action::SelectWordEnd);
2881 assert_ne!(
2882 select_desc, "action.select_word_end",
2883 "SelectWordEnd should resolve to a translated description"
2884 );
2885
2886 assert_eq!(
2888 KeybindingResolver::format_action(&Action::ViMoveWordEnd),
2889 move_desc,
2890 );
2891 assert_eq!(
2892 KeybindingResolver::format_action(&Action::ViSelectWordEnd),
2893 select_desc,
2894 );
2895 }
2896
2897 #[test]
2898 fn test_qualify_and_unqualify_roundtrip_menu_open() {
2899 let mut args = HashMap::new();
2900 args.insert(
2901 "name".to_string(),
2902 serde_json::Value::String("File".to_string()),
2903 );
2904
2905 let qualified = Action::qualify_action("menu_open", &args);
2906 assert_eq!(qualified, "menu_open:File");
2907
2908 let (bare, parsed_args) = Action::unqualify_action(&qualified);
2909 assert_eq!(bare, "menu_open");
2910 assert_eq!(
2911 parsed_args.get("name").and_then(|v| v.as_str()),
2912 Some("File")
2913 );
2914 }
2915
2916 #[test]
2917 fn test_qualify_action_passthrough_for_unparameterised() {
2918 let args = HashMap::new();
2920 assert_eq!(Action::qualify_action("save", &args), "save");
2921 let (bare, parsed) = Action::unqualify_action("save");
2922 assert_eq!(bare, "save");
2923 assert!(parsed.is_empty());
2924 }
2925
2926 #[test]
2927 fn test_qualify_action_no_suffix_when_arg_missing() {
2928 let args = HashMap::new();
2931 assert_eq!(Action::qualify_action("menu_open", &args), "menu_open");
2932 }
2933
2934 #[test]
2935 fn test_unqualify_action_ignores_colon_on_unknown_action() {
2936 let (bare, parsed) = Action::unqualify_action("my_plugin:action_with:colons");
2939 assert_eq!(bare, "my_plugin:action_with:colons");
2940 assert!(parsed.is_empty());
2941 }
2942
2943 #[test]
2944 fn test_to_qualified_action_str_for_menu_open() {
2945 let action = Action::MenuOpen("Edit".to_string());
2946 assert_eq!(action.to_qualified_action_str(), "menu_open:Edit");
2947 }
2948
2949 #[test]
2950 fn test_resolve_basic() {
2951 let config = Config::default();
2952 let resolver = KeybindingResolver::new(&config);
2953
2954 let event = KeyEvent::new(KeyCode::Left, KeyModifiers::empty());
2955 assert_eq!(
2956 resolver.resolve(&event, KeyContext::Normal),
2957 Action::MoveLeft
2958 );
2959
2960 let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
2961 assert_eq!(
2962 resolver.resolve(&event, KeyContext::Normal),
2963 Action::InsertChar('a')
2964 );
2965 }
2966
2967 #[test]
2974 fn test_panel_mode_passthrough_for_ui_actions() {
2975 let config = Config::default();
2976 let resolver = KeybindingResolver::new(&config);
2977 let mode_ctx = KeyContext::Mode("search-replace-list".to_string());
2978
2979 let alt_close = KeyEvent::new(KeyCode::Char(']'), KeyModifiers::ALT);
2981 assert_eq!(
2982 resolver.resolve(&alt_close, mode_ctx.clone()),
2983 Action::NextSplit,
2984 "Alt+] should fall through to next_split inside a panel mode"
2985 );
2986
2987 let alt_open = KeyEvent::new(KeyCode::Char('['), KeyModifiers::ALT);
2989 assert_eq!(
2990 resolver.resolve(&alt_open, mode_ctx.clone()),
2991 Action::PrevSplit,
2992 "Alt+[ should fall through to prev_split inside a panel mode"
2993 );
2994
2995 let ctrl_s = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL);
2999 assert_eq!(
3000 resolver.resolve(&ctrl_s, mode_ctx.clone()),
3001 Action::Save,
3002 "Ctrl+S should still save while a panel mode is active"
3003 );
3004
3005 let ctrl_d = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL);
3009 assert_ne!(
3010 resolver.resolve(&ctrl_d, mode_ctx),
3011 Action::AddCursorNextMatch,
3012 "Ctrl+D (add cursor next match) must not pass through to a panel mode"
3013 );
3014 }
3015
3016 #[test]
3017 fn test_shift_backspace_matches_backspace() {
3018 let config = Config::default();
3022 let resolver = KeybindingResolver::new(&config);
3023
3024 let backspace = KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty());
3025 let shift_backspace = KeyEvent::new(KeyCode::Backspace, KeyModifiers::SHIFT);
3026
3027 assert_eq!(
3029 resolver.resolve(&backspace, KeyContext::Normal),
3030 Action::DeleteBackward,
3031 "Backspace should resolve to DeleteBackward in Normal context"
3032 );
3033 assert_eq!(
3034 resolver.resolve(&shift_backspace, KeyContext::Normal),
3035 Action::DeleteBackward,
3036 "Shift+Backspace should resolve to DeleteBackward (same as Backspace) in Normal context"
3037 );
3038
3039 assert_eq!(
3041 resolver.resolve(&backspace, KeyContext::Prompt),
3042 Action::PromptBackspace,
3043 "Backspace should resolve to PromptBackspace in Prompt context"
3044 );
3045 assert_eq!(
3046 resolver.resolve(&shift_backspace, KeyContext::Prompt),
3047 Action::PromptBackspace,
3048 "Shift+Backspace should resolve to PromptBackspace (same as Backspace) in Prompt context"
3049 );
3050
3051 assert_eq!(
3053 resolver.resolve(&backspace, KeyContext::FileExplorer),
3054 Action::FileExplorerSearchBackspace,
3055 "Backspace should resolve to FileExplorerSearchBackspace in FileExplorer context"
3056 );
3057 assert_eq!(
3058 resolver.resolve(&shift_backspace, KeyContext::FileExplorer),
3059 Action::FileExplorerSearchBackspace,
3060 "Shift+Backspace should resolve to FileExplorerSearchBackspace (same as Backspace) in FileExplorer context"
3061 );
3062 }
3063
3064 #[test]
3065 fn test_file_explorer_ui_fallthrough() {
3066 let config = Config::default();
3073 let resolver = KeybindingResolver::new(&config);
3074
3075 let cases = [
3076 (
3077 KeyCode::PageUp,
3078 KeyModifiers::CONTROL,
3079 Action::PrevBuffer,
3080 "Ctrl+PageUp -> prev_buffer",
3081 ),
3082 (
3083 KeyCode::PageDown,
3084 KeyModifiers::CONTROL,
3085 Action::NextBuffer,
3086 "Ctrl+PageDown -> next_buffer",
3087 ),
3088 (
3089 KeyCode::PageUp,
3090 KeyModifiers::ALT,
3091 Action::ScrollTabsLeft,
3092 "Alt+PageUp -> scroll_tabs_left",
3093 ),
3094 (
3095 KeyCode::PageDown,
3096 KeyModifiers::ALT,
3097 Action::ScrollTabsRight,
3098 "Alt+PageDown -> scroll_tabs_right",
3099 ),
3100 (
3101 KeyCode::Char('w'),
3102 KeyModifiers::ALT,
3103 Action::CloseTab,
3104 "Alt+W -> close_tab",
3105 ),
3106 ];
3107
3108 for (code, mods, expected, label) in cases {
3109 let event = KeyEvent::new(code, mods);
3110 assert_eq!(
3111 resolver.resolve(&event, KeyContext::FileExplorer),
3112 expected,
3113 "{label} should fall through from FileExplorer to Normal"
3114 );
3115 }
3116
3117 let up = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
3122 assert_eq!(
3123 resolver.resolve(&up, KeyContext::FileExplorer),
3124 Action::FileExplorerUp,
3125 "Up must continue to navigate the explorer, not move the cursor"
3126 );
3127
3128 let plain_d = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::empty());
3133 assert_eq!(
3134 resolver.resolve(&plain_d, KeyContext::FileExplorer),
3135 Action::InsertChar('d'),
3136 "Plain 'd' must remain text input for explorer search-as-you-type"
3137 );
3138 }
3139
3140 #[test]
3141 fn test_action_from_str() {
3142 let args = HashMap::new();
3143 assert_eq!(Action::from_str("move_left", &args), Some(Action::MoveLeft));
3144 assert_eq!(Action::from_str("save", &args), Some(Action::Save));
3145 assert_eq!(
3147 Action::from_str("unknown", &args),
3148 Some(Action::PluginAction("unknown".to_string()))
3149 );
3150
3151 assert_eq!(
3153 Action::from_str("keyboard_shortcuts", &args),
3154 Some(Action::ShowKeyboardShortcuts)
3155 );
3156 assert_eq!(
3157 Action::from_str("prompt_confirm", &args),
3158 Some(Action::PromptConfirm)
3159 );
3160 assert_eq!(
3161 Action::from_str("popup_cancel", &args),
3162 Some(Action::PopupCancel)
3163 );
3164
3165 assert_eq!(
3167 Action::from_str("calibrate_input", &args),
3168 Some(Action::CalibrateInput)
3169 );
3170 }
3171
3172 #[test]
3173 fn test_key_context_from_when_clause() {
3174 assert_eq!(
3175 KeyContext::from_when_clause("normal"),
3176 Some(KeyContext::Normal)
3177 );
3178 assert_eq!(
3179 KeyContext::from_when_clause("prompt"),
3180 Some(KeyContext::Prompt)
3181 );
3182 assert_eq!(
3183 KeyContext::from_when_clause("popup"),
3184 Some(KeyContext::Popup)
3185 );
3186 assert_eq!(KeyContext::from_when_clause("help"), None);
3187 assert_eq!(KeyContext::from_when_clause(" help "), None); assert_eq!(KeyContext::from_when_clause("unknown"), None);
3189 assert_eq!(KeyContext::from_when_clause(""), None);
3190 }
3191
3192 #[test]
3193 fn test_key_context_to_when_clause() {
3194 assert_eq!(KeyContext::Normal.to_when_clause(), "normal");
3195 assert_eq!(KeyContext::Prompt.to_when_clause(), "prompt");
3196 assert_eq!(KeyContext::Popup.to_when_clause(), "popup");
3197 }
3198
3199 #[test]
3200 fn test_context_specific_bindings() {
3201 let config = Config::default();
3202 let resolver = KeybindingResolver::new(&config);
3203
3204 let enter_event = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
3206 assert_eq!(
3207 resolver.resolve(&enter_event, KeyContext::Prompt),
3208 Action::PromptConfirm
3209 );
3210 assert_eq!(
3211 resolver.resolve(&enter_event, KeyContext::Normal),
3212 Action::InsertNewline
3213 );
3214
3215 let up_event = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
3217 assert_eq!(
3218 resolver.resolve(&up_event, KeyContext::Popup),
3219 Action::PopupSelectPrev
3220 );
3221 assert_eq!(
3222 resolver.resolve(&up_event, KeyContext::Normal),
3223 Action::MoveUp
3224 );
3225 }
3226
3227 #[test]
3228 fn test_context_fallback_to_normal() {
3229 let config = Config::default();
3230 let resolver = KeybindingResolver::new(&config);
3231
3232 let save_event = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL);
3234 assert_eq!(
3235 resolver.resolve(&save_event, KeyContext::Normal),
3236 Action::Save
3237 );
3238 assert_eq!(
3239 resolver.resolve(&save_event, KeyContext::Popup),
3240 Action::Save
3241 );
3242 }
3244
3245 #[test]
3246 fn test_context_priority_resolution() {
3247 use crate::config::Keybinding;
3248
3249 let mut config = Config::default();
3251 config.keybindings.push(Keybinding {
3252 key: "esc".to_string(),
3253 modifiers: vec![],
3254 keys: vec![],
3255 action: "quit".to_string(), args: HashMap::new(),
3257 when: Some("popup".to_string()),
3258 });
3259
3260 let resolver = KeybindingResolver::new(&config);
3261 let esc_event = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
3262
3263 assert_eq!(
3265 resolver.resolve(&esc_event, KeyContext::Popup),
3266 Action::Quit
3267 );
3268
3269 assert_eq!(
3271 resolver.resolve(&esc_event, KeyContext::Normal),
3272 Action::RemoveSecondaryCursors
3273 );
3274 }
3275
3276 #[test]
3277 fn test_character_input_in_contexts() {
3278 let config = Config::default();
3279 let resolver = KeybindingResolver::new(&config);
3280
3281 let char_event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
3282
3283 assert_eq!(
3285 resolver.resolve(&char_event, KeyContext::Normal),
3286 Action::InsertChar('a')
3287 );
3288 assert_eq!(
3289 resolver.resolve(&char_event, KeyContext::Prompt),
3290 Action::InsertChar('a')
3291 );
3292
3293 assert_eq!(
3295 resolver.resolve(&char_event, KeyContext::Popup),
3296 Action::None
3297 );
3298 }
3299
3300 #[test]
3301 fn test_custom_keybinding_loading() {
3302 use crate::config::Keybinding;
3303
3304 let mut config = Config::default();
3305
3306 config.keybindings.push(Keybinding {
3308 key: "f".to_string(),
3309 modifiers: vec!["ctrl".to_string()],
3310 keys: vec![],
3311 action: "command_palette".to_string(),
3312 args: HashMap::new(),
3313 when: None, });
3315
3316 let resolver = KeybindingResolver::new(&config);
3317
3318 let ctrl_f = KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL);
3320 assert_eq!(
3321 resolver.resolve(&ctrl_f, KeyContext::Normal),
3322 Action::CommandPalette
3323 );
3324
3325 let ctrl_k = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL);
3327 assert_eq!(
3328 resolver.resolve(&ctrl_k, KeyContext::Prompt),
3329 Action::PromptDeleteToLineEnd
3330 );
3331 assert_eq!(
3332 resolver.resolve(&ctrl_k, KeyContext::Normal),
3333 Action::DeleteToLineEnd
3334 );
3335 }
3336
3337 #[test]
3338 fn test_all_context_default_bindings_exist() {
3339 let config = Config::default();
3340 let resolver = KeybindingResolver::new(&config);
3341
3342 assert!(resolver.default_bindings.contains_key(&KeyContext::Normal));
3344 assert!(resolver.default_bindings.contains_key(&KeyContext::Prompt));
3345 assert!(resolver.default_bindings.contains_key(&KeyContext::Popup));
3346 assert!(resolver
3347 .default_bindings
3348 .contains_key(&KeyContext::FileExplorer));
3349 assert!(resolver.default_bindings.contains_key(&KeyContext::Menu));
3350
3351 assert!(!resolver.default_bindings[&KeyContext::Normal].is_empty());
3353 assert!(!resolver.default_bindings[&KeyContext::Prompt].is_empty());
3354 assert!(!resolver.default_bindings[&KeyContext::Popup].is_empty());
3355 assert!(!resolver.default_bindings[&KeyContext::FileExplorer].is_empty());
3356 assert!(!resolver.default_bindings[&KeyContext::Menu].is_empty());
3357 }
3358
3359 #[test]
3370 fn test_all_builtin_keymaps_have_valid_action_names() {
3371 let known_actions: std::collections::HashSet<String> =
3372 Action::all_action_names().into_iter().collect();
3373
3374 const ALLOWED_PLUGIN_ACTIONS_IN_DEFAULTS: &[&str] = &[
3375 "start_search_replace",
3376 "live_grep_toggle_files",
3379 "live_grep_toggle_ignored",
3380 "live_grep_toggle_buffers",
3381 "live_grep_toggle_terminals",
3382 "live_grep_toggle_diagnostics",
3383 "live_grep_toggle_word",
3384 "live_grep_toggle_regex",
3385 "live_grep_export_quickfix",
3389 ];
3390
3391 let config = Config::default();
3392
3393 for map_name in crate::config::KeybindingMapName::BUILTIN_OPTIONS {
3394 let bindings = config.resolve_keymap(map_name);
3395 for binding in &bindings {
3396 let is_known_builtin = known_actions.contains(&binding.action);
3397 let is_allowed_plugin =
3398 ALLOWED_PLUGIN_ACTIONS_IN_DEFAULTS.contains(&binding.action.as_str());
3399 assert!(
3400 is_known_builtin || is_allowed_plugin,
3401 "Keymap '{}' contains unknown action '{}' (key: '{}', when: {:?}). \
3402 This will be treated as a plugin action at runtime. \
3403 Check for typos in the keymap JSON file, or add the action to \
3404 ALLOWED_PLUGIN_ACTIONS_IN_DEFAULTS if it's an intentional \
3405 plugin-action binding.",
3406 map_name,
3407 binding.action,
3408 binding.key,
3409 binding.when,
3410 );
3411 }
3412 }
3413 }
3414
3415 #[test]
3416 fn test_resolve_determinism() {
3417 let config = Config::default();
3419 let resolver = KeybindingResolver::new(&config);
3420
3421 let test_cases = vec![
3422 (KeyCode::Left, KeyModifiers::empty(), KeyContext::Normal),
3423 (
3424 KeyCode::Esc,
3425 KeyModifiers::empty(),
3426 KeyContext::FileExplorer,
3427 ),
3428 (KeyCode::Enter, KeyModifiers::empty(), KeyContext::Prompt),
3429 (KeyCode::Down, KeyModifiers::empty(), KeyContext::Popup),
3430 ];
3431
3432 for (key_code, modifiers, context) in test_cases {
3433 let event = KeyEvent::new(key_code, modifiers);
3434 let action1 = resolver.resolve(&event, context.clone());
3435 let action2 = resolver.resolve(&event, context.clone());
3436 let action3 = resolver.resolve(&event, context);
3437
3438 assert_eq!(action1, action2, "Resolve should be deterministic");
3439 assert_eq!(action2, action3, "Resolve should be deterministic");
3440 }
3441 }
3442
3443 #[test]
3444 fn test_modifier_combinations() {
3445 let config = Config::default();
3446 let resolver = KeybindingResolver::new(&config);
3447
3448 let char_s = KeyCode::Char('s');
3450
3451 let no_mod = KeyEvent::new(char_s, KeyModifiers::empty());
3452 let ctrl = KeyEvent::new(char_s, KeyModifiers::CONTROL);
3453 let shift = KeyEvent::new(char_s, KeyModifiers::SHIFT);
3454 let ctrl_shift = KeyEvent::new(char_s, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
3455
3456 let action_no_mod = resolver.resolve(&no_mod, KeyContext::Normal);
3457 let action_ctrl = resolver.resolve(&ctrl, KeyContext::Normal);
3458 let action_shift = resolver.resolve(&shift, KeyContext::Normal);
3459 let action_ctrl_shift = resolver.resolve(&ctrl_shift, KeyContext::Normal);
3460
3461 assert_eq!(action_no_mod, Action::InsertChar('s'));
3463 assert_eq!(action_ctrl, Action::Save);
3464 assert_eq!(action_shift, Action::InsertChar('s')); assert_eq!(action_ctrl_shift, Action::None);
3467 }
3468
3469 #[test]
3470 fn test_scroll_keybindings() {
3471 let config = Config::default();
3472 let resolver = KeybindingResolver::new(&config);
3473
3474 let ctrl_up = KeyEvent::new(KeyCode::Up, KeyModifiers::CONTROL);
3476 assert_eq!(
3477 resolver.resolve(&ctrl_up, KeyContext::Normal),
3478 Action::ScrollUp,
3479 "Ctrl+Up should resolve to ScrollUp"
3480 );
3481
3482 let ctrl_down = KeyEvent::new(KeyCode::Down, KeyModifiers::CONTROL);
3484 assert_eq!(
3485 resolver.resolve(&ctrl_down, KeyContext::Normal),
3486 Action::ScrollDown,
3487 "Ctrl+Down should resolve to ScrollDown"
3488 );
3489 }
3490
3491 #[test]
3492 fn test_lsp_completion_keybinding() {
3493 let config = Config::default();
3494 let resolver = KeybindingResolver::new(&config);
3495
3496 let ctrl_space = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL);
3498 assert_eq!(
3499 resolver.resolve(&ctrl_space, KeyContext::Normal),
3500 Action::LspCompletion,
3501 "Ctrl+Space should resolve to LspCompletion"
3502 );
3503 }
3504
3505 #[test]
3506 fn test_terminal_key_equivalents() {
3507 let ctrl = KeyModifiers::CONTROL;
3509
3510 let slash_equivs = terminal_key_equivalents(KeyCode::Char('/'), ctrl);
3512 assert_eq!(slash_equivs, vec![(KeyCode::Char('7'), ctrl)]);
3513
3514 let seven_equivs = terminal_key_equivalents(KeyCode::Char('7'), ctrl);
3515 assert_eq!(seven_equivs, vec![(KeyCode::Char('/'), ctrl)]);
3516
3517 let backspace_equivs = terminal_key_equivalents(KeyCode::Backspace, ctrl);
3519 assert_eq!(backspace_equivs, vec![(KeyCode::Char('h'), ctrl)]);
3520
3521 let h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl);
3522 assert_eq!(h_equivs, vec![(KeyCode::Backspace, ctrl)]);
3523
3524 let a_equivs = terminal_key_equivalents(KeyCode::Char('a'), ctrl);
3526 assert!(a_equivs.is_empty());
3527
3528 let slash_no_ctrl = terminal_key_equivalents(KeyCode::Char('/'), KeyModifiers::empty());
3530 assert!(slash_no_ctrl.is_empty());
3531 }
3532
3533 #[test]
3534 fn test_terminal_key_equivalents_auto_binding() {
3535 let config = Config::default();
3536 let resolver = KeybindingResolver::new(&config);
3537
3538 let ctrl_slash = KeyEvent::new(KeyCode::Char('/'), KeyModifiers::CONTROL);
3540 let action_slash = resolver.resolve(&ctrl_slash, KeyContext::Normal);
3541 assert_eq!(
3542 action_slash,
3543 Action::ToggleComment,
3544 "Ctrl+/ should resolve to ToggleComment"
3545 );
3546
3547 let ctrl_7 = KeyEvent::new(KeyCode::Char('7'), KeyModifiers::CONTROL);
3549 let action_7 = resolver.resolve(&ctrl_7, KeyContext::Normal);
3550 assert_eq!(
3551 action_7,
3552 Action::ToggleComment,
3553 "Ctrl+7 should resolve to ToggleComment (terminal equivalent of Ctrl+/)"
3554 );
3555 }
3556
3557 #[test]
3558 fn test_terminal_key_equivalents_normalization() {
3559 let ctrl = KeyModifiers::CONTROL;
3564
3565 let slash_equivs = terminal_key_equivalents(KeyCode::Char('/'), ctrl);
3568 assert_eq!(
3569 slash_equivs,
3570 vec![(KeyCode::Char('7'), ctrl)],
3571 "Ctrl+/ should map to Ctrl+7"
3572 );
3573 let seven_equivs = terminal_key_equivalents(KeyCode::Char('7'), ctrl);
3574 assert_eq!(
3575 seven_equivs,
3576 vec![(KeyCode::Char('/'), ctrl)],
3577 "Ctrl+7 should map back to Ctrl+/"
3578 );
3579
3580 let backspace_equivs = terminal_key_equivalents(KeyCode::Backspace, ctrl);
3583 assert_eq!(
3584 backspace_equivs,
3585 vec![(KeyCode::Char('h'), ctrl)],
3586 "Ctrl+Backspace should map to Ctrl+H"
3587 );
3588 let h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl);
3589 assert_eq!(
3590 h_equivs,
3591 vec![(KeyCode::Backspace, ctrl)],
3592 "Ctrl+H should map back to Ctrl+Backspace"
3593 );
3594
3595 let space_equivs = terminal_key_equivalents(KeyCode::Char(' '), ctrl);
3598 assert_eq!(
3599 space_equivs,
3600 vec![(KeyCode::Char('@'), ctrl)],
3601 "Ctrl+Space should map to Ctrl+@"
3602 );
3603 let at_equivs = terminal_key_equivalents(KeyCode::Char('@'), ctrl);
3604 assert_eq!(
3605 at_equivs,
3606 vec![(KeyCode::Char(' '), ctrl)],
3607 "Ctrl+@ should map back to Ctrl+Space"
3608 );
3609
3610 let minus_equivs = terminal_key_equivalents(KeyCode::Char('-'), ctrl);
3613 assert_eq!(
3614 minus_equivs,
3615 vec![(KeyCode::Char('_'), ctrl)],
3616 "Ctrl+- should map to Ctrl+_"
3617 );
3618 let underscore_equivs = terminal_key_equivalents(KeyCode::Char('_'), ctrl);
3619 assert_eq!(
3620 underscore_equivs,
3621 vec![(KeyCode::Char('-'), ctrl)],
3622 "Ctrl+_ should map back to Ctrl+-"
3623 );
3624
3625 assert!(
3627 terminal_key_equivalents(KeyCode::Char('a'), ctrl).is_empty(),
3628 "Ctrl+A should have no terminal equivalents"
3629 );
3630 assert!(
3631 terminal_key_equivalents(KeyCode::Char('z'), ctrl).is_empty(),
3632 "Ctrl+Z should have no terminal equivalents"
3633 );
3634 assert!(
3635 terminal_key_equivalents(KeyCode::Enter, ctrl).is_empty(),
3636 "Ctrl+Enter should have no terminal equivalents"
3637 );
3638
3639 assert!(
3641 terminal_key_equivalents(KeyCode::Char('/'), KeyModifiers::empty()).is_empty(),
3642 "/ without Ctrl should have no equivalents"
3643 );
3644 assert!(
3645 terminal_key_equivalents(KeyCode::Char('7'), KeyModifiers::SHIFT).is_empty(),
3646 "Shift+7 should have no equivalents"
3647 );
3648 assert!(
3649 terminal_key_equivalents(KeyCode::Char('h'), KeyModifiers::ALT).is_empty(),
3650 "Alt+H should have no equivalents"
3651 );
3652
3653 let ctrl_shift = KeyModifiers::CONTROL | KeyModifiers::SHIFT;
3656 let ctrl_shift_h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl_shift);
3657 assert!(
3658 ctrl_shift_h_equivs.is_empty(),
3659 "Ctrl+Shift+H should NOT map to Ctrl+Shift+Backspace"
3660 );
3661 }
3662
3663 #[test]
3664 fn test_no_duplicate_keybindings_in_keymaps() {
3665 use std::collections::HashMap;
3668
3669 let keymaps: &[(&str, &str)] = &[
3670 ("default", include_str!("../../keymaps/default.json")),
3671 ("macos", include_str!("../../keymaps/macos.json")),
3672 ];
3673
3674 for (keymap_name, json_content) in keymaps {
3675 let keymap: crate::config::KeymapConfig = serde_json::from_str(json_content)
3676 .unwrap_or_else(|e| panic!("Failed to parse keymap '{}': {}", keymap_name, e));
3677
3678 let mut seen: HashMap<(String, Vec<String>, String), String> = HashMap::new();
3680 let mut duplicates: Vec<String> = Vec::new();
3681
3682 for binding in &keymap.bindings {
3683 let when = binding.when.clone().unwrap_or_default();
3684 let key_id = (binding.key.clone(), binding.modifiers.clone(), when.clone());
3685
3686 if let Some(existing_action) = seen.get(&key_id) {
3687 duplicates.push(format!(
3688 "Duplicate in '{}': key='{}', modifiers={:?}, when='{}' -> '{}' vs '{}'",
3689 keymap_name,
3690 binding.key,
3691 binding.modifiers,
3692 when,
3693 existing_action,
3694 binding.action
3695 ));
3696 } else {
3697 seen.insert(key_id, binding.action.clone());
3698 }
3699 }
3700
3701 assert!(
3702 duplicates.is_empty(),
3703 "Found duplicate keybindings:\n{}",
3704 duplicates.join("\n")
3705 );
3706 }
3707 }
3708}