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