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