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