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