1use crate::config::Config;
2use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
3use rust_i18n::t;
4use std::collections::HashMap;
5use std::sync::atomic::{AtomicBool, Ordering};
6
7fn normalize_key(code: KeyCode, modifiers: KeyModifiers) -> (KeyCode, KeyModifiers) {
20 if code == KeyCode::BackTab {
21 return (code, modifiers.difference(KeyModifiers::SHIFT));
22 }
23 if code == KeyCode::Backspace {
24 return (code, modifiers.difference(KeyModifiers::SHIFT));
25 }
26 if let KeyCode::Char(c) = code {
27 if c.is_ascii_uppercase() {
28 return (KeyCode::Char(c.to_ascii_lowercase()), modifiers);
29 }
30 }
31 (code, modifiers)
32}
33
34static FORCE_LINUX_KEYBINDINGS: AtomicBool = AtomicBool::new(false);
37
38pub fn set_force_linux_keybindings(force: bool) {
41 FORCE_LINUX_KEYBINDINGS.store(force, Ordering::SeqCst);
42}
43
44fn use_macos_symbols() -> bool {
46 if FORCE_LINUX_KEYBINDINGS.load(Ordering::SeqCst) {
47 return false;
48 }
49 cfg!(target_os = "macos")
50}
51
52fn is_text_input_modifier(modifiers: KeyModifiers) -> bool {
63 if modifiers.is_empty() || modifiers == KeyModifiers::SHIFT {
64 return true;
65 }
66
67 #[cfg(windows)]
71 if modifiers == (KeyModifiers::CONTROL | KeyModifiers::ALT)
72 || modifiers == (KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT)
73 {
74 return true;
75 }
76
77 false
78}
79
80pub fn format_keybinding(keycode: &KeyCode, modifiers: &KeyModifiers) -> String {
84 let mut result = String::new();
85
86 let (ctrl_label, alt_label, shift_label, super_label) = if use_macos_symbols() {
88 ("⌃", "⌥", "⇧", "⌘")
89 } else {
90 ("Ctrl", "Alt", "Shift", "Super")
91 };
92
93 let use_plus = !use_macos_symbols();
94
95 if modifiers.contains(KeyModifiers::SUPER) {
96 result.push_str(super_label);
97 if use_plus {
98 result.push('+');
99 }
100 }
101 if modifiers.contains(KeyModifiers::CONTROL) {
102 result.push_str(ctrl_label);
103 if use_plus {
104 result.push('+');
105 }
106 }
107 if modifiers.contains(KeyModifiers::ALT) {
108 result.push_str(alt_label);
109 if use_plus {
110 result.push('+');
111 }
112 }
113 if modifiers.contains(KeyModifiers::SHIFT) {
114 result.push_str(shift_label);
115 if use_plus {
116 result.push('+');
117 }
118 }
119
120 match keycode {
121 KeyCode::Enter => result.push_str("Enter"),
122 KeyCode::Backspace => result.push_str("Backspace"),
123 KeyCode::Delete => result.push_str("Del"),
124 KeyCode::Tab => result.push_str("Tab"),
125 KeyCode::Esc => result.push_str("Esc"),
126 KeyCode::Left => result.push('←'),
127 KeyCode::Right => result.push('→'),
128 KeyCode::Up => result.push('↑'),
129 KeyCode::Down => result.push('↓'),
130 KeyCode::Home => result.push_str("Home"),
131 KeyCode::End => result.push_str("End"),
132 KeyCode::PageUp => result.push_str("PgUp"),
133 KeyCode::PageDown => result.push_str("PgDn"),
134 KeyCode::Char(' ') => result.push_str("Space"),
135 KeyCode::Char(c) => result.push_str(&c.to_uppercase().to_string()),
136 KeyCode::F(n) => result.push_str(&format!("F{}", n)),
137 _ => return String::new(),
138 }
139
140 result
141}
142
143fn keybinding_priority_score(key: &KeyCode) -> u32 {
147 match key {
148 KeyCode::Char('@') => 100, KeyCode::Char('7') => 100, KeyCode::Char('_') => 100, _ => 0,
155 }
156}
157
158pub fn terminal_key_equivalents(
169 key: KeyCode,
170 modifiers: KeyModifiers,
171) -> Vec<(KeyCode, KeyModifiers)> {
172 let mut equivalents = Vec::new();
173
174 if modifiers.contains(KeyModifiers::CONTROL) {
176 let base_modifiers = modifiers; match key {
179 KeyCode::Char('/') => {
181 equivalents.push((KeyCode::Char('7'), base_modifiers));
182 }
183 KeyCode::Char('7') => {
184 equivalents.push((KeyCode::Char('/'), base_modifiers));
185 }
186
187 KeyCode::Backspace => {
189 equivalents.push((KeyCode::Char('h'), base_modifiers));
190 }
191 KeyCode::Char('h') if modifiers == KeyModifiers::CONTROL => {
192 equivalents.push((KeyCode::Backspace, base_modifiers));
194 }
195
196 KeyCode::Char(' ') => {
198 equivalents.push((KeyCode::Char('@'), base_modifiers));
199 }
200 KeyCode::Char('@') => {
201 equivalents.push((KeyCode::Char(' '), base_modifiers));
202 }
203
204 KeyCode::Char('-') => {
206 equivalents.push((KeyCode::Char('_'), base_modifiers));
207 }
208 KeyCode::Char('_') => {
209 equivalents.push((KeyCode::Char('-'), base_modifiers));
210 }
211
212 _ => {}
213 }
214 }
215
216 equivalents
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Hash)]
221pub enum KeyContext {
222 Global,
224 Normal,
226 Prompt,
228 Popup,
230 FileExplorer,
232 Menu,
234 Terminal,
236 Settings,
238 CompositeBuffer,
240 Mode(String),
242}
243
244impl KeyContext {
245 pub fn allows_normal_fallthrough(&self) -> bool {
252 matches!(self, Self::CompositeBuffer)
253 }
254
255 pub fn allows_text_input(&self) -> bool {
257 matches!(self, Self::Normal | Self::Prompt | Self::FileExplorer)
258 }
259
260 pub fn from_when_clause(when: &str) -> Option<Self> {
262 let trimmed = when.trim();
263 if let Some(mode_name) = trimmed.strip_prefix("mode:") {
264 return Some(Self::Mode(mode_name.to_string()));
265 }
266 Some(match trimmed {
267 "global" => Self::Global,
268 "prompt" => Self::Prompt,
269 "popup" => Self::Popup,
270 "fileExplorer" | "file_explorer" => Self::FileExplorer,
271 "normal" => Self::Normal,
272 "menu" => Self::Menu,
273 "terminal" => Self::Terminal,
274 "settings" => Self::Settings,
275 "compositeBuffer" | "composite_buffer" => Self::CompositeBuffer,
276 _ => return None,
277 })
278 }
279
280 pub fn to_when_clause(&self) -> String {
282 match self {
283 Self::Global => "global".to_string(),
284 Self::Normal => "normal".to_string(),
285 Self::Prompt => "prompt".to_string(),
286 Self::Popup => "popup".to_string(),
287 Self::FileExplorer => "fileExplorer".to_string(),
288 Self::Menu => "menu".to_string(),
289 Self::Terminal => "terminal".to_string(),
290 Self::Settings => "settings".to_string(),
291 Self::CompositeBuffer => "compositeBuffer".to_string(),
292 Self::Mode(name) => format!("mode:{}", name),
293 }
294 }
295}
296
297#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
299pub enum Action {
300 InsertChar(char),
302 InsertNewline,
303 InsertTab,
304
305 MoveLeft,
307 MoveRight,
308 MoveUp,
309 MoveDown,
310 MoveWordLeft,
311 MoveWordRight,
312 MoveWordEnd, ViMoveWordEnd, MoveLeftInLine, MoveRightInLine, MoveLineStart,
317 MoveLineEnd,
318 MoveLineUp,
319 MoveLineDown,
320 MovePageUp,
321 MovePageDown,
322 MoveDocumentStart,
323 MoveDocumentEnd,
324
325 SelectLeft,
327 SelectRight,
328 SelectUp,
329 SelectDown,
330 SelectToParagraphUp, SelectToParagraphDown, SelectWordLeft,
333 SelectWordRight,
334 SelectWordEnd, ViSelectWordEnd, SelectLineStart,
337 SelectLineEnd,
338 SelectDocumentStart,
339 SelectDocumentEnd,
340 SelectPageUp,
341 SelectPageDown,
342 SelectAll,
343 SelectWord,
344 SelectLine,
345 ExpandSelection,
346
347 BlockSelectLeft,
349 BlockSelectRight,
350 BlockSelectUp,
351 BlockSelectDown,
352
353 DeleteBackward,
355 DeleteForward,
356 DeleteWordBackward,
357 DeleteWordForward,
358 DeleteLine,
359 DeleteToLineEnd,
360 DeleteToLineStart,
361 DeleteViWordEnd, TransposeChars,
363 OpenLine,
364 DuplicateLine,
365
366 Recenter,
368
369 SetMark,
371
372 Copy,
374 CopyWithTheme(String),
375 Cut,
376 Paste,
377
378 YankWordForward,
380 YankWordBackward,
381 YankToLineEnd,
382 YankToLineStart,
383 YankViWordEnd, AddCursorAbove,
387 AddCursorBelow,
388 AddCursorNextMatch,
389 RemoveSecondaryCursors,
390
391 Save,
393 SaveAs,
394 Open,
395 SwitchProject,
396 New,
397 Close,
398 CloseTab,
399 Quit,
400 ForceQuit,
401 Detach,
402 Revert,
403 ToggleAutoRevert,
404 FormatBuffer,
405 TrimTrailingWhitespace,
406 EnsureFinalNewline,
407
408 GotoLine,
410 ScanLineIndex,
411 GoToMatchingBracket,
412 JumpToNextError,
413 JumpToPreviousError,
414
415 SmartHome,
417 DedentSelection,
418 ToggleComment,
419 DabbrevExpand,
420 ToggleFold,
421
422 SetBookmark(char),
424 JumpToBookmark(char),
425 ClearBookmark(char),
426 ListBookmarks,
427
428 ToggleSearchCaseSensitive,
430 ToggleSearchWholeWord,
431 ToggleSearchRegex,
432 ToggleSearchConfirmEach,
433
434 StartMacroRecording,
436 StopMacroRecording,
437 PlayMacro(char),
438 ToggleMacroRecording(char),
439 ShowMacro(char),
440 ListMacros,
441 PromptRecordMacro,
442 PromptPlayMacro,
443 PlayLastMacro,
444
445 PromptSetBookmark,
447 PromptJumpToBookmark,
448
449 Undo,
451 Redo,
452
453 ScrollUp,
455 ScrollDown,
456 ShowHelp,
457 ShowKeyboardShortcuts,
458 ShowWarnings,
459 ShowStatusLog,
460 ShowLspStatus,
461 ShowRemoteIndicatorMenu,
462 ClearWarnings,
463 CommandPalette, QuickOpen,
466 ToggleLineWrap,
467 ToggleCurrentLineHighlight,
468 ToggleReadOnly,
469 TogglePageView,
470 SetPageWidth,
471 InspectThemeAtCursor,
472 SelectTheme,
473 SelectKeybindingMap,
474 SelectCursorStyle,
475 SelectLocale,
476
477 NextBuffer,
479 PrevBuffer,
480 SwitchToPreviousTab,
481 SwitchToTabByName,
482
483 ScrollTabsLeft,
485 ScrollTabsRight,
486
487 NavigateBack,
489 NavigateForward,
490
491 SplitHorizontal,
493 SplitVertical,
494 CloseSplit,
495 NextSplit,
496 PrevSplit,
497 IncreaseSplitSize,
498 DecreaseSplitSize,
499 ToggleMaximizeSplit,
500
501 PromptConfirm,
503 PromptConfirmWithText(String),
505 PromptCancel,
506 PromptBackspace,
507 PromptDelete,
508 PromptMoveLeft,
509 PromptMoveRight,
510 PromptMoveStart,
511 PromptMoveEnd,
512 PromptSelectPrev,
513 PromptSelectNext,
514 PromptPageUp,
515 PromptPageDown,
516 PromptAcceptSuggestion,
517 PromptMoveWordLeft,
518 PromptMoveWordRight,
519 PromptDeleteWordForward,
521 PromptDeleteWordBackward,
522 PromptDeleteToLineEnd,
523 PromptCopy,
524 PromptCut,
525 PromptPaste,
526 PromptMoveLeftSelecting,
528 PromptMoveRightSelecting,
529 PromptMoveHomeSelecting,
530 PromptMoveEndSelecting,
531 PromptSelectWordLeft,
532 PromptSelectWordRight,
533 PromptSelectAll,
534
535 FileBrowserToggleHidden,
537 FileBrowserToggleDetectEncoding,
538
539 PopupSelectNext,
541 PopupSelectPrev,
542 PopupPageUp,
543 PopupPageDown,
544 PopupConfirm,
545 PopupCancel,
546
547 ToggleFileExplorer,
549 ToggleMenuBar,
551 ToggleTabBar,
553 ToggleStatusBar,
555 TogglePromptLine,
557 ToggleVerticalScrollbar,
559 ToggleHorizontalScrollbar,
560 FocusFileExplorer,
561 FocusEditor,
562 FileExplorerUp,
563 FileExplorerDown,
564 FileExplorerPageUp,
565 FileExplorerPageDown,
566 FileExplorerExpand,
567 FileExplorerCollapse,
568 FileExplorerOpen,
569 FileExplorerRefresh,
570 FileExplorerNewFile,
571 FileExplorerNewDirectory,
572 FileExplorerDelete,
573 FileExplorerRename,
574 FileExplorerToggleHidden,
575 FileExplorerToggleGitignored,
576 FileExplorerSearchClear,
577 FileExplorerSearchBackspace,
578 FileExplorerCopy,
579 FileExplorerCut,
580 FileExplorerPaste,
581 FileExplorerExtendSelectionUp,
582 FileExplorerExtendSelectionDown,
583 FileExplorerToggleSelect,
584 FileExplorerSelectAll,
585
586 LspCompletion,
588 LspGotoDefinition,
589 LspReferences,
590 LspRename,
591 LspHover,
592 LspSignatureHelp,
593 LspCodeActions,
594 LspRestart,
595 LspStop,
596 LspToggleForBuffer,
597 ToggleInlayHints,
598 ToggleMouseHover,
599
600 ToggleLineNumbers,
602 ToggleScrollSync,
603 ToggleMouseCapture,
604 ToggleDebugHighlights, SetBackground,
606 SetBackgroundBlend,
607
608 SetTabSize,
610 SetLineEnding,
611 SetEncoding,
612 ReloadWithEncoding,
613 SetLanguage,
614 ToggleIndentationStyle,
615 ToggleTabIndicators,
616 ToggleWhitespaceIndicators,
617 ResetBufferSettings,
618 AddRuler,
619 RemoveRuler,
620
621 DumpConfig,
623
624 RedrawScreen,
626
627 Search,
629 FindInSelection,
630 FindNext,
631 FindPrevious,
632 FindSelectionNext, FindSelectionPrevious, Replace,
635 QueryReplace, MenuActivate, MenuClose, MenuLeft, MenuRight, MenuUp, MenuDown, MenuExecute, MenuOpen(String), SwitchKeybindingMap(String), PluginAction(String),
652
653 OpenSettings, CloseSettings, SettingsSave, SettingsReset, SettingsToggleFocus, SettingsActivate, SettingsSearch, SettingsHelp, SettingsIncrement, SettingsDecrement, SettingsInherit, OpenTerminal, CloseTerminal, FocusTerminal, TerminalEscape, ToggleKeyboardCapture, TerminalPaste, ShellCommand, ShellCommandReplace, ToUpperCase, ToLowerCase, ToggleCase, SortLines, CalibrateInput, EventDebug, SuspendProcess, OpenKeybindingEditor, LoadPluginFromBuffer, InitReload, InitEdit, InitCheck, CompositeNextHunk, CompositePrevHunk, None,
710}
711
712macro_rules! define_action_str_mapping {
725 (
726 $args_name:ident;
727 simple { $($s_name:literal => $s_variant:ident),* $(,)? }
728 alias { $($a_name:literal => $a_variant:ident),* $(,)? }
729 with_char { $($c_name:literal => $c_variant:ident),* $(,)? }
730 custom { $($x_name:literal => $x_variant:ident : $x_body:expr),* $(,)? }
731 ) => {
732 pub fn from_str(s: &str, $args_name: &HashMap<String, serde_json::Value>) -> Option<Self> {
734 Some(match s {
735 $($s_name => Self::$s_variant,)*
736 $($a_name => Self::$a_variant,)*
737 $($c_name => return Self::with_char($args_name, Self::$c_variant),)*
738 $($x_name => $x_body,)*
739 _ => Self::PluginAction(s.to_string()),
742 })
743 }
744
745 pub fn to_action_str(&self) -> String {
748 match self {
749 $(Self::$s_variant => $s_name.to_string(),)*
750 $(Self::$c_variant(_) => $c_name.to_string(),)*
751 $(Self::$x_variant(_) => $x_name.to_string(),)*
752 Self::PluginAction(name) => name.clone(),
753 }
754 }
755
756 pub fn all_action_names() -> Vec<String> {
759 let mut names = vec![
760 $($s_name.to_string(),)*
761 $($a_name.to_string(),)*
762 $($c_name.to_string(),)*
763 $($x_name.to_string(),)*
764 ];
765 names.sort();
766 names
767 }
768 };
769}
770
771impl Action {
772 fn with_char(
773 args: &HashMap<String, serde_json::Value>,
774 make_action: impl FnOnce(char) -> Self,
775 ) -> Option<Self> {
776 if let Some(serde_json::Value::String(value)) = args.get("char") {
777 value.chars().next().map(make_action)
778 } else {
779 None
780 }
781 }
782
783 define_action_str_mapping! {
784 args;
785 simple {
786 "insert_newline" => InsertNewline,
787 "insert_tab" => InsertTab,
788
789 "move_left" => MoveLeft,
790 "move_right" => MoveRight,
791 "move_up" => MoveUp,
792 "move_down" => MoveDown,
793 "move_word_left" => MoveWordLeft,
794 "move_word_right" => MoveWordRight,
795 "move_word_end" => MoveWordEnd,
796 "vi_move_word_end" => ViMoveWordEnd,
797 "move_left_in_line" => MoveLeftInLine,
798 "move_right_in_line" => MoveRightInLine,
799 "move_line_start" => MoveLineStart,
800 "move_line_end" => MoveLineEnd,
801 "move_line_up" => MoveLineUp,
802 "move_line_down" => MoveLineDown,
803 "move_page_up" => MovePageUp,
804 "move_page_down" => MovePageDown,
805 "move_document_start" => MoveDocumentStart,
806 "move_document_end" => MoveDocumentEnd,
807
808 "select_left" => SelectLeft,
809 "select_right" => SelectRight,
810 "select_up" => SelectUp,
811 "select_down" => SelectDown,
812 "select_to_paragraph_up" => SelectToParagraphUp,
813 "select_to_paragraph_down" => SelectToParagraphDown,
814 "select_word_left" => SelectWordLeft,
815 "select_word_right" => SelectWordRight,
816 "select_word_end" => SelectWordEnd,
817 "vi_select_word_end" => ViSelectWordEnd,
818 "select_line_start" => SelectLineStart,
819 "select_line_end" => SelectLineEnd,
820 "select_document_start" => SelectDocumentStart,
821 "select_document_end" => SelectDocumentEnd,
822 "select_page_up" => SelectPageUp,
823 "select_page_down" => SelectPageDown,
824 "select_all" => SelectAll,
825 "select_word" => SelectWord,
826 "select_line" => SelectLine,
827 "expand_selection" => ExpandSelection,
828
829 "block_select_left" => BlockSelectLeft,
830 "block_select_right" => BlockSelectRight,
831 "block_select_up" => BlockSelectUp,
832 "block_select_down" => BlockSelectDown,
833
834 "delete_backward" => DeleteBackward,
835 "delete_forward" => DeleteForward,
836 "delete_word_backward" => DeleteWordBackward,
837 "delete_word_forward" => DeleteWordForward,
838 "delete_line" => DeleteLine,
839 "delete_to_line_end" => DeleteToLineEnd,
840 "delete_to_line_start" => DeleteToLineStart,
841 "delete_vi_word_end" => DeleteViWordEnd,
842 "transpose_chars" => TransposeChars,
843 "open_line" => OpenLine,
844 "duplicate_line" => DuplicateLine,
845 "recenter" => Recenter,
846 "set_mark" => SetMark,
847
848 "copy" => Copy,
849 "cut" => Cut,
850 "paste" => Paste,
851
852 "yank_word_forward" => YankWordForward,
853 "yank_word_backward" => YankWordBackward,
854 "yank_to_line_end" => YankToLineEnd,
855 "yank_to_line_start" => YankToLineStart,
856 "yank_vi_word_end" => YankViWordEnd,
857
858 "add_cursor_above" => AddCursorAbove,
859 "add_cursor_below" => AddCursorBelow,
860 "add_cursor_next_match" => AddCursorNextMatch,
861 "remove_secondary_cursors" => RemoveSecondaryCursors,
862
863 "save" => Save,
864 "save_as" => SaveAs,
865 "open" => Open,
866 "switch_project" => SwitchProject,
867 "new" => New,
868 "close" => Close,
869 "close_tab" => CloseTab,
870 "quit" => Quit,
871 "force_quit" => ForceQuit,
872 "detach" => Detach,
873 "revert" => Revert,
874 "toggle_auto_revert" => ToggleAutoRevert,
875 "format_buffer" => FormatBuffer,
876 "trim_trailing_whitespace" => TrimTrailingWhitespace,
877 "ensure_final_newline" => EnsureFinalNewline,
878 "goto_line" => GotoLine,
879 "scan_line_index" => ScanLineIndex,
880 "goto_matching_bracket" => GoToMatchingBracket,
881 "jump_to_next_error" => JumpToNextError,
882 "jump_to_previous_error" => JumpToPreviousError,
883
884 "smart_home" => SmartHome,
885 "dedent_selection" => DedentSelection,
886 "toggle_comment" => ToggleComment,
887 "dabbrev_expand" => DabbrevExpand,
888 "toggle_fold" => ToggleFold,
889
890 "list_bookmarks" => ListBookmarks,
891
892 "toggle_search_case_sensitive" => ToggleSearchCaseSensitive,
893 "toggle_search_whole_word" => ToggleSearchWholeWord,
894 "toggle_search_regex" => ToggleSearchRegex,
895 "toggle_search_confirm_each" => ToggleSearchConfirmEach,
896
897 "start_macro_recording" => StartMacroRecording,
898 "stop_macro_recording" => StopMacroRecording,
899
900 "list_macros" => ListMacros,
901 "prompt_record_macro" => PromptRecordMacro,
902 "prompt_play_macro" => PromptPlayMacro,
903 "play_last_macro" => PlayLastMacro,
904 "prompt_set_bookmark" => PromptSetBookmark,
905 "prompt_jump_to_bookmark" => PromptJumpToBookmark,
906
907 "undo" => Undo,
908 "redo" => Redo,
909
910 "scroll_up" => ScrollUp,
911 "scroll_down" => ScrollDown,
912 "show_help" => ShowHelp,
913 "keyboard_shortcuts" => ShowKeyboardShortcuts,
914 "show_warnings" => ShowWarnings,
915 "show_status_log" => ShowStatusLog,
916 "show_lsp_status" => ShowLspStatus,
917 "show_remote_indicator_menu" => ShowRemoteIndicatorMenu,
918 "clear_warnings" => ClearWarnings,
919 "command_palette" => CommandPalette,
920 "quick_open" => QuickOpen,
921 "toggle_line_wrap" => ToggleLineWrap,
922 "toggle_current_line_highlight" => ToggleCurrentLineHighlight,
923 "toggle_read_only" => ToggleReadOnly,
924 "toggle_page_view" => TogglePageView,
925 "set_page_width" => SetPageWidth,
926
927 "next_buffer" => NextBuffer,
928 "prev_buffer" => PrevBuffer,
929 "switch_to_previous_tab" => SwitchToPreviousTab,
930 "switch_to_tab_by_name" => SwitchToTabByName,
931 "scroll_tabs_left" => ScrollTabsLeft,
932 "scroll_tabs_right" => ScrollTabsRight,
933
934 "navigate_back" => NavigateBack,
935 "navigate_forward" => NavigateForward,
936
937 "split_horizontal" => SplitHorizontal,
938 "split_vertical" => SplitVertical,
939 "close_split" => CloseSplit,
940 "next_split" => NextSplit,
941 "prev_split" => PrevSplit,
942 "increase_split_size" => IncreaseSplitSize,
943 "decrease_split_size" => DecreaseSplitSize,
944 "toggle_maximize_split" => ToggleMaximizeSplit,
945
946 "prompt_confirm" => PromptConfirm,
947 "prompt_cancel" => PromptCancel,
948 "prompt_backspace" => PromptBackspace,
949 "prompt_move_left" => PromptMoveLeft,
950 "prompt_move_right" => PromptMoveRight,
951 "prompt_move_start" => PromptMoveStart,
952 "prompt_move_end" => PromptMoveEnd,
953 "prompt_select_prev" => PromptSelectPrev,
954 "prompt_select_next" => PromptSelectNext,
955 "prompt_page_up" => PromptPageUp,
956 "prompt_page_down" => PromptPageDown,
957 "prompt_accept_suggestion" => PromptAcceptSuggestion,
958 "prompt_delete_word_forward" => PromptDeleteWordForward,
959 "prompt_delete_word_backward" => PromptDeleteWordBackward,
960 "prompt_delete_to_line_end" => PromptDeleteToLineEnd,
961 "prompt_copy" => PromptCopy,
962 "prompt_cut" => PromptCut,
963 "prompt_paste" => PromptPaste,
964 "prompt_move_left_selecting" => PromptMoveLeftSelecting,
965 "prompt_move_right_selecting" => PromptMoveRightSelecting,
966 "prompt_move_home_selecting" => PromptMoveHomeSelecting,
967 "prompt_move_end_selecting" => PromptMoveEndSelecting,
968 "prompt_select_word_left" => PromptSelectWordLeft,
969 "prompt_select_word_right" => PromptSelectWordRight,
970 "prompt_select_all" => PromptSelectAll,
971 "file_browser_toggle_hidden" => FileBrowserToggleHidden,
972 "file_browser_toggle_detect_encoding" => FileBrowserToggleDetectEncoding,
973 "prompt_move_word_left" => PromptMoveWordLeft,
974 "prompt_move_word_right" => PromptMoveWordRight,
975 "prompt_delete" => PromptDelete,
976
977 "popup_select_next" => PopupSelectNext,
978 "popup_select_prev" => PopupSelectPrev,
979 "popup_page_up" => PopupPageUp,
980 "popup_page_down" => PopupPageDown,
981 "popup_confirm" => PopupConfirm,
982 "popup_cancel" => PopupCancel,
983
984 "toggle_file_explorer" => ToggleFileExplorer,
985 "toggle_menu_bar" => ToggleMenuBar,
986 "toggle_tab_bar" => ToggleTabBar,
987 "toggle_status_bar" => ToggleStatusBar,
988 "toggle_prompt_line" => TogglePromptLine,
989 "toggle_vertical_scrollbar" => ToggleVerticalScrollbar,
990 "toggle_horizontal_scrollbar" => ToggleHorizontalScrollbar,
991 "focus_file_explorer" => FocusFileExplorer,
992 "focus_editor" => FocusEditor,
993 "file_explorer_up" => FileExplorerUp,
994 "file_explorer_down" => FileExplorerDown,
995 "file_explorer_page_up" => FileExplorerPageUp,
996 "file_explorer_page_down" => FileExplorerPageDown,
997 "file_explorer_expand" => FileExplorerExpand,
998 "file_explorer_collapse" => FileExplorerCollapse,
999 "file_explorer_open" => FileExplorerOpen,
1000 "file_explorer_refresh" => FileExplorerRefresh,
1001 "file_explorer_new_file" => FileExplorerNewFile,
1002 "file_explorer_new_directory" => FileExplorerNewDirectory,
1003 "file_explorer_delete" => FileExplorerDelete,
1004 "file_explorer_rename" => FileExplorerRename,
1005 "file_explorer_toggle_hidden" => FileExplorerToggleHidden,
1006 "file_explorer_toggle_gitignored" => FileExplorerToggleGitignored,
1007 "file_explorer_search_clear" => FileExplorerSearchClear,
1008 "file_explorer_search_backspace" => FileExplorerSearchBackspace,
1009 "file_explorer_copy" => FileExplorerCopy,
1010 "file_explorer_cut" => FileExplorerCut,
1011 "file_explorer_paste" => FileExplorerPaste,
1012 "file_explorer_extend_selection_up" => FileExplorerExtendSelectionUp,
1013 "file_explorer_extend_selection_down" => FileExplorerExtendSelectionDown,
1014 "file_explorer_toggle_select" => FileExplorerToggleSelect,
1015 "file_explorer_select_all" => FileExplorerSelectAll,
1016
1017 "lsp_completion" => LspCompletion,
1018 "lsp_goto_definition" => LspGotoDefinition,
1019 "lsp_references" => LspReferences,
1020 "lsp_rename" => LspRename,
1021 "lsp_hover" => LspHover,
1022 "lsp_signature_help" => LspSignatureHelp,
1023 "lsp_code_actions" => LspCodeActions,
1024 "lsp_restart" => LspRestart,
1025 "lsp_stop" => LspStop,
1026 "lsp_toggle_for_buffer" => LspToggleForBuffer,
1027 "toggle_inlay_hints" => ToggleInlayHints,
1028 "toggle_mouse_hover" => ToggleMouseHover,
1029
1030 "toggle_line_numbers" => ToggleLineNumbers,
1031 "toggle_scroll_sync" => ToggleScrollSync,
1032 "toggle_mouse_capture" => ToggleMouseCapture,
1033 "toggle_debug_highlights" => ToggleDebugHighlights,
1034 "set_background" => SetBackground,
1035 "set_background_blend" => SetBackgroundBlend,
1036 "inspect_theme_at_cursor" => InspectThemeAtCursor,
1037 "select_theme" => SelectTheme,
1038 "select_keybinding_map" => SelectKeybindingMap,
1039 "select_cursor_style" => SelectCursorStyle,
1040 "select_locale" => SelectLocale,
1041
1042 "set_tab_size" => SetTabSize,
1043 "set_line_ending" => SetLineEnding,
1044 "set_encoding" => SetEncoding,
1045 "reload_with_encoding" => ReloadWithEncoding,
1046 "set_language" => SetLanguage,
1047 "toggle_indentation_style" => ToggleIndentationStyle,
1048 "toggle_tab_indicators" => ToggleTabIndicators,
1049 "toggle_whitespace_indicators" => ToggleWhitespaceIndicators,
1050 "reset_buffer_settings" => ResetBufferSettings,
1051 "add_ruler" => AddRuler,
1052 "remove_ruler" => RemoveRuler,
1053
1054 "dump_config" => DumpConfig,
1055 "redraw_screen" => RedrawScreen,
1056
1057 "search" => Search,
1058 "find_in_selection" => FindInSelection,
1059 "find_next" => FindNext,
1060 "find_previous" => FindPrevious,
1061 "find_selection_next" => FindSelectionNext,
1062 "find_selection_previous" => FindSelectionPrevious,
1063 "replace" => Replace,
1064 "query_replace" => QueryReplace,
1065
1066 "menu_activate" => MenuActivate,
1067 "menu_close" => MenuClose,
1068 "menu_left" => MenuLeft,
1069 "menu_right" => MenuRight,
1070 "menu_up" => MenuUp,
1071 "menu_down" => MenuDown,
1072 "menu_execute" => MenuExecute,
1073
1074 "open_terminal" => OpenTerminal,
1075 "close_terminal" => CloseTerminal,
1076 "focus_terminal" => FocusTerminal,
1077 "terminal_escape" => TerminalEscape,
1078 "toggle_keyboard_capture" => ToggleKeyboardCapture,
1079 "terminal_paste" => TerminalPaste,
1080
1081 "shell_command" => ShellCommand,
1082 "shell_command_replace" => ShellCommandReplace,
1083
1084 "to_upper_case" => ToUpperCase,
1085 "to_lower_case" => ToLowerCase,
1086 "toggle_case" => ToggleCase,
1087 "sort_lines" => SortLines,
1088
1089 "calibrate_input" => CalibrateInput,
1090 "event_debug" => EventDebug,
1091 "suspend_process" => SuspendProcess,
1092 "load_plugin_from_buffer" => LoadPluginFromBuffer,
1093 "init_reload" => InitReload,
1094 "init_edit" => InitEdit,
1095 "init_check" => InitCheck,
1096 "open_keybinding_editor" => OpenKeybindingEditor,
1097
1098 "composite_next_hunk" => CompositeNextHunk,
1099 "composite_prev_hunk" => CompositePrevHunk,
1100
1101 "noop" => None,
1102
1103 "open_settings" => OpenSettings,
1104 "close_settings" => CloseSettings,
1105 "settings_save" => SettingsSave,
1106 "settings_reset" => SettingsReset,
1107 "settings_toggle_focus" => SettingsToggleFocus,
1108 "settings_activate" => SettingsActivate,
1109 "settings_search" => SettingsSearch,
1110 "settings_help" => SettingsHelp,
1111 "settings_increment" => SettingsIncrement,
1112 "settings_decrement" => SettingsDecrement,
1113 "settings_inherit" => SettingsInherit,
1114 }
1115 alias {
1116 "toggle_compose_mode" => TogglePageView,
1117 "set_compose_width" => SetPageWidth,
1118 }
1119 with_char {
1120 "insert_char" => InsertChar,
1121 "set_bookmark" => SetBookmark,
1122 "jump_to_bookmark" => JumpToBookmark,
1123 "clear_bookmark" => ClearBookmark,
1124 "play_macro" => PlayMacro,
1125 "toggle_macro_recording" => ToggleMacroRecording,
1126 "show_macro" => ShowMacro,
1127 }
1128 custom {
1129 "copy_with_theme" => CopyWithTheme : {
1130 let theme = args.get("theme").and_then(|v| v.as_str()).unwrap_or("");
1132 Self::CopyWithTheme(theme.to_string())
1133 },
1134 "menu_open" => MenuOpen : {
1135 let name = args.get("name")?.as_str()?;
1136 Self::MenuOpen(name.to_string())
1137 },
1138 "switch_keybinding_map" => SwitchKeybindingMap : {
1139 let map_name = args.get("map")?.as_str()?;
1140 Self::SwitchKeybindingMap(map_name.to_string())
1141 },
1142 "prompt_confirm_with_text" => PromptConfirmWithText : {
1143 let text = args.get("text")?.as_str()?;
1144 Self::PromptConfirmWithText(text.to_string())
1145 },
1146 }
1147 }
1148
1149 pub fn variant_arg_key(bare_action: &str) -> Option<&'static str> {
1156 match bare_action {
1157 "menu_open" => Some("name"),
1158 "switch_keybinding_map" => Some("map"),
1159 _ => None,
1160 }
1161 }
1162
1163 pub fn qualify_action(bare_action: &str, args: &HashMap<String, serde_json::Value>) -> String {
1167 if let Some(key) = Self::variant_arg_key(bare_action) {
1168 if let Some(v) = args.get(key).and_then(|v| v.as_str()) {
1169 return format!("{}:{}", bare_action, v);
1170 }
1171 }
1172 bare_action.to_string()
1173 }
1174
1175 pub fn to_qualified_action_str(&self) -> String {
1180 match self {
1181 Self::MenuOpen(name) => format!("menu_open:{}", name),
1182 Self::SwitchKeybindingMap(map) => format!("switch_keybinding_map:{}", map),
1183 other => other.to_action_str(),
1184 }
1185 }
1186
1187 pub fn unqualify_action(qualified: &str) -> (String, HashMap<String, serde_json::Value>) {
1192 if let Some((bare, suffix)) = qualified.split_once(':') {
1193 if let Some(arg_key) = Self::variant_arg_key(bare) {
1194 let mut args = HashMap::new();
1195 args.insert(
1196 arg_key.to_string(),
1197 serde_json::Value::String(suffix.to_string()),
1198 );
1199 return (bare.to_string(), args);
1200 }
1201 }
1202 (qualified.to_string(), HashMap::new())
1203 }
1204
1205 pub fn is_movement_or_editing(&self) -> bool {
1208 matches!(
1209 self,
1210 Action::MoveLeft
1212 | Action::MoveRight
1213 | Action::MoveUp
1214 | Action::MoveDown
1215 | Action::MoveWordLeft
1216 | Action::MoveWordRight
1217 | Action::MoveWordEnd
1218 | Action::ViMoveWordEnd
1219 | Action::MoveLeftInLine
1220 | Action::MoveRightInLine
1221 | Action::MoveLineStart
1222 | Action::MoveLineEnd
1223 | Action::MovePageUp
1224 | Action::MovePageDown
1225 | Action::MoveDocumentStart
1226 | Action::MoveDocumentEnd
1227 | Action::SelectLeft
1229 | Action::SelectRight
1230 | Action::SelectUp
1231 | Action::SelectDown
1232 | Action::SelectToParagraphUp
1233 | Action::SelectToParagraphDown
1234 | Action::SelectWordLeft
1235 | Action::SelectWordRight
1236 | Action::SelectWordEnd
1237 | Action::ViSelectWordEnd
1238 | Action::SelectLineStart
1239 | Action::SelectLineEnd
1240 | Action::SelectDocumentStart
1241 | Action::SelectDocumentEnd
1242 | Action::SelectPageUp
1243 | Action::SelectPageDown
1244 | Action::SelectAll
1245 | Action::SelectWord
1246 | Action::SelectLine
1247 | Action::ExpandSelection
1248 | Action::BlockSelectLeft
1250 | Action::BlockSelectRight
1251 | Action::BlockSelectUp
1252 | Action::BlockSelectDown
1253 | Action::InsertChar(_)
1255 | Action::InsertNewline
1256 | Action::InsertTab
1257 | Action::DeleteBackward
1258 | Action::DeleteForward
1259 | Action::DeleteWordBackward
1260 | Action::DeleteWordForward
1261 | Action::DeleteLine
1262 | Action::DeleteToLineEnd
1263 | Action::DeleteToLineStart
1264 | Action::TransposeChars
1265 | Action::OpenLine
1266 | Action::DuplicateLine
1267 | Action::MoveLineUp
1268 | Action::MoveLineDown
1269 | Action::Cut
1271 | Action::Paste
1272 | Action::Undo
1274 | Action::Redo
1275 )
1276 }
1277
1278 pub fn is_editing(&self) -> bool {
1281 matches!(
1282 self,
1283 Action::InsertChar(_)
1284 | Action::InsertNewline
1285 | Action::InsertTab
1286 | Action::DeleteBackward
1287 | Action::DeleteForward
1288 | Action::DeleteWordBackward
1289 | Action::DeleteWordForward
1290 | Action::DeleteLine
1291 | Action::DeleteToLineEnd
1292 | Action::DeleteToLineStart
1293 | Action::DeleteViWordEnd
1294 | Action::TransposeChars
1295 | Action::OpenLine
1296 | Action::DuplicateLine
1297 | Action::MoveLineUp
1298 | Action::MoveLineDown
1299 | Action::Cut
1300 | Action::Paste
1301 )
1302 }
1303}
1304
1305#[derive(Debug, Clone, PartialEq)]
1307pub enum ChordResolution {
1308 Complete(Action),
1310 Partial,
1312 NoMatch,
1314}
1315
1316#[derive(Clone)]
1318pub struct KeybindingResolver {
1319 bindings: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1322
1323 default_bindings: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1325
1326 plugin_defaults: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1329
1330 chord_bindings: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1333
1334 default_chord_bindings: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1336
1337 plugin_chord_defaults: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1339
1340 inheriting_modes: std::collections::HashSet<String>,
1344}
1345
1346impl KeybindingResolver {
1347 pub fn new(config: &Config) -> Self {
1349 let mut resolver = Self {
1350 bindings: HashMap::new(),
1351 default_bindings: HashMap::new(),
1352 plugin_defaults: HashMap::new(),
1353 chord_bindings: HashMap::new(),
1354 default_chord_bindings: HashMap::new(),
1355 plugin_chord_defaults: HashMap::new(),
1356 inheriting_modes: std::collections::HashSet::new(),
1357 };
1358
1359 let map_bindings = config.resolve_keymap(&config.active_keybinding_map);
1361 resolver.load_default_bindings_from_vec(&map_bindings);
1362
1363 resolver.load_bindings_from_vec(&config.keybindings);
1365
1366 resolver
1367 }
1368
1369 fn load_default_bindings_from_vec(&mut self, bindings: &[crate::config::Keybinding]) {
1371 for binding in bindings {
1372 let context = if let Some(ref when) = binding.when {
1374 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
1375 } else {
1376 KeyContext::Normal
1377 };
1378
1379 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
1380 if !binding.keys.is_empty() {
1382 let mut sequence = Vec::new();
1384 for key_press in &binding.keys {
1385 if let Some(key_code) = Self::parse_key(&key_press.key) {
1386 let modifiers = Self::parse_modifiers(&key_press.modifiers);
1387 sequence.push((key_code, modifiers));
1388 } else {
1389 break;
1391 }
1392 }
1393
1394 if sequence.len() == binding.keys.len() && !sequence.is_empty() {
1396 self.default_chord_bindings
1397 .entry(context)
1398 .or_default()
1399 .insert(sequence, action);
1400 }
1401 } else if let Some(key_code) = Self::parse_key(&binding.key) {
1402 let modifiers = Self::parse_modifiers(&binding.modifiers);
1404
1405 self.insert_binding_with_equivalents(
1407 context,
1408 key_code,
1409 modifiers,
1410 action,
1411 &binding.key,
1412 );
1413 }
1414 }
1415 }
1416 }
1417
1418 fn insert_binding_with_equivalents(
1421 &mut self,
1422 context: KeyContext,
1423 key_code: KeyCode,
1424 modifiers: KeyModifiers,
1425 action: Action,
1426 key_name: &str,
1427 ) {
1428 let context_bindings = self.default_bindings.entry(context.clone()).or_default();
1429
1430 context_bindings.insert((key_code, modifiers), action.clone());
1432
1433 let equivalents = terminal_key_equivalents(key_code, modifiers);
1435 for (equiv_key, equiv_mods) in equivalents {
1436 if let Some(existing_action) = context_bindings.get(&(equiv_key, equiv_mods)) {
1438 if existing_action != &action {
1440 let equiv_name = format!("{:?}", equiv_key);
1441 tracing::warn!(
1442 "Terminal key equivalent conflict in {:?} context: {} (equivalent of {}) \
1443 is bound to {:?}, but {} is bound to {:?}. \
1444 The explicit binding takes precedence.",
1445 context,
1446 equiv_name,
1447 key_name,
1448 existing_action,
1449 key_name,
1450 action
1451 );
1452 }
1453 } else {
1455 context_bindings.insert((equiv_key, equiv_mods), action.clone());
1457 }
1458 }
1459 }
1460
1461 fn load_bindings_from_vec(&mut self, bindings: &[crate::config::Keybinding]) {
1463 for binding in bindings {
1464 let context = if let Some(ref when) = binding.when {
1466 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
1467 } else {
1468 KeyContext::Normal
1469 };
1470
1471 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
1472 if !binding.keys.is_empty() {
1474 let mut sequence = Vec::new();
1476 for key_press in &binding.keys {
1477 if let Some(key_code) = Self::parse_key(&key_press.key) {
1478 let modifiers = Self::parse_modifiers(&key_press.modifiers);
1479 sequence.push((key_code, modifiers));
1480 } else {
1481 break;
1483 }
1484 }
1485
1486 if sequence.len() == binding.keys.len() && !sequence.is_empty() {
1488 self.chord_bindings
1489 .entry(context)
1490 .or_default()
1491 .insert(sequence, action);
1492 }
1493 } else if let Some(key_code) = Self::parse_key(&binding.key) {
1494 let modifiers = Self::parse_modifiers(&binding.modifiers);
1496 self.bindings
1497 .entry(context)
1498 .or_default()
1499 .insert((key_code, modifiers), action);
1500 }
1501 }
1502 }
1503 }
1504
1505 pub fn load_plugin_default(
1507 &mut self,
1508 context: KeyContext,
1509 key_code: KeyCode,
1510 modifiers: KeyModifiers,
1511 action: Action,
1512 ) {
1513 self.plugin_defaults
1514 .entry(context)
1515 .or_default()
1516 .insert((key_code, modifiers), action);
1517 }
1518
1519 pub fn load_plugin_chord_default(
1521 &mut self,
1522 context: KeyContext,
1523 sequence: Vec<(KeyCode, KeyModifiers)>,
1524 action: Action,
1525 ) {
1526 self.plugin_chord_defaults
1527 .entry(context)
1528 .or_default()
1529 .insert(sequence, action);
1530 }
1531
1532 pub fn clear_plugin_defaults_for_mode(&mut self, mode_name: &str) {
1534 let context = KeyContext::Mode(mode_name.to_string());
1535 self.plugin_defaults.remove(&context);
1536 self.plugin_chord_defaults.remove(&context);
1537 self.inheriting_modes.remove(mode_name);
1538 }
1539
1540 pub fn set_mode_inherits_normal_bindings(&mut self, mode_name: &str, inherit: bool) {
1543 if inherit {
1544 self.inheriting_modes.insert(mode_name.to_string());
1545 } else {
1546 self.inheriting_modes.remove(mode_name);
1547 }
1548 }
1549
1550 pub fn get_plugin_defaults(
1552 &self,
1553 ) -> &HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>> {
1554 &self.plugin_defaults
1555 }
1556
1557 fn is_application_wide_action(action: &Action) -> bool {
1559 matches!(
1560 action,
1561 Action::Quit
1562 | Action::ForceQuit
1563 | Action::Save
1564 | Action::SaveAs
1565 | Action::ShowHelp
1566 | Action::ShowKeyboardShortcuts
1567 | Action::PromptCancel | Action::PopupCancel )
1570 }
1571
1572 pub fn is_terminal_ui_action(action: &Action) -> bool {
1576 matches!(
1577 action,
1578 Action::CommandPalette
1580 | Action::QuickOpen
1581 | Action::OpenSettings
1582 | Action::MenuActivate
1583 | Action::MenuOpen(_)
1584 | Action::ShowHelp
1585 | Action::ShowKeyboardShortcuts
1586 | Action::Quit
1587 | Action::ForceQuit
1588 | Action::NextSplit
1590 | Action::PrevSplit
1591 | Action::SplitHorizontal
1592 | Action::SplitVertical
1593 | Action::CloseSplit
1594 | Action::ToggleMaximizeSplit
1595 | Action::NextBuffer
1597 | Action::PrevBuffer
1598 | Action::Close
1599 | Action::ScrollTabsLeft
1600 | Action::ScrollTabsRight
1601 | Action::TerminalEscape
1603 | Action::ToggleKeyboardCapture
1604 | Action::OpenTerminal
1605 | Action::CloseTerminal
1606 | Action::TerminalPaste
1607 | Action::ToggleFileExplorer
1609 | Action::ToggleMenuBar
1611 )
1612 }
1613
1614 pub fn resolve_chord(
1620 &self,
1621 chord_state: &[(KeyCode, KeyModifiers)],
1622 event: &KeyEvent,
1623 context: KeyContext,
1624 ) -> ChordResolution {
1625 let mut full_sequence: Vec<(KeyCode, KeyModifiers)> = chord_state
1627 .iter()
1628 .map(|(c, m)| normalize_key(*c, *m))
1629 .collect();
1630 let (norm_code, norm_mods) = normalize_key(event.code, event.modifiers);
1631 full_sequence.push((norm_code, norm_mods));
1632
1633 tracing::trace!(
1634 "KeybindingResolver.resolve_chord: sequence={:?}, context={:?}",
1635 full_sequence,
1636 context
1637 );
1638
1639 let search_order = vec![
1641 (&self.chord_bindings, &KeyContext::Global, "custom global"),
1642 (
1643 &self.default_chord_bindings,
1644 &KeyContext::Global,
1645 "default global",
1646 ),
1647 (&self.chord_bindings, &context, "custom context"),
1648 (&self.default_chord_bindings, &context, "default context"),
1649 (
1650 &self.plugin_chord_defaults,
1651 &context,
1652 "plugin default context",
1653 ),
1654 ];
1655
1656 let mut has_partial_match = false;
1657
1658 for (binding_map, bind_context, label) in search_order {
1659 if let Some(context_chords) = binding_map.get(bind_context) {
1660 if let Some(action) = context_chords.get(&full_sequence) {
1662 tracing::trace!(" -> Complete chord match in {}: {:?}", label, action);
1663 return ChordResolution::Complete(action.clone());
1664 }
1665
1666 for (chord_seq, _) in context_chords.iter() {
1668 if chord_seq.len() > full_sequence.len()
1669 && chord_seq[..full_sequence.len()] == full_sequence[..]
1670 {
1671 tracing::trace!(" -> Partial chord match in {}", label);
1672 has_partial_match = true;
1673 break;
1674 }
1675 }
1676 }
1677 }
1678
1679 if has_partial_match {
1680 ChordResolution::Partial
1681 } else {
1682 tracing::trace!(" -> No chord match");
1683 ChordResolution::NoMatch
1684 }
1685 }
1686
1687 pub fn resolve(&self, event: &KeyEvent, context: KeyContext) -> Action {
1689 let (norm_code, norm_mods) = normalize_key(event.code, event.modifiers);
1692 let norm = &(norm_code, norm_mods);
1693 tracing::trace!(
1694 "KeybindingResolver.resolve: code={:?}, modifiers={:?}, context={:?}",
1695 event.code,
1696 event.modifiers,
1697 context
1698 );
1699
1700 if let Some(global_bindings) = self.bindings.get(&KeyContext::Global) {
1702 if let Some(action) = global_bindings.get(norm) {
1703 tracing::trace!(" -> Found in custom global bindings: {:?}", action);
1704 return action.clone();
1705 }
1706 }
1707
1708 if let Some(global_bindings) = self.default_bindings.get(&KeyContext::Global) {
1709 if let Some(action) = global_bindings.get(norm) {
1710 tracing::trace!(" -> Found in default global bindings: {:?}", action);
1711 return action.clone();
1712 }
1713 }
1714
1715 if let Some(context_bindings) = self.bindings.get(&context) {
1717 if let Some(action) = context_bindings.get(norm) {
1718 tracing::trace!(
1719 " -> Found in custom {} bindings: {:?}",
1720 context.to_when_clause(),
1721 action
1722 );
1723 return action.clone();
1724 }
1725 }
1726
1727 if let Some(context_bindings) = self.default_bindings.get(&context) {
1729 if let Some(action) = context_bindings.get(norm) {
1730 tracing::trace!(
1731 " -> Found in default {} bindings: {:?}",
1732 context.to_when_clause(),
1733 action
1734 );
1735 return action.clone();
1736 }
1737 }
1738
1739 if let Some(plugin_bindings) = self.plugin_defaults.get(&context) {
1741 if let Some(action) = plugin_bindings.get(norm) {
1742 tracing::trace!(
1743 " -> Found in plugin default {} bindings: {:?}",
1744 context.to_when_clause(),
1745 action
1746 );
1747 return action.clone();
1748 }
1749 }
1750
1751 if context != KeyContext::Normal {
1755 let full_fallthrough = context.allows_normal_fallthrough()
1756 || matches!(&context, KeyContext::Mode(name) if self.inheriting_modes.contains(name));
1757
1758 if let Some(normal_bindings) = self.bindings.get(&KeyContext::Normal) {
1759 if let Some(action) = normal_bindings.get(norm) {
1760 if full_fallthrough || Self::is_application_wide_action(action) {
1761 tracing::trace!(
1762 " -> Found action in custom normal bindings (fallthrough): {:?}",
1763 action
1764 );
1765 return action.clone();
1766 }
1767 }
1768 }
1769
1770 if let Some(normal_bindings) = self.default_bindings.get(&KeyContext::Normal) {
1771 if let Some(action) = normal_bindings.get(norm) {
1772 if full_fallthrough || Self::is_application_wide_action(action) {
1773 tracing::trace!(
1774 " -> Found action in default normal bindings (fallthrough): {:?}",
1775 action
1776 );
1777 return action.clone();
1778 }
1779 }
1780 }
1781 }
1782
1783 if context.allows_text_input() && is_text_input_modifier(event.modifiers) {
1785 if let KeyCode::Char(c) = event.code {
1786 tracing::trace!(" -> Character input: '{}'", c);
1787 return Action::InsertChar(c);
1788 }
1789 }
1790
1791 tracing::trace!(" -> No binding found, returning Action::None");
1792 Action::None
1793 }
1794
1795 pub fn resolve_in_context_only(&self, event: &KeyEvent, context: KeyContext) -> Option<Action> {
1800 let norm = normalize_key(event.code, event.modifiers);
1801 if let Some(context_bindings) = self.bindings.get(&context) {
1803 if let Some(action) = context_bindings.get(&norm) {
1804 return Some(action.clone());
1805 }
1806 }
1807
1808 if let Some(context_bindings) = self.default_bindings.get(&context) {
1810 if let Some(action) = context_bindings.get(&norm) {
1811 return Some(action.clone());
1812 }
1813 }
1814
1815 None
1816 }
1817
1818 pub fn resolve_terminal_ui_action(&self, event: &KeyEvent) -> Action {
1822 let norm = normalize_key(event.code, event.modifiers);
1823 tracing::trace!(
1824 "KeybindingResolver.resolve_terminal_ui_action: code={:?}, modifiers={:?}",
1825 event.code,
1826 event.modifiers
1827 );
1828
1829 for bindings in [&self.bindings, &self.default_bindings] {
1831 if let Some(terminal_bindings) = bindings.get(&KeyContext::Terminal) {
1832 if let Some(action) = terminal_bindings.get(&norm) {
1833 if Self::is_terminal_ui_action(action) {
1834 tracing::trace!(" -> Found UI action in terminal bindings: {:?}", action);
1835 return action.clone();
1836 }
1837 }
1838 }
1839 }
1840
1841 for bindings in [&self.bindings, &self.default_bindings] {
1843 if let Some(global_bindings) = bindings.get(&KeyContext::Global) {
1844 if let Some(action) = global_bindings.get(&norm) {
1845 if Self::is_terminal_ui_action(action) {
1846 tracing::trace!(" -> Found UI action in global bindings: {:?}", action);
1847 return action.clone();
1848 }
1849 }
1850 }
1851 }
1852
1853 for bindings in [&self.bindings, &self.default_bindings] {
1855 if let Some(normal_bindings) = bindings.get(&KeyContext::Normal) {
1856 if let Some(action) = normal_bindings.get(&norm) {
1857 if Self::is_terminal_ui_action(action) {
1858 tracing::trace!(" -> Found UI action in normal bindings: {:?}", action);
1859 return action.clone();
1860 }
1861 }
1862 }
1863 }
1864
1865 tracing::trace!(" -> No UI action found");
1866 Action::None
1867 }
1868
1869 pub fn find_keybinding_for_action(
1872 &self,
1873 action_name: &str,
1874 context: KeyContext,
1875 ) -> Option<String> {
1876 let target_action = Action::from_str(action_name, &HashMap::new())?;
1878
1879 let search_maps = vec![
1881 self.bindings.get(&context),
1882 self.bindings.get(&KeyContext::Global),
1883 self.default_bindings.get(&context),
1884 self.default_bindings.get(&KeyContext::Global),
1885 ];
1886
1887 for map in search_maps.into_iter().flatten() {
1888 let mut matches: Vec<(KeyCode, KeyModifiers)> = map
1890 .iter()
1891 .filter(|(_, action)| {
1892 std::mem::discriminant(*action) == std::mem::discriminant(&target_action)
1893 })
1894 .map(|((key_code, modifiers), _)| (*key_code, *modifiers))
1895 .collect();
1896
1897 if !matches.is_empty() {
1898 matches.sort_by(|(key_a, mod_a), (key_b, mod_b)| {
1900 let mod_count_a = mod_a.bits().count_ones();
1902 let mod_count_b = mod_b.bits().count_ones();
1903 match mod_count_a.cmp(&mod_count_b) {
1904 std::cmp::Ordering::Equal => {
1905 match mod_a.bits().cmp(&mod_b.bits()) {
1907 std::cmp::Ordering::Equal => {
1908 Self::key_code_sort_key(key_a)
1910 .cmp(&Self::key_code_sort_key(key_b))
1911 }
1912 other => other,
1913 }
1914 }
1915 other => other,
1916 }
1917 });
1918
1919 let (key_code, modifiers) = matches[0];
1920 return Some(format_keybinding(&key_code, &modifiers));
1921 }
1922 }
1923
1924 None
1925 }
1926
1927 fn key_code_sort_key(key_code: &KeyCode) -> (u8, u32) {
1929 match key_code {
1930 KeyCode::Char(c) => (0, *c as u32),
1931 KeyCode::F(n) => (1, *n as u32),
1932 KeyCode::Enter => (2, 0),
1933 KeyCode::Tab => (2, 1),
1934 KeyCode::Backspace => (2, 2),
1935 KeyCode::Delete => (2, 3),
1936 KeyCode::Esc => (2, 4),
1937 KeyCode::Left => (3, 0),
1938 KeyCode::Right => (3, 1),
1939 KeyCode::Up => (3, 2),
1940 KeyCode::Down => (3, 3),
1941 KeyCode::Home => (3, 4),
1942 KeyCode::End => (3, 5),
1943 KeyCode::PageUp => (3, 6),
1944 KeyCode::PageDown => (3, 7),
1945 _ => (255, 0),
1946 }
1947 }
1948
1949 pub fn find_menu_mnemonic(&self, menu_name: &str) -> Option<char> {
1952 let search_maps = vec![
1954 self.bindings.get(&KeyContext::Normal),
1955 self.bindings.get(&KeyContext::Global),
1956 self.default_bindings.get(&KeyContext::Normal),
1957 self.default_bindings.get(&KeyContext::Global),
1958 ];
1959
1960 for map in search_maps.into_iter().flatten() {
1961 for ((key_code, modifiers), action) in map {
1962 if let Action::MenuOpen(name) = action {
1964 if name.eq_ignore_ascii_case(menu_name) && *modifiers == KeyModifiers::ALT {
1965 if let KeyCode::Char(c) = key_code {
1967 return Some(c.to_ascii_lowercase());
1968 }
1969 }
1970 }
1971 }
1972 }
1973
1974 None
1975 }
1976
1977 fn parse_key(key: &str) -> Option<KeyCode> {
1979 let lower = key.to_lowercase();
1980 match lower.as_str() {
1981 "enter" => Some(KeyCode::Enter),
1982 "backspace" => Some(KeyCode::Backspace),
1983 "delete" | "del" => Some(KeyCode::Delete),
1984 "tab" => Some(KeyCode::Tab),
1985 "backtab" => Some(KeyCode::BackTab),
1986 "esc" | "escape" => Some(KeyCode::Esc),
1987 "space" => Some(KeyCode::Char(' ')),
1988
1989 "left" => Some(KeyCode::Left),
1990 "right" => Some(KeyCode::Right),
1991 "up" => Some(KeyCode::Up),
1992 "down" => Some(KeyCode::Down),
1993 "home" => Some(KeyCode::Home),
1994 "end" => Some(KeyCode::End),
1995 "pageup" => Some(KeyCode::PageUp),
1996 "pagedown" => Some(KeyCode::PageDown),
1997
1998 s if s.len() == 1 => s.chars().next().map(KeyCode::Char),
1999 s if s.starts_with('f') && s.len() >= 2 => s[1..].parse::<u8>().ok().map(KeyCode::F),
2001 _ => None,
2002 }
2003 }
2004
2005 fn parse_modifiers(modifiers: &[String]) -> KeyModifiers {
2007 let mut result = KeyModifiers::empty();
2008 for m in modifiers {
2009 match m.to_lowercase().as_str() {
2010 "ctrl" | "control" => result |= KeyModifiers::CONTROL,
2011 "shift" => result |= KeyModifiers::SHIFT,
2012 "alt" => result |= KeyModifiers::ALT,
2013 "super" | "cmd" | "command" | "meta" => result |= KeyModifiers::SUPER,
2014 _ => {}
2015 }
2016 }
2017 result
2018 }
2019
2020 pub fn get_all_bindings(&self) -> Vec<(String, String)> {
2024 let mut bindings = Vec::new();
2025
2026 for context in &[
2028 KeyContext::Normal,
2029 KeyContext::Prompt,
2030 KeyContext::Popup,
2031 KeyContext::FileExplorer,
2032 KeyContext::Menu,
2033 KeyContext::CompositeBuffer,
2034 ] {
2035 let mut all_keys: HashMap<(KeyCode, KeyModifiers), Action> = HashMap::new();
2036
2037 if let Some(context_defaults) = self.default_bindings.get(context) {
2039 for (key, action) in context_defaults {
2040 all_keys.insert(*key, action.clone());
2041 }
2042 }
2043
2044 if let Some(context_bindings) = self.bindings.get(context) {
2046 for (key, action) in context_bindings {
2047 all_keys.insert(*key, action.clone());
2048 }
2049 }
2050
2051 let context_str = if *context != KeyContext::Normal {
2053 format!("[{}] ", context.to_when_clause())
2054 } else {
2055 String::new()
2056 };
2057
2058 for ((key_code, modifiers), action) in all_keys {
2059 let key_str = Self::format_key(key_code, modifiers);
2060 let action_str = format!("{}{}", context_str, Self::format_action(&action));
2061 bindings.push((key_str, action_str));
2062 }
2063 }
2064
2065 bindings.sort_by(|a, b| a.1.cmp(&b.1));
2067
2068 bindings
2069 }
2070
2071 fn format_key(key_code: KeyCode, modifiers: KeyModifiers) -> String {
2073 format_keybinding(&key_code, &modifiers)
2074 }
2075
2076 pub fn format_action(action: &Action) -> String {
2078 match action {
2079 Action::InsertChar(c) => t!("action.insert_char", char = c),
2080 Action::InsertNewline => t!("action.insert_newline"),
2081 Action::InsertTab => t!("action.insert_tab"),
2082 Action::MoveLeft => t!("action.move_left"),
2083 Action::MoveRight => t!("action.move_right"),
2084 Action::MoveUp => t!("action.move_up"),
2085 Action::MoveDown => t!("action.move_down"),
2086 Action::MoveWordLeft => t!("action.move_word_left"),
2087 Action::MoveWordRight => t!("action.move_word_right"),
2088 Action::MoveWordEnd => t!("action.move_word_end"),
2089 Action::ViMoveWordEnd => t!("action.move_word_end"),
2090 Action::MoveLeftInLine => t!("action.move_left"),
2091 Action::MoveRightInLine => t!("action.move_right"),
2092 Action::MoveLineStart => t!("action.move_line_start"),
2093 Action::MoveLineEnd => t!("action.move_line_end"),
2094 Action::MoveLineUp => t!("action.move_line_up"),
2095 Action::MoveLineDown => t!("action.move_line_down"),
2096 Action::MovePageUp => t!("action.move_page_up"),
2097 Action::MovePageDown => t!("action.move_page_down"),
2098 Action::MoveDocumentStart => t!("action.move_document_start"),
2099 Action::MoveDocumentEnd => t!("action.move_document_end"),
2100 Action::SelectLeft => t!("action.select_left"),
2101 Action::SelectRight => t!("action.select_right"),
2102 Action::SelectUp => t!("action.select_up"),
2103 Action::SelectDown => t!("action.select_down"),
2104 Action::SelectToParagraphUp => t!("action.select_to_paragraph_up"),
2105 Action::SelectToParagraphDown => t!("action.select_to_paragraph_down"),
2106 Action::SelectWordLeft => t!("action.select_word_left"),
2107 Action::SelectWordRight => t!("action.select_word_right"),
2108 Action::SelectWordEnd => t!("action.select_word_end"),
2109 Action::ViSelectWordEnd => t!("action.select_word_end"),
2110 Action::SelectLineStart => t!("action.select_line_start"),
2111 Action::SelectLineEnd => t!("action.select_line_end"),
2112 Action::SelectDocumentStart => t!("action.select_document_start"),
2113 Action::SelectDocumentEnd => t!("action.select_document_end"),
2114 Action::SelectPageUp => t!("action.select_page_up"),
2115 Action::SelectPageDown => t!("action.select_page_down"),
2116 Action::SelectAll => t!("action.select_all"),
2117 Action::SelectWord => t!("action.select_word"),
2118 Action::SelectLine => t!("action.select_line"),
2119 Action::ExpandSelection => t!("action.expand_selection"),
2120 Action::BlockSelectLeft => t!("action.block_select_left"),
2121 Action::BlockSelectRight => t!("action.block_select_right"),
2122 Action::BlockSelectUp => t!("action.block_select_up"),
2123 Action::BlockSelectDown => t!("action.block_select_down"),
2124 Action::DeleteBackward => t!("action.delete_backward"),
2125 Action::DeleteForward => t!("action.delete_forward"),
2126 Action::DeleteWordBackward => t!("action.delete_word_backward"),
2127 Action::DeleteWordForward => t!("action.delete_word_forward"),
2128 Action::DeleteLine => t!("action.delete_line"),
2129 Action::DeleteToLineEnd => t!("action.delete_to_line_end"),
2130 Action::DeleteToLineStart => t!("action.delete_to_line_start"),
2131 Action::DeleteViWordEnd => t!("action.delete_word_forward"),
2132 Action::TransposeChars => t!("action.transpose_chars"),
2133 Action::OpenLine => t!("action.open_line"),
2134 Action::DuplicateLine => t!("action.duplicate_line"),
2135 Action::Recenter => t!("action.recenter"),
2136 Action::SetMark => t!("action.set_mark"),
2137 Action::Copy => t!("action.copy"),
2138 Action::CopyWithTheme(theme) if theme.is_empty() => t!("action.copy_with_formatting"),
2139 Action::CopyWithTheme(theme) => t!("action.copy_with_theme", theme = theme),
2140 Action::Cut => t!("action.cut"),
2141 Action::Paste => t!("action.paste"),
2142 Action::YankWordForward => t!("action.yank_word_forward"),
2143 Action::YankWordBackward => t!("action.yank_word_backward"),
2144 Action::YankToLineEnd => t!("action.yank_to_line_end"),
2145 Action::YankToLineStart => t!("action.yank_to_line_start"),
2146 Action::YankViWordEnd => t!("action.yank_word_forward"),
2147 Action::AddCursorAbove => t!("action.add_cursor_above"),
2148 Action::AddCursorBelow => t!("action.add_cursor_below"),
2149 Action::AddCursorNextMatch => t!("action.add_cursor_next_match"),
2150 Action::RemoveSecondaryCursors => t!("action.remove_secondary_cursors"),
2151 Action::Save => t!("action.save"),
2152 Action::SaveAs => t!("action.save_as"),
2153 Action::Open => t!("action.open"),
2154 Action::SwitchProject => t!("action.switch_project"),
2155 Action::New => t!("action.new"),
2156 Action::Close => t!("action.close"),
2157 Action::CloseTab => t!("action.close_tab"),
2158 Action::Quit => t!("action.quit"),
2159 Action::ForceQuit => t!("action.force_quit"),
2160 Action::Detach => t!("action.detach"),
2161 Action::Revert => t!("action.revert"),
2162 Action::ToggleAutoRevert => t!("action.toggle_auto_revert"),
2163 Action::FormatBuffer => t!("action.format_buffer"),
2164 Action::TrimTrailingWhitespace => t!("action.trim_trailing_whitespace"),
2165 Action::EnsureFinalNewline => t!("action.ensure_final_newline"),
2166 Action::GotoLine => t!("action.goto_line"),
2167 Action::ScanLineIndex => t!("action.scan_line_index"),
2168 Action::GoToMatchingBracket => t!("action.goto_matching_bracket"),
2169 Action::JumpToNextError => t!("action.jump_to_next_error"),
2170 Action::JumpToPreviousError => t!("action.jump_to_previous_error"),
2171 Action::SmartHome => t!("action.smart_home"),
2172 Action::DedentSelection => t!("action.dedent_selection"),
2173 Action::ToggleComment => t!("action.toggle_comment"),
2174 Action::DabbrevExpand => std::borrow::Cow::Borrowed("Expand abbreviation (dabbrev)"),
2175 Action::ToggleFold => t!("action.toggle_fold"),
2176 Action::SetBookmark(c) => t!("action.set_bookmark", key = c),
2177 Action::JumpToBookmark(c) => t!("action.jump_to_bookmark", key = c),
2178 Action::ClearBookmark(c) => t!("action.clear_bookmark", key = c),
2179 Action::ListBookmarks => t!("action.list_bookmarks"),
2180 Action::ToggleSearchCaseSensitive => t!("action.toggle_search_case_sensitive"),
2181 Action::ToggleSearchWholeWord => t!("action.toggle_search_whole_word"),
2182 Action::ToggleSearchRegex => t!("action.toggle_search_regex"),
2183 Action::ToggleSearchConfirmEach => t!("action.toggle_search_confirm_each"),
2184 Action::StartMacroRecording => t!("action.start_macro_recording"),
2185 Action::StopMacroRecording => t!("action.stop_macro_recording"),
2186 Action::PlayMacro(c) => t!("action.play_macro", key = c),
2187 Action::ToggleMacroRecording(c) => t!("action.toggle_macro_recording", key = c),
2188 Action::ShowMacro(c) => t!("action.show_macro", key = c),
2189 Action::ListMacros => t!("action.list_macros"),
2190 Action::PromptRecordMacro => t!("action.prompt_record_macro"),
2191 Action::PromptPlayMacro => t!("action.prompt_play_macro"),
2192 Action::PlayLastMacro => t!("action.play_last_macro"),
2193 Action::PromptSetBookmark => t!("action.prompt_set_bookmark"),
2194 Action::PromptJumpToBookmark => t!("action.prompt_jump_to_bookmark"),
2195 Action::Undo => t!("action.undo"),
2196 Action::Redo => t!("action.redo"),
2197 Action::ScrollUp => t!("action.scroll_up"),
2198 Action::ScrollDown => t!("action.scroll_down"),
2199 Action::ShowHelp => t!("action.show_help"),
2200 Action::ShowKeyboardShortcuts => t!("action.show_keyboard_shortcuts"),
2201 Action::ShowWarnings => t!("action.show_warnings"),
2202 Action::ShowStatusLog => t!("action.show_status_log"),
2203 Action::ShowLspStatus => t!("action.show_lsp_status"),
2204 Action::ShowRemoteIndicatorMenu => t!("action.show_remote_indicator_menu"),
2205 Action::ClearWarnings => t!("action.clear_warnings"),
2206 Action::CommandPalette => t!("action.command_palette"),
2207 Action::QuickOpen => t!("action.quick_open"),
2208 Action::InspectThemeAtCursor => t!("action.inspect_theme_at_cursor"),
2209 Action::ToggleLineWrap => t!("action.toggle_line_wrap"),
2210 Action::ToggleCurrentLineHighlight => t!("action.toggle_current_line_highlight"),
2211 Action::ToggleReadOnly => t!("action.toggle_read_only"),
2212 Action::TogglePageView => t!("action.toggle_page_view"),
2213 Action::SetPageWidth => t!("action.set_page_width"),
2214 Action::NextBuffer => t!("action.next_buffer"),
2215 Action::PrevBuffer => t!("action.prev_buffer"),
2216 Action::NavigateBack => t!("action.navigate_back"),
2217 Action::NavigateForward => t!("action.navigate_forward"),
2218 Action::SplitHorizontal => t!("action.split_horizontal"),
2219 Action::SplitVertical => t!("action.split_vertical"),
2220 Action::CloseSplit => t!("action.close_split"),
2221 Action::NextSplit => t!("action.next_split"),
2222 Action::PrevSplit => t!("action.prev_split"),
2223 Action::IncreaseSplitSize => t!("action.increase_split_size"),
2224 Action::DecreaseSplitSize => t!("action.decrease_split_size"),
2225 Action::ToggleMaximizeSplit => t!("action.toggle_maximize_split"),
2226 Action::PromptConfirm => t!("action.prompt_confirm"),
2227 Action::PromptConfirmWithText(ref text) => {
2228 format!("{} ({})", t!("action.prompt_confirm"), text).into()
2229 }
2230 Action::PromptCancel => t!("action.prompt_cancel"),
2231 Action::PromptBackspace => t!("action.prompt_backspace"),
2232 Action::PromptDelete => t!("action.prompt_delete"),
2233 Action::PromptMoveLeft => t!("action.prompt_move_left"),
2234 Action::PromptMoveRight => t!("action.prompt_move_right"),
2235 Action::PromptMoveStart => t!("action.prompt_move_start"),
2236 Action::PromptMoveEnd => t!("action.prompt_move_end"),
2237 Action::PromptSelectPrev => t!("action.prompt_select_prev"),
2238 Action::PromptSelectNext => t!("action.prompt_select_next"),
2239 Action::PromptPageUp => t!("action.prompt_page_up"),
2240 Action::PromptPageDown => t!("action.prompt_page_down"),
2241 Action::PromptAcceptSuggestion => t!("action.prompt_accept_suggestion"),
2242 Action::PromptMoveWordLeft => t!("action.prompt_move_word_left"),
2243 Action::PromptMoveWordRight => t!("action.prompt_move_word_right"),
2244 Action::PromptDeleteWordForward => t!("action.prompt_delete_word_forward"),
2245 Action::PromptDeleteWordBackward => t!("action.prompt_delete_word_backward"),
2246 Action::PromptDeleteToLineEnd => t!("action.prompt_delete_to_line_end"),
2247 Action::PromptCopy => t!("action.prompt_copy"),
2248 Action::PromptCut => t!("action.prompt_cut"),
2249 Action::PromptPaste => t!("action.prompt_paste"),
2250 Action::PromptMoveLeftSelecting => t!("action.prompt_move_left_selecting"),
2251 Action::PromptMoveRightSelecting => t!("action.prompt_move_right_selecting"),
2252 Action::PromptMoveHomeSelecting => t!("action.prompt_move_home_selecting"),
2253 Action::PromptMoveEndSelecting => t!("action.prompt_move_end_selecting"),
2254 Action::PromptSelectWordLeft => t!("action.prompt_select_word_left"),
2255 Action::PromptSelectWordRight => t!("action.prompt_select_word_right"),
2256 Action::PromptSelectAll => t!("action.prompt_select_all"),
2257 Action::FileBrowserToggleHidden => t!("action.file_browser_toggle_hidden"),
2258 Action::FileBrowserToggleDetectEncoding => {
2259 t!("action.file_browser_toggle_detect_encoding")
2260 }
2261 Action::PopupSelectNext => t!("action.popup_select_next"),
2262 Action::PopupSelectPrev => t!("action.popup_select_prev"),
2263 Action::PopupPageUp => t!("action.popup_page_up"),
2264 Action::PopupPageDown => t!("action.popup_page_down"),
2265 Action::PopupConfirm => t!("action.popup_confirm"),
2266 Action::PopupCancel => t!("action.popup_cancel"),
2267 Action::ToggleFileExplorer => t!("action.toggle_file_explorer"),
2268 Action::ToggleMenuBar => t!("action.toggle_menu_bar"),
2269 Action::ToggleTabBar => t!("action.toggle_tab_bar"),
2270 Action::ToggleStatusBar => t!("action.toggle_status_bar"),
2271 Action::TogglePromptLine => t!("action.toggle_prompt_line"),
2272 Action::ToggleVerticalScrollbar => t!("action.toggle_vertical_scrollbar"),
2273 Action::ToggleHorizontalScrollbar => t!("action.toggle_horizontal_scrollbar"),
2274 Action::FocusFileExplorer => t!("action.focus_file_explorer"),
2275 Action::FocusEditor => t!("action.focus_editor"),
2276 Action::FileExplorerUp => t!("action.file_explorer_up"),
2277 Action::FileExplorerDown => t!("action.file_explorer_down"),
2278 Action::FileExplorerPageUp => t!("action.file_explorer_page_up"),
2279 Action::FileExplorerPageDown => t!("action.file_explorer_page_down"),
2280 Action::FileExplorerExpand => t!("action.file_explorer_expand"),
2281 Action::FileExplorerCollapse => t!("action.file_explorer_collapse"),
2282 Action::FileExplorerOpen => t!("action.file_explorer_open"),
2283 Action::FileExplorerRefresh => t!("action.file_explorer_refresh"),
2284 Action::FileExplorerNewFile => t!("action.file_explorer_new_file"),
2285 Action::FileExplorerNewDirectory => t!("action.file_explorer_new_directory"),
2286 Action::FileExplorerDelete => t!("action.file_explorer_delete"),
2287 Action::FileExplorerRename => t!("action.file_explorer_rename"),
2288 Action::FileExplorerToggleHidden => t!("action.file_explorer_toggle_hidden"),
2289 Action::FileExplorerToggleGitignored => t!("action.file_explorer_toggle_gitignored"),
2290 Action::FileExplorerSearchClear => t!("action.file_explorer_search_clear"),
2291 Action::FileExplorerSearchBackspace => t!("action.file_explorer_search_backspace"),
2292 Action::FileExplorerCopy => t!("action.file_explorer_copy"),
2293 Action::FileExplorerCut => t!("action.file_explorer_cut"),
2294 Action::FileExplorerPaste => t!("action.file_explorer_paste"),
2295 Action::FileExplorerExtendSelectionUp => t!("action.file_explorer_extend_selection_up"),
2296 Action::FileExplorerExtendSelectionDown => {
2297 t!("action.file_explorer_extend_selection_down")
2298 }
2299 Action::FileExplorerToggleSelect => t!("action.file_explorer_toggle_select"),
2300 Action::FileExplorerSelectAll => t!("action.file_explorer_select_all"),
2301 Action::LspCompletion => t!("action.lsp_completion"),
2302 Action::LspGotoDefinition => t!("action.lsp_goto_definition"),
2303 Action::LspReferences => t!("action.lsp_references"),
2304 Action::LspRename => t!("action.lsp_rename"),
2305 Action::LspHover => t!("action.lsp_hover"),
2306 Action::LspSignatureHelp => t!("action.lsp_signature_help"),
2307 Action::LspCodeActions => t!("action.lsp_code_actions"),
2308 Action::LspRestart => t!("action.lsp_restart"),
2309 Action::LspStop => t!("action.lsp_stop"),
2310 Action::LspToggleForBuffer => t!("action.lsp_toggle_for_buffer"),
2311 Action::ToggleInlayHints => t!("action.toggle_inlay_hints"),
2312 Action::ToggleMouseHover => t!("action.toggle_mouse_hover"),
2313 Action::ToggleLineNumbers => t!("action.toggle_line_numbers"),
2314 Action::ToggleScrollSync => t!("action.toggle_scroll_sync"),
2315 Action::ToggleMouseCapture => t!("action.toggle_mouse_capture"),
2316 Action::ToggleDebugHighlights => t!("action.toggle_debug_highlights"),
2317 Action::SetBackground => t!("action.set_background"),
2318 Action::SetBackgroundBlend => t!("action.set_background_blend"),
2319 Action::AddRuler => t!("action.add_ruler"),
2320 Action::RemoveRuler => t!("action.remove_ruler"),
2321 Action::SetTabSize => t!("action.set_tab_size"),
2322 Action::SetLineEnding => t!("action.set_line_ending"),
2323 Action::SetEncoding => t!("action.set_encoding"),
2324 Action::ReloadWithEncoding => t!("action.reload_with_encoding"),
2325 Action::SetLanguage => t!("action.set_language"),
2326 Action::ToggleIndentationStyle => t!("action.toggle_indentation_style"),
2327 Action::ToggleTabIndicators => t!("action.toggle_tab_indicators"),
2328 Action::ToggleWhitespaceIndicators => t!("action.toggle_whitespace_indicators"),
2329 Action::ResetBufferSettings => t!("action.reset_buffer_settings"),
2330 Action::DumpConfig => t!("action.dump_config"),
2331 Action::RedrawScreen => t!("action.redraw_screen"),
2332 Action::Search => t!("action.search"),
2333 Action::FindInSelection => t!("action.find_in_selection"),
2334 Action::FindNext => t!("action.find_next"),
2335 Action::FindPrevious => t!("action.find_previous"),
2336 Action::FindSelectionNext => t!("action.find_selection_next"),
2337 Action::FindSelectionPrevious => t!("action.find_selection_previous"),
2338 Action::Replace => t!("action.replace"),
2339 Action::QueryReplace => t!("action.query_replace"),
2340 Action::MenuActivate => t!("action.menu_activate"),
2341 Action::MenuClose => t!("action.menu_close"),
2342 Action::MenuLeft => t!("action.menu_left"),
2343 Action::MenuRight => t!("action.menu_right"),
2344 Action::MenuUp => t!("action.menu_up"),
2345 Action::MenuDown => t!("action.menu_down"),
2346 Action::MenuExecute => t!("action.menu_execute"),
2347 Action::MenuOpen(name) => t!("action.menu_open", name = name),
2348 Action::SwitchKeybindingMap(map) => t!("action.switch_keybinding_map", map = map),
2349 Action::PluginAction(name) => t!("action.plugin_action", name = name),
2350 Action::ScrollTabsLeft => t!("action.scroll_tabs_left"),
2351 Action::ScrollTabsRight => t!("action.scroll_tabs_right"),
2352 Action::SelectTheme => t!("action.select_theme"),
2353 Action::SelectKeybindingMap => t!("action.select_keybinding_map"),
2354 Action::SelectCursorStyle => t!("action.select_cursor_style"),
2355 Action::SelectLocale => t!("action.select_locale"),
2356 Action::SwitchToPreviousTab => t!("action.switch_to_previous_tab"),
2357 Action::SwitchToTabByName => t!("action.switch_to_tab_by_name"),
2358 Action::OpenTerminal => t!("action.open_terminal"),
2359 Action::CloseTerminal => t!("action.close_terminal"),
2360 Action::FocusTerminal => t!("action.focus_terminal"),
2361 Action::TerminalEscape => t!("action.terminal_escape"),
2362 Action::ToggleKeyboardCapture => t!("action.toggle_keyboard_capture"),
2363 Action::TerminalPaste => t!("action.terminal_paste"),
2364 Action::OpenSettings => t!("action.open_settings"),
2365 Action::CloseSettings => t!("action.close_settings"),
2366 Action::SettingsSave => t!("action.settings_save"),
2367 Action::SettingsReset => t!("action.settings_reset"),
2368 Action::SettingsToggleFocus => t!("action.settings_toggle_focus"),
2369 Action::SettingsActivate => t!("action.settings_activate"),
2370 Action::SettingsSearch => t!("action.settings_search"),
2371 Action::SettingsHelp => t!("action.settings_help"),
2372 Action::SettingsIncrement => t!("action.settings_increment"),
2373 Action::SettingsDecrement => t!("action.settings_decrement"),
2374 Action::SettingsInherit => t!("action.settings_inherit"),
2375 Action::ShellCommand => t!("action.shell_command"),
2376 Action::ShellCommandReplace => t!("action.shell_command_replace"),
2377 Action::ToUpperCase => t!("action.to_uppercase"),
2378 Action::ToLowerCase => t!("action.to_lowercase"),
2379 Action::ToggleCase => t!("action.to_uppercase"),
2380 Action::SortLines => t!("action.sort_lines"),
2381 Action::CalibrateInput => t!("action.calibrate_input"),
2382 Action::EventDebug => t!("action.event_debug"),
2383 Action::SuspendProcess => t!("action.suspend_process"),
2384 Action::LoadPluginFromBuffer => "Load Plugin from Buffer".into(),
2385 Action::InitReload => "Reload init.ts".into(),
2386 Action::InitEdit => "Edit init.ts".into(),
2387 Action::InitCheck => "Check init.ts".into(),
2388 Action::OpenKeybindingEditor => "Keybinding Editor".into(),
2389 Action::CompositeNextHunk => t!("action.composite_next_hunk"),
2390 Action::CompositePrevHunk => t!("action.composite_prev_hunk"),
2391 Action::None => t!("action.none"),
2392 }
2393 .to_string()
2394 }
2395
2396 pub fn parse_key_public(key: &str) -> Option<KeyCode> {
2398 Self::parse_key(key)
2399 }
2400
2401 pub fn parse_modifiers_public(modifiers: &[String]) -> KeyModifiers {
2403 Self::parse_modifiers(modifiers)
2404 }
2405
2406 pub fn format_action_from_str(action_name: &str) -> String {
2410 Self::format_action_from_str_with_args(action_name, &std::collections::HashMap::new())
2411 }
2412
2413 pub fn format_action_from_str_with_args(
2417 action_name: &str,
2418 args: &std::collections::HashMap<String, serde_json::Value>,
2419 ) -> String {
2420 if let Some(action) = Action::from_str(action_name, args) {
2422 Self::format_action(&action)
2423 } else {
2424 action_name
2426 .split('_')
2427 .map(|word| {
2428 let mut chars = word.chars();
2429 match chars.next() {
2430 Some(c) => {
2431 let upper: String = c.to_uppercase().collect();
2432 format!("{}{}", upper, chars.as_str())
2433 }
2434 None => String::new(),
2435 }
2436 })
2437 .collect::<Vec<_>>()
2438 .join(" ")
2439 }
2440 }
2441
2442 pub fn all_action_names() -> Vec<String> {
2446 Action::all_action_names()
2447 }
2448
2449 pub fn get_keybinding_for_action(
2455 &self,
2456 action: &Action,
2457 context: KeyContext,
2458 ) -> Option<String> {
2459 self.get_keybinding_event_for_action(action, context)
2460 .map(|(k, m)| format_keybinding(&k, &m))
2461 }
2462
2463 pub fn get_keybinding_event_for_action(
2470 &self,
2471 action: &Action,
2472 context: KeyContext,
2473 ) -> Option<(KeyCode, KeyModifiers)> {
2474 fn find_best_keybinding(
2476 bindings: &HashMap<(KeyCode, KeyModifiers), Action>,
2477 action: &Action,
2478 ) -> Option<(KeyCode, KeyModifiers)> {
2479 let matches: Vec<_> = bindings
2480 .iter()
2481 .filter(|(_, a)| *a == action)
2482 .map(|((k, m), _)| (*k, *m))
2483 .collect();
2484
2485 if matches.is_empty() {
2486 return None;
2487 }
2488
2489 let mut sorted = matches;
2492 sorted.sort_by(|(k1, m1), (k2, m2)| {
2493 let score1 = keybinding_priority_score(k1);
2494 let score2 = keybinding_priority_score(k2);
2495 match score1.cmp(&score2) {
2497 std::cmp::Ordering::Equal => {
2498 let s1 = format_keybinding(k1, m1);
2500 let s2 = format_keybinding(k2, m2);
2501 s1.cmp(&s2)
2502 }
2503 other => other,
2504 }
2505 });
2506
2507 sorted.into_iter().next()
2508 }
2509
2510 if let Some(context_bindings) = self.bindings.get(&context) {
2512 if let Some(hit) = find_best_keybinding(context_bindings, action) {
2513 return Some(hit);
2514 }
2515 }
2516
2517 if let Some(context_bindings) = self.default_bindings.get(&context) {
2519 if let Some(hit) = find_best_keybinding(context_bindings, action) {
2520 return Some(hit);
2521 }
2522 }
2523
2524 if context != KeyContext::Normal
2526 && (context.allows_normal_fallthrough() || Self::is_application_wide_action(action))
2527 {
2528 if let Some(normal_bindings) = self.bindings.get(&KeyContext::Normal) {
2530 if let Some(hit) = find_best_keybinding(normal_bindings, action) {
2531 return Some(hit);
2532 }
2533 }
2534
2535 if let Some(normal_bindings) = self.default_bindings.get(&KeyContext::Normal) {
2537 if let Some(hit) = find_best_keybinding(normal_bindings, action) {
2538 return Some(hit);
2539 }
2540 }
2541 }
2542
2543 None
2544 }
2545
2546 pub fn reload(&mut self, config: &Config) {
2548 self.bindings.clear();
2549 for binding in &config.keybindings {
2550 if let Some(key_code) = Self::parse_key(&binding.key) {
2551 let modifiers = Self::parse_modifiers(&binding.modifiers);
2552 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
2553 let context = if let Some(ref when) = binding.when {
2555 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
2556 } else {
2557 KeyContext::Normal
2558 };
2559
2560 self.bindings
2561 .entry(context)
2562 .or_default()
2563 .insert((key_code, modifiers), action);
2564 }
2565 }
2566 }
2567 }
2568}
2569
2570#[cfg(test)]
2571mod tests {
2572 use super::*;
2573
2574 #[test]
2575 fn test_parse_key() {
2576 assert_eq!(KeybindingResolver::parse_key("enter"), Some(KeyCode::Enter));
2577 assert_eq!(
2578 KeybindingResolver::parse_key("backspace"),
2579 Some(KeyCode::Backspace)
2580 );
2581 assert_eq!(KeybindingResolver::parse_key("tab"), Some(KeyCode::Tab));
2582 assert_eq!(
2583 KeybindingResolver::parse_key("backtab"),
2584 Some(KeyCode::BackTab)
2585 );
2586 assert_eq!(
2587 KeybindingResolver::parse_key("BackTab"),
2588 Some(KeyCode::BackTab)
2589 );
2590 assert_eq!(KeybindingResolver::parse_key("a"), Some(KeyCode::Char('a')));
2591 }
2592
2593 #[test]
2594 fn test_parse_modifiers() {
2595 let mods = vec!["ctrl".to_string()];
2596 assert_eq!(
2597 KeybindingResolver::parse_modifiers(&mods),
2598 KeyModifiers::CONTROL
2599 );
2600
2601 let mods = vec!["ctrl".to_string(), "shift".to_string()];
2602 assert_eq!(
2603 KeybindingResolver::parse_modifiers(&mods),
2604 KeyModifiers::CONTROL | KeyModifiers::SHIFT
2605 );
2606 }
2607
2608 #[test]
2609 fn test_format_action_from_str_distinguishes_menu_open_by_name() {
2610 let mut file_args = HashMap::new();
2613 file_args.insert(
2614 "name".to_string(),
2615 serde_json::Value::String("File".to_string()),
2616 );
2617 let mut edit_args = HashMap::new();
2618 edit_args.insert(
2619 "name".to_string(),
2620 serde_json::Value::String("Edit".to_string()),
2621 );
2622
2623 let file_display =
2624 KeybindingResolver::format_action_from_str_with_args("menu_open", &file_args);
2625 let edit_display =
2626 KeybindingResolver::format_action_from_str_with_args("menu_open", &edit_args);
2627 let no_args_display = KeybindingResolver::format_action_from_str("menu_open");
2628
2629 assert_ne!(
2630 file_display, edit_display,
2631 "menu_open with different names should produce different descriptions"
2632 );
2633 assert!(
2634 file_display.contains("File"),
2635 "expected the File menu description to contain \"File\", got {file_display:?}"
2636 );
2637 assert!(
2638 edit_display.contains("Edit"),
2639 "expected the Edit menu description to contain \"Edit\", got {edit_display:?}"
2640 );
2641 assert_eq!(no_args_display, "Menu Open");
2645 }
2646
2647 #[test]
2648 fn test_qualify_and_unqualify_roundtrip_menu_open() {
2649 let mut args = HashMap::new();
2650 args.insert(
2651 "name".to_string(),
2652 serde_json::Value::String("File".to_string()),
2653 );
2654
2655 let qualified = Action::qualify_action("menu_open", &args);
2656 assert_eq!(qualified, "menu_open:File");
2657
2658 let (bare, parsed_args) = Action::unqualify_action(&qualified);
2659 assert_eq!(bare, "menu_open");
2660 assert_eq!(
2661 parsed_args.get("name").and_then(|v| v.as_str()),
2662 Some("File")
2663 );
2664 }
2665
2666 #[test]
2667 fn test_qualify_action_passthrough_for_unparameterised() {
2668 let args = HashMap::new();
2670 assert_eq!(Action::qualify_action("save", &args), "save");
2671 let (bare, parsed) = Action::unqualify_action("save");
2672 assert_eq!(bare, "save");
2673 assert!(parsed.is_empty());
2674 }
2675
2676 #[test]
2677 fn test_qualify_action_no_suffix_when_arg_missing() {
2678 let args = HashMap::new();
2681 assert_eq!(Action::qualify_action("menu_open", &args), "menu_open");
2682 }
2683
2684 #[test]
2685 fn test_unqualify_action_ignores_colon_on_unknown_action() {
2686 let (bare, parsed) = Action::unqualify_action("my_plugin:action_with:colons");
2689 assert_eq!(bare, "my_plugin:action_with:colons");
2690 assert!(parsed.is_empty());
2691 }
2692
2693 #[test]
2694 fn test_to_qualified_action_str_for_menu_open() {
2695 let action = Action::MenuOpen("Edit".to_string());
2696 assert_eq!(action.to_qualified_action_str(), "menu_open:Edit");
2697 }
2698
2699 #[test]
2700 fn test_resolve_basic() {
2701 let config = Config::default();
2702 let resolver = KeybindingResolver::new(&config);
2703
2704 let event = KeyEvent::new(KeyCode::Left, KeyModifiers::empty());
2705 assert_eq!(
2706 resolver.resolve(&event, KeyContext::Normal),
2707 Action::MoveLeft
2708 );
2709
2710 let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
2711 assert_eq!(
2712 resolver.resolve(&event, KeyContext::Normal),
2713 Action::InsertChar('a')
2714 );
2715 }
2716
2717 #[test]
2718 fn test_shift_backspace_matches_backspace() {
2719 let config = Config::default();
2723 let resolver = KeybindingResolver::new(&config);
2724
2725 let backspace = KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty());
2726 let shift_backspace = KeyEvent::new(KeyCode::Backspace, KeyModifiers::SHIFT);
2727
2728 assert_eq!(
2730 resolver.resolve(&backspace, KeyContext::Normal),
2731 Action::DeleteBackward,
2732 "Backspace should resolve to DeleteBackward in Normal context"
2733 );
2734 assert_eq!(
2735 resolver.resolve(&shift_backspace, KeyContext::Normal),
2736 Action::DeleteBackward,
2737 "Shift+Backspace should resolve to DeleteBackward (same as Backspace) in Normal context"
2738 );
2739
2740 assert_eq!(
2742 resolver.resolve(&backspace, KeyContext::Prompt),
2743 Action::PromptBackspace,
2744 "Backspace should resolve to PromptBackspace in Prompt context"
2745 );
2746 assert_eq!(
2747 resolver.resolve(&shift_backspace, KeyContext::Prompt),
2748 Action::PromptBackspace,
2749 "Shift+Backspace should resolve to PromptBackspace (same as Backspace) in Prompt context"
2750 );
2751
2752 assert_eq!(
2754 resolver.resolve(&backspace, KeyContext::FileExplorer),
2755 Action::FileExplorerSearchBackspace,
2756 "Backspace should resolve to FileExplorerSearchBackspace in FileExplorer context"
2757 );
2758 assert_eq!(
2759 resolver.resolve(&shift_backspace, KeyContext::FileExplorer),
2760 Action::FileExplorerSearchBackspace,
2761 "Shift+Backspace should resolve to FileExplorerSearchBackspace (same as Backspace) in FileExplorer context"
2762 );
2763 }
2764
2765 #[test]
2766 fn test_action_from_str() {
2767 let args = HashMap::new();
2768 assert_eq!(Action::from_str("move_left", &args), Some(Action::MoveLeft));
2769 assert_eq!(Action::from_str("save", &args), Some(Action::Save));
2770 assert_eq!(
2772 Action::from_str("unknown", &args),
2773 Some(Action::PluginAction("unknown".to_string()))
2774 );
2775
2776 assert_eq!(
2778 Action::from_str("keyboard_shortcuts", &args),
2779 Some(Action::ShowKeyboardShortcuts)
2780 );
2781 assert_eq!(
2782 Action::from_str("prompt_confirm", &args),
2783 Some(Action::PromptConfirm)
2784 );
2785 assert_eq!(
2786 Action::from_str("popup_cancel", &args),
2787 Some(Action::PopupCancel)
2788 );
2789
2790 assert_eq!(
2792 Action::from_str("calibrate_input", &args),
2793 Some(Action::CalibrateInput)
2794 );
2795 }
2796
2797 #[test]
2798 fn test_key_context_from_when_clause() {
2799 assert_eq!(
2800 KeyContext::from_when_clause("normal"),
2801 Some(KeyContext::Normal)
2802 );
2803 assert_eq!(
2804 KeyContext::from_when_clause("prompt"),
2805 Some(KeyContext::Prompt)
2806 );
2807 assert_eq!(
2808 KeyContext::from_when_clause("popup"),
2809 Some(KeyContext::Popup)
2810 );
2811 assert_eq!(KeyContext::from_when_clause("help"), None);
2812 assert_eq!(KeyContext::from_when_clause(" help "), None); assert_eq!(KeyContext::from_when_clause("unknown"), None);
2814 assert_eq!(KeyContext::from_when_clause(""), None);
2815 }
2816
2817 #[test]
2818 fn test_key_context_to_when_clause() {
2819 assert_eq!(KeyContext::Normal.to_when_clause(), "normal");
2820 assert_eq!(KeyContext::Prompt.to_when_clause(), "prompt");
2821 assert_eq!(KeyContext::Popup.to_when_clause(), "popup");
2822 }
2823
2824 #[test]
2825 fn test_context_specific_bindings() {
2826 let config = Config::default();
2827 let resolver = KeybindingResolver::new(&config);
2828
2829 let enter_event = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
2831 assert_eq!(
2832 resolver.resolve(&enter_event, KeyContext::Prompt),
2833 Action::PromptConfirm
2834 );
2835 assert_eq!(
2836 resolver.resolve(&enter_event, KeyContext::Normal),
2837 Action::InsertNewline
2838 );
2839
2840 let up_event = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
2842 assert_eq!(
2843 resolver.resolve(&up_event, KeyContext::Popup),
2844 Action::PopupSelectPrev
2845 );
2846 assert_eq!(
2847 resolver.resolve(&up_event, KeyContext::Normal),
2848 Action::MoveUp
2849 );
2850 }
2851
2852 #[test]
2853 fn test_context_fallback_to_normal() {
2854 let config = Config::default();
2855 let resolver = KeybindingResolver::new(&config);
2856
2857 let save_event = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL);
2859 assert_eq!(
2860 resolver.resolve(&save_event, KeyContext::Normal),
2861 Action::Save
2862 );
2863 assert_eq!(
2864 resolver.resolve(&save_event, KeyContext::Popup),
2865 Action::Save
2866 );
2867 }
2869
2870 #[test]
2871 fn test_context_priority_resolution() {
2872 use crate::config::Keybinding;
2873
2874 let mut config = Config::default();
2876 config.keybindings.push(Keybinding {
2877 key: "esc".to_string(),
2878 modifiers: vec![],
2879 keys: vec![],
2880 action: "quit".to_string(), args: HashMap::new(),
2882 when: Some("popup".to_string()),
2883 });
2884
2885 let resolver = KeybindingResolver::new(&config);
2886 let esc_event = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
2887
2888 assert_eq!(
2890 resolver.resolve(&esc_event, KeyContext::Popup),
2891 Action::Quit
2892 );
2893
2894 assert_eq!(
2896 resolver.resolve(&esc_event, KeyContext::Normal),
2897 Action::RemoveSecondaryCursors
2898 );
2899 }
2900
2901 #[test]
2902 fn test_character_input_in_contexts() {
2903 let config = Config::default();
2904 let resolver = KeybindingResolver::new(&config);
2905
2906 let char_event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
2907
2908 assert_eq!(
2910 resolver.resolve(&char_event, KeyContext::Normal),
2911 Action::InsertChar('a')
2912 );
2913 assert_eq!(
2914 resolver.resolve(&char_event, KeyContext::Prompt),
2915 Action::InsertChar('a')
2916 );
2917
2918 assert_eq!(
2920 resolver.resolve(&char_event, KeyContext::Popup),
2921 Action::None
2922 );
2923 }
2924
2925 #[test]
2926 fn test_custom_keybinding_loading() {
2927 use crate::config::Keybinding;
2928
2929 let mut config = Config::default();
2930
2931 config.keybindings.push(Keybinding {
2933 key: "f".to_string(),
2934 modifiers: vec!["ctrl".to_string()],
2935 keys: vec![],
2936 action: "command_palette".to_string(),
2937 args: HashMap::new(),
2938 when: None, });
2940
2941 let resolver = KeybindingResolver::new(&config);
2942
2943 let ctrl_f = KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL);
2945 assert_eq!(
2946 resolver.resolve(&ctrl_f, KeyContext::Normal),
2947 Action::CommandPalette
2948 );
2949
2950 let ctrl_k = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL);
2952 assert_eq!(
2953 resolver.resolve(&ctrl_k, KeyContext::Prompt),
2954 Action::PromptDeleteToLineEnd
2955 );
2956 assert_eq!(
2957 resolver.resolve(&ctrl_k, KeyContext::Normal),
2958 Action::DeleteToLineEnd
2959 );
2960 }
2961
2962 #[test]
2963 fn test_all_context_default_bindings_exist() {
2964 let config = Config::default();
2965 let resolver = KeybindingResolver::new(&config);
2966
2967 assert!(resolver.default_bindings.contains_key(&KeyContext::Normal));
2969 assert!(resolver.default_bindings.contains_key(&KeyContext::Prompt));
2970 assert!(resolver.default_bindings.contains_key(&KeyContext::Popup));
2971 assert!(resolver
2972 .default_bindings
2973 .contains_key(&KeyContext::FileExplorer));
2974 assert!(resolver.default_bindings.contains_key(&KeyContext::Menu));
2975
2976 assert!(!resolver.default_bindings[&KeyContext::Normal].is_empty());
2978 assert!(!resolver.default_bindings[&KeyContext::Prompt].is_empty());
2979 assert!(!resolver.default_bindings[&KeyContext::Popup].is_empty());
2980 assert!(!resolver.default_bindings[&KeyContext::FileExplorer].is_empty());
2981 assert!(!resolver.default_bindings[&KeyContext::Menu].is_empty());
2982 }
2983
2984 #[test]
2988 fn test_all_builtin_keymaps_have_valid_action_names() {
2989 let known_actions: std::collections::HashSet<String> =
2990 Action::all_action_names().into_iter().collect();
2991
2992 let config = Config::default();
2993
2994 for map_name in crate::config::KeybindingMapName::BUILTIN_OPTIONS {
2995 let bindings = config.resolve_keymap(map_name);
2996 for binding in &bindings {
2997 assert!(
2998 known_actions.contains(&binding.action),
2999 "Keymap '{}' contains unknown action '{}' (key: '{}', when: {:?}). \
3000 This will be treated as a plugin action at runtime. \
3001 Check for typos in the keymap JSON file.",
3002 map_name,
3003 binding.action,
3004 binding.key,
3005 binding.when,
3006 );
3007 }
3008 }
3009 }
3010
3011 #[test]
3012 fn test_resolve_determinism() {
3013 let config = Config::default();
3015 let resolver = KeybindingResolver::new(&config);
3016
3017 let test_cases = vec![
3018 (KeyCode::Left, KeyModifiers::empty(), KeyContext::Normal),
3019 (
3020 KeyCode::Esc,
3021 KeyModifiers::empty(),
3022 KeyContext::FileExplorer,
3023 ),
3024 (KeyCode::Enter, KeyModifiers::empty(), KeyContext::Prompt),
3025 (KeyCode::Down, KeyModifiers::empty(), KeyContext::Popup),
3026 ];
3027
3028 for (key_code, modifiers, context) in test_cases {
3029 let event = KeyEvent::new(key_code, modifiers);
3030 let action1 = resolver.resolve(&event, context.clone());
3031 let action2 = resolver.resolve(&event, context.clone());
3032 let action3 = resolver.resolve(&event, context);
3033
3034 assert_eq!(action1, action2, "Resolve should be deterministic");
3035 assert_eq!(action2, action3, "Resolve should be deterministic");
3036 }
3037 }
3038
3039 #[test]
3040 fn test_modifier_combinations() {
3041 let config = Config::default();
3042 let resolver = KeybindingResolver::new(&config);
3043
3044 let char_s = KeyCode::Char('s');
3046
3047 let no_mod = KeyEvent::new(char_s, KeyModifiers::empty());
3048 let ctrl = KeyEvent::new(char_s, KeyModifiers::CONTROL);
3049 let shift = KeyEvent::new(char_s, KeyModifiers::SHIFT);
3050 let ctrl_shift = KeyEvent::new(char_s, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
3051
3052 let action_no_mod = resolver.resolve(&no_mod, KeyContext::Normal);
3053 let action_ctrl = resolver.resolve(&ctrl, KeyContext::Normal);
3054 let action_shift = resolver.resolve(&shift, KeyContext::Normal);
3055 let action_ctrl_shift = resolver.resolve(&ctrl_shift, KeyContext::Normal);
3056
3057 assert_eq!(action_no_mod, Action::InsertChar('s'));
3059 assert_eq!(action_ctrl, Action::Save);
3060 assert_eq!(action_shift, Action::InsertChar('s')); assert_eq!(action_ctrl_shift, Action::None);
3063 }
3064
3065 #[test]
3066 fn test_scroll_keybindings() {
3067 let config = Config::default();
3068 let resolver = KeybindingResolver::new(&config);
3069
3070 let ctrl_up = KeyEvent::new(KeyCode::Up, KeyModifiers::CONTROL);
3072 assert_eq!(
3073 resolver.resolve(&ctrl_up, KeyContext::Normal),
3074 Action::ScrollUp,
3075 "Ctrl+Up should resolve to ScrollUp"
3076 );
3077
3078 let ctrl_down = KeyEvent::new(KeyCode::Down, KeyModifiers::CONTROL);
3080 assert_eq!(
3081 resolver.resolve(&ctrl_down, KeyContext::Normal),
3082 Action::ScrollDown,
3083 "Ctrl+Down should resolve to ScrollDown"
3084 );
3085 }
3086
3087 #[test]
3088 fn test_lsp_completion_keybinding() {
3089 let config = Config::default();
3090 let resolver = KeybindingResolver::new(&config);
3091
3092 let ctrl_space = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL);
3094 assert_eq!(
3095 resolver.resolve(&ctrl_space, KeyContext::Normal),
3096 Action::LspCompletion,
3097 "Ctrl+Space should resolve to LspCompletion"
3098 );
3099 }
3100
3101 #[test]
3102 fn test_terminal_key_equivalents() {
3103 let ctrl = KeyModifiers::CONTROL;
3105
3106 let slash_equivs = terminal_key_equivalents(KeyCode::Char('/'), ctrl);
3108 assert_eq!(slash_equivs, vec![(KeyCode::Char('7'), ctrl)]);
3109
3110 let seven_equivs = terminal_key_equivalents(KeyCode::Char('7'), ctrl);
3111 assert_eq!(seven_equivs, vec![(KeyCode::Char('/'), ctrl)]);
3112
3113 let backspace_equivs = terminal_key_equivalents(KeyCode::Backspace, ctrl);
3115 assert_eq!(backspace_equivs, vec![(KeyCode::Char('h'), ctrl)]);
3116
3117 let h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl);
3118 assert_eq!(h_equivs, vec![(KeyCode::Backspace, ctrl)]);
3119
3120 let a_equivs = terminal_key_equivalents(KeyCode::Char('a'), ctrl);
3122 assert!(a_equivs.is_empty());
3123
3124 let slash_no_ctrl = terminal_key_equivalents(KeyCode::Char('/'), KeyModifiers::empty());
3126 assert!(slash_no_ctrl.is_empty());
3127 }
3128
3129 #[test]
3130 fn test_terminal_key_equivalents_auto_binding() {
3131 let config = Config::default();
3132 let resolver = KeybindingResolver::new(&config);
3133
3134 let ctrl_slash = KeyEvent::new(KeyCode::Char('/'), KeyModifiers::CONTROL);
3136 let action_slash = resolver.resolve(&ctrl_slash, KeyContext::Normal);
3137 assert_eq!(
3138 action_slash,
3139 Action::ToggleComment,
3140 "Ctrl+/ should resolve to ToggleComment"
3141 );
3142
3143 let ctrl_7 = KeyEvent::new(KeyCode::Char('7'), KeyModifiers::CONTROL);
3145 let action_7 = resolver.resolve(&ctrl_7, KeyContext::Normal);
3146 assert_eq!(
3147 action_7,
3148 Action::ToggleComment,
3149 "Ctrl+7 should resolve to ToggleComment (terminal equivalent of Ctrl+/)"
3150 );
3151 }
3152
3153 #[test]
3154 fn test_terminal_key_equivalents_normalization() {
3155 let ctrl = KeyModifiers::CONTROL;
3160
3161 let slash_equivs = terminal_key_equivalents(KeyCode::Char('/'), ctrl);
3164 assert_eq!(
3165 slash_equivs,
3166 vec![(KeyCode::Char('7'), ctrl)],
3167 "Ctrl+/ should map to Ctrl+7"
3168 );
3169 let seven_equivs = terminal_key_equivalents(KeyCode::Char('7'), ctrl);
3170 assert_eq!(
3171 seven_equivs,
3172 vec![(KeyCode::Char('/'), ctrl)],
3173 "Ctrl+7 should map back to Ctrl+/"
3174 );
3175
3176 let backspace_equivs = terminal_key_equivalents(KeyCode::Backspace, ctrl);
3179 assert_eq!(
3180 backspace_equivs,
3181 vec![(KeyCode::Char('h'), ctrl)],
3182 "Ctrl+Backspace should map to Ctrl+H"
3183 );
3184 let h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl);
3185 assert_eq!(
3186 h_equivs,
3187 vec![(KeyCode::Backspace, ctrl)],
3188 "Ctrl+H should map back to Ctrl+Backspace"
3189 );
3190
3191 let space_equivs = terminal_key_equivalents(KeyCode::Char(' '), ctrl);
3194 assert_eq!(
3195 space_equivs,
3196 vec![(KeyCode::Char('@'), ctrl)],
3197 "Ctrl+Space should map to Ctrl+@"
3198 );
3199 let at_equivs = terminal_key_equivalents(KeyCode::Char('@'), ctrl);
3200 assert_eq!(
3201 at_equivs,
3202 vec![(KeyCode::Char(' '), ctrl)],
3203 "Ctrl+@ should map back to Ctrl+Space"
3204 );
3205
3206 let minus_equivs = terminal_key_equivalents(KeyCode::Char('-'), ctrl);
3209 assert_eq!(
3210 minus_equivs,
3211 vec![(KeyCode::Char('_'), ctrl)],
3212 "Ctrl+- should map to Ctrl+_"
3213 );
3214 let underscore_equivs = terminal_key_equivalents(KeyCode::Char('_'), ctrl);
3215 assert_eq!(
3216 underscore_equivs,
3217 vec![(KeyCode::Char('-'), ctrl)],
3218 "Ctrl+_ should map back to Ctrl+-"
3219 );
3220
3221 assert!(
3223 terminal_key_equivalents(KeyCode::Char('a'), ctrl).is_empty(),
3224 "Ctrl+A should have no terminal equivalents"
3225 );
3226 assert!(
3227 terminal_key_equivalents(KeyCode::Char('z'), ctrl).is_empty(),
3228 "Ctrl+Z should have no terminal equivalents"
3229 );
3230 assert!(
3231 terminal_key_equivalents(KeyCode::Enter, ctrl).is_empty(),
3232 "Ctrl+Enter should have no terminal equivalents"
3233 );
3234
3235 assert!(
3237 terminal_key_equivalents(KeyCode::Char('/'), KeyModifiers::empty()).is_empty(),
3238 "/ without Ctrl should have no equivalents"
3239 );
3240 assert!(
3241 terminal_key_equivalents(KeyCode::Char('7'), KeyModifiers::SHIFT).is_empty(),
3242 "Shift+7 should have no equivalents"
3243 );
3244 assert!(
3245 terminal_key_equivalents(KeyCode::Char('h'), KeyModifiers::ALT).is_empty(),
3246 "Alt+H should have no equivalents"
3247 );
3248
3249 let ctrl_shift = KeyModifiers::CONTROL | KeyModifiers::SHIFT;
3252 let ctrl_shift_h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl_shift);
3253 assert!(
3254 ctrl_shift_h_equivs.is_empty(),
3255 "Ctrl+Shift+H should NOT map to Ctrl+Shift+Backspace"
3256 );
3257 }
3258
3259 #[test]
3260 fn test_no_duplicate_keybindings_in_keymaps() {
3261 use std::collections::HashMap;
3264
3265 let keymaps: &[(&str, &str)] = &[
3266 ("default", include_str!("../../keymaps/default.json")),
3267 ("macos", include_str!("../../keymaps/macos.json")),
3268 ];
3269
3270 for (keymap_name, json_content) in keymaps {
3271 let keymap: crate::config::KeymapConfig = serde_json::from_str(json_content)
3272 .unwrap_or_else(|e| panic!("Failed to parse keymap '{}': {}", keymap_name, e));
3273
3274 let mut seen: HashMap<(String, Vec<String>, String), String> = HashMap::new();
3276 let mut duplicates: Vec<String> = Vec::new();
3277
3278 for binding in &keymap.bindings {
3279 let when = binding.when.clone().unwrap_or_default();
3280 let key_id = (binding.key.clone(), binding.modifiers.clone(), when.clone());
3281
3282 if let Some(existing_action) = seen.get(&key_id) {
3283 duplicates.push(format!(
3284 "Duplicate in '{}': key='{}', modifiers={:?}, when='{}' -> '{}' vs '{}'",
3285 keymap_name,
3286 binding.key,
3287 binding.modifiers,
3288 when,
3289 existing_action,
3290 binding.action
3291 ));
3292 } else {
3293 seen.insert(key_id, binding.action.clone());
3294 }
3295 }
3296
3297 assert!(
3298 duplicates.is_empty(),
3299 "Found duplicate keybindings:\n{}",
3300 duplicates.join("\n")
3301 );
3302 }
3303 }
3304}