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 CopyFilePath,
385 CopyRelativeFilePath,
388
389 YankWordForward,
391 YankWordBackward,
392 YankToLineEnd,
393 YankToLineStart,
394 YankViWordEnd, AddCursorAbove,
398 AddCursorBelow,
399 AddCursorNextMatch,
400 RemoveSecondaryCursors,
401
402 Save,
404 SaveAs,
405 Open,
406 SwitchProject,
407 New,
408 Close,
409 CloseTab,
410 Quit,
411 ForceQuit,
412 Detach,
413 Revert,
414 ToggleAutoRevert,
415 FormatBuffer,
416 TrimTrailingWhitespace,
417 EnsureFinalNewline,
418
419 GotoLine,
421 ScanLineIndex,
422 GoToMatchingBracket,
423 JumpToNextError,
424 JumpToPreviousError,
425
426 SmartHome,
428 DedentSelection,
429 ToggleComment,
430 DabbrevExpand,
431 ToggleFold,
432
433 SetBookmark(char),
435 JumpToBookmark(char),
436 ClearBookmark(char),
437 ListBookmarks,
438
439 ToggleSearchCaseSensitive,
441 ToggleSearchWholeWord,
442 ToggleSearchRegex,
443 ToggleSearchConfirmEach,
444
445 StartMacroRecording,
447 StopMacroRecording,
448 PlayMacro(char),
449 ToggleMacroRecording(char),
450 ShowMacro(char),
451 ListMacros,
452 PromptRecordMacro,
453 PromptPlayMacro,
454 PlayLastMacro,
455
456 PromptSetBookmark,
458 PromptJumpToBookmark,
459
460 Undo,
462 Redo,
463
464 ScrollUp,
466 ScrollDown,
467 ShowHelp,
468 ShowKeyboardShortcuts,
469 ShowWarnings,
470 ShowStatusLog,
471 ShowLspStatus,
472 ShowRemoteIndicatorMenu,
473 ClearWarnings,
474 CommandPalette, QuickOpen,
477 QuickOpenBuffers,
479 QuickOpenFiles,
481 ToggleLineWrap,
482 ToggleCurrentLineHighlight,
483 ToggleReadOnly,
484 TogglePageView,
485 SetPageWidth,
486 InspectThemeAtCursor,
487 SelectTheme,
488 SelectKeybindingMap,
489 SelectCursorStyle,
490 SelectLocale,
491
492 NextBuffer,
494 PrevBuffer,
495 SwitchToPreviousTab,
496 SwitchToTabByName,
497
498 ScrollTabsLeft,
500 ScrollTabsRight,
501
502 NavigateBack,
504 NavigateForward,
505
506 SplitHorizontal,
508 SplitVertical,
509 CloseSplit,
510 NextSplit,
511 PrevSplit,
512 IncreaseSplitSize,
513 DecreaseSplitSize,
514 ToggleMaximizeSplit,
515
516 PromptConfirm,
518 PromptConfirmWithText(String),
520 PromptCancel,
521 PromptBackspace,
522 PromptDelete,
523 PromptMoveLeft,
524 PromptMoveRight,
525 PromptMoveStart,
526 PromptMoveEnd,
527 PromptSelectPrev,
528 PromptSelectNext,
529 PromptPageUp,
530 PromptPageDown,
531 PromptAcceptSuggestion,
532 PromptMoveWordLeft,
533 PromptMoveWordRight,
534 PromptDeleteWordForward,
536 PromptDeleteWordBackward,
537 PromptDeleteToLineEnd,
538 PromptCopy,
539 PromptCut,
540 PromptPaste,
541 PromptMoveLeftSelecting,
543 PromptMoveRightSelecting,
544 PromptMoveHomeSelecting,
545 PromptMoveEndSelecting,
546 PromptSelectWordLeft,
547 PromptSelectWordRight,
548 PromptSelectAll,
549
550 FileBrowserToggleHidden,
552 FileBrowserToggleDetectEncoding,
553
554 PopupSelectNext,
556 PopupSelectPrev,
557 PopupPageUp,
558 PopupPageDown,
559 PopupConfirm,
560 PopupCancel,
561 PopupFocus,
566
567 CompletionAccept,
569 CompletionDismiss,
570
571 ToggleFileExplorer,
573 ToggleMenuBar,
575 ToggleTabBar,
577 ToggleStatusBar,
579 TogglePromptLine,
581 ToggleVerticalScrollbar,
583 ToggleHorizontalScrollbar,
584 FocusFileExplorer,
585 FocusEditor,
586 FileExplorerUp,
587 FileExplorerDown,
588 FileExplorerPageUp,
589 FileExplorerPageDown,
590 FileExplorerExpand,
591 FileExplorerCollapse,
592 FileExplorerOpen,
593 FileExplorerRefresh,
594 FileExplorerNewFile,
595 FileExplorerNewDirectory,
596 FileExplorerDelete,
597 FileExplorerRename,
598 FileExplorerToggleHidden,
599 FileExplorerToggleGitignored,
600 FileExplorerSearchClear,
601 FileExplorerSearchBackspace,
602 FileExplorerCopy,
603 FileExplorerCut,
604 FileExplorerPaste,
605 FileExplorerExtendSelectionUp,
606 FileExplorerExtendSelectionDown,
607 FileExplorerToggleSelect,
608 FileExplorerSelectAll,
609
610 LspCompletion,
612 LspGotoDefinition,
613 LspReferences,
614 LspRename,
615 LspHover,
616 LspSignatureHelp,
617 LspCodeActions,
618 LspRestart,
619 LspStop,
620 LspToggleForBuffer,
621 ToggleInlayHints,
622 ToggleMouseHover,
623
624 ToggleLineNumbers,
626 ToggleScrollSync,
627 ToggleMouseCapture,
628 ToggleDebugHighlights, SetBackground,
630 SetBackgroundBlend,
631
632 SetTabSize,
634 SetLineEnding,
635 SetEncoding,
636 ReloadWithEncoding,
637 SetLanguage,
638 ToggleIndentationStyle,
639 ToggleTabIndicators,
640 ToggleWhitespaceIndicators,
641 ResetBufferSettings,
642 AddRuler,
643 RemoveRuler,
644
645 DumpConfig,
647
648 RedrawScreen,
650
651 Search,
653 FindInSelection,
654 FindNext,
655 FindPrevious,
656 FindSelectionNext, FindSelectionPrevious, Replace,
659 QueryReplace, MenuActivate, MenuClose, MenuLeft, MenuRight, MenuUp, MenuDown, MenuExecute, MenuOpen(String), SwitchKeybindingMap(String), PluginAction(String),
676
677 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,
734}
735
736macro_rules! define_action_str_mapping {
749 (
750 $args_name:ident;
751 simple { $($s_name:literal => $s_variant:ident),* $(,)? }
752 alias { $($a_name:literal => $a_variant:ident),* $(,)? }
753 with_char { $($c_name:literal => $c_variant:ident),* $(,)? }
754 custom { $($x_name:literal => $x_variant:ident : $x_body:expr),* $(,)? }
755 ) => {
756 pub fn from_str(s: &str, $args_name: &HashMap<String, serde_json::Value>) -> Option<Self> {
758 Some(match s {
759 $($s_name => Self::$s_variant,)*
760 $($a_name => Self::$a_variant,)*
761 $($c_name => return Self::with_char($args_name, Self::$c_variant),)*
762 $($x_name => $x_body,)*
763 _ => Self::PluginAction(s.to_string()),
766 })
767 }
768
769 pub fn to_action_str(&self) -> String {
772 match self {
773 $(Self::$s_variant => $s_name.to_string(),)*
774 $(Self::$c_variant(_) => $c_name.to_string(),)*
775 $(Self::$x_variant(_) => $x_name.to_string(),)*
776 Self::PluginAction(name) => name.clone(),
777 }
778 }
779
780 pub fn all_action_names() -> Vec<String> {
783 let mut names = vec![
784 $($s_name.to_string(),)*
785 $($a_name.to_string(),)*
786 $($c_name.to_string(),)*
787 $($x_name.to_string(),)*
788 ];
789 names.sort();
790 names
791 }
792 };
793}
794
795impl Action {
796 fn with_char(
797 args: &HashMap<String, serde_json::Value>,
798 make_action: impl FnOnce(char) -> Self,
799 ) -> Option<Self> {
800 if let Some(serde_json::Value::String(value)) = args.get("char") {
801 value.chars().next().map(make_action)
802 } else {
803 None
804 }
805 }
806
807 define_action_str_mapping! {
808 args;
809 simple {
810 "insert_newline" => InsertNewline,
811 "insert_tab" => InsertTab,
812
813 "move_left" => MoveLeft,
814 "move_right" => MoveRight,
815 "move_up" => MoveUp,
816 "move_down" => MoveDown,
817 "move_word_left" => MoveWordLeft,
818 "move_word_right" => MoveWordRight,
819 "move_word_end" => MoveWordEnd,
820 "vi_move_word_end" => ViMoveWordEnd,
821 "move_left_in_line" => MoveLeftInLine,
822 "move_right_in_line" => MoveRightInLine,
823 "move_line_start" => MoveLineStart,
824 "move_line_end" => MoveLineEnd,
825 "move_line_up" => MoveLineUp,
826 "move_line_down" => MoveLineDown,
827 "move_page_up" => MovePageUp,
828 "move_page_down" => MovePageDown,
829 "move_document_start" => MoveDocumentStart,
830 "move_document_end" => MoveDocumentEnd,
831
832 "select_left" => SelectLeft,
833 "select_right" => SelectRight,
834 "select_up" => SelectUp,
835 "select_down" => SelectDown,
836 "select_to_paragraph_up" => SelectToParagraphUp,
837 "select_to_paragraph_down" => SelectToParagraphDown,
838 "select_word_left" => SelectWordLeft,
839 "select_word_right" => SelectWordRight,
840 "select_word_end" => SelectWordEnd,
841 "vi_select_word_end" => ViSelectWordEnd,
842 "select_line_start" => SelectLineStart,
843 "select_line_end" => SelectLineEnd,
844 "select_document_start" => SelectDocumentStart,
845 "select_document_end" => SelectDocumentEnd,
846 "select_page_up" => SelectPageUp,
847 "select_page_down" => SelectPageDown,
848 "select_all" => SelectAll,
849 "select_word" => SelectWord,
850 "select_line" => SelectLine,
851 "expand_selection" => ExpandSelection,
852
853 "block_select_left" => BlockSelectLeft,
854 "block_select_right" => BlockSelectRight,
855 "block_select_up" => BlockSelectUp,
856 "block_select_down" => BlockSelectDown,
857
858 "delete_backward" => DeleteBackward,
859 "delete_forward" => DeleteForward,
860 "delete_word_backward" => DeleteWordBackward,
861 "delete_word_forward" => DeleteWordForward,
862 "delete_line" => DeleteLine,
863 "delete_to_line_end" => DeleteToLineEnd,
864 "delete_to_line_start" => DeleteToLineStart,
865 "delete_vi_word_end" => DeleteViWordEnd,
866 "transpose_chars" => TransposeChars,
867 "open_line" => OpenLine,
868 "duplicate_line" => DuplicateLine,
869 "recenter" => Recenter,
870 "set_mark" => SetMark,
871
872 "copy" => Copy,
873 "cut" => Cut,
874 "paste" => Paste,
875 "copy_file_path" => CopyFilePath,
876 "copy_relative_file_path" => CopyRelativeFilePath,
877
878 "yank_word_forward" => YankWordForward,
879 "yank_word_backward" => YankWordBackward,
880 "yank_to_line_end" => YankToLineEnd,
881 "yank_to_line_start" => YankToLineStart,
882 "yank_vi_word_end" => YankViWordEnd,
883
884 "add_cursor_above" => AddCursorAbove,
885 "add_cursor_below" => AddCursorBelow,
886 "add_cursor_next_match" => AddCursorNextMatch,
887 "remove_secondary_cursors" => RemoveSecondaryCursors,
888
889 "save" => Save,
890 "save_as" => SaveAs,
891 "open" => Open,
892 "switch_project" => SwitchProject,
893 "new" => New,
894 "close" => Close,
895 "close_tab" => CloseTab,
896 "quit" => Quit,
897 "force_quit" => ForceQuit,
898 "detach" => Detach,
899 "revert" => Revert,
900 "toggle_auto_revert" => ToggleAutoRevert,
901 "format_buffer" => FormatBuffer,
902 "trim_trailing_whitespace" => TrimTrailingWhitespace,
903 "ensure_final_newline" => EnsureFinalNewline,
904 "goto_line" => GotoLine,
905 "scan_line_index" => ScanLineIndex,
906 "goto_matching_bracket" => GoToMatchingBracket,
907 "jump_to_next_error" => JumpToNextError,
908 "jump_to_previous_error" => JumpToPreviousError,
909
910 "smart_home" => SmartHome,
911 "dedent_selection" => DedentSelection,
912 "toggle_comment" => ToggleComment,
913 "dabbrev_expand" => DabbrevExpand,
914 "toggle_fold" => ToggleFold,
915
916 "list_bookmarks" => ListBookmarks,
917
918 "toggle_search_case_sensitive" => ToggleSearchCaseSensitive,
919 "toggle_search_whole_word" => ToggleSearchWholeWord,
920 "toggle_search_regex" => ToggleSearchRegex,
921 "toggle_search_confirm_each" => ToggleSearchConfirmEach,
922
923 "start_macro_recording" => StartMacroRecording,
924 "stop_macro_recording" => StopMacroRecording,
925
926 "list_macros" => ListMacros,
927 "prompt_record_macro" => PromptRecordMacro,
928 "prompt_play_macro" => PromptPlayMacro,
929 "play_last_macro" => PlayLastMacro,
930 "prompt_set_bookmark" => PromptSetBookmark,
931 "prompt_jump_to_bookmark" => PromptJumpToBookmark,
932
933 "undo" => Undo,
934 "redo" => Redo,
935
936 "scroll_up" => ScrollUp,
937 "scroll_down" => ScrollDown,
938 "show_help" => ShowHelp,
939 "keyboard_shortcuts" => ShowKeyboardShortcuts,
940 "show_warnings" => ShowWarnings,
941 "show_status_log" => ShowStatusLog,
942 "show_lsp_status" => ShowLspStatus,
943 "show_remote_indicator_menu" => ShowRemoteIndicatorMenu,
944 "clear_warnings" => ClearWarnings,
945 "command_palette" => CommandPalette,
946 "quick_open" => QuickOpen,
947 "quick_open_buffers" => QuickOpenBuffers,
948 "quick_open_files" => QuickOpenFiles,
949 "toggle_line_wrap" => ToggleLineWrap,
950 "toggle_current_line_highlight" => ToggleCurrentLineHighlight,
951 "toggle_read_only" => ToggleReadOnly,
952 "toggle_page_view" => TogglePageView,
953 "set_page_width" => SetPageWidth,
954
955 "next_buffer" => NextBuffer,
956 "prev_buffer" => PrevBuffer,
957 "switch_to_previous_tab" => SwitchToPreviousTab,
958 "switch_to_tab_by_name" => SwitchToTabByName,
959 "scroll_tabs_left" => ScrollTabsLeft,
960 "scroll_tabs_right" => ScrollTabsRight,
961
962 "navigate_back" => NavigateBack,
963 "navigate_forward" => NavigateForward,
964
965 "split_horizontal" => SplitHorizontal,
966 "split_vertical" => SplitVertical,
967 "close_split" => CloseSplit,
968 "next_split" => NextSplit,
969 "prev_split" => PrevSplit,
970 "increase_split_size" => IncreaseSplitSize,
971 "decrease_split_size" => DecreaseSplitSize,
972 "toggle_maximize_split" => ToggleMaximizeSplit,
973
974 "prompt_confirm" => PromptConfirm,
975 "prompt_cancel" => PromptCancel,
976 "prompt_backspace" => PromptBackspace,
977 "prompt_move_left" => PromptMoveLeft,
978 "prompt_move_right" => PromptMoveRight,
979 "prompt_move_start" => PromptMoveStart,
980 "prompt_move_end" => PromptMoveEnd,
981 "prompt_select_prev" => PromptSelectPrev,
982 "prompt_select_next" => PromptSelectNext,
983 "prompt_page_up" => PromptPageUp,
984 "prompt_page_down" => PromptPageDown,
985 "prompt_accept_suggestion" => PromptAcceptSuggestion,
986 "prompt_delete_word_forward" => PromptDeleteWordForward,
987 "prompt_delete_word_backward" => PromptDeleteWordBackward,
988 "prompt_delete_to_line_end" => PromptDeleteToLineEnd,
989 "prompt_copy" => PromptCopy,
990 "prompt_cut" => PromptCut,
991 "prompt_paste" => PromptPaste,
992 "prompt_move_left_selecting" => PromptMoveLeftSelecting,
993 "prompt_move_right_selecting" => PromptMoveRightSelecting,
994 "prompt_move_home_selecting" => PromptMoveHomeSelecting,
995 "prompt_move_end_selecting" => PromptMoveEndSelecting,
996 "prompt_select_word_left" => PromptSelectWordLeft,
997 "prompt_select_word_right" => PromptSelectWordRight,
998 "prompt_select_all" => PromptSelectAll,
999 "file_browser_toggle_hidden" => FileBrowserToggleHidden,
1000 "file_browser_toggle_detect_encoding" => FileBrowserToggleDetectEncoding,
1001 "prompt_move_word_left" => PromptMoveWordLeft,
1002 "prompt_move_word_right" => PromptMoveWordRight,
1003 "prompt_delete" => PromptDelete,
1004
1005 "popup_select_next" => PopupSelectNext,
1006 "popup_select_prev" => PopupSelectPrev,
1007 "popup_page_up" => PopupPageUp,
1008 "popup_page_down" => PopupPageDown,
1009 "popup_confirm" => PopupConfirm,
1010 "popup_cancel" => PopupCancel,
1011 "popup_focus" => PopupFocus,
1012
1013 "completion_accept" => CompletionAccept,
1014 "completion_dismiss" => CompletionDismiss,
1015
1016 "toggle_file_explorer" => ToggleFileExplorer,
1017 "toggle_menu_bar" => ToggleMenuBar,
1018 "toggle_tab_bar" => ToggleTabBar,
1019 "toggle_status_bar" => ToggleStatusBar,
1020 "toggle_prompt_line" => TogglePromptLine,
1021 "toggle_vertical_scrollbar" => ToggleVerticalScrollbar,
1022 "toggle_horizontal_scrollbar" => ToggleHorizontalScrollbar,
1023 "focus_file_explorer" => FocusFileExplorer,
1024 "focus_editor" => FocusEditor,
1025 "file_explorer_up" => FileExplorerUp,
1026 "file_explorer_down" => FileExplorerDown,
1027 "file_explorer_page_up" => FileExplorerPageUp,
1028 "file_explorer_page_down" => FileExplorerPageDown,
1029 "file_explorer_expand" => FileExplorerExpand,
1030 "file_explorer_collapse" => FileExplorerCollapse,
1031 "file_explorer_open" => FileExplorerOpen,
1032 "file_explorer_refresh" => FileExplorerRefresh,
1033 "file_explorer_new_file" => FileExplorerNewFile,
1034 "file_explorer_new_directory" => FileExplorerNewDirectory,
1035 "file_explorer_delete" => FileExplorerDelete,
1036 "file_explorer_rename" => FileExplorerRename,
1037 "file_explorer_toggle_hidden" => FileExplorerToggleHidden,
1038 "file_explorer_toggle_gitignored" => FileExplorerToggleGitignored,
1039 "file_explorer_search_clear" => FileExplorerSearchClear,
1040 "file_explorer_search_backspace" => FileExplorerSearchBackspace,
1041 "file_explorer_copy" => FileExplorerCopy,
1042 "file_explorer_cut" => FileExplorerCut,
1043 "file_explorer_paste" => FileExplorerPaste,
1044 "file_explorer_extend_selection_up" => FileExplorerExtendSelectionUp,
1045 "file_explorer_extend_selection_down" => FileExplorerExtendSelectionDown,
1046 "file_explorer_toggle_select" => FileExplorerToggleSelect,
1047 "file_explorer_select_all" => FileExplorerSelectAll,
1048
1049 "lsp_completion" => LspCompletion,
1050 "lsp_goto_definition" => LspGotoDefinition,
1051 "lsp_references" => LspReferences,
1052 "lsp_rename" => LspRename,
1053 "lsp_hover" => LspHover,
1054 "lsp_signature_help" => LspSignatureHelp,
1055 "lsp_code_actions" => LspCodeActions,
1056 "lsp_restart" => LspRestart,
1057 "lsp_stop" => LspStop,
1058 "lsp_toggle_for_buffer" => LspToggleForBuffer,
1059 "toggle_inlay_hints" => ToggleInlayHints,
1060 "toggle_mouse_hover" => ToggleMouseHover,
1061
1062 "toggle_line_numbers" => ToggleLineNumbers,
1063 "toggle_scroll_sync" => ToggleScrollSync,
1064 "toggle_mouse_capture" => ToggleMouseCapture,
1065 "toggle_debug_highlights" => ToggleDebugHighlights,
1066 "set_background" => SetBackground,
1067 "set_background_blend" => SetBackgroundBlend,
1068 "inspect_theme_at_cursor" => InspectThemeAtCursor,
1069 "select_theme" => SelectTheme,
1070 "select_keybinding_map" => SelectKeybindingMap,
1071 "select_cursor_style" => SelectCursorStyle,
1072 "select_locale" => SelectLocale,
1073
1074 "set_tab_size" => SetTabSize,
1075 "set_line_ending" => SetLineEnding,
1076 "set_encoding" => SetEncoding,
1077 "reload_with_encoding" => ReloadWithEncoding,
1078 "set_language" => SetLanguage,
1079 "toggle_indentation_style" => ToggleIndentationStyle,
1080 "toggle_tab_indicators" => ToggleTabIndicators,
1081 "toggle_whitespace_indicators" => ToggleWhitespaceIndicators,
1082 "reset_buffer_settings" => ResetBufferSettings,
1083 "add_ruler" => AddRuler,
1084 "remove_ruler" => RemoveRuler,
1085
1086 "dump_config" => DumpConfig,
1087 "redraw_screen" => RedrawScreen,
1088
1089 "search" => Search,
1090 "find_in_selection" => FindInSelection,
1091 "find_next" => FindNext,
1092 "find_previous" => FindPrevious,
1093 "find_selection_next" => FindSelectionNext,
1094 "find_selection_previous" => FindSelectionPrevious,
1095 "replace" => Replace,
1096 "query_replace" => QueryReplace,
1097
1098 "menu_activate" => MenuActivate,
1099 "menu_close" => MenuClose,
1100 "menu_left" => MenuLeft,
1101 "menu_right" => MenuRight,
1102 "menu_up" => MenuUp,
1103 "menu_down" => MenuDown,
1104 "menu_execute" => MenuExecute,
1105
1106 "open_terminal" => OpenTerminal,
1107 "close_terminal" => CloseTerminal,
1108 "focus_terminal" => FocusTerminal,
1109 "terminal_escape" => TerminalEscape,
1110 "toggle_keyboard_capture" => ToggleKeyboardCapture,
1111 "terminal_paste" => TerminalPaste,
1112
1113 "shell_command" => ShellCommand,
1114 "shell_command_replace" => ShellCommandReplace,
1115
1116 "to_upper_case" => ToUpperCase,
1117 "to_lower_case" => ToLowerCase,
1118 "toggle_case" => ToggleCase,
1119 "sort_lines" => SortLines,
1120
1121 "calibrate_input" => CalibrateInput,
1122 "event_debug" => EventDebug,
1123 "suspend_process" => SuspendProcess,
1124 "load_plugin_from_buffer" => LoadPluginFromBuffer,
1125 "init_reload" => InitReload,
1126 "init_edit" => InitEdit,
1127 "init_check" => InitCheck,
1128 "open_keybinding_editor" => OpenKeybindingEditor,
1129
1130 "composite_next_hunk" => CompositeNextHunk,
1131 "composite_prev_hunk" => CompositePrevHunk,
1132
1133 "noop" => None,
1134
1135 "open_settings" => OpenSettings,
1136 "close_settings" => CloseSettings,
1137 "settings_save" => SettingsSave,
1138 "settings_reset" => SettingsReset,
1139 "settings_toggle_focus" => SettingsToggleFocus,
1140 "settings_activate" => SettingsActivate,
1141 "settings_search" => SettingsSearch,
1142 "settings_help" => SettingsHelp,
1143 "settings_increment" => SettingsIncrement,
1144 "settings_decrement" => SettingsDecrement,
1145 "settings_inherit" => SettingsInherit,
1146 }
1147 alias {
1148 "toggle_compose_mode" => TogglePageView,
1149 "set_compose_width" => SetPageWidth,
1150 }
1151 with_char {
1152 "insert_char" => InsertChar,
1153 "set_bookmark" => SetBookmark,
1154 "jump_to_bookmark" => JumpToBookmark,
1155 "clear_bookmark" => ClearBookmark,
1156 "play_macro" => PlayMacro,
1157 "toggle_macro_recording" => ToggleMacroRecording,
1158 "show_macro" => ShowMacro,
1159 }
1160 custom {
1161 "copy_with_theme" => CopyWithTheme : {
1162 let theme = args.get("theme").and_then(|v| v.as_str()).unwrap_or("");
1164 Self::CopyWithTheme(theme.to_string())
1165 },
1166 "menu_open" => MenuOpen : {
1167 let name = args.get("name")?.as_str()?;
1168 Self::MenuOpen(name.to_string())
1169 },
1170 "switch_keybinding_map" => SwitchKeybindingMap : {
1171 let map_name = args.get("map")?.as_str()?;
1172 Self::SwitchKeybindingMap(map_name.to_string())
1173 },
1174 "prompt_confirm_with_text" => PromptConfirmWithText : {
1175 let text = args.get("text")?.as_str()?;
1176 Self::PromptConfirmWithText(text.to_string())
1177 },
1178 }
1179 }
1180
1181 pub fn variant_arg_key(bare_action: &str) -> Option<&'static str> {
1188 match bare_action {
1189 "menu_open" => Some("name"),
1190 "switch_keybinding_map" => Some("map"),
1191 _ => None,
1192 }
1193 }
1194
1195 pub fn qualify_action(bare_action: &str, args: &HashMap<String, serde_json::Value>) -> String {
1199 if let Some(key) = Self::variant_arg_key(bare_action) {
1200 if let Some(v) = args.get(key).and_then(|v| v.as_str()) {
1201 return format!("{}:{}", bare_action, v);
1202 }
1203 }
1204 bare_action.to_string()
1205 }
1206
1207 pub fn to_qualified_action_str(&self) -> String {
1212 match self {
1213 Self::MenuOpen(name) => format!("menu_open:{}", name),
1214 Self::SwitchKeybindingMap(map) => format!("switch_keybinding_map:{}", map),
1215 other => other.to_action_str(),
1216 }
1217 }
1218
1219 pub fn unqualify_action(qualified: &str) -> (String, HashMap<String, serde_json::Value>) {
1224 if let Some((bare, suffix)) = qualified.split_once(':') {
1225 if let Some(arg_key) = Self::variant_arg_key(bare) {
1226 let mut args = HashMap::new();
1227 args.insert(
1228 arg_key.to_string(),
1229 serde_json::Value::String(suffix.to_string()),
1230 );
1231 return (bare.to_string(), args);
1232 }
1233 }
1234 (qualified.to_string(), HashMap::new())
1235 }
1236
1237 pub fn is_movement_or_editing(&self) -> bool {
1240 matches!(
1241 self,
1242 Action::MoveLeft
1244 | Action::MoveRight
1245 | Action::MoveUp
1246 | Action::MoveDown
1247 | Action::MoveWordLeft
1248 | Action::MoveWordRight
1249 | Action::MoveWordEnd
1250 | Action::ViMoveWordEnd
1251 | Action::MoveLeftInLine
1252 | Action::MoveRightInLine
1253 | Action::MoveLineStart
1254 | Action::MoveLineEnd
1255 | Action::MovePageUp
1256 | Action::MovePageDown
1257 | Action::MoveDocumentStart
1258 | Action::MoveDocumentEnd
1259 | Action::SelectLeft
1261 | Action::SelectRight
1262 | Action::SelectUp
1263 | Action::SelectDown
1264 | Action::SelectToParagraphUp
1265 | Action::SelectToParagraphDown
1266 | Action::SelectWordLeft
1267 | Action::SelectWordRight
1268 | Action::SelectWordEnd
1269 | Action::ViSelectWordEnd
1270 | Action::SelectLineStart
1271 | Action::SelectLineEnd
1272 | Action::SelectDocumentStart
1273 | Action::SelectDocumentEnd
1274 | Action::SelectPageUp
1275 | Action::SelectPageDown
1276 | Action::SelectAll
1277 | Action::SelectWord
1278 | Action::SelectLine
1279 | Action::ExpandSelection
1280 | Action::BlockSelectLeft
1282 | Action::BlockSelectRight
1283 | Action::BlockSelectUp
1284 | Action::BlockSelectDown
1285 | Action::InsertChar(_)
1287 | Action::InsertNewline
1288 | Action::InsertTab
1289 | Action::DeleteBackward
1290 | Action::DeleteForward
1291 | Action::DeleteWordBackward
1292 | Action::DeleteWordForward
1293 | Action::DeleteLine
1294 | Action::DeleteToLineEnd
1295 | Action::DeleteToLineStart
1296 | Action::TransposeChars
1297 | Action::OpenLine
1298 | Action::DuplicateLine
1299 | Action::MoveLineUp
1300 | Action::MoveLineDown
1301 | Action::Cut
1303 | Action::Paste
1304 | Action::Undo
1306 | Action::Redo
1307 )
1308 }
1309
1310 pub fn is_editing(&self) -> bool {
1313 matches!(
1314 self,
1315 Action::InsertChar(_)
1316 | Action::InsertNewline
1317 | Action::InsertTab
1318 | Action::DeleteBackward
1319 | Action::DeleteForward
1320 | Action::DeleteWordBackward
1321 | Action::DeleteWordForward
1322 | Action::DeleteLine
1323 | Action::DeleteToLineEnd
1324 | Action::DeleteToLineStart
1325 | Action::DeleteViWordEnd
1326 | Action::TransposeChars
1327 | Action::OpenLine
1328 | Action::DuplicateLine
1329 | Action::MoveLineUp
1330 | Action::MoveLineDown
1331 | Action::Cut
1332 | Action::Paste
1333 )
1334 }
1335}
1336
1337#[derive(Debug, Clone, PartialEq)]
1339pub enum ChordResolution {
1340 Complete(Action),
1342 Partial,
1344 NoMatch,
1346}
1347
1348#[derive(Clone)]
1350pub struct KeybindingResolver {
1351 bindings: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1354
1355 default_bindings: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1357
1358 plugin_defaults: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1361
1362 chord_bindings: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1365
1366 default_chord_bindings: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1368
1369 plugin_chord_defaults: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1371
1372 inheriting_modes: std::collections::HashSet<String>,
1376}
1377
1378impl KeybindingResolver {
1379 pub fn new(config: &Config) -> Self {
1381 let mut resolver = Self {
1382 bindings: HashMap::new(),
1383 default_bindings: HashMap::new(),
1384 plugin_defaults: HashMap::new(),
1385 chord_bindings: HashMap::new(),
1386 default_chord_bindings: HashMap::new(),
1387 plugin_chord_defaults: HashMap::new(),
1388 inheriting_modes: std::collections::HashSet::new(),
1389 };
1390
1391 let map_bindings = config.resolve_keymap(&config.active_keybinding_map);
1393 resolver.load_default_bindings_from_vec(&map_bindings);
1394
1395 resolver.load_bindings_from_vec(&config.keybindings);
1397
1398 resolver
1399 }
1400
1401 fn load_default_bindings_from_vec(&mut self, bindings: &[crate::config::Keybinding]) {
1403 for binding in bindings {
1404 let context = if let Some(ref when) = binding.when {
1406 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
1407 } else {
1408 KeyContext::Normal
1409 };
1410
1411 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
1412 if !binding.keys.is_empty() {
1414 let mut sequence = Vec::new();
1416 for key_press in &binding.keys {
1417 if let Some(key_code) = Self::parse_key(&key_press.key) {
1418 let modifiers = Self::parse_modifiers(&key_press.modifiers);
1419 sequence.push((key_code, modifiers));
1420 } else {
1421 break;
1423 }
1424 }
1425
1426 if sequence.len() == binding.keys.len() && !sequence.is_empty() {
1428 self.default_chord_bindings
1429 .entry(context)
1430 .or_default()
1431 .insert(sequence, action);
1432 }
1433 } else if let Some(key_code) = Self::parse_key(&binding.key) {
1434 let modifiers = Self::parse_modifiers(&binding.modifiers);
1436
1437 self.insert_binding_with_equivalents(
1439 context,
1440 key_code,
1441 modifiers,
1442 action,
1443 &binding.key,
1444 );
1445 }
1446 }
1447 }
1448 }
1449
1450 fn insert_binding_with_equivalents(
1453 &mut self,
1454 context: KeyContext,
1455 key_code: KeyCode,
1456 modifiers: KeyModifiers,
1457 action: Action,
1458 key_name: &str,
1459 ) {
1460 let context_bindings = self.default_bindings.entry(context.clone()).or_default();
1461
1462 context_bindings.insert((key_code, modifiers), action.clone());
1464
1465 let equivalents = terminal_key_equivalents(key_code, modifiers);
1467 for (equiv_key, equiv_mods) in equivalents {
1468 if let Some(existing_action) = context_bindings.get(&(equiv_key, equiv_mods)) {
1470 if existing_action != &action {
1472 let equiv_name = format!("{:?}", equiv_key);
1473 tracing::warn!(
1474 "Terminal key equivalent conflict in {:?} context: {} (equivalent of {}) \
1475 is bound to {:?}, but {} is bound to {:?}. \
1476 The explicit binding takes precedence.",
1477 context,
1478 equiv_name,
1479 key_name,
1480 existing_action,
1481 key_name,
1482 action
1483 );
1484 }
1485 } else {
1487 context_bindings.insert((equiv_key, equiv_mods), action.clone());
1489 }
1490 }
1491 }
1492
1493 fn load_bindings_from_vec(&mut self, bindings: &[crate::config::Keybinding]) {
1495 for binding in bindings {
1496 let context = if let Some(ref when) = binding.when {
1498 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
1499 } else {
1500 KeyContext::Normal
1501 };
1502
1503 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
1504 if !binding.keys.is_empty() {
1506 let mut sequence = Vec::new();
1508 for key_press in &binding.keys {
1509 if let Some(key_code) = Self::parse_key(&key_press.key) {
1510 let modifiers = Self::parse_modifiers(&key_press.modifiers);
1511 sequence.push((key_code, modifiers));
1512 } else {
1513 break;
1515 }
1516 }
1517
1518 if sequence.len() == binding.keys.len() && !sequence.is_empty() {
1520 self.chord_bindings
1521 .entry(context)
1522 .or_default()
1523 .insert(sequence, action);
1524 }
1525 } else if let Some(key_code) = Self::parse_key(&binding.key) {
1526 let modifiers = Self::parse_modifiers(&binding.modifiers);
1528 self.bindings
1529 .entry(context)
1530 .or_default()
1531 .insert((key_code, modifiers), action);
1532 }
1533 }
1534 }
1535 }
1536
1537 pub fn load_plugin_default(
1539 &mut self,
1540 context: KeyContext,
1541 key_code: KeyCode,
1542 modifiers: KeyModifiers,
1543 action: Action,
1544 ) {
1545 self.plugin_defaults
1546 .entry(context)
1547 .or_default()
1548 .insert((key_code, modifiers), action);
1549 }
1550
1551 pub fn load_plugin_chord_default(
1553 &mut self,
1554 context: KeyContext,
1555 sequence: Vec<(KeyCode, KeyModifiers)>,
1556 action: Action,
1557 ) {
1558 self.plugin_chord_defaults
1559 .entry(context)
1560 .or_default()
1561 .insert(sequence, action);
1562 }
1563
1564 pub fn clear_plugin_defaults_for_mode(&mut self, mode_name: &str) {
1566 let context = KeyContext::Mode(mode_name.to_string());
1567 self.plugin_defaults.remove(&context);
1568 self.plugin_chord_defaults.remove(&context);
1569 self.inheriting_modes.remove(mode_name);
1570 }
1571
1572 pub fn set_mode_inherits_normal_bindings(&mut self, mode_name: &str, inherit: bool) {
1575 if inherit {
1576 self.inheriting_modes.insert(mode_name.to_string());
1577 } else {
1578 self.inheriting_modes.remove(mode_name);
1579 }
1580 }
1581
1582 pub fn get_plugin_defaults(
1584 &self,
1585 ) -> &HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>> {
1586 &self.plugin_defaults
1587 }
1588
1589 fn is_application_wide_action(action: &Action) -> bool {
1591 matches!(
1592 action,
1593 Action::Quit
1594 | Action::ForceQuit
1595 | Action::Save
1596 | Action::SaveAs
1597 | Action::ShowHelp
1598 | Action::ShowKeyboardShortcuts
1599 | Action::PromptCancel | Action::PopupCancel )
1602 }
1603
1604 pub fn is_terminal_ui_action(action: &Action) -> bool {
1608 matches!(
1609 action,
1610 Action::CommandPalette
1612 | Action::QuickOpen
1613 | Action::QuickOpenBuffers
1614 | Action::QuickOpenFiles
1615 | Action::OpenSettings
1616 | Action::MenuActivate
1617 | Action::MenuOpen(_)
1618 | Action::ShowHelp
1619 | Action::ShowKeyboardShortcuts
1620 | Action::Quit
1621 | Action::ForceQuit
1622 | Action::NextSplit
1624 | Action::PrevSplit
1625 | Action::SplitHorizontal
1626 | Action::SplitVertical
1627 | Action::CloseSplit
1628 | Action::ToggleMaximizeSplit
1629 | Action::NextBuffer
1631 | Action::PrevBuffer
1632 | Action::Close
1633 | Action::ScrollTabsLeft
1634 | Action::ScrollTabsRight
1635 | Action::TerminalEscape
1637 | Action::ToggleKeyboardCapture
1638 | Action::OpenTerminal
1639 | Action::CloseTerminal
1640 | Action::TerminalPaste
1641 | Action::ToggleFileExplorer
1643 | Action::ToggleMenuBar
1645 )
1646 }
1647
1648 pub fn resolve_chord(
1654 &self,
1655 chord_state: &[(KeyCode, KeyModifiers)],
1656 event: &KeyEvent,
1657 context: KeyContext,
1658 ) -> ChordResolution {
1659 let mut full_sequence: Vec<(KeyCode, KeyModifiers)> = chord_state
1661 .iter()
1662 .map(|(c, m)| normalize_key(*c, *m))
1663 .collect();
1664 let (norm_code, norm_mods) = normalize_key(event.code, event.modifiers);
1665 full_sequence.push((norm_code, norm_mods));
1666
1667 tracing::trace!(
1668 "KeybindingResolver.resolve_chord: sequence={:?}, context={:?}",
1669 full_sequence,
1670 context
1671 );
1672
1673 let search_order = vec![
1675 (&self.chord_bindings, &KeyContext::Global, "custom global"),
1676 (
1677 &self.default_chord_bindings,
1678 &KeyContext::Global,
1679 "default global",
1680 ),
1681 (&self.chord_bindings, &context, "custom context"),
1682 (&self.default_chord_bindings, &context, "default context"),
1683 (
1684 &self.plugin_chord_defaults,
1685 &context,
1686 "plugin default context",
1687 ),
1688 ];
1689
1690 let mut has_partial_match = false;
1691
1692 for (binding_map, bind_context, label) in search_order {
1693 if let Some(context_chords) = binding_map.get(bind_context) {
1694 if let Some(action) = context_chords.get(&full_sequence) {
1696 tracing::trace!(" -> Complete chord match in {}: {:?}", label, action);
1697 return ChordResolution::Complete(action.clone());
1698 }
1699
1700 for (chord_seq, _) in context_chords.iter() {
1702 if chord_seq.len() > full_sequence.len()
1703 && chord_seq[..full_sequence.len()] == full_sequence[..]
1704 {
1705 tracing::trace!(" -> Partial chord match in {}", label);
1706 has_partial_match = true;
1707 break;
1708 }
1709 }
1710 }
1711 }
1712
1713 if has_partial_match {
1714 ChordResolution::Partial
1715 } else {
1716 tracing::trace!(" -> No chord match");
1717 ChordResolution::NoMatch
1718 }
1719 }
1720
1721 pub fn resolve(&self, event: &KeyEvent, context: KeyContext) -> Action {
1723 let (norm_code, norm_mods) = normalize_key(event.code, event.modifiers);
1726 let norm = &(norm_code, norm_mods);
1727 tracing::trace!(
1728 "KeybindingResolver.resolve: code={:?}, modifiers={:?}, context={:?}",
1729 event.code,
1730 event.modifiers,
1731 context
1732 );
1733
1734 if let Some(global_bindings) = self.bindings.get(&KeyContext::Global) {
1736 if let Some(action) = global_bindings.get(norm) {
1737 tracing::trace!(" -> Found in custom global bindings: {:?}", action);
1738 return action.clone();
1739 }
1740 }
1741
1742 if let Some(global_bindings) = self.default_bindings.get(&KeyContext::Global) {
1743 if let Some(action) = global_bindings.get(norm) {
1744 tracing::trace!(" -> Found in default global bindings: {:?}", action);
1745 return action.clone();
1746 }
1747 }
1748
1749 if let Some(context_bindings) = self.bindings.get(&context) {
1751 if let Some(action) = context_bindings.get(norm) {
1752 tracing::trace!(
1753 " -> Found in custom {} bindings: {:?}",
1754 context.to_when_clause(),
1755 action
1756 );
1757 return action.clone();
1758 }
1759 }
1760
1761 if let Some(context_bindings) = self.default_bindings.get(&context) {
1763 if let Some(action) = context_bindings.get(norm) {
1764 tracing::trace!(
1765 " -> Found in default {} bindings: {:?}",
1766 context.to_when_clause(),
1767 action
1768 );
1769 return action.clone();
1770 }
1771 }
1772
1773 if let Some(plugin_bindings) = self.plugin_defaults.get(&context) {
1775 if let Some(action) = plugin_bindings.get(norm) {
1776 tracing::trace!(
1777 " -> Found in plugin default {} bindings: {:?}",
1778 context.to_when_clause(),
1779 action
1780 );
1781 return action.clone();
1782 }
1783 }
1784
1785 if context != KeyContext::Normal {
1789 let full_fallthrough = context.allows_normal_fallthrough()
1790 || matches!(&context, KeyContext::Mode(name) if self.inheriting_modes.contains(name));
1791
1792 if let Some(normal_bindings) = self.bindings.get(&KeyContext::Normal) {
1793 if let Some(action) = normal_bindings.get(norm) {
1794 if full_fallthrough || Self::is_application_wide_action(action) {
1795 tracing::trace!(
1796 " -> Found action in custom normal bindings (fallthrough): {:?}",
1797 action
1798 );
1799 return action.clone();
1800 }
1801 }
1802 }
1803
1804 if let Some(normal_bindings) = self.default_bindings.get(&KeyContext::Normal) {
1805 if let Some(action) = normal_bindings.get(norm) {
1806 if full_fallthrough || Self::is_application_wide_action(action) {
1807 tracing::trace!(
1808 " -> Found action in default normal bindings (fallthrough): {:?}",
1809 action
1810 );
1811 return action.clone();
1812 }
1813 }
1814 }
1815 }
1816
1817 if context.allows_text_input() && is_text_input_modifier(event.modifiers) {
1819 if let KeyCode::Char(c) = event.code {
1820 tracing::trace!(" -> Character input: '{}'", c);
1821 return Action::InsertChar(c);
1822 }
1823 }
1824
1825 tracing::trace!(" -> No binding found, returning Action::None");
1826 Action::None
1827 }
1828
1829 pub fn resolve_in_context_only(&self, event: &KeyEvent, context: KeyContext) -> Option<Action> {
1834 let norm = normalize_key(event.code, event.modifiers);
1835 if let Some(context_bindings) = self.bindings.get(&context) {
1837 if let Some(action) = context_bindings.get(&norm) {
1838 return Some(action.clone());
1839 }
1840 }
1841
1842 if let Some(context_bindings) = self.default_bindings.get(&context) {
1844 if let Some(action) = context_bindings.get(&norm) {
1845 return Some(action.clone());
1846 }
1847 }
1848
1849 None
1850 }
1851
1852 pub fn resolve_terminal_ui_action(&self, event: &KeyEvent) -> Action {
1856 let norm = normalize_key(event.code, event.modifiers);
1857 tracing::trace!(
1858 "KeybindingResolver.resolve_terminal_ui_action: code={:?}, modifiers={:?}",
1859 event.code,
1860 event.modifiers
1861 );
1862
1863 for bindings in [&self.bindings, &self.default_bindings] {
1865 if let Some(terminal_bindings) = bindings.get(&KeyContext::Terminal) {
1866 if let Some(action) = terminal_bindings.get(&norm) {
1867 if Self::is_terminal_ui_action(action) {
1868 tracing::trace!(" -> Found UI action in terminal bindings: {:?}", action);
1869 return action.clone();
1870 }
1871 }
1872 }
1873 }
1874
1875 for bindings in [&self.bindings, &self.default_bindings] {
1877 if let Some(global_bindings) = bindings.get(&KeyContext::Global) {
1878 if let Some(action) = global_bindings.get(&norm) {
1879 if Self::is_terminal_ui_action(action) {
1880 tracing::trace!(" -> Found UI action in global bindings: {:?}", action);
1881 return action.clone();
1882 }
1883 }
1884 }
1885 }
1886
1887 for bindings in [&self.bindings, &self.default_bindings] {
1889 if let Some(normal_bindings) = bindings.get(&KeyContext::Normal) {
1890 if let Some(action) = normal_bindings.get(&norm) {
1891 if Self::is_terminal_ui_action(action) {
1892 tracing::trace!(" -> Found UI action in normal bindings: {:?}", action);
1893 return action.clone();
1894 }
1895 }
1896 }
1897 }
1898
1899 tracing::trace!(" -> No UI action found");
1900 Action::None
1901 }
1902
1903 pub fn find_keybinding_for_action(
1906 &self,
1907 action_name: &str,
1908 context: KeyContext,
1909 ) -> Option<String> {
1910 let target_action = Action::from_str(action_name, &HashMap::new())?;
1912
1913 let search_maps = vec![
1915 self.bindings.get(&context),
1916 self.bindings.get(&KeyContext::Global),
1917 self.default_bindings.get(&context),
1918 self.default_bindings.get(&KeyContext::Global),
1919 ];
1920
1921 for map in search_maps.into_iter().flatten() {
1922 let mut matches: Vec<(KeyCode, KeyModifiers)> = map
1924 .iter()
1925 .filter(|(_, action)| {
1926 std::mem::discriminant(*action) == std::mem::discriminant(&target_action)
1927 })
1928 .map(|((key_code, modifiers), _)| (*key_code, *modifiers))
1929 .collect();
1930
1931 if !matches.is_empty() {
1932 matches.sort_by(|(key_a, mod_a), (key_b, mod_b)| {
1934 let mod_count_a = mod_a.bits().count_ones();
1936 let mod_count_b = mod_b.bits().count_ones();
1937 match mod_count_a.cmp(&mod_count_b) {
1938 std::cmp::Ordering::Equal => {
1939 match mod_a.bits().cmp(&mod_b.bits()) {
1941 std::cmp::Ordering::Equal => {
1942 Self::key_code_sort_key(key_a)
1944 .cmp(&Self::key_code_sort_key(key_b))
1945 }
1946 other => other,
1947 }
1948 }
1949 other => other,
1950 }
1951 });
1952
1953 let (key_code, modifiers) = matches[0];
1954 return Some(format_keybinding(&key_code, &modifiers));
1955 }
1956 }
1957
1958 None
1959 }
1960
1961 fn key_code_sort_key(key_code: &KeyCode) -> (u8, u32) {
1963 match key_code {
1964 KeyCode::Char(c) => (0, *c as u32),
1965 KeyCode::F(n) => (1, *n as u32),
1966 KeyCode::Enter => (2, 0),
1967 KeyCode::Tab => (2, 1),
1968 KeyCode::Backspace => (2, 2),
1969 KeyCode::Delete => (2, 3),
1970 KeyCode::Esc => (2, 4),
1971 KeyCode::Left => (3, 0),
1972 KeyCode::Right => (3, 1),
1973 KeyCode::Up => (3, 2),
1974 KeyCode::Down => (3, 3),
1975 KeyCode::Home => (3, 4),
1976 KeyCode::End => (3, 5),
1977 KeyCode::PageUp => (3, 6),
1978 KeyCode::PageDown => (3, 7),
1979 _ => (255, 0),
1980 }
1981 }
1982
1983 pub fn find_menu_mnemonic(&self, menu_name: &str) -> Option<char> {
1986 let search_maps = vec![
1988 self.bindings.get(&KeyContext::Normal),
1989 self.bindings.get(&KeyContext::Global),
1990 self.default_bindings.get(&KeyContext::Normal),
1991 self.default_bindings.get(&KeyContext::Global),
1992 ];
1993
1994 for map in search_maps.into_iter().flatten() {
1995 for ((key_code, modifiers), action) in map {
1996 if let Action::MenuOpen(name) = action {
1998 if name.eq_ignore_ascii_case(menu_name) && *modifiers == KeyModifiers::ALT {
1999 if let KeyCode::Char(c) = key_code {
2001 return Some(c.to_ascii_lowercase());
2002 }
2003 }
2004 }
2005 }
2006 }
2007
2008 None
2009 }
2010
2011 fn parse_key(key: &str) -> Option<KeyCode> {
2013 let lower = key.to_lowercase();
2014 match lower.as_str() {
2015 "enter" => Some(KeyCode::Enter),
2016 "backspace" => Some(KeyCode::Backspace),
2017 "delete" | "del" => Some(KeyCode::Delete),
2018 "tab" => Some(KeyCode::Tab),
2019 "backtab" => Some(KeyCode::BackTab),
2020 "esc" | "escape" => Some(KeyCode::Esc),
2021 "space" => Some(KeyCode::Char(' ')),
2022
2023 "left" => Some(KeyCode::Left),
2024 "right" => Some(KeyCode::Right),
2025 "up" => Some(KeyCode::Up),
2026 "down" => Some(KeyCode::Down),
2027 "home" => Some(KeyCode::Home),
2028 "end" => Some(KeyCode::End),
2029 "pageup" => Some(KeyCode::PageUp),
2030 "pagedown" => Some(KeyCode::PageDown),
2031
2032 s if s.len() == 1 => s.chars().next().map(KeyCode::Char),
2033 s if s.starts_with('f') && s.len() >= 2 => s[1..].parse::<u8>().ok().map(KeyCode::F),
2035 _ => None,
2036 }
2037 }
2038
2039 fn parse_modifiers(modifiers: &[String]) -> KeyModifiers {
2041 let mut result = KeyModifiers::empty();
2042 for m in modifiers {
2043 match m.to_lowercase().as_str() {
2044 "ctrl" | "control" => result |= KeyModifiers::CONTROL,
2045 "shift" => result |= KeyModifiers::SHIFT,
2046 "alt" => result |= KeyModifiers::ALT,
2047 "super" | "cmd" | "command" | "meta" => result |= KeyModifiers::SUPER,
2048 _ => {}
2049 }
2050 }
2051 result
2052 }
2053
2054 pub fn get_all_bindings(&self) -> Vec<(String, String)> {
2058 let mut bindings = Vec::new();
2059
2060 for context in &[
2062 KeyContext::Normal,
2063 KeyContext::Prompt,
2064 KeyContext::Popup,
2065 KeyContext::FileExplorer,
2066 KeyContext::Menu,
2067 KeyContext::CompositeBuffer,
2068 ] {
2069 let mut all_keys: HashMap<(KeyCode, KeyModifiers), Action> = HashMap::new();
2070
2071 if let Some(context_defaults) = self.default_bindings.get(context) {
2073 for (key, action) in context_defaults {
2074 all_keys.insert(*key, action.clone());
2075 }
2076 }
2077
2078 if let Some(context_bindings) = self.bindings.get(context) {
2080 for (key, action) in context_bindings {
2081 all_keys.insert(*key, action.clone());
2082 }
2083 }
2084
2085 let context_str = if *context != KeyContext::Normal {
2087 format!("[{}] ", context.to_when_clause())
2088 } else {
2089 String::new()
2090 };
2091
2092 for ((key_code, modifiers), action) in all_keys {
2093 let key_str = Self::format_key(key_code, modifiers);
2094 let action_str = format!("{}{}", context_str, Self::format_action(&action));
2095 bindings.push((key_str, action_str));
2096 }
2097 }
2098
2099 bindings.sort_by(|a, b| a.1.cmp(&b.1));
2101
2102 bindings
2103 }
2104
2105 fn format_key(key_code: KeyCode, modifiers: KeyModifiers) -> String {
2107 format_keybinding(&key_code, &modifiers)
2108 }
2109
2110 pub fn format_action(action: &Action) -> String {
2112 match action {
2113 Action::InsertChar(c) => t!("action.insert_char", char = c),
2114 Action::InsertNewline => t!("action.insert_newline"),
2115 Action::InsertTab => t!("action.insert_tab"),
2116 Action::MoveLeft => t!("action.move_left"),
2117 Action::MoveRight => t!("action.move_right"),
2118 Action::MoveUp => t!("action.move_up"),
2119 Action::MoveDown => t!("action.move_down"),
2120 Action::MoveWordLeft => t!("action.move_word_left"),
2121 Action::MoveWordRight => t!("action.move_word_right"),
2122 Action::MoveWordEnd => t!("action.move_word_end"),
2123 Action::ViMoveWordEnd => t!("action.move_word_end"),
2124 Action::MoveLeftInLine => t!("action.move_left"),
2125 Action::MoveRightInLine => t!("action.move_right"),
2126 Action::MoveLineStart => t!("action.move_line_start"),
2127 Action::MoveLineEnd => t!("action.move_line_end"),
2128 Action::MoveLineUp => t!("action.move_line_up"),
2129 Action::MoveLineDown => t!("action.move_line_down"),
2130 Action::MovePageUp => t!("action.move_page_up"),
2131 Action::MovePageDown => t!("action.move_page_down"),
2132 Action::MoveDocumentStart => t!("action.move_document_start"),
2133 Action::MoveDocumentEnd => t!("action.move_document_end"),
2134 Action::SelectLeft => t!("action.select_left"),
2135 Action::SelectRight => t!("action.select_right"),
2136 Action::SelectUp => t!("action.select_up"),
2137 Action::SelectDown => t!("action.select_down"),
2138 Action::SelectToParagraphUp => t!("action.select_to_paragraph_up"),
2139 Action::SelectToParagraphDown => t!("action.select_to_paragraph_down"),
2140 Action::SelectWordLeft => t!("action.select_word_left"),
2141 Action::SelectWordRight => t!("action.select_word_right"),
2142 Action::SelectWordEnd => t!("action.select_word_end"),
2143 Action::ViSelectWordEnd => t!("action.select_word_end"),
2144 Action::SelectLineStart => t!("action.select_line_start"),
2145 Action::SelectLineEnd => t!("action.select_line_end"),
2146 Action::SelectDocumentStart => t!("action.select_document_start"),
2147 Action::SelectDocumentEnd => t!("action.select_document_end"),
2148 Action::SelectPageUp => t!("action.select_page_up"),
2149 Action::SelectPageDown => t!("action.select_page_down"),
2150 Action::SelectAll => t!("action.select_all"),
2151 Action::SelectWord => t!("action.select_word"),
2152 Action::SelectLine => t!("action.select_line"),
2153 Action::ExpandSelection => t!("action.expand_selection"),
2154 Action::BlockSelectLeft => t!("action.block_select_left"),
2155 Action::BlockSelectRight => t!("action.block_select_right"),
2156 Action::BlockSelectUp => t!("action.block_select_up"),
2157 Action::BlockSelectDown => t!("action.block_select_down"),
2158 Action::DeleteBackward => t!("action.delete_backward"),
2159 Action::DeleteForward => t!("action.delete_forward"),
2160 Action::DeleteWordBackward => t!("action.delete_word_backward"),
2161 Action::DeleteWordForward => t!("action.delete_word_forward"),
2162 Action::DeleteLine => t!("action.delete_line"),
2163 Action::DeleteToLineEnd => t!("action.delete_to_line_end"),
2164 Action::DeleteToLineStart => t!("action.delete_to_line_start"),
2165 Action::DeleteViWordEnd => t!("action.delete_word_forward"),
2166 Action::TransposeChars => t!("action.transpose_chars"),
2167 Action::OpenLine => t!("action.open_line"),
2168 Action::DuplicateLine => t!("action.duplicate_line"),
2169 Action::Recenter => t!("action.recenter"),
2170 Action::SetMark => t!("action.set_mark"),
2171 Action::Copy => t!("action.copy"),
2172 Action::CopyWithTheme(theme) if theme.is_empty() => t!("action.copy_with_formatting"),
2173 Action::CopyWithTheme(theme) => t!("action.copy_with_theme", theme = theme),
2174 Action::Cut => t!("action.cut"),
2175 Action::Paste => t!("action.paste"),
2176 Action::CopyFilePath => t!("action.copy_file_path"),
2177 Action::CopyRelativeFilePath => t!("action.copy_relative_file_path"),
2178 Action::YankWordForward => t!("action.yank_word_forward"),
2179 Action::YankWordBackward => t!("action.yank_word_backward"),
2180 Action::YankToLineEnd => t!("action.yank_to_line_end"),
2181 Action::YankToLineStart => t!("action.yank_to_line_start"),
2182 Action::YankViWordEnd => t!("action.yank_word_forward"),
2183 Action::AddCursorAbove => t!("action.add_cursor_above"),
2184 Action::AddCursorBelow => t!("action.add_cursor_below"),
2185 Action::AddCursorNextMatch => t!("action.add_cursor_next_match"),
2186 Action::RemoveSecondaryCursors => t!("action.remove_secondary_cursors"),
2187 Action::Save => t!("action.save"),
2188 Action::SaveAs => t!("action.save_as"),
2189 Action::Open => t!("action.open"),
2190 Action::SwitchProject => t!("action.switch_project"),
2191 Action::New => t!("action.new"),
2192 Action::Close => t!("action.close"),
2193 Action::CloseTab => t!("action.close_tab"),
2194 Action::Quit => t!("action.quit"),
2195 Action::ForceQuit => t!("action.force_quit"),
2196 Action::Detach => t!("action.detach"),
2197 Action::Revert => t!("action.revert"),
2198 Action::ToggleAutoRevert => t!("action.toggle_auto_revert"),
2199 Action::FormatBuffer => t!("action.format_buffer"),
2200 Action::TrimTrailingWhitespace => t!("action.trim_trailing_whitespace"),
2201 Action::EnsureFinalNewline => t!("action.ensure_final_newline"),
2202 Action::GotoLine => t!("action.goto_line"),
2203 Action::ScanLineIndex => t!("action.scan_line_index"),
2204 Action::GoToMatchingBracket => t!("action.goto_matching_bracket"),
2205 Action::JumpToNextError => t!("action.jump_to_next_error"),
2206 Action::JumpToPreviousError => t!("action.jump_to_previous_error"),
2207 Action::SmartHome => t!("action.smart_home"),
2208 Action::DedentSelection => t!("action.dedent_selection"),
2209 Action::ToggleComment => t!("action.toggle_comment"),
2210 Action::DabbrevExpand => std::borrow::Cow::Borrowed("Expand abbreviation (dabbrev)"),
2211 Action::ToggleFold => t!("action.toggle_fold"),
2212 Action::SetBookmark(c) => t!("action.set_bookmark", key = c),
2213 Action::JumpToBookmark(c) => t!("action.jump_to_bookmark", key = c),
2214 Action::ClearBookmark(c) => t!("action.clear_bookmark", key = c),
2215 Action::ListBookmarks => t!("action.list_bookmarks"),
2216 Action::ToggleSearchCaseSensitive => t!("action.toggle_search_case_sensitive"),
2217 Action::ToggleSearchWholeWord => t!("action.toggle_search_whole_word"),
2218 Action::ToggleSearchRegex => t!("action.toggle_search_regex"),
2219 Action::ToggleSearchConfirmEach => t!("action.toggle_search_confirm_each"),
2220 Action::StartMacroRecording => t!("action.start_macro_recording"),
2221 Action::StopMacroRecording => t!("action.stop_macro_recording"),
2222 Action::PlayMacro(c) => t!("action.play_macro", key = c),
2223 Action::ToggleMacroRecording(c) => t!("action.toggle_macro_recording", key = c),
2224 Action::ShowMacro(c) => t!("action.show_macro", key = c),
2225 Action::ListMacros => t!("action.list_macros"),
2226 Action::PromptRecordMacro => t!("action.prompt_record_macro"),
2227 Action::PromptPlayMacro => t!("action.prompt_play_macro"),
2228 Action::PlayLastMacro => t!("action.play_last_macro"),
2229 Action::PromptSetBookmark => t!("action.prompt_set_bookmark"),
2230 Action::PromptJumpToBookmark => t!("action.prompt_jump_to_bookmark"),
2231 Action::Undo => t!("action.undo"),
2232 Action::Redo => t!("action.redo"),
2233 Action::ScrollUp => t!("action.scroll_up"),
2234 Action::ScrollDown => t!("action.scroll_down"),
2235 Action::ShowHelp => t!("action.show_help"),
2236 Action::ShowKeyboardShortcuts => t!("action.show_keyboard_shortcuts"),
2237 Action::ShowWarnings => t!("action.show_warnings"),
2238 Action::ShowStatusLog => t!("action.show_status_log"),
2239 Action::ShowLspStatus => t!("action.show_lsp_status"),
2240 Action::ShowRemoteIndicatorMenu => t!("action.show_remote_indicator_menu"),
2241 Action::ClearWarnings => t!("action.clear_warnings"),
2242 Action::CommandPalette => t!("action.command_palette"),
2243 Action::QuickOpen => t!("action.quick_open"),
2244 Action::QuickOpenBuffers => t!("action.quick_open_buffers"),
2245 Action::QuickOpenFiles => t!("action.quick_open_files"),
2246 Action::InspectThemeAtCursor => t!("action.inspect_theme_at_cursor"),
2247 Action::ToggleLineWrap => t!("action.toggle_line_wrap"),
2248 Action::ToggleCurrentLineHighlight => t!("action.toggle_current_line_highlight"),
2249 Action::ToggleReadOnly => t!("action.toggle_read_only"),
2250 Action::TogglePageView => t!("action.toggle_page_view"),
2251 Action::SetPageWidth => t!("action.set_page_width"),
2252 Action::NextBuffer => t!("action.next_buffer"),
2253 Action::PrevBuffer => t!("action.prev_buffer"),
2254 Action::NavigateBack => t!("action.navigate_back"),
2255 Action::NavigateForward => t!("action.navigate_forward"),
2256 Action::SplitHorizontal => t!("action.split_horizontal"),
2257 Action::SplitVertical => t!("action.split_vertical"),
2258 Action::CloseSplit => t!("action.close_split"),
2259 Action::NextSplit => t!("action.next_split"),
2260 Action::PrevSplit => t!("action.prev_split"),
2261 Action::IncreaseSplitSize => t!("action.increase_split_size"),
2262 Action::DecreaseSplitSize => t!("action.decrease_split_size"),
2263 Action::ToggleMaximizeSplit => t!("action.toggle_maximize_split"),
2264 Action::PromptConfirm => t!("action.prompt_confirm"),
2265 Action::PromptConfirmWithText(ref text) => {
2266 format!("{} ({})", t!("action.prompt_confirm"), text).into()
2267 }
2268 Action::PromptCancel => t!("action.prompt_cancel"),
2269 Action::PromptBackspace => t!("action.prompt_backspace"),
2270 Action::PromptDelete => t!("action.prompt_delete"),
2271 Action::PromptMoveLeft => t!("action.prompt_move_left"),
2272 Action::PromptMoveRight => t!("action.prompt_move_right"),
2273 Action::PromptMoveStart => t!("action.prompt_move_start"),
2274 Action::PromptMoveEnd => t!("action.prompt_move_end"),
2275 Action::PromptSelectPrev => t!("action.prompt_select_prev"),
2276 Action::PromptSelectNext => t!("action.prompt_select_next"),
2277 Action::PromptPageUp => t!("action.prompt_page_up"),
2278 Action::PromptPageDown => t!("action.prompt_page_down"),
2279 Action::PromptAcceptSuggestion => t!("action.prompt_accept_suggestion"),
2280 Action::PromptMoveWordLeft => t!("action.prompt_move_word_left"),
2281 Action::PromptMoveWordRight => t!("action.prompt_move_word_right"),
2282 Action::PromptDeleteWordForward => t!("action.prompt_delete_word_forward"),
2283 Action::PromptDeleteWordBackward => t!("action.prompt_delete_word_backward"),
2284 Action::PromptDeleteToLineEnd => t!("action.prompt_delete_to_line_end"),
2285 Action::PromptCopy => t!("action.prompt_copy"),
2286 Action::PromptCut => t!("action.prompt_cut"),
2287 Action::PromptPaste => t!("action.prompt_paste"),
2288 Action::PromptMoveLeftSelecting => t!("action.prompt_move_left_selecting"),
2289 Action::PromptMoveRightSelecting => t!("action.prompt_move_right_selecting"),
2290 Action::PromptMoveHomeSelecting => t!("action.prompt_move_home_selecting"),
2291 Action::PromptMoveEndSelecting => t!("action.prompt_move_end_selecting"),
2292 Action::PromptSelectWordLeft => t!("action.prompt_select_word_left"),
2293 Action::PromptSelectWordRight => t!("action.prompt_select_word_right"),
2294 Action::PromptSelectAll => t!("action.prompt_select_all"),
2295 Action::FileBrowserToggleHidden => t!("action.file_browser_toggle_hidden"),
2296 Action::FileBrowserToggleDetectEncoding => {
2297 t!("action.file_browser_toggle_detect_encoding")
2298 }
2299 Action::PopupSelectNext => t!("action.popup_select_next"),
2300 Action::PopupSelectPrev => t!("action.popup_select_prev"),
2301 Action::PopupPageUp => t!("action.popup_page_up"),
2302 Action::PopupPageDown => t!("action.popup_page_down"),
2303 Action::PopupConfirm => t!("action.popup_confirm"),
2304 Action::PopupCancel => t!("action.popup_cancel"),
2305 Action::PopupFocus => t!("action.popup_focus"),
2306 Action::CompletionAccept => t!("action.completion_accept"),
2307 Action::CompletionDismiss => t!("action.completion_dismiss"),
2308 Action::ToggleFileExplorer => t!("action.toggle_file_explorer"),
2309 Action::ToggleMenuBar => t!("action.toggle_menu_bar"),
2310 Action::ToggleTabBar => t!("action.toggle_tab_bar"),
2311 Action::ToggleStatusBar => t!("action.toggle_status_bar"),
2312 Action::TogglePromptLine => t!("action.toggle_prompt_line"),
2313 Action::ToggleVerticalScrollbar => t!("action.toggle_vertical_scrollbar"),
2314 Action::ToggleHorizontalScrollbar => t!("action.toggle_horizontal_scrollbar"),
2315 Action::FocusFileExplorer => t!("action.focus_file_explorer"),
2316 Action::FocusEditor => t!("action.focus_editor"),
2317 Action::FileExplorerUp => t!("action.file_explorer_up"),
2318 Action::FileExplorerDown => t!("action.file_explorer_down"),
2319 Action::FileExplorerPageUp => t!("action.file_explorer_page_up"),
2320 Action::FileExplorerPageDown => t!("action.file_explorer_page_down"),
2321 Action::FileExplorerExpand => t!("action.file_explorer_expand"),
2322 Action::FileExplorerCollapse => t!("action.file_explorer_collapse"),
2323 Action::FileExplorerOpen => t!("action.file_explorer_open"),
2324 Action::FileExplorerRefresh => t!("action.file_explorer_refresh"),
2325 Action::FileExplorerNewFile => t!("action.file_explorer_new_file"),
2326 Action::FileExplorerNewDirectory => t!("action.file_explorer_new_directory"),
2327 Action::FileExplorerDelete => t!("action.file_explorer_delete"),
2328 Action::FileExplorerRename => t!("action.file_explorer_rename"),
2329 Action::FileExplorerToggleHidden => t!("action.file_explorer_toggle_hidden"),
2330 Action::FileExplorerToggleGitignored => t!("action.file_explorer_toggle_gitignored"),
2331 Action::FileExplorerSearchClear => t!("action.file_explorer_search_clear"),
2332 Action::FileExplorerSearchBackspace => t!("action.file_explorer_search_backspace"),
2333 Action::FileExplorerCopy => t!("action.file_explorer_copy"),
2334 Action::FileExplorerCut => t!("action.file_explorer_cut"),
2335 Action::FileExplorerPaste => t!("action.file_explorer_paste"),
2336 Action::FileExplorerExtendSelectionUp => t!("action.file_explorer_extend_selection_up"),
2337 Action::FileExplorerExtendSelectionDown => {
2338 t!("action.file_explorer_extend_selection_down")
2339 }
2340 Action::FileExplorerToggleSelect => t!("action.file_explorer_toggle_select"),
2341 Action::FileExplorerSelectAll => t!("action.file_explorer_select_all"),
2342 Action::LspCompletion => t!("action.lsp_completion"),
2343 Action::LspGotoDefinition => t!("action.lsp_goto_definition"),
2344 Action::LspReferences => t!("action.lsp_references"),
2345 Action::LspRename => t!("action.lsp_rename"),
2346 Action::LspHover => t!("action.lsp_hover"),
2347 Action::LspSignatureHelp => t!("action.lsp_signature_help"),
2348 Action::LspCodeActions => t!("action.lsp_code_actions"),
2349 Action::LspRestart => t!("action.lsp_restart"),
2350 Action::LspStop => t!("action.lsp_stop"),
2351 Action::LspToggleForBuffer => t!("action.lsp_toggle_for_buffer"),
2352 Action::ToggleInlayHints => t!("action.toggle_inlay_hints"),
2353 Action::ToggleMouseHover => t!("action.toggle_mouse_hover"),
2354 Action::ToggleLineNumbers => t!("action.toggle_line_numbers"),
2355 Action::ToggleScrollSync => t!("action.toggle_scroll_sync"),
2356 Action::ToggleMouseCapture => t!("action.toggle_mouse_capture"),
2357 Action::ToggleDebugHighlights => t!("action.toggle_debug_highlights"),
2358 Action::SetBackground => t!("action.set_background"),
2359 Action::SetBackgroundBlend => t!("action.set_background_blend"),
2360 Action::AddRuler => t!("action.add_ruler"),
2361 Action::RemoveRuler => t!("action.remove_ruler"),
2362 Action::SetTabSize => t!("action.set_tab_size"),
2363 Action::SetLineEnding => t!("action.set_line_ending"),
2364 Action::SetEncoding => t!("action.set_encoding"),
2365 Action::ReloadWithEncoding => t!("action.reload_with_encoding"),
2366 Action::SetLanguage => t!("action.set_language"),
2367 Action::ToggleIndentationStyle => t!("action.toggle_indentation_style"),
2368 Action::ToggleTabIndicators => t!("action.toggle_tab_indicators"),
2369 Action::ToggleWhitespaceIndicators => t!("action.toggle_whitespace_indicators"),
2370 Action::ResetBufferSettings => t!("action.reset_buffer_settings"),
2371 Action::DumpConfig => t!("action.dump_config"),
2372 Action::RedrawScreen => t!("action.redraw_screen"),
2373 Action::Search => t!("action.search"),
2374 Action::FindInSelection => t!("action.find_in_selection"),
2375 Action::FindNext => t!("action.find_next"),
2376 Action::FindPrevious => t!("action.find_previous"),
2377 Action::FindSelectionNext => t!("action.find_selection_next"),
2378 Action::FindSelectionPrevious => t!("action.find_selection_previous"),
2379 Action::Replace => t!("action.replace"),
2380 Action::QueryReplace => t!("action.query_replace"),
2381 Action::MenuActivate => t!("action.menu_activate"),
2382 Action::MenuClose => t!("action.menu_close"),
2383 Action::MenuLeft => t!("action.menu_left"),
2384 Action::MenuRight => t!("action.menu_right"),
2385 Action::MenuUp => t!("action.menu_up"),
2386 Action::MenuDown => t!("action.menu_down"),
2387 Action::MenuExecute => t!("action.menu_execute"),
2388 Action::MenuOpen(name) => t!("action.menu_open", name = name),
2389 Action::SwitchKeybindingMap(map) => t!("action.switch_keybinding_map", map = map),
2390 Action::PluginAction(name) => t!("action.plugin_action", name = name),
2391 Action::ScrollTabsLeft => t!("action.scroll_tabs_left"),
2392 Action::ScrollTabsRight => t!("action.scroll_tabs_right"),
2393 Action::SelectTheme => t!("action.select_theme"),
2394 Action::SelectKeybindingMap => t!("action.select_keybinding_map"),
2395 Action::SelectCursorStyle => t!("action.select_cursor_style"),
2396 Action::SelectLocale => t!("action.select_locale"),
2397 Action::SwitchToPreviousTab => t!("action.switch_to_previous_tab"),
2398 Action::SwitchToTabByName => t!("action.switch_to_tab_by_name"),
2399 Action::OpenTerminal => t!("action.open_terminal"),
2400 Action::CloseTerminal => t!("action.close_terminal"),
2401 Action::FocusTerminal => t!("action.focus_terminal"),
2402 Action::TerminalEscape => t!("action.terminal_escape"),
2403 Action::ToggleKeyboardCapture => t!("action.toggle_keyboard_capture"),
2404 Action::TerminalPaste => t!("action.terminal_paste"),
2405 Action::OpenSettings => t!("action.open_settings"),
2406 Action::CloseSettings => t!("action.close_settings"),
2407 Action::SettingsSave => t!("action.settings_save"),
2408 Action::SettingsReset => t!("action.settings_reset"),
2409 Action::SettingsToggleFocus => t!("action.settings_toggle_focus"),
2410 Action::SettingsActivate => t!("action.settings_activate"),
2411 Action::SettingsSearch => t!("action.settings_search"),
2412 Action::SettingsHelp => t!("action.settings_help"),
2413 Action::SettingsIncrement => t!("action.settings_increment"),
2414 Action::SettingsDecrement => t!("action.settings_decrement"),
2415 Action::SettingsInherit => t!("action.settings_inherit"),
2416 Action::ShellCommand => t!("action.shell_command"),
2417 Action::ShellCommandReplace => t!("action.shell_command_replace"),
2418 Action::ToUpperCase => t!("action.to_uppercase"),
2419 Action::ToLowerCase => t!("action.to_lowercase"),
2420 Action::ToggleCase => t!("action.to_uppercase"),
2421 Action::SortLines => t!("action.sort_lines"),
2422 Action::CalibrateInput => t!("action.calibrate_input"),
2423 Action::EventDebug => t!("action.event_debug"),
2424 Action::SuspendProcess => t!("action.suspend_process"),
2425 Action::LoadPluginFromBuffer => "Load Plugin from Buffer".into(),
2426 Action::InitReload => "Reload init.ts".into(),
2427 Action::InitEdit => "Edit init.ts".into(),
2428 Action::InitCheck => "Check init.ts".into(),
2429 Action::OpenKeybindingEditor => "Keybinding Editor".into(),
2430 Action::CompositeNextHunk => t!("action.composite_next_hunk"),
2431 Action::CompositePrevHunk => t!("action.composite_prev_hunk"),
2432 Action::None => t!("action.none"),
2433 }
2434 .to_string()
2435 }
2436
2437 pub fn parse_key_public(key: &str) -> Option<KeyCode> {
2439 Self::parse_key(key)
2440 }
2441
2442 pub fn parse_modifiers_public(modifiers: &[String]) -> KeyModifiers {
2444 Self::parse_modifiers(modifiers)
2445 }
2446
2447 pub fn format_action_from_str(action_name: &str) -> String {
2451 Self::format_action_from_str_with_args(action_name, &std::collections::HashMap::new())
2452 }
2453
2454 pub fn format_action_from_str_with_args(
2458 action_name: &str,
2459 args: &std::collections::HashMap<String, serde_json::Value>,
2460 ) -> String {
2461 if let Some(action) = Action::from_str(action_name, args) {
2463 Self::format_action(&action)
2464 } else {
2465 action_name
2467 .split('_')
2468 .map(|word| {
2469 let mut chars = word.chars();
2470 match chars.next() {
2471 Some(c) => {
2472 let upper: String = c.to_uppercase().collect();
2473 format!("{}{}", upper, chars.as_str())
2474 }
2475 None => String::new(),
2476 }
2477 })
2478 .collect::<Vec<_>>()
2479 .join(" ")
2480 }
2481 }
2482
2483 pub fn all_action_names() -> Vec<String> {
2487 Action::all_action_names()
2488 }
2489
2490 pub fn get_keybinding_for_action(
2496 &self,
2497 action: &Action,
2498 context: KeyContext,
2499 ) -> Option<String> {
2500 self.get_keybinding_event_for_action(action, context)
2501 .map(|(k, m)| format_keybinding(&k, &m))
2502 }
2503
2504 pub fn get_keybinding_event_for_action(
2511 &self,
2512 action: &Action,
2513 context: KeyContext,
2514 ) -> Option<(KeyCode, KeyModifiers)> {
2515 fn find_best_keybinding(
2517 bindings: &HashMap<(KeyCode, KeyModifiers), Action>,
2518 action: &Action,
2519 ) -> Option<(KeyCode, KeyModifiers)> {
2520 let matches: Vec<_> = bindings
2521 .iter()
2522 .filter(|(_, a)| *a == action)
2523 .map(|((k, m), _)| (*k, *m))
2524 .collect();
2525
2526 if matches.is_empty() {
2527 return None;
2528 }
2529
2530 let mut sorted = matches;
2533 sorted.sort_by(|(k1, m1), (k2, m2)| {
2534 let score1 = keybinding_priority_score(k1);
2535 let score2 = keybinding_priority_score(k2);
2536 match score1.cmp(&score2) {
2538 std::cmp::Ordering::Equal => {
2539 let s1 = format_keybinding(k1, m1);
2541 let s2 = format_keybinding(k2, m2);
2542 s1.cmp(&s2)
2543 }
2544 other => other,
2545 }
2546 });
2547
2548 sorted.into_iter().next()
2549 }
2550
2551 if let Some(context_bindings) = self.bindings.get(&context) {
2553 if let Some(hit) = find_best_keybinding(context_bindings, action) {
2554 return Some(hit);
2555 }
2556 }
2557
2558 if let Some(context_bindings) = self.default_bindings.get(&context) {
2560 if let Some(hit) = find_best_keybinding(context_bindings, action) {
2561 return Some(hit);
2562 }
2563 }
2564
2565 if context != KeyContext::Normal
2567 && (context.allows_normal_fallthrough() || Self::is_application_wide_action(action))
2568 {
2569 if let Some(normal_bindings) = self.bindings.get(&KeyContext::Normal) {
2571 if let Some(hit) = find_best_keybinding(normal_bindings, action) {
2572 return Some(hit);
2573 }
2574 }
2575
2576 if let Some(normal_bindings) = self.default_bindings.get(&KeyContext::Normal) {
2578 if let Some(hit) = find_best_keybinding(normal_bindings, action) {
2579 return Some(hit);
2580 }
2581 }
2582 }
2583
2584 None
2585 }
2586
2587 pub fn reload(&mut self, config: &Config) {
2589 self.bindings.clear();
2590 for binding in &config.keybindings {
2591 if let Some(key_code) = Self::parse_key(&binding.key) {
2592 let modifiers = Self::parse_modifiers(&binding.modifiers);
2593 if let Some(action) = Action::from_str(&binding.action, &binding.args) {
2594 let context = if let Some(ref when) = binding.when {
2596 KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
2597 } else {
2598 KeyContext::Normal
2599 };
2600
2601 self.bindings
2602 .entry(context)
2603 .or_default()
2604 .insert((key_code, modifiers), action);
2605 }
2606 }
2607 }
2608 }
2609}
2610
2611#[cfg(test)]
2612mod tests {
2613 use super::*;
2614
2615 #[test]
2616 fn test_parse_key() {
2617 assert_eq!(KeybindingResolver::parse_key("enter"), Some(KeyCode::Enter));
2618 assert_eq!(
2619 KeybindingResolver::parse_key("backspace"),
2620 Some(KeyCode::Backspace)
2621 );
2622 assert_eq!(KeybindingResolver::parse_key("tab"), Some(KeyCode::Tab));
2623 assert_eq!(
2624 KeybindingResolver::parse_key("backtab"),
2625 Some(KeyCode::BackTab)
2626 );
2627 assert_eq!(
2628 KeybindingResolver::parse_key("BackTab"),
2629 Some(KeyCode::BackTab)
2630 );
2631 assert_eq!(KeybindingResolver::parse_key("a"), Some(KeyCode::Char('a')));
2632 }
2633
2634 #[test]
2635 fn test_parse_modifiers() {
2636 let mods = vec!["ctrl".to_string()];
2637 assert_eq!(
2638 KeybindingResolver::parse_modifiers(&mods),
2639 KeyModifiers::CONTROL
2640 );
2641
2642 let mods = vec!["ctrl".to_string(), "shift".to_string()];
2643 assert_eq!(
2644 KeybindingResolver::parse_modifiers(&mods),
2645 KeyModifiers::CONTROL | KeyModifiers::SHIFT
2646 );
2647 }
2648
2649 #[test]
2650 fn test_format_action_from_str_distinguishes_menu_open_by_name() {
2651 let mut file_args = HashMap::new();
2654 file_args.insert(
2655 "name".to_string(),
2656 serde_json::Value::String("File".to_string()),
2657 );
2658 let mut edit_args = HashMap::new();
2659 edit_args.insert(
2660 "name".to_string(),
2661 serde_json::Value::String("Edit".to_string()),
2662 );
2663
2664 let file_display =
2665 KeybindingResolver::format_action_from_str_with_args("menu_open", &file_args);
2666 let edit_display =
2667 KeybindingResolver::format_action_from_str_with_args("menu_open", &edit_args);
2668 let no_args_display = KeybindingResolver::format_action_from_str("menu_open");
2669
2670 assert_ne!(
2671 file_display, edit_display,
2672 "menu_open with different names should produce different descriptions"
2673 );
2674 assert!(
2675 file_display.contains("File"),
2676 "expected the File menu description to contain \"File\", got {file_display:?}"
2677 );
2678 assert!(
2679 edit_display.contains("Edit"),
2680 "expected the Edit menu description to contain \"Edit\", got {edit_display:?}"
2681 );
2682 assert_eq!(no_args_display, "Menu Open");
2686 }
2687
2688 #[test]
2689 fn test_qualify_and_unqualify_roundtrip_menu_open() {
2690 let mut args = HashMap::new();
2691 args.insert(
2692 "name".to_string(),
2693 serde_json::Value::String("File".to_string()),
2694 );
2695
2696 let qualified = Action::qualify_action("menu_open", &args);
2697 assert_eq!(qualified, "menu_open:File");
2698
2699 let (bare, parsed_args) = Action::unqualify_action(&qualified);
2700 assert_eq!(bare, "menu_open");
2701 assert_eq!(
2702 parsed_args.get("name").and_then(|v| v.as_str()),
2703 Some("File")
2704 );
2705 }
2706
2707 #[test]
2708 fn test_qualify_action_passthrough_for_unparameterised() {
2709 let args = HashMap::new();
2711 assert_eq!(Action::qualify_action("save", &args), "save");
2712 let (bare, parsed) = Action::unqualify_action("save");
2713 assert_eq!(bare, "save");
2714 assert!(parsed.is_empty());
2715 }
2716
2717 #[test]
2718 fn test_qualify_action_no_suffix_when_arg_missing() {
2719 let args = HashMap::new();
2722 assert_eq!(Action::qualify_action("menu_open", &args), "menu_open");
2723 }
2724
2725 #[test]
2726 fn test_unqualify_action_ignores_colon_on_unknown_action() {
2727 let (bare, parsed) = Action::unqualify_action("my_plugin:action_with:colons");
2730 assert_eq!(bare, "my_plugin:action_with:colons");
2731 assert!(parsed.is_empty());
2732 }
2733
2734 #[test]
2735 fn test_to_qualified_action_str_for_menu_open() {
2736 let action = Action::MenuOpen("Edit".to_string());
2737 assert_eq!(action.to_qualified_action_str(), "menu_open:Edit");
2738 }
2739
2740 #[test]
2741 fn test_resolve_basic() {
2742 let config = Config::default();
2743 let resolver = KeybindingResolver::new(&config);
2744
2745 let event = KeyEvent::new(KeyCode::Left, KeyModifiers::empty());
2746 assert_eq!(
2747 resolver.resolve(&event, KeyContext::Normal),
2748 Action::MoveLeft
2749 );
2750
2751 let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
2752 assert_eq!(
2753 resolver.resolve(&event, KeyContext::Normal),
2754 Action::InsertChar('a')
2755 );
2756 }
2757
2758 #[test]
2759 fn test_shift_backspace_matches_backspace() {
2760 let config = Config::default();
2764 let resolver = KeybindingResolver::new(&config);
2765
2766 let backspace = KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty());
2767 let shift_backspace = KeyEvent::new(KeyCode::Backspace, KeyModifiers::SHIFT);
2768
2769 assert_eq!(
2771 resolver.resolve(&backspace, KeyContext::Normal),
2772 Action::DeleteBackward,
2773 "Backspace should resolve to DeleteBackward in Normal context"
2774 );
2775 assert_eq!(
2776 resolver.resolve(&shift_backspace, KeyContext::Normal),
2777 Action::DeleteBackward,
2778 "Shift+Backspace should resolve to DeleteBackward (same as Backspace) in Normal context"
2779 );
2780
2781 assert_eq!(
2783 resolver.resolve(&backspace, KeyContext::Prompt),
2784 Action::PromptBackspace,
2785 "Backspace should resolve to PromptBackspace in Prompt context"
2786 );
2787 assert_eq!(
2788 resolver.resolve(&shift_backspace, KeyContext::Prompt),
2789 Action::PromptBackspace,
2790 "Shift+Backspace should resolve to PromptBackspace (same as Backspace) in Prompt context"
2791 );
2792
2793 assert_eq!(
2795 resolver.resolve(&backspace, KeyContext::FileExplorer),
2796 Action::FileExplorerSearchBackspace,
2797 "Backspace should resolve to FileExplorerSearchBackspace in FileExplorer context"
2798 );
2799 assert_eq!(
2800 resolver.resolve(&shift_backspace, KeyContext::FileExplorer),
2801 Action::FileExplorerSearchBackspace,
2802 "Shift+Backspace should resolve to FileExplorerSearchBackspace (same as Backspace) in FileExplorer context"
2803 );
2804 }
2805
2806 #[test]
2807 fn test_action_from_str() {
2808 let args = HashMap::new();
2809 assert_eq!(Action::from_str("move_left", &args), Some(Action::MoveLeft));
2810 assert_eq!(Action::from_str("save", &args), Some(Action::Save));
2811 assert_eq!(
2813 Action::from_str("unknown", &args),
2814 Some(Action::PluginAction("unknown".to_string()))
2815 );
2816
2817 assert_eq!(
2819 Action::from_str("keyboard_shortcuts", &args),
2820 Some(Action::ShowKeyboardShortcuts)
2821 );
2822 assert_eq!(
2823 Action::from_str("prompt_confirm", &args),
2824 Some(Action::PromptConfirm)
2825 );
2826 assert_eq!(
2827 Action::from_str("popup_cancel", &args),
2828 Some(Action::PopupCancel)
2829 );
2830
2831 assert_eq!(
2833 Action::from_str("calibrate_input", &args),
2834 Some(Action::CalibrateInput)
2835 );
2836 }
2837
2838 #[test]
2839 fn test_key_context_from_when_clause() {
2840 assert_eq!(
2841 KeyContext::from_when_clause("normal"),
2842 Some(KeyContext::Normal)
2843 );
2844 assert_eq!(
2845 KeyContext::from_when_clause("prompt"),
2846 Some(KeyContext::Prompt)
2847 );
2848 assert_eq!(
2849 KeyContext::from_when_clause("popup"),
2850 Some(KeyContext::Popup)
2851 );
2852 assert_eq!(KeyContext::from_when_clause("help"), None);
2853 assert_eq!(KeyContext::from_when_clause(" help "), None); assert_eq!(KeyContext::from_when_clause("unknown"), None);
2855 assert_eq!(KeyContext::from_when_clause(""), None);
2856 }
2857
2858 #[test]
2859 fn test_key_context_to_when_clause() {
2860 assert_eq!(KeyContext::Normal.to_when_clause(), "normal");
2861 assert_eq!(KeyContext::Prompt.to_when_clause(), "prompt");
2862 assert_eq!(KeyContext::Popup.to_when_clause(), "popup");
2863 }
2864
2865 #[test]
2866 fn test_context_specific_bindings() {
2867 let config = Config::default();
2868 let resolver = KeybindingResolver::new(&config);
2869
2870 let enter_event = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
2872 assert_eq!(
2873 resolver.resolve(&enter_event, KeyContext::Prompt),
2874 Action::PromptConfirm
2875 );
2876 assert_eq!(
2877 resolver.resolve(&enter_event, KeyContext::Normal),
2878 Action::InsertNewline
2879 );
2880
2881 let up_event = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
2883 assert_eq!(
2884 resolver.resolve(&up_event, KeyContext::Popup),
2885 Action::PopupSelectPrev
2886 );
2887 assert_eq!(
2888 resolver.resolve(&up_event, KeyContext::Normal),
2889 Action::MoveUp
2890 );
2891 }
2892
2893 #[test]
2894 fn test_context_fallback_to_normal() {
2895 let config = Config::default();
2896 let resolver = KeybindingResolver::new(&config);
2897
2898 let save_event = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL);
2900 assert_eq!(
2901 resolver.resolve(&save_event, KeyContext::Normal),
2902 Action::Save
2903 );
2904 assert_eq!(
2905 resolver.resolve(&save_event, KeyContext::Popup),
2906 Action::Save
2907 );
2908 }
2910
2911 #[test]
2912 fn test_context_priority_resolution() {
2913 use crate::config::Keybinding;
2914
2915 let mut config = Config::default();
2917 config.keybindings.push(Keybinding {
2918 key: "esc".to_string(),
2919 modifiers: vec![],
2920 keys: vec![],
2921 action: "quit".to_string(), args: HashMap::new(),
2923 when: Some("popup".to_string()),
2924 });
2925
2926 let resolver = KeybindingResolver::new(&config);
2927 let esc_event = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
2928
2929 assert_eq!(
2931 resolver.resolve(&esc_event, KeyContext::Popup),
2932 Action::Quit
2933 );
2934
2935 assert_eq!(
2937 resolver.resolve(&esc_event, KeyContext::Normal),
2938 Action::RemoveSecondaryCursors
2939 );
2940 }
2941
2942 #[test]
2943 fn test_character_input_in_contexts() {
2944 let config = Config::default();
2945 let resolver = KeybindingResolver::new(&config);
2946
2947 let char_event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
2948
2949 assert_eq!(
2951 resolver.resolve(&char_event, KeyContext::Normal),
2952 Action::InsertChar('a')
2953 );
2954 assert_eq!(
2955 resolver.resolve(&char_event, KeyContext::Prompt),
2956 Action::InsertChar('a')
2957 );
2958
2959 assert_eq!(
2961 resolver.resolve(&char_event, KeyContext::Popup),
2962 Action::None
2963 );
2964 }
2965
2966 #[test]
2967 fn test_custom_keybinding_loading() {
2968 use crate::config::Keybinding;
2969
2970 let mut config = Config::default();
2971
2972 config.keybindings.push(Keybinding {
2974 key: "f".to_string(),
2975 modifiers: vec!["ctrl".to_string()],
2976 keys: vec![],
2977 action: "command_palette".to_string(),
2978 args: HashMap::new(),
2979 when: None, });
2981
2982 let resolver = KeybindingResolver::new(&config);
2983
2984 let ctrl_f = KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL);
2986 assert_eq!(
2987 resolver.resolve(&ctrl_f, KeyContext::Normal),
2988 Action::CommandPalette
2989 );
2990
2991 let ctrl_k = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL);
2993 assert_eq!(
2994 resolver.resolve(&ctrl_k, KeyContext::Prompt),
2995 Action::PromptDeleteToLineEnd
2996 );
2997 assert_eq!(
2998 resolver.resolve(&ctrl_k, KeyContext::Normal),
2999 Action::DeleteToLineEnd
3000 );
3001 }
3002
3003 #[test]
3004 fn test_all_context_default_bindings_exist() {
3005 let config = Config::default();
3006 let resolver = KeybindingResolver::new(&config);
3007
3008 assert!(resolver.default_bindings.contains_key(&KeyContext::Normal));
3010 assert!(resolver.default_bindings.contains_key(&KeyContext::Prompt));
3011 assert!(resolver.default_bindings.contains_key(&KeyContext::Popup));
3012 assert!(resolver
3013 .default_bindings
3014 .contains_key(&KeyContext::FileExplorer));
3015 assert!(resolver.default_bindings.contains_key(&KeyContext::Menu));
3016
3017 assert!(!resolver.default_bindings[&KeyContext::Normal].is_empty());
3019 assert!(!resolver.default_bindings[&KeyContext::Prompt].is_empty());
3020 assert!(!resolver.default_bindings[&KeyContext::Popup].is_empty());
3021 assert!(!resolver.default_bindings[&KeyContext::FileExplorer].is_empty());
3022 assert!(!resolver.default_bindings[&KeyContext::Menu].is_empty());
3023 }
3024
3025 #[test]
3029 fn test_all_builtin_keymaps_have_valid_action_names() {
3030 let known_actions: std::collections::HashSet<String> =
3031 Action::all_action_names().into_iter().collect();
3032
3033 let config = Config::default();
3034
3035 for map_name in crate::config::KeybindingMapName::BUILTIN_OPTIONS {
3036 let bindings = config.resolve_keymap(map_name);
3037 for binding in &bindings {
3038 assert!(
3039 known_actions.contains(&binding.action),
3040 "Keymap '{}' contains unknown action '{}' (key: '{}', when: {:?}). \
3041 This will be treated as a plugin action at runtime. \
3042 Check for typos in the keymap JSON file.",
3043 map_name,
3044 binding.action,
3045 binding.key,
3046 binding.when,
3047 );
3048 }
3049 }
3050 }
3051
3052 #[test]
3053 fn test_resolve_determinism() {
3054 let config = Config::default();
3056 let resolver = KeybindingResolver::new(&config);
3057
3058 let test_cases = vec![
3059 (KeyCode::Left, KeyModifiers::empty(), KeyContext::Normal),
3060 (
3061 KeyCode::Esc,
3062 KeyModifiers::empty(),
3063 KeyContext::FileExplorer,
3064 ),
3065 (KeyCode::Enter, KeyModifiers::empty(), KeyContext::Prompt),
3066 (KeyCode::Down, KeyModifiers::empty(), KeyContext::Popup),
3067 ];
3068
3069 for (key_code, modifiers, context) in test_cases {
3070 let event = KeyEvent::new(key_code, modifiers);
3071 let action1 = resolver.resolve(&event, context.clone());
3072 let action2 = resolver.resolve(&event, context.clone());
3073 let action3 = resolver.resolve(&event, context);
3074
3075 assert_eq!(action1, action2, "Resolve should be deterministic");
3076 assert_eq!(action2, action3, "Resolve should be deterministic");
3077 }
3078 }
3079
3080 #[test]
3081 fn test_modifier_combinations() {
3082 let config = Config::default();
3083 let resolver = KeybindingResolver::new(&config);
3084
3085 let char_s = KeyCode::Char('s');
3087
3088 let no_mod = KeyEvent::new(char_s, KeyModifiers::empty());
3089 let ctrl = KeyEvent::new(char_s, KeyModifiers::CONTROL);
3090 let shift = KeyEvent::new(char_s, KeyModifiers::SHIFT);
3091 let ctrl_shift = KeyEvent::new(char_s, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
3092
3093 let action_no_mod = resolver.resolve(&no_mod, KeyContext::Normal);
3094 let action_ctrl = resolver.resolve(&ctrl, KeyContext::Normal);
3095 let action_shift = resolver.resolve(&shift, KeyContext::Normal);
3096 let action_ctrl_shift = resolver.resolve(&ctrl_shift, KeyContext::Normal);
3097
3098 assert_eq!(action_no_mod, Action::InsertChar('s'));
3100 assert_eq!(action_ctrl, Action::Save);
3101 assert_eq!(action_shift, Action::InsertChar('s')); assert_eq!(action_ctrl_shift, Action::None);
3104 }
3105
3106 #[test]
3107 fn test_scroll_keybindings() {
3108 let config = Config::default();
3109 let resolver = KeybindingResolver::new(&config);
3110
3111 let ctrl_up = KeyEvent::new(KeyCode::Up, KeyModifiers::CONTROL);
3113 assert_eq!(
3114 resolver.resolve(&ctrl_up, KeyContext::Normal),
3115 Action::ScrollUp,
3116 "Ctrl+Up should resolve to ScrollUp"
3117 );
3118
3119 let ctrl_down = KeyEvent::new(KeyCode::Down, KeyModifiers::CONTROL);
3121 assert_eq!(
3122 resolver.resolve(&ctrl_down, KeyContext::Normal),
3123 Action::ScrollDown,
3124 "Ctrl+Down should resolve to ScrollDown"
3125 );
3126 }
3127
3128 #[test]
3129 fn test_lsp_completion_keybinding() {
3130 let config = Config::default();
3131 let resolver = KeybindingResolver::new(&config);
3132
3133 let ctrl_space = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL);
3135 assert_eq!(
3136 resolver.resolve(&ctrl_space, KeyContext::Normal),
3137 Action::LspCompletion,
3138 "Ctrl+Space should resolve to LspCompletion"
3139 );
3140 }
3141
3142 #[test]
3143 fn test_terminal_key_equivalents() {
3144 let ctrl = KeyModifiers::CONTROL;
3146
3147 let slash_equivs = terminal_key_equivalents(KeyCode::Char('/'), ctrl);
3149 assert_eq!(slash_equivs, vec![(KeyCode::Char('7'), ctrl)]);
3150
3151 let seven_equivs = terminal_key_equivalents(KeyCode::Char('7'), ctrl);
3152 assert_eq!(seven_equivs, vec![(KeyCode::Char('/'), ctrl)]);
3153
3154 let backspace_equivs = terminal_key_equivalents(KeyCode::Backspace, ctrl);
3156 assert_eq!(backspace_equivs, vec![(KeyCode::Char('h'), ctrl)]);
3157
3158 let h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl);
3159 assert_eq!(h_equivs, vec![(KeyCode::Backspace, ctrl)]);
3160
3161 let a_equivs = terminal_key_equivalents(KeyCode::Char('a'), ctrl);
3163 assert!(a_equivs.is_empty());
3164
3165 let slash_no_ctrl = terminal_key_equivalents(KeyCode::Char('/'), KeyModifiers::empty());
3167 assert!(slash_no_ctrl.is_empty());
3168 }
3169
3170 #[test]
3171 fn test_terminal_key_equivalents_auto_binding() {
3172 let config = Config::default();
3173 let resolver = KeybindingResolver::new(&config);
3174
3175 let ctrl_slash = KeyEvent::new(KeyCode::Char('/'), KeyModifiers::CONTROL);
3177 let action_slash = resolver.resolve(&ctrl_slash, KeyContext::Normal);
3178 assert_eq!(
3179 action_slash,
3180 Action::ToggleComment,
3181 "Ctrl+/ should resolve to ToggleComment"
3182 );
3183
3184 let ctrl_7 = KeyEvent::new(KeyCode::Char('7'), KeyModifiers::CONTROL);
3186 let action_7 = resolver.resolve(&ctrl_7, KeyContext::Normal);
3187 assert_eq!(
3188 action_7,
3189 Action::ToggleComment,
3190 "Ctrl+7 should resolve to ToggleComment (terminal equivalent of Ctrl+/)"
3191 );
3192 }
3193
3194 #[test]
3195 fn test_terminal_key_equivalents_normalization() {
3196 let ctrl = KeyModifiers::CONTROL;
3201
3202 let slash_equivs = terminal_key_equivalents(KeyCode::Char('/'), ctrl);
3205 assert_eq!(
3206 slash_equivs,
3207 vec![(KeyCode::Char('7'), ctrl)],
3208 "Ctrl+/ should map to Ctrl+7"
3209 );
3210 let seven_equivs = terminal_key_equivalents(KeyCode::Char('7'), ctrl);
3211 assert_eq!(
3212 seven_equivs,
3213 vec![(KeyCode::Char('/'), ctrl)],
3214 "Ctrl+7 should map back to Ctrl+/"
3215 );
3216
3217 let backspace_equivs = terminal_key_equivalents(KeyCode::Backspace, ctrl);
3220 assert_eq!(
3221 backspace_equivs,
3222 vec![(KeyCode::Char('h'), ctrl)],
3223 "Ctrl+Backspace should map to Ctrl+H"
3224 );
3225 let h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl);
3226 assert_eq!(
3227 h_equivs,
3228 vec![(KeyCode::Backspace, ctrl)],
3229 "Ctrl+H should map back to Ctrl+Backspace"
3230 );
3231
3232 let space_equivs = terminal_key_equivalents(KeyCode::Char(' '), ctrl);
3235 assert_eq!(
3236 space_equivs,
3237 vec![(KeyCode::Char('@'), ctrl)],
3238 "Ctrl+Space should map to Ctrl+@"
3239 );
3240 let at_equivs = terminal_key_equivalents(KeyCode::Char('@'), ctrl);
3241 assert_eq!(
3242 at_equivs,
3243 vec![(KeyCode::Char(' '), ctrl)],
3244 "Ctrl+@ should map back to Ctrl+Space"
3245 );
3246
3247 let minus_equivs = terminal_key_equivalents(KeyCode::Char('-'), ctrl);
3250 assert_eq!(
3251 minus_equivs,
3252 vec![(KeyCode::Char('_'), ctrl)],
3253 "Ctrl+- should map to Ctrl+_"
3254 );
3255 let underscore_equivs = terminal_key_equivalents(KeyCode::Char('_'), ctrl);
3256 assert_eq!(
3257 underscore_equivs,
3258 vec![(KeyCode::Char('-'), ctrl)],
3259 "Ctrl+_ should map back to Ctrl+-"
3260 );
3261
3262 assert!(
3264 terminal_key_equivalents(KeyCode::Char('a'), ctrl).is_empty(),
3265 "Ctrl+A should have no terminal equivalents"
3266 );
3267 assert!(
3268 terminal_key_equivalents(KeyCode::Char('z'), ctrl).is_empty(),
3269 "Ctrl+Z should have no terminal equivalents"
3270 );
3271 assert!(
3272 terminal_key_equivalents(KeyCode::Enter, ctrl).is_empty(),
3273 "Ctrl+Enter should have no terminal equivalents"
3274 );
3275
3276 assert!(
3278 terminal_key_equivalents(KeyCode::Char('/'), KeyModifiers::empty()).is_empty(),
3279 "/ without Ctrl should have no equivalents"
3280 );
3281 assert!(
3282 terminal_key_equivalents(KeyCode::Char('7'), KeyModifiers::SHIFT).is_empty(),
3283 "Shift+7 should have no equivalents"
3284 );
3285 assert!(
3286 terminal_key_equivalents(KeyCode::Char('h'), KeyModifiers::ALT).is_empty(),
3287 "Alt+H should have no equivalents"
3288 );
3289
3290 let ctrl_shift = KeyModifiers::CONTROL | KeyModifiers::SHIFT;
3293 let ctrl_shift_h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl_shift);
3294 assert!(
3295 ctrl_shift_h_equivs.is_empty(),
3296 "Ctrl+Shift+H should NOT map to Ctrl+Shift+Backspace"
3297 );
3298 }
3299
3300 #[test]
3301 fn test_no_duplicate_keybindings_in_keymaps() {
3302 use std::collections::HashMap;
3305
3306 let keymaps: &[(&str, &str)] = &[
3307 ("default", include_str!("../../keymaps/default.json")),
3308 ("macos", include_str!("../../keymaps/macos.json")),
3309 ];
3310
3311 for (keymap_name, json_content) in keymaps {
3312 let keymap: crate::config::KeymapConfig = serde_json::from_str(json_content)
3313 .unwrap_or_else(|e| panic!("Failed to parse keymap '{}': {}", keymap_name, e));
3314
3315 let mut seen: HashMap<(String, Vec<String>, String), String> = HashMap::new();
3317 let mut duplicates: Vec<String> = Vec::new();
3318
3319 for binding in &keymap.bindings {
3320 let when = binding.when.clone().unwrap_or_default();
3321 let key_id = (binding.key.clone(), binding.modifiers.clone(), when.clone());
3322
3323 if let Some(existing_action) = seen.get(&key_id) {
3324 duplicates.push(format!(
3325 "Duplicate in '{}': key='{}', modifiers={:?}, when='{}' -> '{}' vs '{}'",
3326 keymap_name,
3327 binding.key,
3328 binding.modifiers,
3329 when,
3330 existing_action,
3331 binding.action
3332 ));
3333 } else {
3334 seen.insert(key_id, binding.action.clone());
3335 }
3336 }
3337
3338 assert!(
3339 duplicates.is_empty(),
3340 "Found duplicate keybindings:\n{}",
3341 duplicates.join("\n")
3342 );
3343 }
3344 }
3345}