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