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
7pub(crate) fn normalize_key(code: KeyCode, modifiers: KeyModifiers) -> (KeyCode, KeyModifiers) {
26 if code == KeyCode::BackTab {
27 return (code, modifiers.difference(KeyModifiers::SHIFT));
28 }
29 if code == KeyCode::Backspace {
30 return (code, modifiers.difference(KeyModifiers::SHIFT));
31 }
32 if let KeyCode::Char(c) = code {
33 if c.is_ascii_uppercase() {
34 let new_modifiers = if modifiers.contains(KeyModifiers::CONTROL) {
35 modifiers
36 } else {
37 modifiers | KeyModifiers::SHIFT
38 };
39 return (KeyCode::Char(c.to_ascii_lowercase()), new_modifiers);
40 }
41 }
42 (code, modifiers)
43}
44
45static FORCE_LINUX_KEYBINDINGS: AtomicBool = AtomicBool::new(false);
48
49pub fn set_force_linux_keybindings(force: bool) {
52 FORCE_LINUX_KEYBINDINGS.store(force, Ordering::SeqCst);
53}
54
55fn use_macos_symbols() -> bool {
57 if FORCE_LINUX_KEYBINDINGS.load(Ordering::SeqCst) {
58 return false;
59 }
60 cfg!(target_os = "macos")
61}
62
63fn is_text_input_modifier(modifiers: KeyModifiers) -> bool {
74 if modifiers.is_empty() || modifiers == KeyModifiers::SHIFT {
75 return true;
76 }
77
78 #[cfg(windows)]
82 if modifiers == (KeyModifiers::CONTROL | KeyModifiers::ALT)
83 || modifiers == (KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT)
84 {
85 return true;
86 }
87
88 false
89}
90
91pub fn format_keybinding(keycode: &KeyCode, modifiers: &KeyModifiers) -> String {
95 let mut result = String::new();
96
97 let (ctrl_label, alt_label, shift_label, super_label) = if use_macos_symbols() {
99 ("⌃", "⌥", "⇧", "⌘")
100 } else {
101 ("Ctrl", "Alt", "Shift", "Super")
102 };
103
104 let use_plus = !use_macos_symbols();
105
106 if modifiers.contains(KeyModifiers::SUPER) {
107 result.push_str(super_label);
108 if use_plus {
109 result.push('+');
110 }
111 }
112 if modifiers.contains(KeyModifiers::CONTROL) {
113 result.push_str(ctrl_label);
114 if use_plus {
115 result.push('+');
116 }
117 }
118 if modifiers.contains(KeyModifiers::ALT) {
119 result.push_str(alt_label);
120 if use_plus {
121 result.push('+');
122 }
123 }
124 if modifiers.contains(KeyModifiers::SHIFT) {
125 result.push_str(shift_label);
126 if use_plus {
127 result.push('+');
128 }
129 }
130
131 match keycode {
132 KeyCode::Enter => result.push_str("Enter"),
133 KeyCode::Backspace => result.push_str("Backspace"),
134 KeyCode::Delete => result.push_str("Del"),
135 KeyCode::Tab => result.push_str("Tab"),
136 KeyCode::Esc => result.push_str("Esc"),
137 KeyCode::Left => result.push('←'),
138 KeyCode::Right => result.push('→'),
139 KeyCode::Up => result.push('↑'),
140 KeyCode::Down => result.push('↓'),
141 KeyCode::Home => result.push_str("Home"),
142 KeyCode::End => result.push_str("End"),
143 KeyCode::PageUp => result.push_str("PgUp"),
144 KeyCode::PageDown => result.push_str("PgDn"),
145 KeyCode::Char(' ') => result.push_str("Space"),
146 KeyCode::Char(c) => result.push_str(&c.to_uppercase().to_string()),
147 KeyCode::F(n) => result.push_str(&format!("F{}", n)),
148 _ => return String::new(),
149 }
150
151 result
152}
153
154fn keybinding_priority_score(key: &KeyCode) -> u32 {
158 match key {
159 KeyCode::Char('@') => 100, KeyCode::Char('7') => 100, KeyCode::Char('_') => 100, _ => 0,
166 }
167}
168
169pub fn terminal_key_equivalents(
180 key: KeyCode,
181 modifiers: KeyModifiers,
182) -> Vec<(KeyCode, KeyModifiers)> {
183 let mut equivalents = Vec::new();
184
185 if modifiers.contains(KeyModifiers::CONTROL) {
187 let base_modifiers = modifiers; match key {
190 KeyCode::Char('/') => {
192 equivalents.push((KeyCode::Char('7'), base_modifiers));
193 }
194 KeyCode::Char('7') => {
195 equivalents.push((KeyCode::Char('/'), base_modifiers));
196 }
197
198 KeyCode::Backspace => {
200 equivalents.push((KeyCode::Char('h'), base_modifiers));
201 }
202 KeyCode::Char('h') if modifiers == KeyModifiers::CONTROL => {
203 equivalents.push((KeyCode::Backspace, base_modifiers));
205 }
206
207 KeyCode::Char(' ') => {
209 equivalents.push((KeyCode::Char('@'), base_modifiers));
210 }
211 KeyCode::Char('@') => {
212 equivalents.push((KeyCode::Char(' '), base_modifiers));
213 }
214
215 KeyCode::Char('-') => {
217 equivalents.push((KeyCode::Char('_'), base_modifiers));
218 }
219 KeyCode::Char('_') => {
220 equivalents.push((KeyCode::Char('-'), base_modifiers));
221 }
222
223 _ => {}
224 }
225 }
226
227 equivalents
228}
229
230#[derive(Debug, Clone, PartialEq, Eq, Hash)]
232pub enum KeyContext {
233 Global,
235 Normal,
237 Prompt,
239 SearchPrompt,
246 Popup,
248 Completion,
252 FileExplorer,
254 Dock,
259 Menu,
261 Terminal,
263 Settings,
265 CompositeBuffer,
267 Mode(String),
269}
270
271impl KeyContext {
272 pub fn allows_normal_fallthrough(&self) -> bool {
279 matches!(self, Self::CompositeBuffer)
280 }
281
282 pub fn allows_ui_fallthrough(&self) -> bool {
297 matches!(self, Self::FileExplorer | Self::Dock | Self::Mode(_))
298 }
299
300 pub fn allows_text_input(&self) -> bool {
302 matches!(
303 self,
304 Self::Normal | Self::Prompt | Self::SearchPrompt | Self::FileExplorer
305 )
306 }
307
308 pub fn parent_context(&self) -> Option<Self> {
314 match self {
315 Self::SearchPrompt => Some(Self::Prompt),
316 _ => None,
317 }
318 }
319
320 pub fn from_when_clause(when: &str) -> Option<Self> {
322 let trimmed = when.trim();
323 if let Some(mode_name) = trimmed.strip_prefix("mode:") {
324 return Some(Self::Mode(mode_name.to_string()));
325 }
326 Some(match trimmed {
327 "global" => Self::Global,
328 "prompt" => Self::Prompt,
329 "searchPrompt" | "search_prompt" => Self::SearchPrompt,
330 "popup" => Self::Popup,
331 "completion" => Self::Completion,
332 "fileExplorer" | "file_explorer" => Self::FileExplorer,
333 "dock" => Self::Dock,
334 "normal" => Self::Normal,
335 "menu" => Self::Menu,
336 "terminal" => Self::Terminal,
337 "settings" => Self::Settings,
338 "compositeBuffer" | "composite_buffer" => Self::CompositeBuffer,
339 _ => return None,
340 })
341 }
342
343 pub fn to_when_clause(&self) -> String {
345 match self {
346 Self::Global => "global".to_string(),
347 Self::Normal => "normal".to_string(),
348 Self::Prompt => "prompt".to_string(),
349 Self::SearchPrompt => "searchPrompt".to_string(),
350 Self::Popup => "popup".to_string(),
351 Self::Completion => "completion".to_string(),
352 Self::FileExplorer => "fileExplorer".to_string(),
353 Self::Dock => "dock".to_string(),
354 Self::Menu => "menu".to_string(),
355 Self::Terminal => "terminal".to_string(),
356 Self::Settings => "settings".to_string(),
357 Self::CompositeBuffer => "compositeBuffer".to_string(),
358 Self::Mode(name) => format!("mode:{}", name),
359 }
360 }
361}
362
363#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
365pub enum Action {
366 InsertChar(char),
368 InsertNewline,
369 InsertTab,
370
371 MoveLeft,
373 MoveRight,
374 MoveUp,
375 MoveDown,
376 MoveWordLeft,
377 MoveWordRight,
378 MoveWordEnd, ViMoveWordEnd, MoveLeftInLine, MoveRightInLine, MoveLineStart,
383 MoveLineEnd,
384 MoveLineUp,
385 MoveLineDown,
386 MoveToParagraphUp,
387 MoveToParagraphDown,
388 MovePageUp,
389 MovePageDown,
390 MoveDocumentStart,
391 MoveDocumentEnd,
392
393 SelectLeft,
395 SelectRight,
396 SelectUp,
397 SelectDown,
398 SelectToParagraphUp, SelectToParagraphDown, SelectWordLeft,
401 SelectWordRight,
402 SelectWordEnd, ViSelectWordEnd, SelectLineStart,
405 SelectLineEnd,
406 SelectDocumentStart,
407 SelectDocumentEnd,
408 SelectPageUp,
409 SelectPageDown,
410 SelectAll,
411 SelectWord,
412 SelectLine,
413 ExpandSelection,
414
415 BlockSelectLeft,
417 BlockSelectRight,
418 BlockSelectUp,
419 BlockSelectDown,
420
421 DeleteBackward,
423 DeleteForward,
424 DeleteWordBackward,
425 DeleteWordForward,
426 DeleteLine,
427 DeleteToLineEnd,
428 DeleteToLineStart,
429 DeleteViWordEnd, TransposeChars,
431 OpenLine,
432 DuplicateLine,
433
434 Recenter,
436
437 SetMark,
439 CancelMark,
440 ClearMark,
441
442 Copy,
444 CopyWithTheme(String),
445 Cut,
446 Paste,
447 CopyFilePath,
449 CopyRelativeFilePath,
452
453 YankWordForward,
455 YankWordBackward,
456 YankToLineEnd,
457 YankToLineStart,
458 YankViWordEnd, AddCursorAbove,
462 AddCursorBelow,
463 AddCursorNextMatch,
464 AddCursorsToLineEnds,
465 RemoveSecondaryCursors,
466
467 Save,
469 SaveAs,
470 Open,
471 SwitchProject,
472 New,
473 Close,
474 CloseTab,
475 Quit,
476 ForceQuit,
477 Detach,
478 Revert,
479 ToggleAutoRevert,
480 FormatBuffer,
481 TrimTrailingWhitespace,
482 EnsureFinalNewline,
483
484 GotoLine,
486 ScanLineIndex,
487 GoToMatchingBracket,
488 JumpToNextError,
489 JumpToPreviousError,
490
491 SmartHome,
493 DedentSelection,
494 ToggleComment,
495 DabbrevExpand,
496 ToggleFold,
497
498 SetBookmark(char),
500 JumpToBookmark(char),
501 ClearBookmark(char),
502 ListBookmarks,
503
504 ToggleSearchCaseSensitive,
506 ToggleSearchWholeWord,
507 ToggleSearchRegex,
508 ToggleSearchConfirmEach,
509
510 StartMacroRecording,
512 StopMacroRecording,
513 PlayMacro(char),
514 ToggleMacroRecording(char),
515 ShowMacro(char),
516 ListMacros,
517 PromptRecordMacro,
518 PromptPlayMacro,
519 PlayLastMacro,
520 PromptSaveMacroToInit,
523 PromptPromoteMacro,
526
527 PromptSetBookmark,
529 PromptJumpToBookmark,
530
531 Undo,
533 Redo,
534
535 ScrollUp,
537 ScrollDown,
538 ShowHelp,
539 ShowKeyboardShortcuts,
540 ShowWarnings,
541 ShowStatusLog,
542 ShowLspStatus,
543 ShowRemoteIndicatorMenu,
544 ShowReadOnlyMenu,
545 ClearWarnings,
546 CommandPalette, QuickOpen,
549 QuickOpenBuffers,
551 QuickOpenFiles,
553 OpenLiveGrep,
555 ResumeLiveGrep,
557 ToggleUtilityDock,
561 OpenTerminalInDock,
564 CycleLiveGrepProvider,
569 ToggleLineWrap,
570 ToggleCurrentLineHighlight,
571 ToggleOccurrenceHighlight,
572 ToggleReadOnly,
573 TogglePageView,
574 SetPageWidth,
575 InspectThemeAtCursor,
576 SelectTheme,
577 SelectKeybindingMap,
578 SelectCursorStyle,
579 SelectLocale,
580
581 NextBuffer,
583 PrevBuffer,
584 SwitchToPreviousTab,
585 SwitchToTabByName,
586
587 ScrollTabsLeft,
589 ScrollTabsRight,
590
591 NavigateBack,
593 NavigateForward,
594
595 SplitHorizontal,
597 SplitVertical,
598 CloseSplit,
599 NextSplit,
600 PrevSplit,
601 NextWindow,
602 PrevWindow,
603 IncreaseSplitSize,
604 DecreaseSplitSize,
605 ToggleMaximizeSplit,
606
607 PromptConfirm,
609 PromptConfirmWithText(String),
611 PromptCancel,
612 PromptBackspace,
613 PromptDelete,
614 PromptMoveLeft,
615 PromptMoveRight,
616 PromptMoveStart,
617 PromptMoveEnd,
618 PromptSelectPrev,
619 PromptSelectNext,
620 PromptPageUp,
621 PromptPageDown,
622 PromptAcceptSuggestion,
623 PromptMoveWordLeft,
624 PromptMoveWordRight,
625 PromptDeleteWordForward,
627 PromptDeleteWordBackward,
628 PromptDeleteToLineEnd,
629 PromptCopy,
630 PromptCut,
631 PromptPaste,
632 PromptMoveLeftSelecting,
634 PromptMoveRightSelecting,
635 PromptMoveHomeSelecting,
636 PromptMoveEndSelecting,
637 PromptSelectWordLeft,
638 PromptSelectWordRight,
639 PromptSelectAll,
640
641 FileBrowserToggleHidden,
643 FileBrowserToggleDetectEncoding,
644
645 PopupSelectNext,
647 PopupSelectPrev,
648 PopupPageUp,
649 PopupPageDown,
650 PopupConfirm,
651 PopupCancel,
652 PopupFocus,
657
658 CompletionAccept,
660 CompletionDismiss,
661
662 ToggleFileExplorer,
664 ToggleFileExplorerSide,
666 ToggleMenuBar,
668 ToggleTabBar,
670 ToggleStatusBar,
672 TogglePromptLine,
674 ToggleVerticalScrollbar,
676 ToggleHorizontalScrollbar,
677 FocusFileExplorer,
678 FocusEditor,
679 ToggleDockFocus,
683 FileExplorerUp,
684 FileExplorerDown,
685 FileExplorerPageUp,
686 FileExplorerPageDown,
687 FileExplorerExpand,
688 FileExplorerCollapse,
689 FileExplorerOpen,
690 FileExplorerRefresh,
691 FileExplorerNewFile,
692 FileExplorerNewDirectory,
693 FileExplorerDelete,
694 FileExplorerRename,
695 FileExplorerToggleHidden,
696 FileExplorerToggleGitignored,
697 FileExplorerSearchClear,
698 FileExplorerSearchBackspace,
699 FileExplorerCopy,
700 FileExplorerCut,
701 FileExplorerPaste,
702 FileExplorerDuplicate,
703 FileExplorerCopyFullPath,
704 FileExplorerCopyRelativePath,
705 FileExplorerExtendSelectionUp,
706 FileExplorerExtendSelectionDown,
707 FileExplorerToggleSelect,
708 FileExplorerSelectAll,
709
710 LspCompletion,
712 LspGotoDefinition,
713 LspReferences,
714 LspImplementation,
715 LspRename,
716 LspHover,
717 LspSignatureHelp,
718 LspCodeActions,
719 LspRestart,
720 LspStop,
721 LspToggleForBuffer,
722 ToggleInlayHints,
723 ToggleMouseHover,
724
725 ToggleLineNumbers,
727 ToggleLineNumbersCurrentBuffer,
731 ToggleLineWrapCurrentBuffer,
735 TriggerWaveAnimation,
737 ToggleScrollSync,
738 ToggleMouseCapture,
739 ToggleDebugHighlights, SetBackground,
741 SetBackgroundBlend,
742
743 SetTabSize,
745 SetLineEnding,
746 SetEncoding,
747 ReloadWithEncoding,
748 SetLanguage,
749 ToggleIndentationStyle,
750 ToggleTabIndicators,
751 ToggleWhitespaceIndicators,
752 ResetBufferSettings,
753 AddRuler,
754 RemoveRuler,
755
756 DumpConfig,
758
759 RedrawScreen,
761
762 Search,
764 FindInSelection,
765 FindNext,
766 FindPrevious,
767 FindSelectionNext, FindSelectionPrevious, ClearSearch, Replace,
771 QueryReplace, MenuActivate, MenuClose, MenuLeft, MenuRight, MenuUp, MenuDown, MenuExecute, MenuOpen(String), SwitchKeybindingMap(String), PluginAction(String),
788
789 OpenSettings, CloseSettings, SettingsSave, SettingsReset, SettingsToggleFocus, SettingsActivate, SettingsSearch, SettingsHelp, SettingsIncrement, SettingsDecrement, SettingsInherit, OpenTerminal, OpenTerminalRight, OpenTerminalBelow, 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,
855}
856
857macro_rules! define_action_str_mapping {
870 (
871 $args_name:ident;
872 simple { $($s_name:literal => $s_variant:ident),* $(,)? }
873 alias { $($a_name:literal => $a_variant:ident),* $(,)? }
874 with_char { $($c_name:literal => $c_variant:ident),* $(,)? }
875 custom { $($x_name:literal => $x_variant:ident : $x_body:expr),* $(,)? }
876 ) => {
877 pub fn from_str(s: &str, $args_name: &HashMap<String, serde_json::Value>) -> Option<Self> {
879 Some(match s {
880 $($s_name => Self::$s_variant,)*
881 $($a_name => Self::$a_variant,)*
882 $($c_name => return Self::with_char($args_name, Self::$c_variant),)*
883 $($x_name => $x_body,)*
884 _ => Self::PluginAction(s.to_string()),
887 })
888 }
889
890 pub fn to_action_str(&self) -> String {
893 match self {
894 $(Self::$s_variant => $s_name.to_string(),)*
895 $(Self::$c_variant(_) => $c_name.to_string(),)*
896 $(Self::$x_variant(_) => $x_name.to_string(),)*
897 Self::PluginAction(name) => name.clone(),
898 }
899 }
900
901 pub fn all_action_names() -> Vec<String> {
904 let mut names = vec![
905 $($s_name.to_string(),)*
906 $($a_name.to_string(),)*
907 $($c_name.to_string(),)*
908 $($x_name.to_string(),)*
909 ];
910 names.sort();
911 names
912 }
913 };
914}
915
916impl Action {
917 fn with_char(
918 args: &HashMap<String, serde_json::Value>,
919 make_action: impl FnOnce(char) -> Self,
920 ) -> Option<Self> {
921 if let Some(serde_json::Value::String(value)) = args.get("char") {
922 value.chars().next().map(make_action)
923 } else {
924 None
925 }
926 }
927
928 define_action_str_mapping! {
929 args;
930 simple {
931 "insert_newline" => InsertNewline,
932 "insert_tab" => InsertTab,
933
934 "move_left" => MoveLeft,
935 "move_right" => MoveRight,
936 "move_up" => MoveUp,
937 "move_down" => MoveDown,
938 "move_word_left" => MoveWordLeft,
939 "move_word_right" => MoveWordRight,
940 "move_word_end" => MoveWordEnd,
941 "vi_move_word_end" => ViMoveWordEnd,
942 "move_left_in_line" => MoveLeftInLine,
943 "move_right_in_line" => MoveRightInLine,
944 "move_line_start" => MoveLineStart,
945 "move_line_end" => MoveLineEnd,
946 "move_line_up" => MoveLineUp,
947 "move_line_down" => MoveLineDown,
948 "move_page_up" => MovePageUp,
949 "move_page_down" => MovePageDown,
950 "move_document_start" => MoveDocumentStart,
951 "move_document_end" => MoveDocumentEnd,
952 "move_to_paragraph_up" => MoveToParagraphUp,
953 "move_to_paragraph_down" => MoveToParagraphDown,
954
955 "select_left" => SelectLeft,
956 "select_right" => SelectRight,
957 "select_up" => SelectUp,
958 "select_down" => SelectDown,
959 "select_to_paragraph_up" => SelectToParagraphUp,
960 "select_to_paragraph_down" => SelectToParagraphDown,
961 "select_word_left" => SelectWordLeft,
962 "select_word_right" => SelectWordRight,
963 "select_word_end" => SelectWordEnd,
964 "vi_select_word_end" => ViSelectWordEnd,
965 "select_line_start" => SelectLineStart,
966 "select_line_end" => SelectLineEnd,
967 "select_document_start" => SelectDocumentStart,
968 "select_document_end" => SelectDocumentEnd,
969 "select_page_up" => SelectPageUp,
970 "select_page_down" => SelectPageDown,
971 "select_all" => SelectAll,
972 "select_word" => SelectWord,
973 "select_line" => SelectLine,
974 "expand_selection" => ExpandSelection,
975
976 "block_select_left" => BlockSelectLeft,
977 "block_select_right" => BlockSelectRight,
978 "block_select_up" => BlockSelectUp,
979 "block_select_down" => BlockSelectDown,
980
981 "delete_backward" => DeleteBackward,
982 "delete_forward" => DeleteForward,
983 "delete_word_backward" => DeleteWordBackward,
984 "delete_word_forward" => DeleteWordForward,
985 "delete_line" => DeleteLine,
986 "delete_to_line_end" => DeleteToLineEnd,
987 "delete_to_line_start" => DeleteToLineStart,
988 "delete_vi_word_end" => DeleteViWordEnd,
989 "transpose_chars" => TransposeChars,
990 "open_line" => OpenLine,
991 "duplicate_line" => DuplicateLine,
992 "recenter" => Recenter,
993 "set_mark" => SetMark,
994 "cancel_mark" => CancelMark,
995 "clear_mark" => ClearMark,
996
997 "copy" => Copy,
998 "cut" => Cut,
999 "paste" => Paste,
1000 "copy_file_path" => CopyFilePath,
1001 "copy_relative_file_path" => CopyRelativeFilePath,
1002
1003 "yank_word_forward" => YankWordForward,
1004 "yank_word_backward" => YankWordBackward,
1005 "yank_to_line_end" => YankToLineEnd,
1006 "yank_to_line_start" => YankToLineStart,
1007 "yank_vi_word_end" => YankViWordEnd,
1008
1009 "add_cursor_above" => AddCursorAbove,
1010 "add_cursor_below" => AddCursorBelow,
1011 "add_cursor_next_match" => AddCursorNextMatch,
1012 "add_cursors_to_line_ends" => AddCursorsToLineEnds,
1013 "remove_secondary_cursors" => RemoveSecondaryCursors,
1014
1015 "save" => Save,
1016 "save_as" => SaveAs,
1017 "open" => Open,
1018 "switch_project" => SwitchProject,
1019 "new" => New,
1020 "close" => Close,
1021 "close_tab" => CloseTab,
1022 "quit" => Quit,
1023 "force_quit" => ForceQuit,
1024 "detach" => Detach,
1025 "revert" => Revert,
1026 "toggle_auto_revert" => ToggleAutoRevert,
1027 "format_buffer" => FormatBuffer,
1028 "trim_trailing_whitespace" => TrimTrailingWhitespace,
1029 "ensure_final_newline" => EnsureFinalNewline,
1030 "goto_line" => GotoLine,
1031 "scan_line_index" => ScanLineIndex,
1032 "goto_matching_bracket" => GoToMatchingBracket,
1033 "jump_to_next_error" => JumpToNextError,
1034 "jump_to_previous_error" => JumpToPreviousError,
1035
1036 "smart_home" => SmartHome,
1037 "dedent_selection" => DedentSelection,
1038 "toggle_comment" => ToggleComment,
1039 "dabbrev_expand" => DabbrevExpand,
1040 "toggle_fold" => ToggleFold,
1041
1042 "list_bookmarks" => ListBookmarks,
1043
1044 "toggle_search_case_sensitive" => ToggleSearchCaseSensitive,
1045 "toggle_search_whole_word" => ToggleSearchWholeWord,
1046 "toggle_search_regex" => ToggleSearchRegex,
1047 "toggle_search_confirm_each" => ToggleSearchConfirmEach,
1048
1049 "start_macro_recording" => StartMacroRecording,
1050 "stop_macro_recording" => StopMacroRecording,
1051
1052 "list_macros" => ListMacros,
1053 "prompt_record_macro" => PromptRecordMacro,
1054 "prompt_play_macro" => PromptPlayMacro,
1055 "play_last_macro" => PlayLastMacro,
1056 "prompt_save_macro_to_init" => PromptSaveMacroToInit,
1057 "prompt_promote_macro" => PromptPromoteMacro,
1058 "prompt_set_bookmark" => PromptSetBookmark,
1059 "prompt_jump_to_bookmark" => PromptJumpToBookmark,
1060
1061 "undo" => Undo,
1062 "redo" => Redo,
1063
1064 "scroll_up" => ScrollUp,
1065 "scroll_down" => ScrollDown,
1066 "show_help" => ShowHelp,
1067 "keyboard_shortcuts" => ShowKeyboardShortcuts,
1068 "show_warnings" => ShowWarnings,
1069 "show_status_log" => ShowStatusLog,
1070 "show_lsp_status" => ShowLspStatus,
1071 "show_remote_indicator_menu" => ShowRemoteIndicatorMenu,
1072 "show_read_only_menu" => ShowReadOnlyMenu,
1073 "clear_warnings" => ClearWarnings,
1074 "command_palette" => CommandPalette,
1075 "quick_open" => QuickOpen,
1076 "quick_open_buffers" => QuickOpenBuffers,
1077 "quick_open_files" => QuickOpenFiles,
1078 "open_live_grep" => OpenLiveGrep,
1079 "resume_live_grep" => ResumeLiveGrep,
1080 "toggle_utility_dock" => ToggleUtilityDock,
1081 "open_terminal_in_dock" => OpenTerminalInDock,
1082 "cycle_live_grep_provider" => CycleLiveGrepProvider,
1083 "toggle_line_wrap" => ToggleLineWrap,
1084 "toggle_current_line_highlight" => ToggleCurrentLineHighlight,
1085 "toggle_occurrence_highlight" => ToggleOccurrenceHighlight,
1086 "toggle_read_only" => ToggleReadOnly,
1087 "toggle_page_view" => TogglePageView,
1088 "set_page_width" => SetPageWidth,
1089
1090 "next_buffer" => NextBuffer,
1091 "prev_buffer" => PrevBuffer,
1092 "switch_to_previous_tab" => SwitchToPreviousTab,
1093 "switch_to_tab_by_name" => SwitchToTabByName,
1094 "scroll_tabs_left" => ScrollTabsLeft,
1095 "scroll_tabs_right" => ScrollTabsRight,
1096
1097 "navigate_back" => NavigateBack,
1098 "navigate_forward" => NavigateForward,
1099
1100 "split_horizontal" => SplitHorizontal,
1101 "split_vertical" => SplitVertical,
1102 "close_split" => CloseSplit,
1103 "next_split" => NextSplit,
1104 "prev_split" => PrevSplit,
1105 "next_window" => NextWindow,
1106 "prev_window" => PrevWindow,
1107 "increase_split_size" => IncreaseSplitSize,
1108 "decrease_split_size" => DecreaseSplitSize,
1109 "toggle_maximize_split" => ToggleMaximizeSplit,
1110
1111 "prompt_confirm" => PromptConfirm,
1112 "prompt_cancel" => PromptCancel,
1113 "prompt_backspace" => PromptBackspace,
1114 "prompt_move_left" => PromptMoveLeft,
1115 "prompt_move_right" => PromptMoveRight,
1116 "prompt_move_start" => PromptMoveStart,
1117 "prompt_move_end" => PromptMoveEnd,
1118 "prompt_select_prev" => PromptSelectPrev,
1119 "prompt_select_next" => PromptSelectNext,
1120 "prompt_page_up" => PromptPageUp,
1121 "prompt_page_down" => PromptPageDown,
1122 "prompt_accept_suggestion" => PromptAcceptSuggestion,
1123 "prompt_delete_word_forward" => PromptDeleteWordForward,
1124 "prompt_delete_word_backward" => PromptDeleteWordBackward,
1125 "prompt_delete_to_line_end" => PromptDeleteToLineEnd,
1126 "prompt_copy" => PromptCopy,
1127 "prompt_cut" => PromptCut,
1128 "prompt_paste" => PromptPaste,
1129 "prompt_move_left_selecting" => PromptMoveLeftSelecting,
1130 "prompt_move_right_selecting" => PromptMoveRightSelecting,
1131 "prompt_move_home_selecting" => PromptMoveHomeSelecting,
1132 "prompt_move_end_selecting" => PromptMoveEndSelecting,
1133 "prompt_select_word_left" => PromptSelectWordLeft,
1134 "prompt_select_word_right" => PromptSelectWordRight,
1135 "prompt_select_all" => PromptSelectAll,
1136 "file_browser_toggle_hidden" => FileBrowserToggleHidden,
1137 "file_browser_toggle_detect_encoding" => FileBrowserToggleDetectEncoding,
1138 "prompt_move_word_left" => PromptMoveWordLeft,
1139 "prompt_move_word_right" => PromptMoveWordRight,
1140 "prompt_delete" => PromptDelete,
1141
1142 "popup_select_next" => PopupSelectNext,
1143 "popup_select_prev" => PopupSelectPrev,
1144 "popup_page_up" => PopupPageUp,
1145 "popup_page_down" => PopupPageDown,
1146 "popup_confirm" => PopupConfirm,
1147 "popup_cancel" => PopupCancel,
1148 "popup_focus" => PopupFocus,
1149
1150 "completion_accept" => CompletionAccept,
1151 "completion_dismiss" => CompletionDismiss,
1152
1153 "toggle_file_explorer" => ToggleFileExplorer,
1154 "toggle_file_explorer_side" => ToggleFileExplorerSide,
1155 "toggle_menu_bar" => ToggleMenuBar,
1156 "toggle_tab_bar" => ToggleTabBar,
1157 "toggle_status_bar" => ToggleStatusBar,
1158 "toggle_prompt_line" => TogglePromptLine,
1159 "toggle_vertical_scrollbar" => ToggleVerticalScrollbar,
1160 "toggle_horizontal_scrollbar" => ToggleHorizontalScrollbar,
1161 "focus_file_explorer" => FocusFileExplorer,
1162 "focus_editor" => FocusEditor,
1163 "toggle_dock_focus" => ToggleDockFocus,
1164 "file_explorer_up" => FileExplorerUp,
1165 "file_explorer_down" => FileExplorerDown,
1166 "file_explorer_page_up" => FileExplorerPageUp,
1167 "file_explorer_page_down" => FileExplorerPageDown,
1168 "file_explorer_expand" => FileExplorerExpand,
1169 "file_explorer_collapse" => FileExplorerCollapse,
1170 "file_explorer_open" => FileExplorerOpen,
1171 "file_explorer_refresh" => FileExplorerRefresh,
1172 "file_explorer_new_file" => FileExplorerNewFile,
1173 "file_explorer_new_directory" => FileExplorerNewDirectory,
1174 "file_explorer_delete" => FileExplorerDelete,
1175 "file_explorer_rename" => FileExplorerRename,
1176 "file_explorer_toggle_hidden" => FileExplorerToggleHidden,
1177 "file_explorer_toggle_gitignored" => FileExplorerToggleGitignored,
1178 "file_explorer_search_clear" => FileExplorerSearchClear,
1179 "file_explorer_search_backspace" => FileExplorerSearchBackspace,
1180 "file_explorer_copy" => FileExplorerCopy,
1181 "file_explorer_cut" => FileExplorerCut,
1182 "file_explorer_paste" => FileExplorerPaste,
1183 "file_explorer_duplicate" => FileExplorerDuplicate,
1184 "file_explorer_copy_full_path" => FileExplorerCopyFullPath,
1185 "file_explorer_copy_relative_path" => FileExplorerCopyRelativePath,
1186 "file_explorer_extend_selection_up" => FileExplorerExtendSelectionUp,
1187 "file_explorer_extend_selection_down" => FileExplorerExtendSelectionDown,
1188 "file_explorer_toggle_select" => FileExplorerToggleSelect,
1189 "file_explorer_select_all" => FileExplorerSelectAll,
1190
1191 "lsp_completion" => LspCompletion,
1192 "lsp_goto_definition" => LspGotoDefinition,
1193 "lsp_references" => LspReferences,
1194 "lsp_implementation" => LspImplementation,
1195 "lsp_rename" => LspRename,
1196 "lsp_hover" => LspHover,
1197 "lsp_signature_help" => LspSignatureHelp,
1198 "lsp_code_actions" => LspCodeActions,
1199 "lsp_restart" => LspRestart,
1200 "lsp_stop" => LspStop,
1201 "lsp_toggle_for_buffer" => LspToggleForBuffer,
1202 "toggle_inlay_hints" => ToggleInlayHints,
1203 "toggle_mouse_hover" => ToggleMouseHover,
1204
1205 "toggle_line_numbers" => ToggleLineNumbers,
1206 "toggle_line_numbers_current_buffer" => ToggleLineNumbersCurrentBuffer,
1207 "toggle_line_wrap_current_buffer" => ToggleLineWrapCurrentBuffer,
1208 "trigger_wave_animation" => TriggerWaveAnimation,
1209 "toggle_scroll_sync" => ToggleScrollSync,
1210 "toggle_mouse_capture" => ToggleMouseCapture,
1211 "toggle_debug_highlights" => ToggleDebugHighlights,
1212 "set_background" => SetBackground,
1213 "set_background_blend" => SetBackgroundBlend,
1214 "inspect_theme_at_cursor" => InspectThemeAtCursor,
1215 "select_theme" => SelectTheme,
1216 "select_keybinding_map" => SelectKeybindingMap,
1217 "select_cursor_style" => SelectCursorStyle,
1218 "select_locale" => SelectLocale,
1219
1220 "set_tab_size" => SetTabSize,
1221 "set_line_ending" => SetLineEnding,
1222 "set_encoding" => SetEncoding,
1223 "reload_with_encoding" => ReloadWithEncoding,
1224 "set_language" => SetLanguage,
1225 "toggle_indentation_style" => ToggleIndentationStyle,
1226 "toggle_tab_indicators" => ToggleTabIndicators,
1227 "toggle_whitespace_indicators" => ToggleWhitespaceIndicators,
1228 "reset_buffer_settings" => ResetBufferSettings,
1229 "add_ruler" => AddRuler,
1230 "remove_ruler" => RemoveRuler,
1231
1232 "dump_config" => DumpConfig,
1233 "redraw_screen" => RedrawScreen,
1234
1235 "search" => Search,
1236 "find_in_selection" => FindInSelection,
1237 "find_next" => FindNext,
1238 "find_previous" => FindPrevious,
1239 "find_selection_next" => FindSelectionNext,
1240 "find_selection_previous" => FindSelectionPrevious,
1241 "clear_search" => ClearSearch,
1242 "replace" => Replace,
1243 "query_replace" => QueryReplace,
1244
1245 "menu_activate" => MenuActivate,
1246 "menu_close" => MenuClose,
1247 "menu_left" => MenuLeft,
1248 "menu_right" => MenuRight,
1249 "menu_up" => MenuUp,
1250 "menu_down" => MenuDown,
1251 "menu_execute" => MenuExecute,
1252
1253 "open_terminal" => OpenTerminal,
1254 "open_terminal_right" => OpenTerminalRight,
1255 "open_terminal_below" => OpenTerminalBelow,
1256 "close_terminal" => CloseTerminal,
1257 "focus_terminal" => FocusTerminal,
1258 "terminal_escape" => TerminalEscape,
1259 "toggle_keyboard_capture" => ToggleKeyboardCapture,
1260 "terminal_paste" => TerminalPaste,
1261 "send_selection_to_terminal" => SendSelectionToTerminal,
1262
1263 "shell_command" => ShellCommand,
1264 "shell_command_replace" => ShellCommandReplace,
1265
1266 "to_upper_case" => ToUpperCase,
1267 "to_lower_case" => ToLowerCase,
1268 "toggle_case" => ToggleCase,
1269 "sort_lines" => SortLines,
1270
1271 "calibrate_input" => CalibrateInput,
1272 "event_debug" => EventDebug,
1273 "suspend_process" => SuspendProcess,
1274 "load_plugin_from_buffer" => LoadPluginFromBuffer,
1275 "init_reload" => InitReload,
1276 "init_edit" => InitEdit,
1277 "init_check" => InitCheck,
1278 "open_keybinding_editor" => OpenKeybindingEditor,
1279
1280 "composite_next_hunk" => CompositeNextHunk,
1281 "composite_prev_hunk" => CompositePrevHunk,
1282
1283 "workspace_trust_trust" => WorkspaceTrustTrust,
1284 "workspace_trust_restrict" => WorkspaceTrustRestrict,
1285 "workspace_trust_block" => WorkspaceTrustBlock,
1286 "workspace_trust_prompt" => WorkspaceTrustPrompt,
1287
1288 "noop" => None,
1289
1290 "open_settings" => OpenSettings,
1291 "close_settings" => CloseSettings,
1292 "settings_save" => SettingsSave,
1293 "settings_reset" => SettingsReset,
1294 "settings_toggle_focus" => SettingsToggleFocus,
1295 "settings_activate" => SettingsActivate,
1296 "settings_search" => SettingsSearch,
1297 "settings_help" => SettingsHelp,
1298 "settings_increment" => SettingsIncrement,
1299 "settings_decrement" => SettingsDecrement,
1300 "settings_inherit" => SettingsInherit,
1301 }
1302 alias {
1303 "toggle_compose_mode" => TogglePageView,
1304 "set_compose_width" => SetPageWidth,
1305 "none" => None,
1310 }
1311 with_char {
1312 "insert_char" => InsertChar,
1313 "set_bookmark" => SetBookmark,
1314 "jump_to_bookmark" => JumpToBookmark,
1315 "clear_bookmark" => ClearBookmark,
1316 "play_macro" => PlayMacro,
1317 "toggle_macro_recording" => ToggleMacroRecording,
1318 "show_macro" => ShowMacro,
1319 }
1320 custom {
1321 "copy_with_theme" => CopyWithTheme : {
1322 let theme = args.get("theme").and_then(|v| v.as_str()).unwrap_or("");
1324 Self::CopyWithTheme(theme.to_string())
1325 },
1326 "menu_open" => MenuOpen : {
1327 let name = args.get("name")?.as_str()?;
1328 Self::MenuOpen(name.to_string())
1329 },
1330 "switch_keybinding_map" => SwitchKeybindingMap : {
1331 let map_name = args.get("map")?.as_str()?;
1332 Self::SwitchKeybindingMap(map_name.to_string())
1333 },
1334 "prompt_confirm_with_text" => PromptConfirmWithText : {
1335 let text = args.get("text")?.as_str()?;
1336 Self::PromptConfirmWithText(text.to_string())
1337 },
1338 }
1339 }
1340
1341 pub fn variant_arg_key(bare_action: &str) -> Option<&'static str> {
1348 match bare_action {
1349 "menu_open" => Some("name"),
1350 "switch_keybinding_map" => Some("map"),
1351 _ => None,
1352 }
1353 }
1354
1355 pub fn qualify_action(bare_action: &str, args: &HashMap<String, serde_json::Value>) -> String {
1359 if let Some(key) = Self::variant_arg_key(bare_action) {
1360 if let Some(v) = args.get(key).and_then(|v| v.as_str()) {
1361 return format!("{}:{}", bare_action, v);
1362 }
1363 }
1364 bare_action.to_string()
1365 }
1366
1367 pub fn to_qualified_action_str(&self) -> String {
1372 match self {
1373 Self::MenuOpen(name) => format!("menu_open:{}", name),
1374 Self::SwitchKeybindingMap(map) => format!("switch_keybinding_map:{}", map),
1375 other => other.to_action_str(),
1376 }
1377 }
1378
1379 pub fn to_action_spec(&self) -> fresh_core::api::ActionSpec {
1392 use serde_json::Value;
1393 let mut args: HashMap<String, Value> = HashMap::new();
1394 match self {
1395 Self::InsertChar(c)
1397 | Self::SetBookmark(c)
1398 | Self::JumpToBookmark(c)
1399 | Self::ClearBookmark(c)
1400 | Self::PlayMacro(c)
1401 | Self::ToggleMacroRecording(c)
1402 | Self::ShowMacro(c) => {
1403 args.insert("char".to_string(), Value::String(c.to_string()));
1404 }
1405 Self::CopyWithTheme(theme) => {
1407 args.insert("theme".to_string(), Value::String(theme.clone()));
1408 }
1409 Self::MenuOpen(name) => {
1410 args.insert("name".to_string(), Value::String(name.clone()));
1411 }
1412 Self::SwitchKeybindingMap(map) => {
1413 args.insert("map".to_string(), Value::String(map.clone()));
1414 }
1415 Self::PromptConfirmWithText(text) => {
1416 args.insert("text".to_string(), Value::String(text.clone()));
1417 }
1418 _ => {}
1420 }
1421 fresh_core::api::ActionSpec {
1422 action: self.to_action_str(),
1423 count: 1,
1424 args,
1425 }
1426 }
1427
1428 pub fn unqualify_action(qualified: &str) -> (String, HashMap<String, serde_json::Value>) {
1433 if let Some((bare, suffix)) = qualified.split_once(':') {
1434 if let Some(arg_key) = Self::variant_arg_key(bare) {
1435 let mut args = HashMap::new();
1436 args.insert(
1437 arg_key.to_string(),
1438 serde_json::Value::String(suffix.to_string()),
1439 );
1440 return (bare.to_string(), args);
1441 }
1442 }
1443 (qualified.to_string(), HashMap::new())
1444 }
1445
1446 pub fn is_movement_or_editing(&self) -> bool {
1449 matches!(
1450 self,
1451 Action::MoveLeft
1453 | Action::MoveRight
1454 | Action::MoveUp
1455 | Action::MoveDown
1456 | Action::MoveWordLeft
1457 | Action::MoveWordRight
1458 | Action::MoveWordEnd
1459 | Action::ViMoveWordEnd
1460 | Action::MoveLeftInLine
1461 | Action::MoveRightInLine
1462 | Action::MoveLineStart
1463 | Action::MoveLineEnd
1464 | Action::MovePageUp
1465 | Action::MovePageDown
1466 | Action::MoveDocumentStart
1467 | Action::MoveDocumentEnd
1468 | Action::MoveToParagraphUp
1469 | Action::MoveToParagraphDown
1470 | Action::SelectLeft
1472 | Action::SelectRight
1473 | Action::SelectUp
1474 | Action::SelectDown
1475 | Action::SelectToParagraphUp
1476 | Action::SelectToParagraphDown
1477 | Action::SelectWordLeft
1478 | Action::SelectWordRight
1479 | Action::SelectWordEnd
1480 | Action::ViSelectWordEnd
1481 | Action::SelectLineStart
1482 | Action::SelectLineEnd
1483 | Action::SelectDocumentStart
1484 | Action::SelectDocumentEnd
1485 | Action::SelectPageUp
1486 | Action::SelectPageDown
1487 | Action::SelectAll
1488 | Action::SelectWord
1489 | Action::SelectLine
1490 | Action::ExpandSelection
1491 | Action::BlockSelectLeft
1493 | Action::BlockSelectRight
1494 | Action::BlockSelectUp
1495 | Action::BlockSelectDown
1496 | Action::InsertChar(_)
1498 | Action::InsertNewline
1499 | Action::InsertTab
1500 | Action::DeleteBackward
1501 | Action::DeleteForward
1502 | Action::DeleteWordBackward
1503 | Action::DeleteWordForward
1504 | Action::DeleteLine
1505 | Action::DeleteToLineEnd
1506 | Action::DeleteToLineStart
1507 | Action::TransposeChars
1508 | Action::OpenLine
1509 | Action::DuplicateLine
1510 | Action::MoveLineUp
1511 | Action::MoveLineDown
1512 | Action::Cut
1514 | Action::Paste
1515 | Action::Undo
1517 | Action::Redo
1518 )
1519 }
1520
1521 pub fn is_editing(&self) -> bool {
1524 matches!(
1525 self,
1526 Action::InsertChar(_)
1527 | Action::InsertNewline
1528 | Action::InsertTab
1529 | Action::DeleteBackward
1530 | Action::DeleteForward
1531 | Action::DeleteWordBackward
1532 | Action::DeleteWordForward
1533 | Action::DeleteLine
1534 | Action::DeleteToLineEnd
1535 | Action::DeleteToLineStart
1536 | Action::DeleteViWordEnd
1537 | Action::TransposeChars
1538 | Action::OpenLine
1539 | Action::DuplicateLine
1540 | Action::MoveLineUp
1541 | Action::MoveLineDown
1542 | Action::Cut
1543 | Action::Paste
1544 )
1545 }
1546}
1547
1548#[derive(Debug, Clone, PartialEq)]
1550pub enum ChordResolution {
1551 Complete(Action),
1553 Partial,
1555 NoMatch,
1557}
1558
1559#[derive(Clone)]
1561pub struct KeybindingResolver {
1562 bindings: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1565
1566 default_bindings: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1568
1569 plugin_defaults: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1572
1573 chord_bindings: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1576
1577 default_chord_bindings: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1579
1580 plugin_chord_defaults: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1582
1583 inheriting_modes: std::collections::HashSet<String>,
1587}
1588
1589impl KeybindingResolver {
1590 pub fn new(config: &Config) -> Self {
1592 let mut resolver = Self {
1593 bindings: HashMap::new(),
1594 default_bindings: HashMap::new(),
1595 plugin_defaults: HashMap::new(),
1596 chord_bindings: HashMap::new(),
1597 default_chord_bindings: HashMap::new(),
1598 plugin_chord_defaults: HashMap::new(),
1599 inheriting_modes: std::collections::HashSet::new(),
1600 };
1601
1602 let map_bindings = config.resolve_keymap(&config.active_keybinding_map);
1604 resolver.load_default_bindings_from_vec(&map_bindings);
1605
1606 resolver.load_bindings_from_vec(&config.keybindings);
1608
1609 resolver
1610 }
1611
1612 pub fn reload_from_config(&mut self, config: &Config) {
1629 let mut rebuilt = Self::new(config);
1630 rebuilt.plugin_defaults = std::mem::take(&mut self.plugin_defaults);
1632 rebuilt.plugin_chord_defaults = std::mem::take(&mut self.plugin_chord_defaults);
1633 rebuilt.inheriting_modes = std::mem::take(&mut self.inheriting_modes);
1634 *self = rebuilt;
1635 }
1636
1637 fn load_default_bindings_from_vec(&mut self, bindings: &[crate::config::Keybinding]) {
1639 for binding in bindings {
1640 let context = if let Some(ref when) = binding.when {
1642 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
1643 } else {
1644 KeyContext::Normal
1645 };
1646
1647 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
1648 if !binding.keys.is_empty() {
1650 let mut sequence = Vec::new();
1652 for key_press in &binding.keys {
1653 if let Some(key_code) = Self::parse_key(&key_press.key) {
1654 let modifiers = Self::parse_modifiers(&key_press.modifiers);
1655 sequence.push((key_code, modifiers));
1656 } else {
1657 break;
1659 }
1660 }
1661
1662 if sequence.len() == binding.keys.len() && !sequence.is_empty() {
1664 self.default_chord_bindings
1665 .entry(context)
1666 .or_default()
1667 .insert(sequence, action);
1668 }
1669 } else if let Some(key_code) = Self::parse_key(&binding.key) {
1670 let modifiers = Self::parse_modifiers(&binding.modifiers);
1672
1673 self.insert_binding_with_equivalents(
1675 context,
1676 key_code,
1677 modifiers,
1678 action,
1679 &binding.key,
1680 );
1681 }
1682 }
1683 }
1684 }
1685
1686 fn insert_binding_with_equivalents(
1689 &mut self,
1690 context: KeyContext,
1691 key_code: KeyCode,
1692 modifiers: KeyModifiers,
1693 action: Action,
1694 key_name: &str,
1695 ) {
1696 let context_bindings = self.default_bindings.entry(context.clone()).or_default();
1697
1698 context_bindings.insert((key_code, modifiers), action.clone());
1700
1701 let equivalents = terminal_key_equivalents(key_code, modifiers);
1703 for (equiv_key, equiv_mods) in equivalents {
1704 if let Some(existing_action) = context_bindings.get(&(equiv_key, equiv_mods)) {
1706 if existing_action != &action {
1708 let equiv_name = format!("{:?}", equiv_key);
1709 tracing::warn!(
1710 "Terminal key equivalent conflict in {:?} context: {} (equivalent of {}) \
1711 is bound to {:?}, but {} is bound to {:?}. \
1712 The explicit binding takes precedence.",
1713 context,
1714 equiv_name,
1715 key_name,
1716 existing_action,
1717 key_name,
1718 action
1719 );
1720 }
1721 } else {
1723 context_bindings.insert((equiv_key, equiv_mods), action.clone());
1725 }
1726 }
1727 }
1728
1729 fn load_bindings_from_vec(&mut self, bindings: &[crate::config::Keybinding]) {
1731 for binding in bindings {
1732 let context = if let Some(ref when) = binding.when {
1734 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
1735 } else {
1736 KeyContext::Normal
1737 };
1738
1739 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
1740 if !binding.keys.is_empty() {
1742 let mut sequence = Vec::new();
1744 for key_press in &binding.keys {
1745 if let Some(key_code) = Self::parse_key(&key_press.key) {
1746 let modifiers = Self::parse_modifiers(&key_press.modifiers);
1747 sequence.push((key_code, modifiers));
1748 } else {
1749 break;
1751 }
1752 }
1753
1754 if sequence.len() == binding.keys.len() && !sequence.is_empty() {
1756 self.chord_bindings
1757 .entry(context)
1758 .or_default()
1759 .insert(sequence, action);
1760 }
1761 } else if let Some(key_code) = Self::parse_key(&binding.key) {
1762 let modifiers = Self::parse_modifiers(&binding.modifiers);
1764 self.bindings
1765 .entry(context)
1766 .or_default()
1767 .insert((key_code, modifiers), action);
1768 }
1769 }
1770 }
1771 }
1772
1773 pub fn load_plugin_default(
1775 &mut self,
1776 context: KeyContext,
1777 key_code: KeyCode,
1778 modifiers: KeyModifiers,
1779 action: Action,
1780 ) {
1781 self.plugin_defaults
1782 .entry(context)
1783 .or_default()
1784 .insert((key_code, modifiers), action);
1785 }
1786
1787 pub fn load_plugin_chord_default(
1789 &mut self,
1790 context: KeyContext,
1791 sequence: Vec<(KeyCode, KeyModifiers)>,
1792 action: Action,
1793 ) {
1794 self.plugin_chord_defaults
1795 .entry(context)
1796 .or_default()
1797 .insert(sequence, action);
1798 }
1799
1800 pub fn clear_plugin_defaults_for_mode(&mut self, mode_name: &str) {
1802 let context = KeyContext::Mode(mode_name.to_string());
1803 self.plugin_defaults.remove(&context);
1804 self.plugin_chord_defaults.remove(&context);
1805 self.inheriting_modes.remove(mode_name);
1806 }
1807
1808 pub fn set_mode_inherits_normal_bindings(&mut self, mode_name: &str, inherit: bool) {
1811 if inherit {
1812 self.inheriting_modes.insert(mode_name.to_string());
1813 } else {
1814 self.inheriting_modes.remove(mode_name);
1815 }
1816 }
1817
1818 pub fn get_plugin_defaults(
1820 &self,
1821 ) -> &HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>> {
1822 &self.plugin_defaults
1823 }
1824
1825 fn is_application_wide_action(action: &Action) -> bool {
1827 matches!(
1828 action,
1829 Action::Quit
1830 | Action::ForceQuit
1831 | Action::Save
1832 | Action::SaveAs
1833 | Action::ShowHelp
1834 | Action::ShowKeyboardShortcuts
1835 | Action::PromptCancel | Action::PopupCancel )
1838 }
1839
1840 pub fn is_terminal_ui_action(action: &Action) -> bool {
1844 matches!(
1845 action,
1846 Action::CommandPalette
1848 | Action::QuickOpen
1849 | Action::QuickOpenBuffers
1850 | Action::QuickOpenFiles
1851 | Action::OpenLiveGrep
1852 | Action::ResumeLiveGrep
1853 | Action::ToggleUtilityDock
1854 | Action::OpenTerminalInDock
1855 | Action::ToggleDockFocus
1856 | Action::CycleLiveGrepProvider
1857 | Action::OpenSettings
1858 | Action::MenuActivate
1859 | Action::MenuOpen(_)
1860 | Action::ShowHelp
1861 | Action::ShowKeyboardShortcuts
1862 | Action::Quit
1863 | Action::ForceQuit
1864 | Action::NextSplit
1866 | Action::PrevSplit
1867 | Action::NextWindow
1869 | Action::PrevWindow
1870 | Action::SplitHorizontal
1871 | Action::SplitVertical
1872 | Action::CloseSplit
1873 | Action::ToggleMaximizeSplit
1874 | Action::NextBuffer
1876 | Action::PrevBuffer
1877 | Action::Close
1878 | Action::CloseTab
1879 | Action::ScrollTabsLeft
1880 | Action::ScrollTabsRight
1881 | Action::TerminalEscape
1883 | Action::ToggleKeyboardCapture
1884 | Action::OpenTerminal
1885 | Action::OpenTerminalRight
1886 | Action::OpenTerminalBelow
1887 | Action::CloseTerminal
1888 | Action::TerminalPaste
1889 | Action::ToggleFileExplorer
1891 | Action::ToggleFileExplorerSide
1892 | Action::ToggleMenuBar
1894 )
1895 }
1896
1897 pub fn resolve_chord(
1903 &self,
1904 chord_state: &[(KeyCode, KeyModifiers)],
1905 event: &KeyEvent,
1906 context: KeyContext,
1907 ) -> ChordResolution {
1908 let mut full_sequence: Vec<(KeyCode, KeyModifiers)> = chord_state
1910 .iter()
1911 .map(|(c, m)| normalize_key(*c, *m))
1912 .collect();
1913 let (norm_code, norm_mods) = normalize_key(event.code, event.modifiers);
1914 full_sequence.push((norm_code, norm_mods));
1915
1916 tracing::trace!(
1917 "KeybindingResolver.resolve_chord: sequence={:?}, context={:?}",
1918 full_sequence,
1919 context
1920 );
1921
1922 let search_order = vec![
1924 (&self.chord_bindings, &KeyContext::Global, "custom global"),
1925 (
1926 &self.default_chord_bindings,
1927 &KeyContext::Global,
1928 "default global",
1929 ),
1930 (&self.chord_bindings, &context, "custom context"),
1931 (&self.default_chord_bindings, &context, "default context"),
1932 (
1933 &self.plugin_chord_defaults,
1934 &context,
1935 "plugin default context",
1936 ),
1937 ];
1938
1939 let mut has_partial_match = false;
1940
1941 for (binding_map, bind_context, label) in search_order {
1942 if let Some(context_chords) = binding_map.get(bind_context) {
1943 if let Some(action) = context_chords.get(&full_sequence) {
1945 tracing::trace!(" -> Complete chord match in {}: {:?}", label, action);
1946 return ChordResolution::Complete(action.clone());
1947 }
1948
1949 for (chord_seq, _) in context_chords.iter() {
1951 if chord_seq.len() > full_sequence.len()
1952 && chord_seq[..full_sequence.len()] == full_sequence[..]
1953 {
1954 tracing::trace!(" -> Partial chord match in {}", label);
1955 has_partial_match = true;
1956 break;
1957 }
1958 }
1959 }
1960 }
1961
1962 if has_partial_match {
1963 ChordResolution::Partial
1964 } else {
1965 tracing::trace!(" -> No chord match");
1966 ChordResolution::NoMatch
1967 }
1968 }
1969
1970 pub fn resolve(&self, event: &KeyEvent, context: KeyContext) -> Action {
1972 let (norm_code, norm_mods) = normalize_key(event.code, event.modifiers);
1975 let norm = &(norm_code, norm_mods);
1976 tracing::trace!(
1977 "KeybindingResolver.resolve: code={:?}, modifiers={:?}, context={:?}",
1978 event.code,
1979 event.modifiers,
1980 context
1981 );
1982
1983 if let Some(global_bindings) = self.bindings.get(&KeyContext::Global) {
1985 if let Some(action) = global_bindings.get(norm) {
1986 tracing::trace!(" -> Found in custom global bindings: {:?}", action);
1987 return action.clone();
1988 }
1989 }
1990
1991 if let Some(global_bindings) = self.default_bindings.get(&KeyContext::Global) {
1992 if let Some(action) = global_bindings.get(norm) {
1993 tracing::trace!(" -> Found in default global bindings: {:?}", action);
1994 return action.clone();
1995 }
1996 }
1997
1998 if let Some(context_bindings) = self.bindings.get(&context) {
2000 if let Some(action) = context_bindings.get(norm) {
2001 tracing::trace!(
2002 " -> Found in custom {} bindings: {:?}",
2003 context.to_when_clause(),
2004 action
2005 );
2006 return action.clone();
2007 }
2008 }
2009
2010 if let Some(context_bindings) = self.default_bindings.get(&context) {
2012 if let Some(action) = context_bindings.get(norm) {
2013 tracing::trace!(
2014 " -> Found in default {} bindings: {:?}",
2015 context.to_when_clause(),
2016 action
2017 );
2018 return action.clone();
2019 }
2020 }
2021
2022 if let Some(plugin_bindings) = self.plugin_defaults.get(&context) {
2024 if let Some(action) = plugin_bindings.get(norm) {
2025 tracing::trace!(
2026 " -> Found in plugin default {} bindings: {:?}",
2027 context.to_when_clause(),
2028 action
2029 );
2030 return action.clone();
2031 }
2032 }
2033
2034 if let Some(parent) = context.parent_context() {
2040 if let Some(parent_bindings) = self.bindings.get(&parent) {
2041 if let Some(action) = parent_bindings.get(norm) {
2042 return action.clone();
2043 }
2044 }
2045 if let Some(parent_bindings) = self.default_bindings.get(&parent) {
2046 if let Some(action) = parent_bindings.get(norm) {
2047 return action.clone();
2048 }
2049 }
2050 }
2051
2052 if context != KeyContext::Normal {
2056 let full_fallthrough = context.allows_normal_fallthrough()
2057 || matches!(&context, KeyContext::Mode(name) if self.inheriting_modes.contains(name));
2058
2059 let ui_fallthrough = context.allows_ui_fallthrough();
2060
2061 let custom_normal_has_binding = self
2070 .bindings
2071 .get(&KeyContext::Normal)
2072 .and_then(|m| m.get(norm))
2073 .is_some();
2074
2075 if let Some(normal_bindings) = self.bindings.get(&KeyContext::Normal) {
2076 if let Some(action) = normal_bindings.get(norm) {
2077 if full_fallthrough
2078 || Self::is_application_wide_action(action)
2079 || (ui_fallthrough && Self::is_terminal_ui_action(action))
2080 {
2081 tracing::trace!(
2082 " -> Found action in custom normal bindings (fallthrough): {:?}",
2083 action
2084 );
2085 return action.clone();
2086 }
2087 }
2088 }
2089
2090 if !custom_normal_has_binding {
2091 if let Some(normal_bindings) = self.default_bindings.get(&KeyContext::Normal) {
2092 if let Some(action) = normal_bindings.get(norm) {
2093 if full_fallthrough
2094 || Self::is_application_wide_action(action)
2095 || (ui_fallthrough && Self::is_terminal_ui_action(action))
2096 {
2097 tracing::trace!(
2098 " -> Found action in default normal bindings (fallthrough): {:?}",
2099 action
2100 );
2101 return action.clone();
2102 }
2103 }
2104 }
2105 }
2106 }
2107
2108 if context.allows_text_input() && is_text_input_modifier(event.modifiers) {
2110 if let KeyCode::Char(c) = event.code {
2111 tracing::trace!(" -> Character input: '{}'", c);
2112 return Action::InsertChar(c);
2113 }
2114 }
2115
2116 tracing::trace!(" -> No binding found, returning Action::None");
2117 Action::None
2118 }
2119
2120 pub fn resolve_in_context_only(&self, event: &KeyEvent, context: KeyContext) -> Option<Action> {
2125 let norm = normalize_key(event.code, event.modifiers);
2126 if let Some(context_bindings) = self.bindings.get(&context) {
2128 if let Some(action) = context_bindings.get(&norm) {
2129 return Some(action.clone());
2130 }
2131 }
2132
2133 if let Some(context_bindings) = self.default_bindings.get(&context) {
2135 if let Some(action) = context_bindings.get(&norm) {
2136 return Some(action.clone());
2137 }
2138 }
2139
2140 None
2141 }
2142
2143 pub fn has_explicit_binding(&self, event: &KeyEvent, context: &KeyContext) -> bool {
2151 let norm = normalize_key(event.code, event.modifiers);
2152 if let Some(bindings) = self.bindings.get(context) {
2153 if bindings.contains_key(&norm) {
2154 return true;
2155 }
2156 }
2157 if let Some(bindings) = self.default_bindings.get(context) {
2158 if bindings.contains_key(&norm) {
2159 return true;
2160 }
2161 }
2162 if let Some(bindings) = self.plugin_defaults.get(context) {
2163 if bindings.contains_key(&norm) {
2164 return true;
2165 }
2166 }
2167 false
2168 }
2169
2170 pub fn resolve_terminal_ui_action(&self, event: &KeyEvent) -> Action {
2174 let norm = normalize_key(event.code, event.modifiers);
2175 tracing::trace!(
2176 "KeybindingResolver.resolve_terminal_ui_action: code={:?}, modifiers={:?}",
2177 event.code,
2178 event.modifiers
2179 );
2180
2181 for bindings in [&self.bindings, &self.default_bindings] {
2183 if let Some(terminal_bindings) = bindings.get(&KeyContext::Terminal) {
2184 if let Some(action) = terminal_bindings.get(&norm) {
2185 if Self::is_terminal_ui_action(action) {
2186 tracing::trace!(" -> Found UI action in terminal bindings: {:?}", action);
2187 return action.clone();
2188 }
2189 }
2190 }
2191 }
2192
2193 for bindings in [&self.bindings, &self.default_bindings] {
2195 if let Some(global_bindings) = bindings.get(&KeyContext::Global) {
2196 if let Some(action) = global_bindings.get(&norm) {
2197 if Self::is_terminal_ui_action(action) {
2198 tracing::trace!(" -> Found UI action in global bindings: {:?}", action);
2199 return action.clone();
2200 }
2201 }
2202 }
2203 }
2204
2205 for bindings in [&self.bindings, &self.default_bindings] {
2207 if let Some(normal_bindings) = bindings.get(&KeyContext::Normal) {
2208 if let Some(action) = normal_bindings.get(&norm) {
2209 if Self::is_terminal_ui_action(action) {
2210 tracing::trace!(" -> Found UI action in normal bindings: {:?}", action);
2211 return action.clone();
2212 }
2213 }
2214 }
2215 }
2216
2217 tracing::trace!(" -> No UI action found");
2218 Action::None
2219 }
2220
2221 pub fn bound_plugin_action_names(&self) -> std::collections::HashSet<String> {
2227 let mut names = std::collections::HashSet::new();
2228 for map in self.bindings.values().chain(self.default_bindings.values()) {
2229 for action in map.values() {
2230 if let Action::PluginAction(name) = action {
2231 names.insert(name.clone());
2232 }
2233 }
2234 }
2235 names
2236 }
2237
2238 pub fn find_keybinding_for_action(
2239 &self,
2240 action_name: &str,
2241 context: KeyContext,
2242 ) -> Option<String> {
2243 let target_action = Action::from_str(action_name, &HashMap::new())?;
2245
2246 let search_maps = vec![
2248 self.bindings.get(&context),
2249 self.bindings.get(&KeyContext::Global),
2250 self.default_bindings.get(&context),
2251 self.default_bindings.get(&KeyContext::Global),
2252 ];
2253
2254 for map in search_maps.into_iter().flatten() {
2255 let mut matches: Vec<(KeyCode, KeyModifiers)> = map
2257 .iter()
2258 .filter(|(_, action)| {
2259 match (*action, &target_action) {
2264 (Action::PluginAction(a), Action::PluginAction(b)) => a == b,
2265 (a, b) => std::mem::discriminant(a) == std::mem::discriminant(b),
2266 }
2267 })
2268 .map(|((key_code, modifiers), _)| (*key_code, *modifiers))
2269 .collect();
2270
2271 if !matches.is_empty() {
2272 matches.sort_by(|(key_a, mod_a), (key_b, mod_b)| {
2274 let mod_count_a = mod_a.bits().count_ones();
2276 let mod_count_b = mod_b.bits().count_ones();
2277 match mod_count_a.cmp(&mod_count_b) {
2278 std::cmp::Ordering::Equal => {
2279 match mod_a.bits().cmp(&mod_b.bits()) {
2281 std::cmp::Ordering::Equal => {
2282 Self::key_code_sort_key(key_a)
2284 .cmp(&Self::key_code_sort_key(key_b))
2285 }
2286 other => other,
2287 }
2288 }
2289 other => other,
2290 }
2291 });
2292
2293 let (key_code, modifiers) = matches[0];
2294 return Some(format_keybinding(&key_code, &modifiers));
2295 }
2296 }
2297
2298 None
2299 }
2300
2301 fn key_code_sort_key(key_code: &KeyCode) -> (u8, u32) {
2303 match key_code {
2304 KeyCode::Char(c) => (0, *c as u32),
2305 KeyCode::F(n) => (1, *n as u32),
2306 KeyCode::Enter => (2, 0),
2307 KeyCode::Tab => (2, 1),
2308 KeyCode::Backspace => (2, 2),
2309 KeyCode::Delete => (2, 3),
2310 KeyCode::Esc => (2, 4),
2311 KeyCode::Left => (3, 0),
2312 KeyCode::Right => (3, 1),
2313 KeyCode::Up => (3, 2),
2314 KeyCode::Down => (3, 3),
2315 KeyCode::Home => (3, 4),
2316 KeyCode::End => (3, 5),
2317 KeyCode::PageUp => (3, 6),
2318 KeyCode::PageDown => (3, 7),
2319 _ => (255, 0),
2320 }
2321 }
2322
2323 pub fn find_menu_mnemonic(&self, menu_name: &str) -> Option<char> {
2326 let search_maps = vec![
2328 self.bindings.get(&KeyContext::Normal),
2329 self.bindings.get(&KeyContext::Global),
2330 self.default_bindings.get(&KeyContext::Normal),
2331 self.default_bindings.get(&KeyContext::Global),
2332 ];
2333
2334 for map in search_maps.into_iter().flatten() {
2335 for ((key_code, modifiers), action) in map {
2336 if let Action::MenuOpen(name) = action {
2338 if name.eq_ignore_ascii_case(menu_name) && *modifiers == KeyModifiers::ALT {
2339 if let KeyCode::Char(c) = key_code {
2341 return Some(c.to_ascii_lowercase());
2342 }
2343 }
2344 }
2345 }
2346 }
2347
2348 None
2349 }
2350
2351 fn parse_key(key: &str) -> Option<KeyCode> {
2353 let lower = key.to_lowercase();
2354 match lower.as_str() {
2355 "enter" => Some(KeyCode::Enter),
2356 "backspace" => Some(KeyCode::Backspace),
2357 "delete" | "del" => Some(KeyCode::Delete),
2358 "tab" => Some(KeyCode::Tab),
2359 "backtab" => Some(KeyCode::BackTab),
2360 "esc" | "escape" => Some(KeyCode::Esc),
2361 "space" => Some(KeyCode::Char(' ')),
2362
2363 "left" => Some(KeyCode::Left),
2364 "right" => Some(KeyCode::Right),
2365 "up" => Some(KeyCode::Up),
2366 "down" => Some(KeyCode::Down),
2367 "home" => Some(KeyCode::Home),
2368 "end" => Some(KeyCode::End),
2369 "pageup" => Some(KeyCode::PageUp),
2370 "pagedown" => Some(KeyCode::PageDown),
2371
2372 s if s.len() == 1 => s.chars().next().map(KeyCode::Char),
2373 s if s.starts_with('f') && s.len() >= 2 => s[1..].parse::<u8>().ok().map(KeyCode::F),
2375 _ => None,
2376 }
2377 }
2378
2379 fn parse_modifiers(modifiers: &[String]) -> KeyModifiers {
2381 let mut result = KeyModifiers::empty();
2382 for m in modifiers {
2383 match m.to_lowercase().as_str() {
2384 "ctrl" | "control" => result |= KeyModifiers::CONTROL,
2385 "shift" => result |= KeyModifiers::SHIFT,
2386 "alt" => result |= KeyModifiers::ALT,
2387 "super" | "cmd" | "command" | "meta" => result |= KeyModifiers::SUPER,
2388 _ => {}
2389 }
2390 }
2391 result
2392 }
2393
2394 pub fn get_all_bindings(&self) -> Vec<(String, String)> {
2398 let mut bindings = Vec::new();
2399
2400 for context in &[
2402 KeyContext::Normal,
2403 KeyContext::Prompt,
2404 KeyContext::Popup,
2405 KeyContext::FileExplorer,
2406 KeyContext::Menu,
2407 KeyContext::CompositeBuffer,
2408 ] {
2409 let mut all_keys: HashMap<(KeyCode, KeyModifiers), Action> = HashMap::new();
2410
2411 if let Some(context_defaults) = self.default_bindings.get(context) {
2413 for (key, action) in context_defaults {
2414 all_keys.insert(*key, action.clone());
2415 }
2416 }
2417
2418 if let Some(context_bindings) = self.bindings.get(context) {
2420 for (key, action) in context_bindings {
2421 all_keys.insert(*key, action.clone());
2422 }
2423 }
2424
2425 let context_str = if *context != KeyContext::Normal {
2427 format!("[{}] ", context.to_when_clause())
2428 } else {
2429 String::new()
2430 };
2431
2432 for ((key_code, modifiers), action) in all_keys {
2433 let key_str = Self::format_key(key_code, modifiers);
2434 let action_str = format!("{}{}", context_str, Self::format_action(&action));
2435 bindings.push((key_str, action_str));
2436 }
2437 }
2438
2439 bindings.sort_by(|a, b| a.1.cmp(&b.1));
2441
2442 bindings
2443 }
2444
2445 fn format_key(key_code: KeyCode, modifiers: KeyModifiers) -> String {
2447 format_keybinding(&key_code, &modifiers)
2448 }
2449
2450 pub fn format_action(action: &Action) -> String {
2452 match action {
2453 Action::InsertChar(c) => t!("action.insert_char", char = c),
2454 Action::InsertNewline => t!("action.insert_newline"),
2455 Action::InsertTab => t!("action.insert_tab"),
2456 Action::MoveLeft => t!("action.move_left"),
2457 Action::MoveRight => t!("action.move_right"),
2458 Action::MoveUp => t!("action.move_up"),
2459 Action::MoveDown => t!("action.move_down"),
2460 Action::MoveWordLeft => t!("action.move_word_left"),
2461 Action::MoveWordRight => t!("action.move_word_right"),
2462 Action::MoveWordEnd => t!("action.move_word_end"),
2463 Action::ViMoveWordEnd => t!("action.move_word_end"),
2464 Action::MoveLeftInLine => t!("action.move_left"),
2465 Action::MoveRightInLine => t!("action.move_right"),
2466 Action::MoveLineStart => t!("action.move_line_start"),
2467 Action::MoveLineEnd => t!("action.move_line_end"),
2468 Action::MoveLineUp => t!("action.move_line_up"),
2469 Action::MoveLineDown => t!("action.move_line_down"),
2470 Action::MovePageUp => t!("action.move_page_up"),
2471 Action::MovePageDown => t!("action.move_page_down"),
2472 Action::MoveDocumentStart => t!("action.move_document_start"),
2473 Action::MoveDocumentEnd => t!("action.move_document_end"),
2474 Action::SelectLeft => t!("action.select_left"),
2475 Action::SelectRight => t!("action.select_right"),
2476 Action::SelectUp => t!("action.select_up"),
2477 Action::SelectDown => t!("action.select_down"),
2478 Action::SelectToParagraphUp => t!("action.select_to_paragraph_up"),
2479 Action::SelectToParagraphDown => t!("action.select_to_paragraph_down"),
2480 Action::MoveToParagraphUp => t!("action.move_to_paragraph_up"),
2481 Action::MoveToParagraphDown => t!("action.move_to_paragraph_down"),
2482 Action::SelectWordLeft => t!("action.select_word_left"),
2483 Action::SelectWordRight => t!("action.select_word_right"),
2484 Action::SelectWordEnd => t!("action.select_word_end"),
2485 Action::ViSelectWordEnd => t!("action.select_word_end"),
2486 Action::SelectLineStart => t!("action.select_line_start"),
2487 Action::SelectLineEnd => t!("action.select_line_end"),
2488 Action::SelectDocumentStart => t!("action.select_document_start"),
2489 Action::SelectDocumentEnd => t!("action.select_document_end"),
2490 Action::SelectPageUp => t!("action.select_page_up"),
2491 Action::SelectPageDown => t!("action.select_page_down"),
2492 Action::SelectAll => t!("action.select_all"),
2493 Action::SelectWord => t!("action.select_word"),
2494 Action::SelectLine => t!("action.select_line"),
2495 Action::ExpandSelection => t!("action.expand_selection"),
2496 Action::BlockSelectLeft => t!("action.block_select_left"),
2497 Action::BlockSelectRight => t!("action.block_select_right"),
2498 Action::BlockSelectUp => t!("action.block_select_up"),
2499 Action::BlockSelectDown => t!("action.block_select_down"),
2500 Action::DeleteBackward => t!("action.delete_backward"),
2501 Action::DeleteForward => t!("action.delete_forward"),
2502 Action::DeleteWordBackward => t!("action.delete_word_backward"),
2503 Action::DeleteWordForward => t!("action.delete_word_forward"),
2504 Action::DeleteLine => t!("action.delete_line"),
2505 Action::DeleteToLineEnd => t!("action.delete_to_line_end"),
2506 Action::DeleteToLineStart => t!("action.delete_to_line_start"),
2507 Action::DeleteViWordEnd => t!("action.delete_word_forward"),
2508 Action::TransposeChars => t!("action.transpose_chars"),
2509 Action::OpenLine => t!("action.open_line"),
2510 Action::DuplicateLine => t!("action.duplicate_line"),
2511 Action::Recenter => t!("action.recenter"),
2512 Action::SetMark => t!("action.set_mark"),
2513 Action::CancelMark => t!("action.cancel_mark"),
2514 Action::ClearMark => t!("action.clear_mark"),
2515 Action::Copy => t!("action.copy"),
2516 Action::CopyWithTheme(theme) if theme.is_empty() => t!("action.copy_with_formatting"),
2517 Action::CopyWithTheme(theme) => t!("action.copy_with_theme", theme = theme),
2518 Action::Cut => t!("action.cut"),
2519 Action::Paste => t!("action.paste"),
2520 Action::CopyFilePath => t!("action.copy_file_path"),
2521 Action::CopyRelativeFilePath => t!("action.copy_relative_file_path"),
2522 Action::YankWordForward => t!("action.yank_word_forward"),
2523 Action::YankWordBackward => t!("action.yank_word_backward"),
2524 Action::YankToLineEnd => t!("action.yank_to_line_end"),
2525 Action::YankToLineStart => t!("action.yank_to_line_start"),
2526 Action::YankViWordEnd => t!("action.yank_word_forward"),
2527 Action::AddCursorAbove => t!("action.add_cursor_above"),
2528 Action::AddCursorBelow => t!("action.add_cursor_below"),
2529 Action::AddCursorNextMatch => t!("action.add_cursor_next_match"),
2530 Action::AddCursorsToLineEnds => t!("action.add_cursors_to_line_ends"),
2531 Action::RemoveSecondaryCursors => t!("action.remove_secondary_cursors"),
2532 Action::Save => t!("action.save"),
2533 Action::SaveAs => t!("action.save_as"),
2534 Action::Open => t!("action.open"),
2535 Action::SwitchProject => t!("action.switch_project"),
2536 Action::New => t!("action.new"),
2537 Action::Close => t!("action.close"),
2538 Action::CloseTab => t!("action.close_tab"),
2539 Action::Quit => t!("action.quit"),
2540 Action::ForceQuit => t!("action.force_quit"),
2541 Action::Detach => t!("action.detach"),
2542 Action::Revert => t!("action.revert"),
2543 Action::ToggleAutoRevert => t!("action.toggle_auto_revert"),
2544 Action::FormatBuffer => t!("action.format_buffer"),
2545 Action::TrimTrailingWhitespace => t!("action.trim_trailing_whitespace"),
2546 Action::EnsureFinalNewline => t!("action.ensure_final_newline"),
2547 Action::GotoLine => t!("action.goto_line"),
2548 Action::ScanLineIndex => t!("action.scan_line_index"),
2549 Action::GoToMatchingBracket => t!("action.goto_matching_bracket"),
2550 Action::JumpToNextError => t!("action.jump_to_next_error"),
2551 Action::JumpToPreviousError => t!("action.jump_to_previous_error"),
2552 Action::SmartHome => t!("action.smart_home"),
2553 Action::DedentSelection => t!("action.dedent_selection"),
2554 Action::ToggleComment => t!("action.toggle_comment"),
2555 Action::DabbrevExpand => std::borrow::Cow::Borrowed("Expand abbreviation (dabbrev)"),
2556 Action::ToggleFold => t!("action.toggle_fold"),
2557 Action::SetBookmark(c) => t!("action.set_bookmark", key = c),
2558 Action::JumpToBookmark(c) => t!("action.jump_to_bookmark", key = c),
2559 Action::ClearBookmark(c) => t!("action.clear_bookmark", key = c),
2560 Action::ListBookmarks => t!("action.list_bookmarks"),
2561 Action::ToggleSearchCaseSensitive => t!("action.toggle_search_case_sensitive"),
2562 Action::ToggleSearchWholeWord => t!("action.toggle_search_whole_word"),
2563 Action::ToggleSearchRegex => t!("action.toggle_search_regex"),
2564 Action::ToggleSearchConfirmEach => t!("action.toggle_search_confirm_each"),
2565 Action::StartMacroRecording => t!("action.start_macro_recording"),
2566 Action::StopMacroRecording => t!("action.stop_macro_recording"),
2567 Action::PlayMacro(c) => t!("action.play_macro", key = c),
2568 Action::ToggleMacroRecording(c) => t!("action.toggle_macro_recording", key = c),
2569 Action::ShowMacro(c) => t!("action.show_macro", key = c),
2570 Action::ListMacros => t!("action.list_macros"),
2571 Action::PromptRecordMacro => t!("action.prompt_record_macro"),
2572 Action::PromptPlayMacro => t!("action.prompt_play_macro"),
2573 Action::PlayLastMacro => t!("action.play_last_macro"),
2574 Action::PromptSaveMacroToInit => t!("action.prompt_save_macro_to_init"),
2575 Action::PromptPromoteMacro => t!("action.prompt_promote_macro"),
2576 Action::PromptSetBookmark => t!("action.prompt_set_bookmark"),
2577 Action::PromptJumpToBookmark => t!("action.prompt_jump_to_bookmark"),
2578 Action::Undo => t!("action.undo"),
2579 Action::Redo => t!("action.redo"),
2580 Action::ScrollUp => t!("action.scroll_up"),
2581 Action::ScrollDown => t!("action.scroll_down"),
2582 Action::ShowHelp => t!("action.show_help"),
2583 Action::ShowKeyboardShortcuts => t!("action.show_keyboard_shortcuts"),
2584 Action::ShowWarnings => t!("action.show_warnings"),
2585 Action::ShowStatusLog => t!("action.show_status_log"),
2586 Action::ShowLspStatus => t!("action.show_lsp_status"),
2587 Action::ShowRemoteIndicatorMenu => t!("action.show_remote_indicator_menu"),
2588 Action::ShowReadOnlyMenu => t!("action.show_read_only_menu"),
2589 Action::ClearWarnings => t!("action.clear_warnings"),
2590 Action::CommandPalette => t!("action.command_palette"),
2591 Action::QuickOpen => t!("action.quick_open"),
2592 Action::QuickOpenBuffers => t!("action.quick_open_buffers"),
2593 Action::QuickOpenFiles => t!("action.quick_open_files"),
2594 Action::OpenLiveGrep => t!("action.open_live_grep"),
2595 Action::ResumeLiveGrep => t!("action.resume_live_grep"),
2596 Action::ToggleUtilityDock => t!("action.toggle_utility_dock"),
2597 Action::OpenTerminalInDock => t!("action.open_terminal_in_dock"),
2598 Action::CycleLiveGrepProvider => t!("action.cycle_live_grep_provider"),
2599 Action::InspectThemeAtCursor => t!("action.inspect_theme_at_cursor"),
2600 Action::ToggleLineWrap => t!("action.toggle_line_wrap"),
2601 Action::ToggleCurrentLineHighlight => t!("action.toggle_current_line_highlight"),
2602 Action::ToggleOccurrenceHighlight => t!("action.toggle_occurrence_highlight"),
2603 Action::ToggleReadOnly => t!("action.toggle_read_only"),
2604 Action::TogglePageView => t!("action.toggle_page_view"),
2605 Action::SetPageWidth => t!("action.set_page_width"),
2606 Action::NextBuffer => t!("action.next_buffer"),
2607 Action::PrevBuffer => t!("action.prev_buffer"),
2608 Action::NavigateBack => t!("action.navigate_back"),
2609 Action::NavigateForward => t!("action.navigate_forward"),
2610 Action::SplitHorizontal => t!("action.split_horizontal"),
2611 Action::SplitVertical => t!("action.split_vertical"),
2612 Action::CloseSplit => t!("action.close_split"),
2613 Action::NextSplit => t!("action.next_split"),
2614 Action::PrevSplit => t!("action.prev_split"),
2615 Action::NextWindow => t!("action.next_window"),
2616 Action::PrevWindow => t!("action.prev_window"),
2617 Action::IncreaseSplitSize => t!("action.increase_split_size"),
2618 Action::DecreaseSplitSize => t!("action.decrease_split_size"),
2619 Action::ToggleMaximizeSplit => t!("action.toggle_maximize_split"),
2620 Action::PromptConfirm => t!("action.prompt_confirm"),
2621 Action::PromptConfirmWithText(ref text) => {
2622 format!("{} ({})", t!("action.prompt_confirm"), text).into()
2623 }
2624 Action::PromptCancel => t!("action.prompt_cancel"),
2625 Action::PromptBackspace => t!("action.prompt_backspace"),
2626 Action::PromptDelete => t!("action.prompt_delete"),
2627 Action::PromptMoveLeft => t!("action.prompt_move_left"),
2628 Action::PromptMoveRight => t!("action.prompt_move_right"),
2629 Action::PromptMoveStart => t!("action.prompt_move_start"),
2630 Action::PromptMoveEnd => t!("action.prompt_move_end"),
2631 Action::PromptSelectPrev => t!("action.prompt_select_prev"),
2632 Action::PromptSelectNext => t!("action.prompt_select_next"),
2633 Action::PromptPageUp => t!("action.prompt_page_up"),
2634 Action::PromptPageDown => t!("action.prompt_page_down"),
2635 Action::PromptAcceptSuggestion => t!("action.prompt_accept_suggestion"),
2636 Action::PromptMoveWordLeft => t!("action.prompt_move_word_left"),
2637 Action::PromptMoveWordRight => t!("action.prompt_move_word_right"),
2638 Action::PromptDeleteWordForward => t!("action.prompt_delete_word_forward"),
2639 Action::PromptDeleteWordBackward => t!("action.prompt_delete_word_backward"),
2640 Action::PromptDeleteToLineEnd => t!("action.prompt_delete_to_line_end"),
2641 Action::PromptCopy => t!("action.prompt_copy"),
2642 Action::PromptCut => t!("action.prompt_cut"),
2643 Action::PromptPaste => t!("action.prompt_paste"),
2644 Action::PromptMoveLeftSelecting => t!("action.prompt_move_left_selecting"),
2645 Action::PromptMoveRightSelecting => t!("action.prompt_move_right_selecting"),
2646 Action::PromptMoveHomeSelecting => t!("action.prompt_move_home_selecting"),
2647 Action::PromptMoveEndSelecting => t!("action.prompt_move_end_selecting"),
2648 Action::PromptSelectWordLeft => t!("action.prompt_select_word_left"),
2649 Action::PromptSelectWordRight => t!("action.prompt_select_word_right"),
2650 Action::PromptSelectAll => t!("action.prompt_select_all"),
2651 Action::FileBrowserToggleHidden => t!("action.file_browser_toggle_hidden"),
2652 Action::FileBrowserToggleDetectEncoding => {
2653 t!("action.file_browser_toggle_detect_encoding")
2654 }
2655 Action::PopupSelectNext => t!("action.popup_select_next"),
2656 Action::PopupSelectPrev => t!("action.popup_select_prev"),
2657 Action::PopupPageUp => t!("action.popup_page_up"),
2658 Action::PopupPageDown => t!("action.popup_page_down"),
2659 Action::PopupConfirm => t!("action.popup_confirm"),
2660 Action::PopupCancel => t!("action.popup_cancel"),
2661 Action::PopupFocus => t!("action.popup_focus"),
2662 Action::CompletionAccept => t!("action.completion_accept"),
2663 Action::CompletionDismiss => t!("action.completion_dismiss"),
2664 Action::ToggleFileExplorer => t!("action.toggle_file_explorer"),
2665 Action::ToggleFileExplorerSide => t!("action.toggle_file_explorer_side"),
2666 Action::ToggleMenuBar => t!("action.toggle_menu_bar"),
2667 Action::ToggleTabBar => t!("action.toggle_tab_bar"),
2668 Action::ToggleStatusBar => t!("action.toggle_status_bar"),
2669 Action::TogglePromptLine => t!("action.toggle_prompt_line"),
2670 Action::ToggleVerticalScrollbar => t!("action.toggle_vertical_scrollbar"),
2671 Action::ToggleHorizontalScrollbar => t!("action.toggle_horizontal_scrollbar"),
2672 Action::FocusFileExplorer => t!("action.focus_file_explorer"),
2673 Action::FocusEditor => t!("action.focus_editor"),
2674 Action::ToggleDockFocus => t!("action.toggle_dock_focus"),
2675 Action::FileExplorerUp => t!("action.file_explorer_up"),
2676 Action::FileExplorerDown => t!("action.file_explorer_down"),
2677 Action::FileExplorerPageUp => t!("action.file_explorer_page_up"),
2678 Action::FileExplorerPageDown => t!("action.file_explorer_page_down"),
2679 Action::FileExplorerExpand => t!("action.file_explorer_expand"),
2680 Action::FileExplorerCollapse => t!("action.file_explorer_collapse"),
2681 Action::FileExplorerOpen => t!("action.file_explorer_open"),
2682 Action::FileExplorerRefresh => t!("action.file_explorer_refresh"),
2683 Action::FileExplorerNewFile => t!("action.file_explorer_new_file"),
2684 Action::FileExplorerNewDirectory => t!("action.file_explorer_new_directory"),
2685 Action::FileExplorerDelete => t!("action.file_explorer_delete"),
2686 Action::FileExplorerRename => t!("action.file_explorer_rename"),
2687 Action::FileExplorerToggleHidden => t!("action.file_explorer_toggle_hidden"),
2688 Action::FileExplorerToggleGitignored => t!("action.file_explorer_toggle_gitignored"),
2689 Action::FileExplorerSearchClear => t!("action.file_explorer_search_clear"),
2690 Action::FileExplorerSearchBackspace => t!("action.file_explorer_search_backspace"),
2691 Action::FileExplorerCopy => t!("action.file_explorer_copy"),
2692 Action::FileExplorerCut => t!("action.file_explorer_cut"),
2693 Action::FileExplorerPaste => t!("action.file_explorer_paste"),
2694 Action::FileExplorerDuplicate => t!("action.file_explorer_duplicate"),
2695 Action::FileExplorerCopyFullPath => t!("action.file_explorer_copy_full_path"),
2696 Action::FileExplorerCopyRelativePath => t!("action.file_explorer_copy_relative_path"),
2697 Action::FileExplorerExtendSelectionUp => t!("action.file_explorer_extend_selection_up"),
2698 Action::FileExplorerExtendSelectionDown => {
2699 t!("action.file_explorer_extend_selection_down")
2700 }
2701 Action::FileExplorerToggleSelect => t!("action.file_explorer_toggle_select"),
2702 Action::FileExplorerSelectAll => t!("action.file_explorer_select_all"),
2703 Action::LspCompletion => t!("action.lsp_completion"),
2704 Action::LspGotoDefinition => t!("action.lsp_goto_definition"),
2705 Action::LspReferences => t!("action.lsp_references"),
2706 Action::LspImplementation => t!("action.lsp_implementation"),
2707 Action::LspRename => t!("action.lsp_rename"),
2708 Action::LspHover => t!("action.lsp_hover"),
2709 Action::LspSignatureHelp => t!("action.lsp_signature_help"),
2710 Action::LspCodeActions => t!("action.lsp_code_actions"),
2711 Action::LspRestart => t!("action.lsp_restart"),
2712 Action::LspStop => t!("action.lsp_stop"),
2713 Action::LspToggleForBuffer => t!("action.lsp_toggle_for_buffer"),
2714 Action::ToggleInlayHints => t!("action.toggle_inlay_hints"),
2715 Action::ToggleMouseHover => t!("action.toggle_mouse_hover"),
2716 Action::ToggleLineNumbers => t!("action.toggle_line_numbers"),
2717 Action::ToggleLineNumbersCurrentBuffer => {
2718 t!("action.toggle_line_numbers_current_buffer")
2719 }
2720 Action::ToggleLineWrapCurrentBuffer => t!("action.toggle_line_wrap_current_buffer"),
2721 Action::TriggerWaveAnimation => t!("action.trigger_wave_animation"),
2722 Action::ToggleScrollSync => t!("action.toggle_scroll_sync"),
2723 Action::ToggleMouseCapture => t!("action.toggle_mouse_capture"),
2724 Action::ToggleDebugHighlights => t!("action.toggle_debug_highlights"),
2725 Action::SetBackground => t!("action.set_background"),
2726 Action::SetBackgroundBlend => t!("action.set_background_blend"),
2727 Action::AddRuler => t!("action.add_ruler"),
2728 Action::RemoveRuler => t!("action.remove_ruler"),
2729 Action::SetTabSize => t!("action.set_tab_size"),
2730 Action::SetLineEnding => t!("action.set_line_ending"),
2731 Action::SetEncoding => t!("action.set_encoding"),
2732 Action::ReloadWithEncoding => t!("action.reload_with_encoding"),
2733 Action::SetLanguage => t!("action.set_language"),
2734 Action::ToggleIndentationStyle => t!("action.toggle_indentation_style"),
2735 Action::ToggleTabIndicators => t!("action.toggle_tab_indicators"),
2736 Action::ToggleWhitespaceIndicators => t!("action.toggle_whitespace_indicators"),
2737 Action::ResetBufferSettings => t!("action.reset_buffer_settings"),
2738 Action::DumpConfig => t!("action.dump_config"),
2739 Action::RedrawScreen => t!("action.redraw_screen"),
2740 Action::Search => t!("action.search"),
2741 Action::FindInSelection => t!("action.find_in_selection"),
2742 Action::FindNext => t!("action.find_next"),
2743 Action::FindPrevious => t!("action.find_previous"),
2744 Action::FindSelectionNext => t!("action.find_selection_next"),
2745 Action::FindSelectionPrevious => t!("action.find_selection_previous"),
2746 Action::ClearSearch => t!("action.clear_search"),
2747 Action::Replace => t!("action.replace"),
2748 Action::QueryReplace => t!("action.query_replace"),
2749 Action::MenuActivate => t!("action.menu_activate"),
2750 Action::MenuClose => t!("action.menu_close"),
2751 Action::MenuLeft => t!("action.menu_left"),
2752 Action::MenuRight => t!("action.menu_right"),
2753 Action::MenuUp => t!("action.menu_up"),
2754 Action::MenuDown => t!("action.menu_down"),
2755 Action::MenuExecute => t!("action.menu_execute"),
2756 Action::MenuOpen(name) => t!("action.menu_open", name = name),
2757 Action::SwitchKeybindingMap(map) => t!("action.switch_keybinding_map", map = map),
2758 Action::PluginAction(name) => t!("action.plugin_action", name = name),
2759 Action::ScrollTabsLeft => t!("action.scroll_tabs_left"),
2760 Action::ScrollTabsRight => t!("action.scroll_tabs_right"),
2761 Action::SelectTheme => t!("action.select_theme"),
2762 Action::SelectKeybindingMap => t!("action.select_keybinding_map"),
2763 Action::SelectCursorStyle => t!("action.select_cursor_style"),
2764 Action::SelectLocale => t!("action.select_locale"),
2765 Action::SwitchToPreviousTab => t!("action.switch_to_previous_tab"),
2766 Action::SwitchToTabByName => t!("action.switch_to_tab_by_name"),
2767 Action::OpenTerminal => t!("action.open_terminal"),
2768 Action::OpenTerminalRight => t!("action.open_terminal_right"),
2769 Action::OpenTerminalBelow => t!("action.open_terminal_below"),
2770 Action::CloseTerminal => t!("action.close_terminal"),
2771 Action::FocusTerminal => t!("action.focus_terminal"),
2772 Action::TerminalEscape => t!("action.terminal_escape"),
2773 Action::ToggleKeyboardCapture => t!("action.toggle_keyboard_capture"),
2774 Action::TerminalPaste => t!("action.terminal_paste"),
2775 Action::SendSelectionToTerminal => t!("action.send_selection_to_terminal"),
2776 Action::OpenSettings => t!("action.open_settings"),
2777 Action::CloseSettings => t!("action.close_settings"),
2778 Action::SettingsSave => t!("action.settings_save"),
2779 Action::SettingsReset => t!("action.settings_reset"),
2780 Action::SettingsToggleFocus => t!("action.settings_toggle_focus"),
2781 Action::SettingsActivate => t!("action.settings_activate"),
2782 Action::SettingsSearch => t!("action.settings_search"),
2783 Action::SettingsHelp => t!("action.settings_help"),
2784 Action::SettingsIncrement => t!("action.settings_increment"),
2785 Action::SettingsDecrement => t!("action.settings_decrement"),
2786 Action::SettingsInherit => t!("action.settings_inherit"),
2787 Action::ShellCommand => t!("action.shell_command"),
2788 Action::ShellCommandReplace => t!("action.shell_command_replace"),
2789 Action::ToUpperCase => t!("action.to_uppercase"),
2790 Action::ToLowerCase => t!("action.to_lowercase"),
2791 Action::ToggleCase => t!("action.to_uppercase"),
2792 Action::SortLines => t!("action.sort_lines"),
2793 Action::CalibrateInput => t!("action.calibrate_input"),
2794 Action::EventDebug => t!("action.event_debug"),
2795 Action::SuspendProcess => t!("action.suspend_process"),
2796 Action::LoadPluginFromBuffer => "Load Plugin from Buffer".into(),
2797 Action::InitReload => "Reload init.ts".into(),
2798 Action::InitEdit => "Edit init.ts".into(),
2799 Action::InitCheck => "Check init.ts".into(),
2800 Action::OpenKeybindingEditor => "Keybinding Editor".into(),
2801 Action::CompositeNextHunk => t!("action.composite_next_hunk"),
2802 Action::CompositePrevHunk => t!("action.composite_prev_hunk"),
2803 Action::WorkspaceTrustTrust => t!("action.workspace_trust_trust"),
2804 Action::WorkspaceTrustRestrict => t!("action.workspace_trust_restrict"),
2805 Action::WorkspaceTrustBlock => t!("action.workspace_trust_block"),
2806 Action::WorkspaceTrustPrompt => t!("action.workspace_trust_prompt"),
2807 Action::None => t!("action.none"),
2808 }
2809 .to_string()
2810 }
2811
2812 pub fn parse_key_public(key: &str) -> Option<KeyCode> {
2814 Self::parse_key(key)
2815 }
2816
2817 pub fn parse_modifiers_public(modifiers: &[String]) -> KeyModifiers {
2819 Self::parse_modifiers(modifiers)
2820 }
2821
2822 pub fn format_action_from_str(action_name: &str) -> String {
2826 Self::format_action_from_str_with_args(action_name, &std::collections::HashMap::new())
2827 }
2828
2829 pub fn format_action_from_str_with_args(
2833 action_name: &str,
2834 args: &std::collections::HashMap<String, serde_json::Value>,
2835 ) -> String {
2836 if let Some(action) = Action::from_str(action_name, args) {
2838 Self::format_action(&action)
2839 } else {
2840 action_name
2842 .split('_')
2843 .map(|word| {
2844 let mut chars = word.chars();
2845 match chars.next() {
2846 Some(c) => {
2847 let upper: String = c.to_uppercase().collect();
2848 format!("{}{}", upper, chars.as_str())
2849 }
2850 None => String::new(),
2851 }
2852 })
2853 .collect::<Vec<_>>()
2854 .join(" ")
2855 }
2856 }
2857
2858 pub fn all_action_names() -> Vec<String> {
2862 Action::all_action_names()
2863 }
2864
2865 pub fn get_keybinding_for_action(
2871 &self,
2872 action: &Action,
2873 context: KeyContext,
2874 ) -> Option<String> {
2875 self.get_keybinding_event_for_action(action, context)
2876 .map(|(k, m)| format_keybinding(&k, &m))
2877 }
2878
2879 pub fn get_keybinding_event_for_action(
2886 &self,
2887 action: &Action,
2888 context: KeyContext,
2889 ) -> Option<(KeyCode, KeyModifiers)> {
2890 fn find_best_keybinding(
2892 bindings: &HashMap<(KeyCode, KeyModifiers), Action>,
2893 action: &Action,
2894 ) -> Option<(KeyCode, KeyModifiers)> {
2895 let matches: Vec<_> = bindings
2896 .iter()
2897 .filter(|(_, a)| *a == action)
2898 .map(|((k, m), _)| (*k, *m))
2899 .collect();
2900
2901 if matches.is_empty() {
2902 return None;
2903 }
2904
2905 let mut sorted = matches;
2908 sorted.sort_by(|(k1, m1), (k2, m2)| {
2909 let score1 = keybinding_priority_score(k1);
2910 let score2 = keybinding_priority_score(k2);
2911 match score1.cmp(&score2) {
2913 std::cmp::Ordering::Equal => {
2914 let s1 = format_keybinding(k1, m1);
2916 let s2 = format_keybinding(k2, m2);
2917 s1.cmp(&s2)
2918 }
2919 other => other,
2920 }
2921 });
2922
2923 sorted.into_iter().next()
2924 }
2925
2926 if let Some(context_bindings) = self.bindings.get(&context) {
2928 if let Some(hit) = find_best_keybinding(context_bindings, action) {
2929 return Some(hit);
2930 }
2931 }
2932
2933 if let Some(context_bindings) = self.default_bindings.get(&context) {
2935 if let Some(hit) = find_best_keybinding(context_bindings, action) {
2936 return Some(hit);
2937 }
2938 }
2939
2940 if context != KeyContext::Normal
2942 && (context.allows_normal_fallthrough()
2943 || Self::is_application_wide_action(action)
2944 || (context.allows_ui_fallthrough() && Self::is_terminal_ui_action(action)))
2945 {
2946 if let Some(normal_bindings) = self.bindings.get(&KeyContext::Normal) {
2948 if let Some(hit) = find_best_keybinding(normal_bindings, action) {
2949 return Some(hit);
2950 }
2951 }
2952
2953 if let Some(normal_bindings) = self.default_bindings.get(&KeyContext::Normal) {
2955 if let Some(hit) = find_best_keybinding(normal_bindings, action) {
2956 return Some(hit);
2957 }
2958 }
2959 }
2960
2961 None
2962 }
2963
2964 pub fn reload(&mut self, config: &Config) {
2966 self.bindings.clear();
2967 for binding in &config.keybindings {
2968 if let Some(key_code) = Self::parse_key(&binding.key) {
2969 let modifiers = Self::parse_modifiers(&binding.modifiers);
2970 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
2971 let context = if let Some(ref when) = binding.when {
2973 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
2974 } else {
2975 KeyContext::Normal
2976 };
2977
2978 self.bindings
2979 .entry(context)
2980 .or_default()
2981 .insert((key_code, modifiers), action);
2982 }
2983 }
2984 }
2985 }
2986}
2987
2988#[cfg(test)]
2989mod tests {
2990 use super::*;
2991
2992 #[test]
2993 fn test_parse_key() {
2994 assert_eq!(KeybindingResolver::parse_key("enter"), Some(KeyCode::Enter));
2995 assert_eq!(
2996 KeybindingResolver::parse_key("backspace"),
2997 Some(KeyCode::Backspace)
2998 );
2999 assert_eq!(KeybindingResolver::parse_key("tab"), Some(KeyCode::Tab));
3000 assert_eq!(
3001 KeybindingResolver::parse_key("backtab"),
3002 Some(KeyCode::BackTab)
3003 );
3004 assert_eq!(
3005 KeybindingResolver::parse_key("BackTab"),
3006 Some(KeyCode::BackTab)
3007 );
3008 assert_eq!(KeybindingResolver::parse_key("a"), Some(KeyCode::Char('a')));
3009 }
3010
3011 #[test]
3012 fn test_parse_modifiers() {
3013 let mods = vec!["ctrl".to_string()];
3014 assert_eq!(
3015 KeybindingResolver::parse_modifiers(&mods),
3016 KeyModifiers::CONTROL
3017 );
3018
3019 let mods = vec!["ctrl".to_string(), "shift".to_string()];
3020 assert_eq!(
3021 KeybindingResolver::parse_modifiers(&mods),
3022 KeyModifiers::CONTROL | KeyModifiers::SHIFT
3023 );
3024 }
3025
3026 #[test]
3027 fn test_format_action_from_str_distinguishes_menu_open_by_name() {
3028 let mut file_args = HashMap::new();
3031 file_args.insert(
3032 "name".to_string(),
3033 serde_json::Value::String("File".to_string()),
3034 );
3035 let mut edit_args = HashMap::new();
3036 edit_args.insert(
3037 "name".to_string(),
3038 serde_json::Value::String("Edit".to_string()),
3039 );
3040
3041 let file_display =
3042 KeybindingResolver::format_action_from_str_with_args("menu_open", &file_args);
3043 let edit_display =
3044 KeybindingResolver::format_action_from_str_with_args("menu_open", &edit_args);
3045 let no_args_display = KeybindingResolver::format_action_from_str("menu_open");
3046
3047 assert_ne!(
3048 file_display, edit_display,
3049 "menu_open with different names should produce different descriptions"
3050 );
3051 assert!(
3052 file_display.contains("File"),
3053 "expected the File menu description to contain \"File\", got {file_display:?}"
3054 );
3055 assert!(
3056 edit_display.contains("Edit"),
3057 "expected the Edit menu description to contain \"Edit\", got {edit_display:?}"
3058 );
3059 assert_eq!(no_args_display, "Menu Open");
3063 }
3064
3065 #[test]
3066 fn test_format_action_word_end_actions_are_localized() {
3067 crate::i18n::set_locale("en");
3072
3073 let move_desc = KeybindingResolver::format_action(&Action::MoveWordEnd);
3074 assert_ne!(
3075 move_desc, "action.move_word_end",
3076 "MoveWordEnd should resolve to a translated description"
3077 );
3078 let select_desc = KeybindingResolver::format_action(&Action::SelectWordEnd);
3079 assert_ne!(
3080 select_desc, "action.select_word_end",
3081 "SelectWordEnd should resolve to a translated description"
3082 );
3083
3084 assert_eq!(
3086 KeybindingResolver::format_action(&Action::ViMoveWordEnd),
3087 move_desc,
3088 );
3089 assert_eq!(
3090 KeybindingResolver::format_action(&Action::ViSelectWordEnd),
3091 select_desc,
3092 );
3093 }
3094
3095 #[test]
3096 fn test_qualify_and_unqualify_roundtrip_menu_open() {
3097 let mut args = HashMap::new();
3098 args.insert(
3099 "name".to_string(),
3100 serde_json::Value::String("File".to_string()),
3101 );
3102
3103 let qualified = Action::qualify_action("menu_open", &args);
3104 assert_eq!(qualified, "menu_open:File");
3105
3106 let (bare, parsed_args) = Action::unqualify_action(&qualified);
3107 assert_eq!(bare, "menu_open");
3108 assert_eq!(
3109 parsed_args.get("name").and_then(|v| v.as_str()),
3110 Some("File")
3111 );
3112 }
3113
3114 #[test]
3115 fn test_qualify_action_passthrough_for_unparameterised() {
3116 let args = HashMap::new();
3118 assert_eq!(Action::qualify_action("save", &args), "save");
3119 let (bare, parsed) = Action::unqualify_action("save");
3120 assert_eq!(bare, "save");
3121 assert!(parsed.is_empty());
3122 }
3123
3124 #[test]
3125 fn test_qualify_action_no_suffix_when_arg_missing() {
3126 let args = HashMap::new();
3129 assert_eq!(Action::qualify_action("menu_open", &args), "menu_open");
3130 }
3131
3132 #[test]
3133 fn test_unqualify_action_ignores_colon_on_unknown_action() {
3134 let (bare, parsed) = Action::unqualify_action("my_plugin:action_with:colons");
3137 assert_eq!(bare, "my_plugin:action_with:colons");
3138 assert!(parsed.is_empty());
3139 }
3140
3141 #[test]
3142 fn test_to_qualified_action_str_for_menu_open() {
3143 let action = Action::MenuOpen("Edit".to_string());
3144 assert_eq!(action.to_qualified_action_str(), "menu_open:Edit");
3145 }
3146
3147 #[test]
3148 fn test_resolve_basic() {
3149 let config = Config::default();
3150 let resolver = KeybindingResolver::new(&config);
3151
3152 let event = KeyEvent::new(KeyCode::Left, KeyModifiers::empty());
3153 assert_eq!(
3154 resolver.resolve(&event, KeyContext::Normal),
3155 Action::MoveLeft
3156 );
3157
3158 let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
3159 assert_eq!(
3160 resolver.resolve(&event, KeyContext::Normal),
3161 Action::InsertChar('a')
3162 );
3163 }
3164
3165 #[test]
3172 fn test_panel_mode_passthrough_for_ui_actions() {
3173 let config = Config::default();
3174 let resolver = KeybindingResolver::new(&config);
3175 let mode_ctx = KeyContext::Mode("search-replace-list".to_string());
3176
3177 let alt_close = KeyEvent::new(KeyCode::Char(']'), KeyModifiers::ALT);
3179 assert_eq!(
3180 resolver.resolve(&alt_close, mode_ctx.clone()),
3181 Action::NextSplit,
3182 "Alt+] should fall through to next_split inside a panel mode"
3183 );
3184
3185 let alt_open = KeyEvent::new(KeyCode::Char('['), KeyModifiers::ALT);
3187 assert_eq!(
3188 resolver.resolve(&alt_open, mode_ctx.clone()),
3189 Action::PrevSplit,
3190 "Alt+[ should fall through to prev_split inside a panel mode"
3191 );
3192
3193 let ctrl_s = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL);
3197 assert_eq!(
3198 resolver.resolve(&ctrl_s, mode_ctx.clone()),
3199 Action::Save,
3200 "Ctrl+S should still save while a panel mode is active"
3201 );
3202
3203 let ctrl_d = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL);
3207 assert_ne!(
3208 resolver.resolve(&ctrl_d, mode_ctx),
3209 Action::AddCursorNextMatch,
3210 "Ctrl+D (add cursor next match) must not pass through to a panel mode"
3211 );
3212 }
3213
3214 #[test]
3215 fn test_shift_backspace_matches_backspace() {
3216 let config = Config::default();
3220 let resolver = KeybindingResolver::new(&config);
3221
3222 let backspace = KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty());
3223 let shift_backspace = KeyEvent::new(KeyCode::Backspace, KeyModifiers::SHIFT);
3224
3225 assert_eq!(
3227 resolver.resolve(&backspace, KeyContext::Normal),
3228 Action::DeleteBackward,
3229 "Backspace should resolve to DeleteBackward in Normal context"
3230 );
3231 assert_eq!(
3232 resolver.resolve(&shift_backspace, KeyContext::Normal),
3233 Action::DeleteBackward,
3234 "Shift+Backspace should resolve to DeleteBackward (same as Backspace) in Normal context"
3235 );
3236
3237 assert_eq!(
3239 resolver.resolve(&backspace, KeyContext::Prompt),
3240 Action::PromptBackspace,
3241 "Backspace should resolve to PromptBackspace in Prompt context"
3242 );
3243 assert_eq!(
3244 resolver.resolve(&shift_backspace, KeyContext::Prompt),
3245 Action::PromptBackspace,
3246 "Shift+Backspace should resolve to PromptBackspace (same as Backspace) in Prompt context"
3247 );
3248
3249 assert_eq!(
3251 resolver.resolve(&backspace, KeyContext::FileExplorer),
3252 Action::FileExplorerSearchBackspace,
3253 "Backspace should resolve to FileExplorerSearchBackspace in FileExplorer context"
3254 );
3255 assert_eq!(
3256 resolver.resolve(&shift_backspace, KeyContext::FileExplorer),
3257 Action::FileExplorerSearchBackspace,
3258 "Shift+Backspace should resolve to FileExplorerSearchBackspace (same as Backspace) in FileExplorer context"
3259 );
3260 }
3261
3262 #[test]
3263 fn test_shift_letter_binding_works_without_terminal_shift_modifier() {
3264 let mut config = Config::default();
3270 config.keybindings.push(crate::config::Keybinding {
3271 key: "p".to_string(),
3272 modifiers: vec!["shift".to_string()],
3273 keys: Vec::new(),
3274 action: "save".to_string(),
3275 args: HashMap::new(),
3276 when: Some("normal".to_string()),
3277 });
3278 let resolver = KeybindingResolver::new(&config);
3279
3280 let kitty_upper_shift = KeyEvent::new(KeyCode::Char('P'), KeyModifiers::SHIFT);
3282 assert_eq!(
3283 resolver.resolve(&kitty_upper_shift, KeyContext::Normal),
3284 Action::Save,
3285 "Char('P')+SHIFT should match Shift+P binding"
3286 );
3287
3288 let kitty_lower_shift = KeyEvent::new(KeyCode::Char('p'), KeyModifiers::SHIFT);
3290 assert_eq!(
3291 resolver.resolve(&kitty_lower_shift, KeyContext::Normal),
3292 Action::Save,
3293 "Char('p')+SHIFT should match Shift+P binding"
3294 );
3295
3296 let plain_upper = KeyEvent::new(KeyCode::Char('P'), KeyModifiers::empty());
3299 assert_eq!(
3300 resolver.resolve(&plain_upper, KeyContext::Normal),
3301 Action::Save,
3302 "Char('P') with no modifier should match Shift+P binding \
3303 (typical terminal behavior — case alone carries shift)"
3304 );
3305 }
3306
3307 #[test]
3308 fn test_alt_shift_letter_binding_matches_across_terminal_encodings() {
3309 let mut config = Config::default();
3314 config.keybindings.push(crate::config::Keybinding {
3315 key: "f".to_string(),
3316 modifiers: vec!["alt".to_string(), "shift".to_string()],
3317 keys: Vec::new(),
3318 action: "save".to_string(),
3319 args: HashMap::new(),
3320 when: Some("normal".to_string()),
3321 });
3322 let resolver = KeybindingResolver::new(&config);
3323
3324 let no_shift_bit = KeyEvent::new(KeyCode::Char('F'), KeyModifiers::ALT);
3326 assert_eq!(
3327 resolver.resolve(&no_shift_bit, KeyContext::Normal),
3328 Action::Save,
3329 "Char('F')+ALT (no SHIFT bit) should match Alt+Shift+F"
3330 );
3331
3332 let with_shift_bit =
3334 KeyEvent::new(KeyCode::Char('F'), KeyModifiers::ALT | KeyModifiers::SHIFT);
3335 assert_eq!(
3336 resolver.resolve(&with_shift_bit, KeyContext::Normal),
3337 Action::Save,
3338 "Char('F')+ALT+SHIFT should match Alt+Shift+F"
3339 );
3340
3341 let alt_f = KeyEvent::new(KeyCode::Char('f'), KeyModifiers::ALT);
3343 assert_ne!(
3344 resolver.resolve(&alt_f, KeyContext::Normal),
3345 Action::Save,
3346 "Alt+f (lowercase, no shift) must stay distinct from Alt+Shift+F"
3347 );
3348 }
3349
3350 #[test]
3351 fn test_capslock_ctrl_letter_still_matches_ctrl_letter_binding() {
3352 let config = Config::default();
3358 let resolver = KeybindingResolver::new(&config);
3359
3360 let ctrl_a = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL);
3362 let action_ctrl_a = resolver.resolve(&ctrl_a, KeyContext::Normal);
3363
3364 let caps_ctrl_a = KeyEvent::new(KeyCode::Char('A'), KeyModifiers::CONTROL);
3366 let action_caps_ctrl_a = resolver.resolve(&caps_ctrl_a, KeyContext::Normal);
3367
3368 assert_eq!(
3369 action_ctrl_a, action_caps_ctrl_a,
3370 "CapsLock+Ctrl+A must resolve to the same action as Ctrl+A"
3371 );
3372 }
3373
3374 #[test]
3375 fn test_uppercase_without_binding_falls_through_to_insert_char() {
3376 let config = Config::default();
3381 let resolver = KeybindingResolver::new(&config);
3382
3383 let upper_z_no_mod = KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::empty());
3385 assert_eq!(
3386 resolver.resolve(&upper_z_no_mod, KeyContext::Normal),
3387 Action::InsertChar('Z'),
3388 "Char('Z') with no modifier should still be inserted as 'Z'"
3389 );
3390
3391 let upper_z_shift = KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::SHIFT);
3392 assert_eq!(
3393 resolver.resolve(&upper_z_shift, KeyContext::Normal),
3394 Action::InsertChar('Z'),
3395 "Char('Z')+SHIFT should still be inserted as 'Z'"
3396 );
3397 }
3398
3399 #[test]
3400 fn test_file_explorer_ui_fallthrough() {
3401 let config = Config::default();
3408 let resolver = KeybindingResolver::new(&config);
3409
3410 let cases = [
3411 (
3412 KeyCode::PageUp,
3413 KeyModifiers::CONTROL,
3414 Action::PrevBuffer,
3415 "Ctrl+PageUp -> prev_buffer",
3416 ),
3417 (
3418 KeyCode::PageDown,
3419 KeyModifiers::CONTROL,
3420 Action::NextBuffer,
3421 "Ctrl+PageDown -> next_buffer",
3422 ),
3423 (
3424 KeyCode::PageUp,
3425 KeyModifiers::ALT,
3426 Action::ScrollTabsLeft,
3427 "Alt+PageUp -> scroll_tabs_left",
3428 ),
3429 (
3430 KeyCode::PageDown,
3431 KeyModifiers::ALT,
3432 Action::ScrollTabsRight,
3433 "Alt+PageDown -> scroll_tabs_right",
3434 ),
3435 (
3436 KeyCode::Char('w'),
3437 KeyModifiers::ALT,
3438 Action::CloseTab,
3439 "Alt+W -> close_tab",
3440 ),
3441 ];
3442
3443 for (code, mods, expected, label) in cases {
3444 let event = KeyEvent::new(code, mods);
3445 assert_eq!(
3446 resolver.resolve(&event, KeyContext::FileExplorer),
3447 expected,
3448 "{label} should fall through from FileExplorer to Normal"
3449 );
3450 }
3451
3452 let up = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
3457 assert_eq!(
3458 resolver.resolve(&up, KeyContext::FileExplorer),
3459 Action::FileExplorerUp,
3460 "Up must continue to navigate the explorer, not move the cursor"
3461 );
3462
3463 let plain_d = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::empty());
3468 assert_eq!(
3469 resolver.resolve(&plain_d, KeyContext::FileExplorer),
3470 Action::InsertChar('d'),
3471 "Plain 'd' must remain text input for explorer search-as-you-type"
3472 );
3473 }
3474
3475 #[test]
3476 fn to_action_spec_round_trips_through_from_str() {
3477 let cases = vec![
3481 Action::MoveLeft,
3482 Action::DeleteLine,
3483 Action::InsertChar('x'),
3484 Action::InsertChar(' '),
3485 Action::SetBookmark('3'),
3486 Action::PlayMacro('q'),
3487 Action::ShowMacro('0'),
3488 Action::PromptConfirmWithText("hello world".to_string()),
3489 Action::MenuOpen("File".to_string()),
3490 Action::SwitchKeybindingMap("emacs".to_string()),
3491 Action::CopyWithTheme("one-dark".to_string()),
3492 ];
3493 for action in cases {
3494 let spec = action.to_action_spec();
3495 assert_eq!(spec.count, 1, "to_action_spec emits count == 1");
3496 let back = Action::from_str(&spec.action, &spec.args)
3497 .unwrap_or_else(|| panic!("from_str failed for {:?}", action));
3498 assert_eq!(back, action, "round-trip mismatch via spec {:?}", spec);
3499 }
3500 }
3501
3502 #[test]
3503 fn to_action_spec_omits_args_for_plain_actions() {
3504 assert!(Action::MoveRight.to_action_spec().args.is_empty());
3505 let spec = Action::InsertChar('z').to_action_spec();
3507 assert_eq!(spec.args.get("char").and_then(|v| v.as_str()), Some("z"));
3508 }
3509
3510 #[test]
3511 fn test_action_from_str() {
3512 let args = HashMap::new();
3513 assert_eq!(Action::from_str("move_left", &args), Some(Action::MoveLeft));
3514 assert_eq!(Action::from_str("save", &args), Some(Action::Save));
3515 assert_eq!(
3517 Action::from_str("unknown", &args),
3518 Some(Action::PluginAction("unknown".to_string()))
3519 );
3520
3521 assert_eq!(
3523 Action::from_str("keyboard_shortcuts", &args),
3524 Some(Action::ShowKeyboardShortcuts)
3525 );
3526 assert_eq!(
3527 Action::from_str("prompt_confirm", &args),
3528 Some(Action::PromptConfirm)
3529 );
3530 assert_eq!(
3531 Action::from_str("popup_cancel", &args),
3532 Some(Action::PopupCancel)
3533 );
3534
3535 assert_eq!(
3537 Action::from_str("calibrate_input", &args),
3538 Some(Action::CalibrateInput)
3539 );
3540 }
3541
3542 #[test]
3543 fn test_key_context_from_when_clause() {
3544 assert_eq!(
3545 KeyContext::from_when_clause("normal"),
3546 Some(KeyContext::Normal)
3547 );
3548 assert_eq!(
3549 KeyContext::from_when_clause("prompt"),
3550 Some(KeyContext::Prompt)
3551 );
3552 assert_eq!(
3553 KeyContext::from_when_clause("searchPrompt"),
3554 Some(KeyContext::SearchPrompt)
3555 );
3556 assert_eq!(
3557 KeyContext::from_when_clause("search_prompt"),
3558 Some(KeyContext::SearchPrompt)
3559 );
3560 assert_eq!(
3561 KeyContext::from_when_clause("popup"),
3562 Some(KeyContext::Popup)
3563 );
3564 assert_eq!(KeyContext::from_when_clause("help"), None);
3565 assert_eq!(KeyContext::from_when_clause(" help "), None); assert_eq!(KeyContext::from_when_clause("unknown"), None);
3567 assert_eq!(KeyContext::from_when_clause(""), None);
3568 }
3569
3570 #[test]
3571 fn test_key_context_to_when_clause() {
3572 assert_eq!(KeyContext::Normal.to_when_clause(), "normal");
3573 assert_eq!(KeyContext::Prompt.to_when_clause(), "prompt");
3574 assert_eq!(KeyContext::SearchPrompt.to_when_clause(), "searchPrompt");
3575 assert_eq!(KeyContext::Popup.to_when_clause(), "popup");
3576 assert_eq!(
3578 KeyContext::from_when_clause(&KeyContext::SearchPrompt.to_when_clause()),
3579 Some(KeyContext::SearchPrompt)
3580 );
3581 }
3582
3583 #[test]
3584 fn test_context_specific_bindings() {
3585 let config = Config::default();
3586 let resolver = KeybindingResolver::new(&config);
3587
3588 let enter_event = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
3590 assert_eq!(
3591 resolver.resolve(&enter_event, KeyContext::Prompt),
3592 Action::PromptConfirm
3593 );
3594 assert_eq!(
3595 resolver.resolve(&enter_event, KeyContext::Normal),
3596 Action::InsertNewline
3597 );
3598
3599 let up_event = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
3601 assert_eq!(
3602 resolver.resolve(&up_event, KeyContext::Popup),
3603 Action::PopupSelectPrev
3604 );
3605 assert_eq!(
3606 resolver.resolve(&up_event, KeyContext::Normal),
3607 Action::MoveUp
3608 );
3609 }
3610
3611 #[test]
3612 fn test_search_prompt_owns_toggles_and_inherits_prompt() {
3613 let config = Config::default();
3614 let resolver = KeybindingResolver::new(&config);
3615
3616 let alt_w = KeyEvent::new(KeyCode::Char('w'), KeyModifiers::ALT);
3617
3618 assert_eq!(
3620 resolver.resolve(&alt_w, KeyContext::SearchPrompt),
3621 Action::ToggleSearchWholeWord,
3622 );
3623 assert_ne!(
3626 resolver.resolve(&alt_w, KeyContext::Prompt),
3627 Action::ToggleSearchWholeWord,
3628 );
3629
3630 let alt_c = KeyEvent::new(KeyCode::Char('c'), KeyModifiers::ALT);
3632 let alt_r = KeyEvent::new(KeyCode::Char('r'), KeyModifiers::ALT);
3633 assert_eq!(
3634 resolver.resolve(&alt_c, KeyContext::SearchPrompt),
3635 Action::ToggleSearchCaseSensitive,
3636 );
3637 assert_eq!(
3638 resolver.resolve(&alt_r, KeyContext::SearchPrompt),
3639 Action::ToggleSearchRegex,
3640 );
3641
3642 let enter = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
3645 assert_eq!(
3646 resolver.resolve(&enter, KeyContext::SearchPrompt),
3647 resolver.resolve(&enter, KeyContext::Prompt),
3648 );
3649 let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
3650 assert_eq!(
3651 resolver.resolve(&esc, KeyContext::SearchPrompt),
3652 resolver.resolve(&esc, KeyContext::Prompt),
3653 );
3654 assert_eq!(
3655 resolver.resolve(
3656 &KeyEvent::new(KeyCode::Char('x'), KeyModifiers::empty()),
3657 KeyContext::SearchPrompt,
3658 ),
3659 Action::InsertChar('x'),
3660 );
3661 }
3662
3663 #[test]
3664 fn test_context_fallback_to_normal() {
3665 let config = Config::default();
3666 let resolver = KeybindingResolver::new(&config);
3667
3668 let save_event = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL);
3670 assert_eq!(
3671 resolver.resolve(&save_event, KeyContext::Normal),
3672 Action::Save
3673 );
3674 assert_eq!(
3675 resolver.resolve(&save_event, KeyContext::Popup),
3676 Action::Save
3677 );
3678 }
3680
3681 #[test]
3682 fn test_context_priority_resolution() {
3683 use crate::config::Keybinding;
3684
3685 let mut config = Config::default();
3687 config.keybindings.push(Keybinding {
3688 key: "esc".to_string(),
3689 modifiers: vec![],
3690 keys: vec![],
3691 action: "quit".to_string(), args: HashMap::new(),
3693 when: Some("popup".to_string()),
3694 });
3695
3696 let resolver = KeybindingResolver::new(&config);
3697 let esc_event = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
3698
3699 assert_eq!(
3701 resolver.resolve(&esc_event, KeyContext::Popup),
3702 Action::Quit
3703 );
3704
3705 assert_eq!(
3707 resolver.resolve(&esc_event, KeyContext::Normal),
3708 Action::RemoveSecondaryCursors
3709 );
3710 }
3711
3712 #[test]
3713 fn test_character_input_in_contexts() {
3714 let config = Config::default();
3715 let resolver = KeybindingResolver::new(&config);
3716
3717 let char_event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
3718
3719 assert_eq!(
3721 resolver.resolve(&char_event, KeyContext::Normal),
3722 Action::InsertChar('a')
3723 );
3724 assert_eq!(
3725 resolver.resolve(&char_event, KeyContext::Prompt),
3726 Action::InsertChar('a')
3727 );
3728
3729 assert_eq!(
3731 resolver.resolve(&char_event, KeyContext::Popup),
3732 Action::None
3733 );
3734 }
3735
3736 #[test]
3737 fn test_custom_keybinding_loading() {
3738 use crate::config::Keybinding;
3739
3740 let mut config = Config::default();
3741
3742 config.keybindings.push(Keybinding {
3744 key: "f".to_string(),
3745 modifiers: vec!["ctrl".to_string()],
3746 keys: vec![],
3747 action: "command_palette".to_string(),
3748 args: HashMap::new(),
3749 when: None, });
3751
3752 let resolver = KeybindingResolver::new(&config);
3753
3754 let ctrl_f = KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL);
3756 assert_eq!(
3757 resolver.resolve(&ctrl_f, KeyContext::Normal),
3758 Action::CommandPalette
3759 );
3760
3761 let ctrl_k = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL);
3763 assert_eq!(
3764 resolver.resolve(&ctrl_k, KeyContext::Prompt),
3765 Action::PromptDeleteToLineEnd
3766 );
3767 assert_eq!(
3768 resolver.resolve(&ctrl_k, KeyContext::Normal),
3769 Action::DeleteToLineEnd
3770 );
3771 }
3772
3773 #[test]
3774 fn test_all_context_default_bindings_exist() {
3775 let config = Config::default();
3776 let resolver = KeybindingResolver::new(&config);
3777
3778 assert!(resolver.default_bindings.contains_key(&KeyContext::Normal));
3780 assert!(resolver.default_bindings.contains_key(&KeyContext::Prompt));
3781 assert!(resolver.default_bindings.contains_key(&KeyContext::Popup));
3782 assert!(resolver
3783 .default_bindings
3784 .contains_key(&KeyContext::FileExplorer));
3785 assert!(resolver.default_bindings.contains_key(&KeyContext::Menu));
3786
3787 assert!(!resolver.default_bindings[&KeyContext::Normal].is_empty());
3789 assert!(!resolver.default_bindings[&KeyContext::Prompt].is_empty());
3790 assert!(!resolver.default_bindings[&KeyContext::Popup].is_empty());
3791 assert!(!resolver.default_bindings[&KeyContext::FileExplorer].is_empty());
3792 assert!(!resolver.default_bindings[&KeyContext::Menu].is_empty());
3793 }
3794
3795 #[test]
3806 fn test_all_builtin_keymaps_have_valid_action_names() {
3807 let known_actions: std::collections::HashSet<String> =
3808 Action::all_action_names().into_iter().collect();
3809
3810 const ALLOWED_PLUGIN_ACTIONS_IN_DEFAULTS: &[&str] = &[
3811 "start_search_replace",
3812 "live_grep_toggle_files",
3815 "live_grep_toggle_ignored",
3816 "live_grep_toggle_buffers",
3817 "live_grep_toggle_terminals",
3818 "live_grep_toggle_diagnostics",
3819 "live_grep_toggle_word",
3820 "live_grep_toggle_regex",
3821 "live_grep_export_quickfix",
3825 ];
3826
3827 let config = Config::default();
3828
3829 for map_name in crate::config::KeybindingMapName::BUILTIN_OPTIONS {
3830 let bindings = config.resolve_keymap(map_name);
3831 for binding in &bindings {
3832 let is_known_builtin = known_actions.contains(&binding.action);
3833 let is_allowed_plugin =
3834 ALLOWED_PLUGIN_ACTIONS_IN_DEFAULTS.contains(&binding.action.as_str());
3835 assert!(
3836 is_known_builtin || is_allowed_plugin,
3837 "Keymap '{}' contains unknown action '{}' (key: '{}', when: {:?}). \
3838 This will be treated as a plugin action at runtime. \
3839 Check for typos in the keymap JSON file, or add the action to \
3840 ALLOWED_PLUGIN_ACTIONS_IN_DEFAULTS if it's an intentional \
3841 plugin-action binding.",
3842 map_name,
3843 binding.action,
3844 binding.key,
3845 binding.when,
3846 );
3847 }
3848 }
3849 }
3850
3851 #[test]
3852 fn test_resolve_determinism() {
3853 let config = Config::default();
3855 let resolver = KeybindingResolver::new(&config);
3856
3857 let test_cases = vec![
3858 (KeyCode::Left, KeyModifiers::empty(), KeyContext::Normal),
3859 (
3860 KeyCode::Esc,
3861 KeyModifiers::empty(),
3862 KeyContext::FileExplorer,
3863 ),
3864 (KeyCode::Enter, KeyModifiers::empty(), KeyContext::Prompt),
3865 (KeyCode::Down, KeyModifiers::empty(), KeyContext::Popup),
3866 ];
3867
3868 for (key_code, modifiers, context) in test_cases {
3869 let event = KeyEvent::new(key_code, modifiers);
3870 let action1 = resolver.resolve(&event, context.clone());
3871 let action2 = resolver.resolve(&event, context.clone());
3872 let action3 = resolver.resolve(&event, context);
3873
3874 assert_eq!(action1, action2, "Resolve should be deterministic");
3875 assert_eq!(action2, action3, "Resolve should be deterministic");
3876 }
3877 }
3878
3879 #[test]
3880 fn test_modifier_combinations() {
3881 let config = Config::default();
3882 let resolver = KeybindingResolver::new(&config);
3883
3884 let char_s = KeyCode::Char('s');
3886
3887 let no_mod = KeyEvent::new(char_s, KeyModifiers::empty());
3888 let ctrl = KeyEvent::new(char_s, KeyModifiers::CONTROL);
3889 let shift = KeyEvent::new(char_s, KeyModifiers::SHIFT);
3890 let ctrl_shift = KeyEvent::new(char_s, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
3891
3892 let action_no_mod = resolver.resolve(&no_mod, KeyContext::Normal);
3893 let action_ctrl = resolver.resolve(&ctrl, KeyContext::Normal);
3894 let action_shift = resolver.resolve(&shift, KeyContext::Normal);
3895 let action_ctrl_shift = resolver.resolve(&ctrl_shift, KeyContext::Normal);
3896
3897 assert_eq!(action_no_mod, Action::InsertChar('s'));
3899 assert_eq!(action_ctrl, Action::Save);
3900 assert_eq!(action_shift, Action::InsertChar('s')); assert_eq!(action_ctrl_shift, Action::None);
3903 }
3904
3905 #[test]
3906 fn test_scroll_keybindings() {
3907 let config = Config::default();
3908 let resolver = KeybindingResolver::new(&config);
3909
3910 let ctrl_up = KeyEvent::new(KeyCode::Up, KeyModifiers::CONTROL);
3912 assert_eq!(
3913 resolver.resolve(&ctrl_up, KeyContext::Normal),
3914 Action::ScrollUp,
3915 "Ctrl+Up should resolve to ScrollUp"
3916 );
3917
3918 let ctrl_down = KeyEvent::new(KeyCode::Down, KeyModifiers::CONTROL);
3920 assert_eq!(
3921 resolver.resolve(&ctrl_down, KeyContext::Normal),
3922 Action::ScrollDown,
3923 "Ctrl+Down should resolve to ScrollDown"
3924 );
3925 }
3926
3927 #[test]
3928 fn test_lsp_completion_keybinding() {
3929 let config = Config::default();
3930 let resolver = KeybindingResolver::new(&config);
3931
3932 let ctrl_space = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL);
3934 assert_eq!(
3935 resolver.resolve(&ctrl_space, KeyContext::Normal),
3936 Action::LspCompletion,
3937 "Ctrl+Space should resolve to LspCompletion"
3938 );
3939 }
3940
3941 #[test]
3942 fn test_terminal_key_equivalents() {
3943 let ctrl = KeyModifiers::CONTROL;
3945
3946 let slash_equivs = terminal_key_equivalents(KeyCode::Char('/'), ctrl);
3948 assert_eq!(slash_equivs, vec![(KeyCode::Char('7'), ctrl)]);
3949
3950 let seven_equivs = terminal_key_equivalents(KeyCode::Char('7'), ctrl);
3951 assert_eq!(seven_equivs, vec![(KeyCode::Char('/'), ctrl)]);
3952
3953 let backspace_equivs = terminal_key_equivalents(KeyCode::Backspace, ctrl);
3955 assert_eq!(backspace_equivs, vec![(KeyCode::Char('h'), ctrl)]);
3956
3957 let h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl);
3958 assert_eq!(h_equivs, vec![(KeyCode::Backspace, ctrl)]);
3959
3960 let a_equivs = terminal_key_equivalents(KeyCode::Char('a'), ctrl);
3962 assert!(a_equivs.is_empty());
3963
3964 let slash_no_ctrl = terminal_key_equivalents(KeyCode::Char('/'), KeyModifiers::empty());
3966 assert!(slash_no_ctrl.is_empty());
3967 }
3968
3969 #[test]
3970 fn test_terminal_key_equivalents_auto_binding() {
3971 let config = Config::default();
3972 let resolver = KeybindingResolver::new(&config);
3973
3974 let ctrl_slash = KeyEvent::new(KeyCode::Char('/'), KeyModifiers::CONTROL);
3976 let action_slash = resolver.resolve(&ctrl_slash, KeyContext::Normal);
3977 assert_eq!(
3978 action_slash,
3979 Action::ToggleComment,
3980 "Ctrl+/ should resolve to ToggleComment"
3981 );
3982
3983 let ctrl_7 = KeyEvent::new(KeyCode::Char('7'), KeyModifiers::CONTROL);
3985 let action_7 = resolver.resolve(&ctrl_7, KeyContext::Normal);
3986 assert_eq!(
3987 action_7,
3988 Action::ToggleComment,
3989 "Ctrl+7 should resolve to ToggleComment (terminal equivalent of Ctrl+/)"
3990 );
3991 }
3992
3993 #[test]
3994 fn test_terminal_key_equivalents_normalization() {
3995 let ctrl = KeyModifiers::CONTROL;
4000
4001 let slash_equivs = terminal_key_equivalents(KeyCode::Char('/'), ctrl);
4004 assert_eq!(
4005 slash_equivs,
4006 vec![(KeyCode::Char('7'), ctrl)],
4007 "Ctrl+/ should map to Ctrl+7"
4008 );
4009 let seven_equivs = terminal_key_equivalents(KeyCode::Char('7'), ctrl);
4010 assert_eq!(
4011 seven_equivs,
4012 vec![(KeyCode::Char('/'), ctrl)],
4013 "Ctrl+7 should map back to Ctrl+/"
4014 );
4015
4016 let backspace_equivs = terminal_key_equivalents(KeyCode::Backspace, ctrl);
4019 assert_eq!(
4020 backspace_equivs,
4021 vec![(KeyCode::Char('h'), ctrl)],
4022 "Ctrl+Backspace should map to Ctrl+H"
4023 );
4024 let h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl);
4025 assert_eq!(
4026 h_equivs,
4027 vec![(KeyCode::Backspace, ctrl)],
4028 "Ctrl+H should map back to Ctrl+Backspace"
4029 );
4030
4031 let space_equivs = terminal_key_equivalents(KeyCode::Char(' '), ctrl);
4034 assert_eq!(
4035 space_equivs,
4036 vec![(KeyCode::Char('@'), ctrl)],
4037 "Ctrl+Space should map to Ctrl+@"
4038 );
4039 let at_equivs = terminal_key_equivalents(KeyCode::Char('@'), ctrl);
4040 assert_eq!(
4041 at_equivs,
4042 vec![(KeyCode::Char(' '), ctrl)],
4043 "Ctrl+@ should map back to Ctrl+Space"
4044 );
4045
4046 let minus_equivs = terminal_key_equivalents(KeyCode::Char('-'), ctrl);
4049 assert_eq!(
4050 minus_equivs,
4051 vec![(KeyCode::Char('_'), ctrl)],
4052 "Ctrl+- should map to Ctrl+_"
4053 );
4054 let underscore_equivs = terminal_key_equivalents(KeyCode::Char('_'), ctrl);
4055 assert_eq!(
4056 underscore_equivs,
4057 vec![(KeyCode::Char('-'), ctrl)],
4058 "Ctrl+_ should map back to Ctrl+-"
4059 );
4060
4061 assert!(
4063 terminal_key_equivalents(KeyCode::Char('a'), ctrl).is_empty(),
4064 "Ctrl+A should have no terminal equivalents"
4065 );
4066 assert!(
4067 terminal_key_equivalents(KeyCode::Char('z'), ctrl).is_empty(),
4068 "Ctrl+Z should have no terminal equivalents"
4069 );
4070 assert!(
4071 terminal_key_equivalents(KeyCode::Enter, ctrl).is_empty(),
4072 "Ctrl+Enter should have no terminal equivalents"
4073 );
4074
4075 assert!(
4077 terminal_key_equivalents(KeyCode::Char('/'), KeyModifiers::empty()).is_empty(),
4078 "/ without Ctrl should have no equivalents"
4079 );
4080 assert!(
4081 terminal_key_equivalents(KeyCode::Char('7'), KeyModifiers::SHIFT).is_empty(),
4082 "Shift+7 should have no equivalents"
4083 );
4084 assert!(
4085 terminal_key_equivalents(KeyCode::Char('h'), KeyModifiers::ALT).is_empty(),
4086 "Alt+H should have no equivalents"
4087 );
4088
4089 let ctrl_shift = KeyModifiers::CONTROL | KeyModifiers::SHIFT;
4092 let ctrl_shift_h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl_shift);
4093 assert!(
4094 ctrl_shift_h_equivs.is_empty(),
4095 "Ctrl+Shift+H should NOT map to Ctrl+Shift+Backspace"
4096 );
4097 }
4098
4099 #[test]
4100 fn test_no_duplicate_keybindings_in_keymaps() {
4101 use std::collections::HashMap;
4104
4105 let keymaps: &[(&str, &str)] = &[
4106 ("default", include_str!("../../keymaps/default.json")),
4107 ("macos", include_str!("../../keymaps/macos.json")),
4108 ];
4109
4110 for (keymap_name, json_content) in keymaps {
4111 let keymap: crate::config::KeymapConfig = serde_json::from_str(json_content)
4112 .unwrap_or_else(|e| panic!("Failed to parse keymap '{}': {}", keymap_name, e));
4113
4114 let mut seen: HashMap<(String, Vec<String>, String), String> = HashMap::new();
4116 let mut duplicates: Vec<String> = Vec::new();
4117
4118 for binding in &keymap.bindings {
4119 let when = binding.when.clone().unwrap_or_default();
4120 let key_id = (binding.key.clone(), binding.modifiers.clone(), when.clone());
4121
4122 if let Some(existing_action) = seen.get(&key_id) {
4123 duplicates.push(format!(
4124 "Duplicate in '{}': key='{}', modifiers={:?}, when='{}' -> '{}' vs '{}'",
4125 keymap_name,
4126 binding.key,
4127 binding.modifiers,
4128 when,
4129 existing_action,
4130 binding.action
4131 ));
4132 } else {
4133 seen.insert(key_id, binding.action.clone());
4134 }
4135 }
4136
4137 assert!(
4138 duplicates.is_empty(),
4139 "Found duplicate keybindings:\n{}",
4140 duplicates.join("\n")
4141 );
4142 }
4143 }
4144
4145 #[test]
4149 fn test_reload_from_config_preserves_plugin_state() {
4150 let config = Config::default();
4151 let mut resolver = KeybindingResolver::new(&config);
4152
4153 let mode_ctx = KeyContext::Mode("test-plugin-mode".to_string());
4154 let single_action = Action::PluginAction("test-plugin.single".to_string());
4155 let chord_action = Action::PluginAction("test-plugin.chord".to_string());
4156
4157 resolver.load_plugin_default(
4160 mode_ctx.clone(),
4161 KeyCode::Char('z'),
4162 KeyModifiers::NONE,
4163 single_action.clone(),
4164 );
4165 resolver.load_plugin_chord_default(
4166 mode_ctx.clone(),
4167 vec![
4168 (KeyCode::Char('g'), KeyModifiers::NONE),
4169 (KeyCode::Char('g'), KeyModifiers::NONE),
4170 ],
4171 chord_action.clone(),
4172 );
4173 resolver.set_mode_inherits_normal_bindings("test-plugin-mode", true);
4174
4175 resolver.reload_from_config(&config);
4177
4178 let single_event = KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE);
4180 assert_eq!(
4181 resolver.resolve(&single_event, mode_ctx.clone()),
4182 single_action,
4183 "single-key plugin binding must survive reload_from_config"
4184 );
4185
4186 let chord_prefix = [(KeyCode::Char('g'), KeyModifiers::NONE)];
4189 let second_g = KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE);
4190 assert_eq!(
4191 resolver.resolve_chord(&chord_prefix, &second_g, mode_ctx.clone()),
4192 ChordResolution::Complete(chord_action),
4193 "chord plugin binding must survive reload_from_config"
4194 );
4195
4196 let left = KeyEvent::new(KeyCode::Left, KeyModifiers::empty());
4199 assert_eq!(
4200 resolver.resolve(&left, mode_ctx),
4201 Action::MoveLeft,
4202 "inheriting-modes membership must survive reload_from_config"
4203 );
4204 }
4205}