Skip to main content

fresh/input/
keybindings.rs

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
7/// Normalize a key for consistent lookup in keybinding resolution.
8///
9/// Terminals vary in how they report certain keys:
10/// - BackTab already encodes Shift+Tab, but some terminals also set SHIFT —
11///   strip the redundant SHIFT so bindings defined as "BackTab" match.
12/// - Uppercase letters may arrive as `Char('P')` + SHIFT (real Shift press)
13///   or `Char('A')` without SHIFT (CapsLock on, kitty keyboard protocol).
14///   In both cases, lowercase the character and preserve the existing
15///   modifiers. This ensures CapsLock+Ctrl+A matches the `Ctrl+A` binding,
16///   while Shift+P still matches the `Shift+P` binding.
17fn normalize_key(code: KeyCode, modifiers: KeyModifiers) -> (KeyCode, KeyModifiers) {
18    if code == KeyCode::BackTab {
19        return (code, modifiers.difference(KeyModifiers::SHIFT));
20    }
21    if let KeyCode::Char(c) = code {
22        if c.is_ascii_uppercase() {
23            return (KeyCode::Char(c.to_ascii_lowercase()), modifiers);
24        }
25    }
26    (code, modifiers)
27}
28
29/// Global flag to force Linux-style keybinding display (Alt/Shift instead of ⌥/⇧)
30/// This is primarily used in tests to ensure consistent output across platforms.
31static FORCE_LINUX_KEYBINDINGS: AtomicBool = AtomicBool::new(false);
32
33/// Force Linux-style keybinding display (Alt/Shift instead of ⌥/⇧)
34/// Call this in tests to ensure consistent output regardless of platform.
35pub fn set_force_linux_keybindings(force: bool) {
36    FORCE_LINUX_KEYBINDINGS.store(force, Ordering::SeqCst);
37}
38
39/// Check if we should use macOS-style symbols for Alt and Shift keybindings
40fn use_macos_symbols() -> bool {
41    if FORCE_LINUX_KEYBINDINGS.load(Ordering::SeqCst) {
42        return false;
43    }
44    cfg!(target_os = "macos")
45}
46
47/// Check if the given modifiers allow text input (character insertion).
48///
49/// Returns true for:
50/// - No modifiers
51/// - Shift only (for uppercase letters, symbols)
52/// - Ctrl+Alt on Windows (AltGr key, used for special characters on international keyboards)
53///
54/// On Windows, the AltGr key is reported as Ctrl+Alt by crossterm, which is needed for
55/// typing characters like @, [, ], {, }, etc. on German, French, and other keyboard layouts.
56/// See: https://github.com/crossterm-rs/crossterm/issues/820
57fn is_text_input_modifier(modifiers: KeyModifiers) -> bool {
58    if modifiers.is_empty() || modifiers == KeyModifiers::SHIFT {
59        return true;
60    }
61
62    // Windows: AltGr is reported as Ctrl+Alt by crossterm.
63    // AltGr+Shift is needed for some layouts (e.g. Italian: AltGr+Shift+è = '{').
64    // See: https://github.com/sinelaw/fresh/issues/993
65    #[cfg(windows)]
66    if modifiers == (KeyModifiers::CONTROL | KeyModifiers::ALT)
67        || modifiers == (KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT)
68    {
69        return true;
70    }
71
72    false
73}
74
75/// Format a keybinding as a user-friendly string
76/// On macOS, uses native symbols: ⌃ (Control), ⌥ (Option), ⇧ (Shift) without separators
77/// On other platforms, uses "Ctrl+Alt+Shift+" format
78pub fn format_keybinding(keycode: &KeyCode, modifiers: &KeyModifiers) -> String {
79    let mut result = String::new();
80
81    // On macOS, use native symbols: ⌃ (Control), ⌥ (Option/Alt), ⇧ (Shift), ⌘ (Command)
82    let (ctrl_label, alt_label, shift_label, super_label) = if use_macos_symbols() {
83        ("⌃", "⌥", "⇧", "⌘")
84    } else {
85        ("Ctrl", "Alt", "Shift", "Super")
86    };
87
88    let use_plus = !use_macos_symbols();
89
90    if modifiers.contains(KeyModifiers::SUPER) {
91        result.push_str(super_label);
92        if use_plus {
93            result.push('+');
94        }
95    }
96    if modifiers.contains(KeyModifiers::CONTROL) {
97        result.push_str(ctrl_label);
98        if use_plus {
99            result.push('+');
100        }
101    }
102    if modifiers.contains(KeyModifiers::ALT) {
103        result.push_str(alt_label);
104        if use_plus {
105            result.push('+');
106        }
107    }
108    if modifiers.contains(KeyModifiers::SHIFT) {
109        result.push_str(shift_label);
110        if use_plus {
111            result.push('+');
112        }
113    }
114
115    match keycode {
116        KeyCode::Enter => result.push_str("Enter"),
117        KeyCode::Backspace => result.push_str("Backspace"),
118        KeyCode::Delete => result.push_str("Del"),
119        KeyCode::Tab => result.push_str("Tab"),
120        KeyCode::Esc => result.push_str("Esc"),
121        KeyCode::Left => result.push('←'),
122        KeyCode::Right => result.push('→'),
123        KeyCode::Up => result.push('↑'),
124        KeyCode::Down => result.push('↓'),
125        KeyCode::Home => result.push_str("Home"),
126        KeyCode::End => result.push_str("End"),
127        KeyCode::PageUp => result.push_str("PgUp"),
128        KeyCode::PageDown => result.push_str("PgDn"),
129        KeyCode::Char(' ') => result.push_str("Space"),
130        KeyCode::Char(c) => result.push_str(&c.to_uppercase().to_string()),
131        KeyCode::F(n) => result.push_str(&format!("F{}", n)),
132        _ => return String::new(),
133    }
134
135    result
136}
137
138/// Returns a priority score for a keybinding key.
139/// Lower scores indicate canonical/preferred keys, higher scores indicate terminal equivalents.
140/// This helps ensure deterministic selection when multiple keybindings exist for an action.
141fn keybinding_priority_score(key: &KeyCode) -> u32 {
142    match key {
143        // Terminal equivalents get higher scores (deprioritized)
144        KeyCode::Char('@') => 100, // Equivalent of Space
145        KeyCode::Char('7') => 100, // Equivalent of /
146        KeyCode::Char('_') => 100, // Equivalent of -
147        // Ctrl+H as backspace equivalent is handled differently (only plain Ctrl+H)
148        // All other keys get default priority
149        _ => 0,
150    }
151}
152
153/// Returns terminal key equivalents for a given key combination.
154///
155/// Some key combinations are sent differently by terminals:
156/// - Ctrl+/ is often sent as Ctrl+7
157/// - Ctrl+Backspace is often sent as Ctrl+H
158/// - Ctrl+Space is often sent as Ctrl+@ (NUL)
159/// - Ctrl+[ is often sent as Escape
160///
161/// This function returns any equivalent key combinations that should be
162/// treated as aliases for the given key.
163pub fn terminal_key_equivalents(
164    key: KeyCode,
165    modifiers: KeyModifiers,
166) -> Vec<(KeyCode, KeyModifiers)> {
167    let mut equivalents = Vec::new();
168
169    // Only consider equivalents when Ctrl is pressed
170    if modifiers.contains(KeyModifiers::CONTROL) {
171        let base_modifiers = modifiers; // Keep all modifiers including Ctrl
172
173        match key {
174            // Ctrl+/ is often sent as Ctrl+7
175            KeyCode::Char('/') => {
176                equivalents.push((KeyCode::Char('7'), base_modifiers));
177            }
178            KeyCode::Char('7') => {
179                equivalents.push((KeyCode::Char('/'), base_modifiers));
180            }
181
182            // Ctrl+Backspace is often sent as Ctrl+H
183            KeyCode::Backspace => {
184                equivalents.push((KeyCode::Char('h'), base_modifiers));
185            }
186            KeyCode::Char('h') if modifiers == KeyModifiers::CONTROL => {
187                // Only add Backspace equivalent for plain Ctrl+H (not Ctrl+Shift+H etc.)
188                equivalents.push((KeyCode::Backspace, base_modifiers));
189            }
190
191            // Ctrl+Space is often sent as Ctrl+@ (NUL character, code 0)
192            KeyCode::Char(' ') => {
193                equivalents.push((KeyCode::Char('@'), base_modifiers));
194            }
195            KeyCode::Char('@') => {
196                equivalents.push((KeyCode::Char(' '), base_modifiers));
197            }
198
199            // Ctrl+- is often sent as Ctrl+_
200            KeyCode::Char('-') => {
201                equivalents.push((KeyCode::Char('_'), base_modifiers));
202            }
203            KeyCode::Char('_') => {
204                equivalents.push((KeyCode::Char('-'), base_modifiers));
205            }
206
207            _ => {}
208        }
209    }
210
211    equivalents
212}
213
214/// Context in which a keybinding is active
215#[derive(Debug, Clone, PartialEq, Eq, Hash)]
216pub enum KeyContext {
217    /// Global bindings that work in all contexts (checked first with highest priority)
218    Global,
219    /// Normal editing mode
220    Normal,
221    /// Prompt/minibuffer is active
222    Prompt,
223    /// Popup window is visible
224    Popup,
225    /// File explorer has focus
226    FileExplorer,
227    /// Menu bar is active
228    Menu,
229    /// Terminal has focus
230    Terminal,
231    /// Settings modal is active
232    Settings,
233    /// Buffer-local mode context (e.g. "search-replace-list")
234    Mode(String),
235}
236
237impl KeyContext {
238    /// Check if a context should allow input
239    pub fn allows_text_input(&self) -> bool {
240        matches!(self, Self::Normal | Self::Prompt | Self::FileExplorer)
241    }
242
243    /// Parse context from a "when" string
244    pub fn from_when_clause(when: &str) -> Option<Self> {
245        let trimmed = when.trim();
246        if let Some(mode_name) = trimmed.strip_prefix("mode:") {
247            return Some(Self::Mode(mode_name.to_string()));
248        }
249        Some(match trimmed {
250            "global" => Self::Global,
251            "prompt" => Self::Prompt,
252            "popup" => Self::Popup,
253            "fileExplorer" | "file_explorer" => Self::FileExplorer,
254            "normal" => Self::Normal,
255            "menu" => Self::Menu,
256            "terminal" => Self::Terminal,
257            "settings" => Self::Settings,
258            _ => return None,
259        })
260    }
261
262    /// Convert context to "when" clause string
263    pub fn to_when_clause(&self) -> String {
264        match self {
265            Self::Global => "global".to_string(),
266            Self::Normal => "normal".to_string(),
267            Self::Prompt => "prompt".to_string(),
268            Self::Popup => "popup".to_string(),
269            Self::FileExplorer => "fileExplorer".to_string(),
270            Self::Menu => "menu".to_string(),
271            Self::Terminal => "terminal".to_string(),
272            Self::Settings => "settings".to_string(),
273            Self::Mode(name) => format!("mode:{}", name),
274        }
275    }
276}
277
278/// High-level actions that can be performed in the editor
279#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
280pub enum Action {
281    // Character input
282    InsertChar(char),
283    InsertNewline,
284    InsertTab,
285
286    // Basic movement
287    MoveLeft,
288    MoveRight,
289    MoveUp,
290    MoveDown,
291    MoveWordLeft,
292    MoveWordRight,
293    MoveWordEnd,     // Move to end of current word (Ctrl+Right style, past the end)
294    ViMoveWordEnd,   // Vim 'e' - move to end of word (ON last char, advances from word-end)
295    MoveLeftInLine,  // Move left without crossing line boundaries
296    MoveRightInLine, // Move right without crossing line boundaries
297    MoveLineStart,
298    MoveLineEnd,
299    MoveLineUp,
300    MoveLineDown,
301    MovePageUp,
302    MovePageDown,
303    MoveDocumentStart,
304    MoveDocumentEnd,
305
306    // Selection movement (extends selection while moving)
307    SelectLeft,
308    SelectRight,
309    SelectUp,
310    SelectDown,
311    SelectToParagraphUp,   // Jump to previous empty line with selection
312    SelectToParagraphDown, // Jump to next empty line with selection
313    SelectWordLeft,
314    SelectWordRight,
315    SelectWordEnd,   // Select to end of current word
316    ViSelectWordEnd, // Vim 'e' selection - select to end of word (ON last char)
317    SelectLineStart,
318    SelectLineEnd,
319    SelectDocumentStart,
320    SelectDocumentEnd,
321    SelectPageUp,
322    SelectPageDown,
323    SelectAll,
324    SelectWord,
325    SelectLine,
326    ExpandSelection,
327
328    // Block/rectangular selection (column-wise)
329    BlockSelectLeft,
330    BlockSelectRight,
331    BlockSelectUp,
332    BlockSelectDown,
333
334    // Editing
335    DeleteBackward,
336    DeleteForward,
337    DeleteWordBackward,
338    DeleteWordForward,
339    DeleteLine,
340    DeleteToLineEnd,
341    DeleteToLineStart,
342    DeleteViWordEnd, // Delete from cursor to end of word (vim de)
343    TransposeChars,
344    OpenLine,
345    DuplicateLine,
346
347    // View
348    Recenter,
349
350    // Selection
351    SetMark,
352
353    // Clipboard
354    Copy,
355    CopyWithTheme(String),
356    Cut,
357    Paste,
358
359    // Vi-style yank (copy without selection, then restore cursor)
360    YankWordForward,
361    YankWordBackward,
362    YankToLineEnd,
363    YankToLineStart,
364    YankViWordEnd, // Yank from cursor to end of word (vim ye)
365
366    // Multi-cursor
367    AddCursorAbove,
368    AddCursorBelow,
369    AddCursorNextMatch,
370    RemoveSecondaryCursors,
371
372    // File operations
373    Save,
374    SaveAs,
375    Open,
376    SwitchProject,
377    New,
378    Close,
379    CloseTab,
380    Quit,
381    ForceQuit,
382    Detach,
383    Revert,
384    ToggleAutoRevert,
385    FormatBuffer,
386    TrimTrailingWhitespace,
387    EnsureFinalNewline,
388
389    // Navigation
390    GotoLine,
391    ScanLineIndex,
392    GoToMatchingBracket,
393    JumpToNextError,
394    JumpToPreviousError,
395
396    // Smart editing
397    SmartHome,
398    DedentSelection,
399    ToggleComment,
400    DabbrevExpand,
401    ToggleFold,
402
403    // Bookmarks
404    SetBookmark(char),
405    JumpToBookmark(char),
406    ClearBookmark(char),
407    ListBookmarks,
408
409    // Search options
410    ToggleSearchCaseSensitive,
411    ToggleSearchWholeWord,
412    ToggleSearchRegex,
413    ToggleSearchConfirmEach,
414
415    // Macros
416    StartMacroRecording,
417    StopMacroRecording,
418    PlayMacro(char),
419    ToggleMacroRecording(char),
420    ShowMacro(char),
421    ListMacros,
422    PromptRecordMacro,
423    PromptPlayMacro,
424    PlayLastMacro,
425
426    // Bookmarks (prompt-based)
427    PromptSetBookmark,
428    PromptJumpToBookmark,
429
430    // Undo/redo
431    Undo,
432    Redo,
433
434    // View
435    ScrollUp,
436    ScrollDown,
437    ShowHelp,
438    ShowKeyboardShortcuts,
439    ShowWarnings,
440    ShowStatusLog,
441    ShowLspStatus,
442    ClearWarnings,
443    CommandPalette, // TODO: Consider dropping this now that we have QuickOpen
444    /// Quick Open - unified prompt with prefix-based provider routing
445    QuickOpen,
446    ToggleLineWrap,
447    ToggleCurrentLineHighlight,
448    ToggleReadOnly,
449    TogglePageView,
450    SetPageWidth,
451    InspectThemeAtCursor,
452    SelectTheme,
453    SelectKeybindingMap,
454    SelectCursorStyle,
455    SelectLocale,
456
457    // Buffer/tab navigation
458    NextBuffer,
459    PrevBuffer,
460    SwitchToPreviousTab,
461    SwitchToTabByName,
462
463    // Tab scrolling
464    ScrollTabsLeft,
465    ScrollTabsRight,
466
467    // Position history navigation
468    NavigateBack,
469    NavigateForward,
470
471    // Split view operations
472    SplitHorizontal,
473    SplitVertical,
474    CloseSplit,
475    NextSplit,
476    PrevSplit,
477    IncreaseSplitSize,
478    DecreaseSplitSize,
479    ToggleMaximizeSplit,
480
481    // Prompt mode actions
482    PromptConfirm,
483    /// PromptConfirm with recorded text for macro playback
484    PromptConfirmWithText(String),
485    PromptCancel,
486    PromptBackspace,
487    PromptDelete,
488    PromptMoveLeft,
489    PromptMoveRight,
490    PromptMoveStart,
491    PromptMoveEnd,
492    PromptSelectPrev,
493    PromptSelectNext,
494    PromptPageUp,
495    PromptPageDown,
496    PromptAcceptSuggestion,
497    PromptMoveWordLeft,
498    PromptMoveWordRight,
499    // Advanced prompt editing (word operations, clipboard)
500    PromptDeleteWordForward,
501    PromptDeleteWordBackward,
502    PromptDeleteToLineEnd,
503    PromptCopy,
504    PromptCut,
505    PromptPaste,
506    // Prompt selection actions
507    PromptMoveLeftSelecting,
508    PromptMoveRightSelecting,
509    PromptMoveHomeSelecting,
510    PromptMoveEndSelecting,
511    PromptSelectWordLeft,
512    PromptSelectWordRight,
513    PromptSelectAll,
514
515    // File browser actions
516    FileBrowserToggleHidden,
517    FileBrowserToggleDetectEncoding,
518
519    // Popup mode actions
520    PopupSelectNext,
521    PopupSelectPrev,
522    PopupPageUp,
523    PopupPageDown,
524    PopupConfirm,
525    PopupCancel,
526
527    // File explorer operations
528    ToggleFileExplorer,
529    // Menu bar visibility
530    ToggleMenuBar,
531    // Tab bar visibility
532    ToggleTabBar,
533    // Status bar visibility
534    ToggleStatusBar,
535    // Prompt line visibility
536    TogglePromptLine,
537    // Scrollbar visibility
538    ToggleVerticalScrollbar,
539    ToggleHorizontalScrollbar,
540    FocusFileExplorer,
541    FocusEditor,
542    FileExplorerUp,
543    FileExplorerDown,
544    FileExplorerPageUp,
545    FileExplorerPageDown,
546    FileExplorerExpand,
547    FileExplorerCollapse,
548    FileExplorerOpen,
549    FileExplorerRefresh,
550    FileExplorerNewFile,
551    FileExplorerNewDirectory,
552    FileExplorerDelete,
553    FileExplorerRename,
554    FileExplorerToggleHidden,
555    FileExplorerToggleGitignored,
556    FileExplorerSearchClear,
557    FileExplorerSearchBackspace,
558
559    // LSP operations
560    LspCompletion,
561    LspGotoDefinition,
562    LspReferences,
563    LspRename,
564    LspHover,
565    LspSignatureHelp,
566    LspCodeActions,
567    LspRestart,
568    LspStop,
569    LspToggleForBuffer,
570    ToggleInlayHints,
571    ToggleMouseHover,
572
573    // View toggles
574    ToggleLineNumbers,
575    ToggleScrollSync,
576    ToggleMouseCapture,
577    ToggleDebugHighlights, // Debug mode: show highlight/overlay byte ranges
578    SetBackground,
579    SetBackgroundBlend,
580
581    // Buffer settings (per-buffer overrides)
582    SetTabSize,
583    SetLineEnding,
584    SetEncoding,
585    ReloadWithEncoding,
586    SetLanguage,
587    ToggleIndentationStyle,
588    ToggleTabIndicators,
589    ToggleWhitespaceIndicators,
590    ResetBufferSettings,
591    AddRuler,
592    RemoveRuler,
593
594    // Config operations
595    DumpConfig,
596
597    // Search and replace
598    Search,
599    FindInSelection,
600    FindNext,
601    FindPrevious,
602    FindSelectionNext,     // Quick find next occurrence of selection (Ctrl+F3)
603    FindSelectionPrevious, // Quick find previous occurrence of selection (Ctrl+Shift+F3)
604    Replace,
605    QueryReplace, // Interactive replace (y/n/!/q for each match)
606
607    // Menu navigation
608    MenuActivate,     // Open menu bar (Alt or F10)
609    MenuClose,        // Close menu (Esc)
610    MenuLeft,         // Navigate to previous menu
611    MenuRight,        // Navigate to next menu
612    MenuUp,           // Navigate to previous item in menu
613    MenuDown,         // Navigate to next item in menu
614    MenuExecute,      // Execute selected menu item (Enter)
615    MenuOpen(String), // Open a specific menu by name (e.g., "File", "Edit")
616
617    // Keybinding map switching
618    SwitchKeybindingMap(String), // Switch to a named keybinding map (e.g., "default", "emacs", "vscode")
619
620    // Plugin custom actions
621    PluginAction(String),
622
623    // Settings operations
624    OpenSettings,        // Open the settings modal
625    CloseSettings,       // Close the settings modal
626    SettingsSave,        // Save settings changes
627    SettingsReset,       // Reset current setting to default
628    SettingsToggleFocus, // Toggle focus between category and settings panels
629    SettingsActivate,    // Activate/toggle the current setting
630    SettingsSearch,      // Start search in settings
631    SettingsHelp,        // Show settings help overlay
632    SettingsIncrement,   // Increment number value or next dropdown option
633    SettingsDecrement,   // Decrement number value or previous dropdown option
634
635    // Terminal operations
636    OpenTerminal,          // Open a new terminal in the current split
637    CloseTerminal,         // Close the current terminal
638    FocusTerminal,         // Focus the terminal buffer (if viewing terminal, focus input)
639    TerminalEscape,        // Escape from terminal mode back to editor
640    ToggleKeyboardCapture, // Toggle keyboard capture mode (all keys go to terminal)
641    TerminalPaste,         // Paste clipboard contents into terminal as a single batch
642
643    // Shell command operations
644    ShellCommand,        // Run shell command on buffer/selection, output to new buffer
645    ShellCommandReplace, // Run shell command on buffer/selection, replace content
646
647    // Case conversion
648    ToUpperCase, // Convert selection to uppercase
649    ToLowerCase, // Convert selection to lowercase
650    ToggleCase,  // Toggle case of character under cursor (vim ~)
651    SortLines,   // Sort selected lines alphabetically
652
653    // Input calibration
654    CalibrateInput, // Open the input calibration wizard
655
656    // Event debug
657    EventDebug, // Open the event debug dialog
658
659    // Keybinding editor
660    OpenKeybindingEditor, // Open the keybinding editor modal
661
662    // Plugin development
663    LoadPluginFromBuffer, // Load current buffer as a plugin
664
665    // No-op
666    None,
667}
668
669/// Macro that generates both `Action::from_str` and `Action::all_action_names` from a single
670/// definition, ensuring the list of valid action name strings is always in sync at compile time.
671///
672/// The first argument (`$args_name`) is the identifier used for the args parameter in custom
673/// bodies. This is needed so that macro hygiene allows the custom body expressions to reference
674/// the function parameter (both the definition and usage share the call-site span).
675///
676/// Four categories of action mappings:
677/// - `simple`: `"name" => Variant` — no args needed
678/// - `alias`: `"name" => Variant` — like simple, but only for from_str (not to_action_str)
679/// - `with_char`: `"name" => Variant` — passes through `with_char(args, ...)` for char-arg actions
680/// - `custom`: `"name" => { body }` — arbitrary expression using `$args_name` for complex arg parsing
681macro_rules! define_action_str_mapping {
682    (
683        $args_name:ident;
684        simple { $($s_name:literal => $s_variant:ident),* $(,)? }
685        alias { $($a_name:literal => $a_variant:ident),* $(,)? }
686        with_char { $($c_name:literal => $c_variant:ident),* $(,)? }
687        custom { $($x_name:literal => $x_variant:ident : $x_body:expr),* $(,)? }
688    ) => {
689        /// Parse action from string (used when loading from config)
690        pub fn from_str(s: &str, $args_name: &HashMap<String, serde_json::Value>) -> Option<Self> {
691            Some(match s {
692                $($s_name => Self::$s_variant,)*
693                $($a_name => Self::$a_variant,)*
694                $($c_name => return Self::with_char($args_name, Self::$c_variant),)*
695                $($x_name => $x_body,)*
696                // Unrecognized action names are treated as plugin actions, allowing
697                // keybindings for plugin-registered commands to load from config.
698                _ => Self::PluginAction(s.to_string()),
699            })
700        }
701
702        /// Convert an action back to its string name (inverse of from_str).
703        /// Returns the canonical action name string.
704        pub fn to_action_str(&self) -> String {
705            match self {
706                $(Self::$s_variant => $s_name.to_string(),)*
707                $(Self::$c_variant(_) => $c_name.to_string(),)*
708                $(Self::$x_variant(_) => $x_name.to_string(),)*
709                Self::PluginAction(name) => name.clone(),
710            }
711        }
712
713        /// All valid action name strings, sorted alphabetically.
714        /// Generated from the same macro as `from_str`, guaranteeing compile-time completeness.
715        pub fn all_action_names() -> Vec<String> {
716            let mut names = vec![
717                $($s_name.to_string(),)*
718                $($a_name.to_string(),)*
719                $($c_name.to_string(),)*
720                $($x_name.to_string(),)*
721            ];
722            names.sort();
723            names
724        }
725    };
726}
727
728impl Action {
729    fn with_char(
730        args: &HashMap<String, serde_json::Value>,
731        make_action: impl FnOnce(char) -> Self,
732    ) -> Option<Self> {
733        if let Some(serde_json::Value::String(value)) = args.get("char") {
734            value.chars().next().map(make_action)
735        } else {
736            None
737        }
738    }
739
740    define_action_str_mapping! {
741        args;
742        simple {
743            "insert_newline" => InsertNewline,
744            "insert_tab" => InsertTab,
745
746            "move_left" => MoveLeft,
747            "move_right" => MoveRight,
748            "move_up" => MoveUp,
749            "move_down" => MoveDown,
750            "move_word_left" => MoveWordLeft,
751            "move_word_right" => MoveWordRight,
752            "move_word_end" => MoveWordEnd,
753            "vi_move_word_end" => ViMoveWordEnd,
754            "move_left_in_line" => MoveLeftInLine,
755            "move_right_in_line" => MoveRightInLine,
756            "move_line_start" => MoveLineStart,
757            "move_line_end" => MoveLineEnd,
758            "move_line_up" => MoveLineUp,
759            "move_line_down" => MoveLineDown,
760            "move_page_up" => MovePageUp,
761            "move_page_down" => MovePageDown,
762            "move_document_start" => MoveDocumentStart,
763            "move_document_end" => MoveDocumentEnd,
764
765            "select_left" => SelectLeft,
766            "select_right" => SelectRight,
767            "select_up" => SelectUp,
768            "select_down" => SelectDown,
769            "select_to_paragraph_up" => SelectToParagraphUp,
770            "select_to_paragraph_down" => SelectToParagraphDown,
771            "select_word_left" => SelectWordLeft,
772            "select_word_right" => SelectWordRight,
773            "select_word_end" => SelectWordEnd,
774            "vi_select_word_end" => ViSelectWordEnd,
775            "select_line_start" => SelectLineStart,
776            "select_line_end" => SelectLineEnd,
777            "select_document_start" => SelectDocumentStart,
778            "select_document_end" => SelectDocumentEnd,
779            "select_page_up" => SelectPageUp,
780            "select_page_down" => SelectPageDown,
781            "select_all" => SelectAll,
782            "select_word" => SelectWord,
783            "select_line" => SelectLine,
784            "expand_selection" => ExpandSelection,
785
786            "block_select_left" => BlockSelectLeft,
787            "block_select_right" => BlockSelectRight,
788            "block_select_up" => BlockSelectUp,
789            "block_select_down" => BlockSelectDown,
790
791            "delete_backward" => DeleteBackward,
792            "delete_forward" => DeleteForward,
793            "delete_word_backward" => DeleteWordBackward,
794            "delete_word_forward" => DeleteWordForward,
795            "delete_line" => DeleteLine,
796            "delete_to_line_end" => DeleteToLineEnd,
797            "delete_to_line_start" => DeleteToLineStart,
798            "delete_vi_word_end" => DeleteViWordEnd,
799            "transpose_chars" => TransposeChars,
800            "open_line" => OpenLine,
801            "duplicate_line" => DuplicateLine,
802            "recenter" => Recenter,
803            "set_mark" => SetMark,
804
805            "copy" => Copy,
806            "cut" => Cut,
807            "paste" => Paste,
808
809            "yank_word_forward" => YankWordForward,
810            "yank_word_backward" => YankWordBackward,
811            "yank_to_line_end" => YankToLineEnd,
812            "yank_to_line_start" => YankToLineStart,
813            "yank_vi_word_end" => YankViWordEnd,
814
815            "add_cursor_above" => AddCursorAbove,
816            "add_cursor_below" => AddCursorBelow,
817            "add_cursor_next_match" => AddCursorNextMatch,
818            "remove_secondary_cursors" => RemoveSecondaryCursors,
819
820            "save" => Save,
821            "save_as" => SaveAs,
822            "open" => Open,
823            "switch_project" => SwitchProject,
824            "new" => New,
825            "close" => Close,
826            "close_tab" => CloseTab,
827            "quit" => Quit,
828            "force_quit" => ForceQuit,
829            "detach" => Detach,
830            "revert" => Revert,
831            "toggle_auto_revert" => ToggleAutoRevert,
832            "format_buffer" => FormatBuffer,
833            "trim_trailing_whitespace" => TrimTrailingWhitespace,
834            "ensure_final_newline" => EnsureFinalNewline,
835            "goto_line" => GotoLine,
836            "scan_line_index" => ScanLineIndex,
837            "goto_matching_bracket" => GoToMatchingBracket,
838            "jump_to_next_error" => JumpToNextError,
839            "jump_to_previous_error" => JumpToPreviousError,
840
841            "smart_home" => SmartHome,
842            "dedent_selection" => DedentSelection,
843            "toggle_comment" => ToggleComment,
844            "dabbrev_expand" => DabbrevExpand,
845            "toggle_fold" => ToggleFold,
846
847            "list_bookmarks" => ListBookmarks,
848
849            "toggle_search_case_sensitive" => ToggleSearchCaseSensitive,
850            "toggle_search_whole_word" => ToggleSearchWholeWord,
851            "toggle_search_regex" => ToggleSearchRegex,
852            "toggle_search_confirm_each" => ToggleSearchConfirmEach,
853
854            "start_macro_recording" => StartMacroRecording,
855            "stop_macro_recording" => StopMacroRecording,
856
857            "list_macros" => ListMacros,
858            "prompt_record_macro" => PromptRecordMacro,
859            "prompt_play_macro" => PromptPlayMacro,
860            "play_last_macro" => PlayLastMacro,
861            "prompt_set_bookmark" => PromptSetBookmark,
862            "prompt_jump_to_bookmark" => PromptJumpToBookmark,
863
864            "undo" => Undo,
865            "redo" => Redo,
866
867            "scroll_up" => ScrollUp,
868            "scroll_down" => ScrollDown,
869            "show_help" => ShowHelp,
870            "keyboard_shortcuts" => ShowKeyboardShortcuts,
871            "show_warnings" => ShowWarnings,
872            "show_status_log" => ShowStatusLog,
873            "show_lsp_status" => ShowLspStatus,
874            "clear_warnings" => ClearWarnings,
875            "command_palette" => CommandPalette,
876            "quick_open" => QuickOpen,
877            "toggle_line_wrap" => ToggleLineWrap,
878            "toggle_current_line_highlight" => ToggleCurrentLineHighlight,
879            "toggle_read_only" => ToggleReadOnly,
880            "toggle_page_view" => TogglePageView,
881            "set_page_width" => SetPageWidth,
882
883            "next_buffer" => NextBuffer,
884            "prev_buffer" => PrevBuffer,
885            "switch_to_previous_tab" => SwitchToPreviousTab,
886            "switch_to_tab_by_name" => SwitchToTabByName,
887            "scroll_tabs_left" => ScrollTabsLeft,
888            "scroll_tabs_right" => ScrollTabsRight,
889
890            "navigate_back" => NavigateBack,
891            "navigate_forward" => NavigateForward,
892
893            "split_horizontal" => SplitHorizontal,
894            "split_vertical" => SplitVertical,
895            "close_split" => CloseSplit,
896            "next_split" => NextSplit,
897            "prev_split" => PrevSplit,
898            "increase_split_size" => IncreaseSplitSize,
899            "decrease_split_size" => DecreaseSplitSize,
900            "toggle_maximize_split" => ToggleMaximizeSplit,
901
902            "prompt_confirm" => PromptConfirm,
903            "prompt_cancel" => PromptCancel,
904            "prompt_backspace" => PromptBackspace,
905            "prompt_move_left" => PromptMoveLeft,
906            "prompt_move_right" => PromptMoveRight,
907            "prompt_move_start" => PromptMoveStart,
908            "prompt_move_end" => PromptMoveEnd,
909            "prompt_select_prev" => PromptSelectPrev,
910            "prompt_select_next" => PromptSelectNext,
911            "prompt_page_up" => PromptPageUp,
912            "prompt_page_down" => PromptPageDown,
913            "prompt_accept_suggestion" => PromptAcceptSuggestion,
914            "prompt_delete_word_forward" => PromptDeleteWordForward,
915            "prompt_delete_word_backward" => PromptDeleteWordBackward,
916            "prompt_delete_to_line_end" => PromptDeleteToLineEnd,
917            "prompt_copy" => PromptCopy,
918            "prompt_cut" => PromptCut,
919            "prompt_paste" => PromptPaste,
920            "prompt_move_left_selecting" => PromptMoveLeftSelecting,
921            "prompt_move_right_selecting" => PromptMoveRightSelecting,
922            "prompt_move_home_selecting" => PromptMoveHomeSelecting,
923            "prompt_move_end_selecting" => PromptMoveEndSelecting,
924            "prompt_select_word_left" => PromptSelectWordLeft,
925            "prompt_select_word_right" => PromptSelectWordRight,
926            "prompt_select_all" => PromptSelectAll,
927            "file_browser_toggle_hidden" => FileBrowserToggleHidden,
928            "file_browser_toggle_detect_encoding" => FileBrowserToggleDetectEncoding,
929            "prompt_move_word_left" => PromptMoveWordLeft,
930            "prompt_move_word_right" => PromptMoveWordRight,
931            "prompt_delete" => PromptDelete,
932
933            "popup_select_next" => PopupSelectNext,
934            "popup_select_prev" => PopupSelectPrev,
935            "popup_page_up" => PopupPageUp,
936            "popup_page_down" => PopupPageDown,
937            "popup_confirm" => PopupConfirm,
938            "popup_cancel" => PopupCancel,
939
940            "toggle_file_explorer" => ToggleFileExplorer,
941            "toggle_menu_bar" => ToggleMenuBar,
942            "toggle_tab_bar" => ToggleTabBar,
943            "toggle_status_bar" => ToggleStatusBar,
944            "toggle_prompt_line" => TogglePromptLine,
945            "toggle_vertical_scrollbar" => ToggleVerticalScrollbar,
946            "toggle_horizontal_scrollbar" => ToggleHorizontalScrollbar,
947            "focus_file_explorer" => FocusFileExplorer,
948            "focus_editor" => FocusEditor,
949            "file_explorer_up" => FileExplorerUp,
950            "file_explorer_down" => FileExplorerDown,
951            "file_explorer_page_up" => FileExplorerPageUp,
952            "file_explorer_page_down" => FileExplorerPageDown,
953            "file_explorer_expand" => FileExplorerExpand,
954            "file_explorer_collapse" => FileExplorerCollapse,
955            "file_explorer_open" => FileExplorerOpen,
956            "file_explorer_refresh" => FileExplorerRefresh,
957            "file_explorer_new_file" => FileExplorerNewFile,
958            "file_explorer_new_directory" => FileExplorerNewDirectory,
959            "file_explorer_delete" => FileExplorerDelete,
960            "file_explorer_rename" => FileExplorerRename,
961            "file_explorer_toggle_hidden" => FileExplorerToggleHidden,
962            "file_explorer_toggle_gitignored" => FileExplorerToggleGitignored,
963            "file_explorer_search_clear" => FileExplorerSearchClear,
964            "file_explorer_search_backspace" => FileExplorerSearchBackspace,
965
966            "lsp_completion" => LspCompletion,
967            "lsp_goto_definition" => LspGotoDefinition,
968            "lsp_references" => LspReferences,
969            "lsp_rename" => LspRename,
970            "lsp_hover" => LspHover,
971            "lsp_signature_help" => LspSignatureHelp,
972            "lsp_code_actions" => LspCodeActions,
973            "lsp_restart" => LspRestart,
974            "lsp_stop" => LspStop,
975            "lsp_toggle_for_buffer" => LspToggleForBuffer,
976            "toggle_inlay_hints" => ToggleInlayHints,
977            "toggle_mouse_hover" => ToggleMouseHover,
978
979            "toggle_line_numbers" => ToggleLineNumbers,
980            "toggle_scroll_sync" => ToggleScrollSync,
981            "toggle_mouse_capture" => ToggleMouseCapture,
982            "toggle_debug_highlights" => ToggleDebugHighlights,
983            "set_background" => SetBackground,
984            "set_background_blend" => SetBackgroundBlend,
985            "inspect_theme_at_cursor" => InspectThemeAtCursor,
986            "select_theme" => SelectTheme,
987            "select_keybinding_map" => SelectKeybindingMap,
988            "select_cursor_style" => SelectCursorStyle,
989            "select_locale" => SelectLocale,
990
991            "set_tab_size" => SetTabSize,
992            "set_line_ending" => SetLineEnding,
993            "set_encoding" => SetEncoding,
994            "reload_with_encoding" => ReloadWithEncoding,
995            "set_language" => SetLanguage,
996            "toggle_indentation_style" => ToggleIndentationStyle,
997            "toggle_tab_indicators" => ToggleTabIndicators,
998            "toggle_whitespace_indicators" => ToggleWhitespaceIndicators,
999            "reset_buffer_settings" => ResetBufferSettings,
1000            "add_ruler" => AddRuler,
1001            "remove_ruler" => RemoveRuler,
1002
1003            "dump_config" => DumpConfig,
1004
1005            "search" => Search,
1006            "find_in_selection" => FindInSelection,
1007            "find_next" => FindNext,
1008            "find_previous" => FindPrevious,
1009            "find_selection_next" => FindSelectionNext,
1010            "find_selection_previous" => FindSelectionPrevious,
1011            "replace" => Replace,
1012            "query_replace" => QueryReplace,
1013
1014            "menu_activate" => MenuActivate,
1015            "menu_close" => MenuClose,
1016            "menu_left" => MenuLeft,
1017            "menu_right" => MenuRight,
1018            "menu_up" => MenuUp,
1019            "menu_down" => MenuDown,
1020            "menu_execute" => MenuExecute,
1021
1022            "open_terminal" => OpenTerminal,
1023            "close_terminal" => CloseTerminal,
1024            "focus_terminal" => FocusTerminal,
1025            "terminal_escape" => TerminalEscape,
1026            "toggle_keyboard_capture" => ToggleKeyboardCapture,
1027            "terminal_paste" => TerminalPaste,
1028
1029            "shell_command" => ShellCommand,
1030            "shell_command_replace" => ShellCommandReplace,
1031
1032            "to_upper_case" => ToUpperCase,
1033            "to_lower_case" => ToLowerCase,
1034            "toggle_case" => ToggleCase,
1035            "sort_lines" => SortLines,
1036
1037            "calibrate_input" => CalibrateInput,
1038            "event_debug" => EventDebug,
1039            "load_plugin_from_buffer" => LoadPluginFromBuffer,
1040            "open_keybinding_editor" => OpenKeybindingEditor,
1041
1042            "noop" => None,
1043
1044            "open_settings" => OpenSettings,
1045            "close_settings" => CloseSettings,
1046            "settings_save" => SettingsSave,
1047            "settings_reset" => SettingsReset,
1048            "settings_toggle_focus" => SettingsToggleFocus,
1049            "settings_activate" => SettingsActivate,
1050            "settings_search" => SettingsSearch,
1051            "settings_help" => SettingsHelp,
1052            "settings_increment" => SettingsIncrement,
1053            "settings_decrement" => SettingsDecrement,
1054        }
1055        alias {
1056            "toggle_compose_mode" => TogglePageView,
1057            "set_compose_width" => SetPageWidth,
1058        }
1059        with_char {
1060            "insert_char" => InsertChar,
1061            "set_bookmark" => SetBookmark,
1062            "jump_to_bookmark" => JumpToBookmark,
1063            "clear_bookmark" => ClearBookmark,
1064            "play_macro" => PlayMacro,
1065            "toggle_macro_recording" => ToggleMacroRecording,
1066            "show_macro" => ShowMacro,
1067        }
1068        custom {
1069            "copy_with_theme" => CopyWithTheme : {
1070                // Empty theme = open theme picker prompt
1071                let theme = args.get("theme").and_then(|v| v.as_str()).unwrap_or("");
1072                Self::CopyWithTheme(theme.to_string())
1073            },
1074            "menu_open" => MenuOpen : {
1075                let name = args.get("name")?.as_str()?;
1076                Self::MenuOpen(name.to_string())
1077            },
1078            "switch_keybinding_map" => SwitchKeybindingMap : {
1079                let map_name = args.get("map")?.as_str()?;
1080                Self::SwitchKeybindingMap(map_name.to_string())
1081            },
1082            "prompt_confirm_with_text" => PromptConfirmWithText : {
1083                let text = args.get("text")?.as_str()?;
1084                Self::PromptConfirmWithText(text.to_string())
1085            },
1086        }
1087    }
1088
1089    /// Check if this action is a movement or editing action that should be
1090    /// ignored in virtual buffers with hidden cursors.
1091    pub fn is_movement_or_editing(&self) -> bool {
1092        matches!(
1093            self,
1094            // Movement actions
1095            Action::MoveLeft
1096                | Action::MoveRight
1097                | Action::MoveUp
1098                | Action::MoveDown
1099                | Action::MoveWordLeft
1100                | Action::MoveWordRight
1101                | Action::MoveWordEnd
1102                | Action::ViMoveWordEnd
1103                | Action::MoveLeftInLine
1104                | Action::MoveRightInLine
1105                | Action::MoveLineStart
1106                | Action::MoveLineEnd
1107                | Action::MovePageUp
1108                | Action::MovePageDown
1109                | Action::MoveDocumentStart
1110                | Action::MoveDocumentEnd
1111                // Selection actions
1112                | Action::SelectLeft
1113                | Action::SelectRight
1114                | Action::SelectUp
1115                | Action::SelectDown
1116                | Action::SelectToParagraphUp
1117                | Action::SelectToParagraphDown
1118                | Action::SelectWordLeft
1119                | Action::SelectWordRight
1120                | Action::SelectWordEnd
1121                | Action::ViSelectWordEnd
1122                | Action::SelectLineStart
1123                | Action::SelectLineEnd
1124                | Action::SelectDocumentStart
1125                | Action::SelectDocumentEnd
1126                | Action::SelectPageUp
1127                | Action::SelectPageDown
1128                | Action::SelectAll
1129                | Action::SelectWord
1130                | Action::SelectLine
1131                | Action::ExpandSelection
1132                // Block selection
1133                | Action::BlockSelectLeft
1134                | Action::BlockSelectRight
1135                | Action::BlockSelectUp
1136                | Action::BlockSelectDown
1137                // Editing actions
1138                | Action::InsertChar(_)
1139                | Action::InsertNewline
1140                | Action::InsertTab
1141                | Action::DeleteBackward
1142                | Action::DeleteForward
1143                | Action::DeleteWordBackward
1144                | Action::DeleteWordForward
1145                | Action::DeleteLine
1146                | Action::DeleteToLineEnd
1147                | Action::DeleteToLineStart
1148                | Action::TransposeChars
1149                | Action::OpenLine
1150                | Action::DuplicateLine
1151                | Action::MoveLineUp
1152                | Action::MoveLineDown
1153                // Clipboard editing (but not Copy)
1154                | Action::Cut
1155                | Action::Paste
1156                // Undo/Redo
1157                | Action::Undo
1158                | Action::Redo
1159        )
1160    }
1161
1162    /// Check if this action modifies buffer content (for block selection conversion).
1163    /// Block selections should be converted to multi-cursor before these actions.
1164    pub fn is_editing(&self) -> bool {
1165        matches!(
1166            self,
1167            Action::InsertChar(_)
1168                | Action::InsertNewline
1169                | Action::InsertTab
1170                | Action::DeleteBackward
1171                | Action::DeleteForward
1172                | Action::DeleteWordBackward
1173                | Action::DeleteWordForward
1174                | Action::DeleteLine
1175                | Action::DeleteToLineEnd
1176                | Action::DeleteToLineStart
1177                | Action::DeleteViWordEnd
1178                | Action::TransposeChars
1179                | Action::OpenLine
1180                | Action::DuplicateLine
1181                | Action::MoveLineUp
1182                | Action::MoveLineDown
1183                | Action::Cut
1184                | Action::Paste
1185        )
1186    }
1187}
1188
1189/// Result of chord resolution
1190#[derive(Debug, Clone, PartialEq)]
1191pub enum ChordResolution {
1192    /// Complete match: execute the action
1193    Complete(Action),
1194    /// Partial match: continue waiting for more keys in the sequence
1195    Partial,
1196    /// No match: the sequence doesn't match any binding
1197    NoMatch,
1198}
1199
1200/// Resolves key events to actions based on configuration
1201#[derive(Clone)]
1202pub struct KeybindingResolver {
1203    /// Map from context to key bindings (single key bindings)
1204    /// Context-specific bindings have priority over normal bindings
1205    bindings: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1206
1207    /// Default bindings for each context (single key bindings)
1208    default_bindings: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1209
1210    /// Plugin default bindings (third tier, after custom and keymap defaults)
1211    /// Used for mode bindings registered by plugins via defineMode()
1212    plugin_defaults: HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>>,
1213
1214    /// Chord bindings (multi-key sequences)
1215    /// Maps context -> sequence -> action
1216    chord_bindings: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1217
1218    /// Default chord bindings for each context
1219    default_chord_bindings: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1220
1221    /// Plugin default chord bindings (for mode chord bindings from defineMode)
1222    plugin_chord_defaults: HashMap<KeyContext, HashMap<Vec<(KeyCode, KeyModifiers)>, Action>>,
1223}
1224
1225impl KeybindingResolver {
1226    /// Create a new resolver from configuration
1227    pub fn new(config: &Config) -> Self {
1228        let mut resolver = Self {
1229            bindings: HashMap::new(),
1230            default_bindings: HashMap::new(),
1231            plugin_defaults: HashMap::new(),
1232            chord_bindings: HashMap::new(),
1233            default_chord_bindings: HashMap::new(),
1234            plugin_chord_defaults: HashMap::new(),
1235        };
1236
1237        // Load bindings from the active keymap (with inheritance resolution) into default_bindings
1238        let map_bindings = config.resolve_keymap(&config.active_keybinding_map);
1239        resolver.load_default_bindings_from_vec(&map_bindings);
1240
1241        // Then, load custom keybindings (these override the default map bindings)
1242        resolver.load_bindings_from_vec(&config.keybindings);
1243
1244        resolver
1245    }
1246
1247    /// Load default bindings from a vector of keybinding definitions (into default_bindings/default_chord_bindings)
1248    fn load_default_bindings_from_vec(&mut self, bindings: &[crate::config::Keybinding]) {
1249        for binding in bindings {
1250            // Determine context from "when" clause
1251            let context = if let Some(ref when) = binding.when {
1252                KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
1253            } else {
1254                KeyContext::Normal
1255            };
1256
1257            if let Some(action) = Action::from_str(&binding.action, &binding.args) {
1258                // Check if this is a chord binding (has keys field)
1259                if !binding.keys.is_empty() {
1260                    // Parse the chord sequence
1261                    let mut sequence = Vec::new();
1262                    for key_press in &binding.keys {
1263                        if let Some(key_code) = Self::parse_key(&key_press.key) {
1264                            let modifiers = Self::parse_modifiers(&key_press.modifiers);
1265                            sequence.push((key_code, modifiers));
1266                        } else {
1267                            // Invalid key in sequence, skip this binding
1268                            break;
1269                        }
1270                    }
1271
1272                    // Only add if all keys in sequence were valid
1273                    if sequence.len() == binding.keys.len() && !sequence.is_empty() {
1274                        self.default_chord_bindings
1275                            .entry(context)
1276                            .or_default()
1277                            .insert(sequence, action);
1278                    }
1279                } else if let Some(key_code) = Self::parse_key(&binding.key) {
1280                    // Single key binding (legacy format)
1281                    let modifiers = Self::parse_modifiers(&binding.modifiers);
1282
1283                    // Insert the primary binding
1284                    self.insert_binding_with_equivalents(
1285                        context,
1286                        key_code,
1287                        modifiers,
1288                        action,
1289                        &binding.key,
1290                    );
1291                }
1292            }
1293        }
1294    }
1295
1296    /// Insert a binding and automatically add terminal key equivalents.
1297    /// Logs a warning if an equivalent key is already bound to a different action.
1298    fn insert_binding_with_equivalents(
1299        &mut self,
1300        context: KeyContext,
1301        key_code: KeyCode,
1302        modifiers: KeyModifiers,
1303        action: Action,
1304        key_name: &str,
1305    ) {
1306        let context_bindings = self.default_bindings.entry(context.clone()).or_default();
1307
1308        // Insert the primary binding
1309        context_bindings.insert((key_code, modifiers), action.clone());
1310
1311        // Get terminal key equivalents and add them as aliases
1312        let equivalents = terminal_key_equivalents(key_code, modifiers);
1313        for (equiv_key, equiv_mods) in equivalents {
1314            // Check if this equivalent is already bound
1315            if let Some(existing_action) = context_bindings.get(&(equiv_key, equiv_mods)) {
1316                // Only warn if bound to a DIFFERENT action
1317                if existing_action != &action {
1318                    let equiv_name = format!("{:?}", equiv_key);
1319                    tracing::warn!(
1320                        "Terminal key equivalent conflict in {:?} context: {} (equivalent of {}) \
1321                         is bound to {:?}, but {} is bound to {:?}. \
1322                         The explicit binding takes precedence.",
1323                        context,
1324                        equiv_name,
1325                        key_name,
1326                        existing_action,
1327                        key_name,
1328                        action
1329                    );
1330                }
1331                // Don't override explicit bindings with auto-generated equivalents
1332            } else {
1333                // Add the equivalent binding
1334                context_bindings.insert((equiv_key, equiv_mods), action.clone());
1335            }
1336        }
1337    }
1338
1339    /// Load custom bindings from a vector of keybinding definitions (into bindings/chord_bindings)
1340    fn load_bindings_from_vec(&mut self, bindings: &[crate::config::Keybinding]) {
1341        for binding in bindings {
1342            // Determine context from "when" clause
1343            let context = if let Some(ref when) = binding.when {
1344                KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
1345            } else {
1346                KeyContext::Normal
1347            };
1348
1349            if let Some(action) = Action::from_str(&binding.action, &binding.args) {
1350                // Check if this is a chord binding (has keys field)
1351                if !binding.keys.is_empty() {
1352                    // Parse the chord sequence
1353                    let mut sequence = Vec::new();
1354                    for key_press in &binding.keys {
1355                        if let Some(key_code) = Self::parse_key(&key_press.key) {
1356                            let modifiers = Self::parse_modifiers(&key_press.modifiers);
1357                            sequence.push((key_code, modifiers));
1358                        } else {
1359                            // Invalid key in sequence, skip this binding
1360                            break;
1361                        }
1362                    }
1363
1364                    // Only add if all keys in sequence were valid
1365                    if sequence.len() == binding.keys.len() && !sequence.is_empty() {
1366                        self.chord_bindings
1367                            .entry(context)
1368                            .or_default()
1369                            .insert(sequence, action);
1370                    }
1371                } else if let Some(key_code) = Self::parse_key(&binding.key) {
1372                    // Single key binding (legacy format)
1373                    let modifiers = Self::parse_modifiers(&binding.modifiers);
1374                    self.bindings
1375                        .entry(context)
1376                        .or_default()
1377                        .insert((key_code, modifiers), action);
1378                }
1379            }
1380        }
1381    }
1382
1383    /// Load a plugin default binding (for mode bindings registered via defineMode)
1384    pub fn load_plugin_default(
1385        &mut self,
1386        context: KeyContext,
1387        key_code: KeyCode,
1388        modifiers: KeyModifiers,
1389        action: Action,
1390    ) {
1391        self.plugin_defaults
1392            .entry(context)
1393            .or_default()
1394            .insert((key_code, modifiers), action);
1395    }
1396
1397    /// Load a plugin default chord binding (for mode chord bindings from defineMode)
1398    pub fn load_plugin_chord_default(
1399        &mut self,
1400        context: KeyContext,
1401        sequence: Vec<(KeyCode, KeyModifiers)>,
1402        action: Action,
1403    ) {
1404        self.plugin_chord_defaults
1405            .entry(context)
1406            .or_default()
1407            .insert(sequence, action);
1408    }
1409
1410    /// Clear all plugin default bindings (single-key and chord) for a specific mode
1411    pub fn clear_plugin_defaults_for_mode(&mut self, mode_name: &str) {
1412        let context = KeyContext::Mode(mode_name.to_string());
1413        self.plugin_defaults.remove(&context);
1414        self.plugin_chord_defaults.remove(&context);
1415    }
1416
1417    /// Get all plugin default bindings (for keybinding editor display)
1418    pub fn get_plugin_defaults(
1419        &self,
1420    ) -> &HashMap<KeyContext, HashMap<(KeyCode, KeyModifiers), Action>> {
1421        &self.plugin_defaults
1422    }
1423
1424    /// Check if an action is application-wide (should be accessible in all contexts)
1425    fn is_application_wide_action(action: &Action) -> bool {
1426        matches!(
1427            action,
1428            Action::Quit
1429                | Action::ForceQuit
1430                | Action::Save
1431                | Action::SaveAs
1432                | Action::ShowHelp
1433                | Action::ShowKeyboardShortcuts
1434                | Action::PromptCancel  // Esc should always cancel
1435                | Action::PopupCancel // Esc should always cancel
1436        )
1437    }
1438
1439    /// Check if an action is a UI action that should work in terminal mode
1440    /// (without keyboard capture). These are general navigation and UI actions
1441    /// that don't involve text editing.
1442    pub fn is_terminal_ui_action(action: &Action) -> bool {
1443        matches!(
1444            action,
1445            // Global UI actions
1446            Action::CommandPalette
1447                | Action::QuickOpen
1448                | Action::OpenSettings
1449                | Action::MenuActivate
1450                | Action::MenuOpen(_)
1451                | Action::ShowHelp
1452                | Action::ShowKeyboardShortcuts
1453                | Action::Quit
1454                | Action::ForceQuit
1455                // Split navigation
1456                | Action::NextSplit
1457                | Action::PrevSplit
1458                | Action::SplitHorizontal
1459                | Action::SplitVertical
1460                | Action::CloseSplit
1461                | Action::ToggleMaximizeSplit
1462                // Tab/buffer navigation
1463                | Action::NextBuffer
1464                | Action::PrevBuffer
1465                | Action::Close
1466                | Action::ScrollTabsLeft
1467                | Action::ScrollTabsRight
1468                // Terminal control
1469                | Action::TerminalEscape
1470                | Action::ToggleKeyboardCapture
1471                | Action::OpenTerminal
1472                | Action::CloseTerminal
1473                | Action::TerminalPaste
1474                // File explorer
1475                | Action::ToggleFileExplorer
1476                // Menu bar
1477                | Action::ToggleMenuBar
1478        )
1479    }
1480
1481    /// Resolve a key event with chord state to check for multi-key sequences
1482    /// Returns:
1483    /// - Complete(action): The sequence is complete, execute the action
1484    /// - Partial: The sequence is partial (prefix of a chord), wait for more keys
1485    /// - NoMatch: The sequence doesn't match any chord binding
1486    pub fn resolve_chord(
1487        &self,
1488        chord_state: &[(KeyCode, KeyModifiers)],
1489        event: &KeyEvent,
1490        context: KeyContext,
1491    ) -> ChordResolution {
1492        // Build the full sequence: existing chord state + new key, all normalized
1493        let mut full_sequence: Vec<(KeyCode, KeyModifiers)> = chord_state
1494            .iter()
1495            .map(|(c, m)| normalize_key(*c, *m))
1496            .collect();
1497        let (norm_code, norm_mods) = normalize_key(event.code, event.modifiers);
1498        full_sequence.push((norm_code, norm_mods));
1499
1500        tracing::trace!(
1501            "KeybindingResolver.resolve_chord: sequence={:?}, context={:?}",
1502            full_sequence,
1503            context
1504        );
1505
1506        // Check all chord binding sources in priority order
1507        let search_order = vec![
1508            (&self.chord_bindings, &KeyContext::Global, "custom global"),
1509            (
1510                &self.default_chord_bindings,
1511                &KeyContext::Global,
1512                "default global",
1513            ),
1514            (&self.chord_bindings, &context, "custom context"),
1515            (&self.default_chord_bindings, &context, "default context"),
1516            (
1517                &self.plugin_chord_defaults,
1518                &context,
1519                "plugin default context",
1520            ),
1521        ];
1522
1523        let mut has_partial_match = false;
1524
1525        for (binding_map, bind_context, label) in search_order {
1526            if let Some(context_chords) = binding_map.get(bind_context) {
1527                // Check for exact match
1528                if let Some(action) = context_chords.get(&full_sequence) {
1529                    tracing::trace!("  -> Complete chord match in {}: {:?}", label, action);
1530                    return ChordResolution::Complete(action.clone());
1531                }
1532
1533                // Check for partial match (our sequence is a prefix of any binding)
1534                for (chord_seq, _) in context_chords.iter() {
1535                    if chord_seq.len() > full_sequence.len()
1536                        && chord_seq[..full_sequence.len()] == full_sequence[..]
1537                    {
1538                        tracing::trace!("  -> Partial chord match in {}", label);
1539                        has_partial_match = true;
1540                        break;
1541                    }
1542                }
1543            }
1544        }
1545
1546        if has_partial_match {
1547            ChordResolution::Partial
1548        } else {
1549            tracing::trace!("  -> No chord match");
1550            ChordResolution::NoMatch
1551        }
1552    }
1553
1554    /// Resolve a key event to an action in the given context
1555    pub fn resolve(&self, event: &KeyEvent, context: KeyContext) -> Action {
1556        // Normalize key for lookups (e.g., BackTab+SHIFT → BackTab, Char('T')+SHIFT → Char('t')+SHIFT)
1557        // but keep original event for the InsertChar fallback at the end.
1558        let (norm_code, norm_mods) = normalize_key(event.code, event.modifiers);
1559        let norm = &(norm_code, norm_mods);
1560        tracing::trace!(
1561            "KeybindingResolver.resolve: code={:?}, modifiers={:?}, context={:?}",
1562            event.code,
1563            event.modifiers,
1564            context
1565        );
1566
1567        // Check Global bindings first (highest priority - work in all contexts)
1568        if let Some(global_bindings) = self.bindings.get(&KeyContext::Global) {
1569            if let Some(action) = global_bindings.get(norm) {
1570                tracing::trace!("  -> Found in custom global bindings: {:?}", action);
1571                return action.clone();
1572            }
1573        }
1574
1575        if let Some(global_bindings) = self.default_bindings.get(&KeyContext::Global) {
1576            if let Some(action) = global_bindings.get(norm) {
1577                tracing::trace!("  -> Found in default global bindings: {:?}", action);
1578                return action.clone();
1579            }
1580        }
1581
1582        // Try context-specific custom bindings
1583        if let Some(context_bindings) = self.bindings.get(&context) {
1584            if let Some(action) = context_bindings.get(norm) {
1585                tracing::trace!(
1586                    "  -> Found in custom {} bindings: {:?}",
1587                    context.to_when_clause(),
1588                    action
1589                );
1590                return action.clone();
1591            }
1592        }
1593
1594        // Try context-specific default bindings
1595        if let Some(context_bindings) = self.default_bindings.get(&context) {
1596            if let Some(action) = context_bindings.get(norm) {
1597                tracing::trace!(
1598                    "  -> Found in default {} bindings: {:?}",
1599                    context.to_when_clause(),
1600                    action
1601                );
1602                return action.clone();
1603            }
1604        }
1605
1606        // Try plugin default bindings (mode bindings from defineMode)
1607        if let Some(plugin_bindings) = self.plugin_defaults.get(&context) {
1608            if let Some(action) = plugin_bindings.get(norm) {
1609                tracing::trace!(
1610                    "  -> Found in plugin default {} bindings: {:?}",
1611                    context.to_when_clause(),
1612                    action
1613                );
1614                return action.clone();
1615            }
1616        }
1617
1618        // Fall back to normal context ONLY for application-wide actions
1619        // This prevents keys from leaking through to the editor when in special contexts
1620        if context != KeyContext::Normal {
1621            if let Some(normal_bindings) = self.bindings.get(&KeyContext::Normal) {
1622                if let Some(action) = normal_bindings.get(norm) {
1623                    if Self::is_application_wide_action(action) {
1624                        tracing::trace!(
1625                            "  -> Found application-wide action in custom normal bindings: {:?}",
1626                            action
1627                        );
1628                        return action.clone();
1629                    }
1630                }
1631            }
1632
1633            if let Some(normal_bindings) = self.default_bindings.get(&KeyContext::Normal) {
1634                if let Some(action) = normal_bindings.get(norm) {
1635                    if Self::is_application_wide_action(action) {
1636                        tracing::trace!(
1637                            "  -> Found application-wide action in default normal bindings: {:?}",
1638                            action
1639                        );
1640                        return action.clone();
1641                    }
1642                }
1643            }
1644        }
1645
1646        // Handle regular character input in text input contexts
1647        if context.allows_text_input() && is_text_input_modifier(event.modifiers) {
1648            if let KeyCode::Char(c) = event.code {
1649                tracing::trace!("  -> Character input: '{}'", c);
1650                return Action::InsertChar(c);
1651            }
1652        }
1653
1654        tracing::trace!("  -> No binding found, returning Action::None");
1655        Action::None
1656    }
1657
1658    /// Resolve a key event looking only in the specified context (no Global fallback).
1659    /// This is used when a modal context (like Prompt) needs to check if it has
1660    /// a specific binding without being overridden by Global bindings.
1661    /// Returns None if no binding found in the specified context.
1662    pub fn resolve_in_context_only(&self, event: &KeyEvent, context: KeyContext) -> Option<Action> {
1663        let norm = normalize_key(event.code, event.modifiers);
1664        // Try custom bindings for this context
1665        if let Some(context_bindings) = self.bindings.get(&context) {
1666            if let Some(action) = context_bindings.get(&norm) {
1667                return Some(action.clone());
1668            }
1669        }
1670
1671        // Try default bindings for this context
1672        if let Some(context_bindings) = self.default_bindings.get(&context) {
1673            if let Some(action) = context_bindings.get(&norm) {
1674                return Some(action.clone());
1675            }
1676        }
1677
1678        None
1679    }
1680
1681    /// Resolve a key event to a UI action for terminal mode.
1682    /// Only returns actions that are classified as UI actions (is_terminal_ui_action).
1683    /// Returns Action::None if the key doesn't map to a UI action.
1684    pub fn resolve_terminal_ui_action(&self, event: &KeyEvent) -> Action {
1685        let norm = normalize_key(event.code, event.modifiers);
1686        tracing::trace!(
1687            "KeybindingResolver.resolve_terminal_ui_action: code={:?}, modifiers={:?}",
1688            event.code,
1689            event.modifiers
1690        );
1691
1692        // Check Terminal context bindings first (highest priority for terminal mode)
1693        for bindings in [&self.bindings, &self.default_bindings] {
1694            if let Some(terminal_bindings) = bindings.get(&KeyContext::Terminal) {
1695                if let Some(action) = terminal_bindings.get(&norm) {
1696                    if Self::is_terminal_ui_action(action) {
1697                        tracing::trace!("  -> Found UI action in terminal bindings: {:?}", action);
1698                        return action.clone();
1699                    }
1700                }
1701            }
1702        }
1703
1704        // Check Global bindings (work in all contexts)
1705        for bindings in [&self.bindings, &self.default_bindings] {
1706            if let Some(global_bindings) = bindings.get(&KeyContext::Global) {
1707                if let Some(action) = global_bindings.get(&norm) {
1708                    if Self::is_terminal_ui_action(action) {
1709                        tracing::trace!("  -> Found UI action in global bindings: {:?}", action);
1710                        return action.clone();
1711                    }
1712                }
1713            }
1714        }
1715
1716        // Check Normal context bindings (for actions like next_split that are in Normal context)
1717        for bindings in [&self.bindings, &self.default_bindings] {
1718            if let Some(normal_bindings) = bindings.get(&KeyContext::Normal) {
1719                if let Some(action) = normal_bindings.get(&norm) {
1720                    if Self::is_terminal_ui_action(action) {
1721                        tracing::trace!("  -> Found UI action in normal bindings: {:?}", action);
1722                        return action.clone();
1723                    }
1724                }
1725            }
1726        }
1727
1728        tracing::trace!("  -> No UI action found");
1729        Action::None
1730    }
1731
1732    /// Find the primary keybinding for a given action (for display in menus)
1733    /// Returns a formatted string like "Ctrl+S" or "F12"
1734    pub fn find_keybinding_for_action(
1735        &self,
1736        action_name: &str,
1737        context: KeyContext,
1738    ) -> Option<String> {
1739        // Parse the action from the action name
1740        let target_action = Action::from_str(action_name, &HashMap::new())?;
1741
1742        // Search in custom bindings first, then default bindings
1743        let search_maps = vec![
1744            self.bindings.get(&context),
1745            self.bindings.get(&KeyContext::Global),
1746            self.default_bindings.get(&context),
1747            self.default_bindings.get(&KeyContext::Global),
1748        ];
1749
1750        for map in search_maps.into_iter().flatten() {
1751            // Collect all matching keybindings for deterministic selection
1752            let mut matches: Vec<(KeyCode, KeyModifiers)> = map
1753                .iter()
1754                .filter(|(_, action)| {
1755                    std::mem::discriminant(*action) == std::mem::discriminant(&target_action)
1756                })
1757                .map(|((key_code, modifiers), _)| (*key_code, *modifiers))
1758                .collect();
1759
1760            if !matches.is_empty() {
1761                // Sort to get deterministic order: prefer fewer modifiers, then by key
1762                matches.sort_by(|(key_a, mod_a), (key_b, mod_b)| {
1763                    // Compare by number of modifiers first (prefer simpler bindings)
1764                    let mod_count_a = mod_a.bits().count_ones();
1765                    let mod_count_b = mod_b.bits().count_ones();
1766                    match mod_count_a.cmp(&mod_count_b) {
1767                        std::cmp::Ordering::Equal => {
1768                            // Then by modifier bits (for consistent ordering)
1769                            match mod_a.bits().cmp(&mod_b.bits()) {
1770                                std::cmp::Ordering::Equal => {
1771                                    // Finally by key code
1772                                    Self::key_code_sort_key(key_a)
1773                                        .cmp(&Self::key_code_sort_key(key_b))
1774                                }
1775                                other => other,
1776                            }
1777                        }
1778                        other => other,
1779                    }
1780                });
1781
1782                let (key_code, modifiers) = matches[0];
1783                return Some(format_keybinding(&key_code, &modifiers));
1784            }
1785        }
1786
1787        None
1788    }
1789
1790    /// Generate a sort key for KeyCode to ensure deterministic ordering
1791    fn key_code_sort_key(key_code: &KeyCode) -> (u8, u32) {
1792        match key_code {
1793            KeyCode::Char(c) => (0, *c as u32),
1794            KeyCode::F(n) => (1, *n as u32),
1795            KeyCode::Enter => (2, 0),
1796            KeyCode::Tab => (2, 1),
1797            KeyCode::Backspace => (2, 2),
1798            KeyCode::Delete => (2, 3),
1799            KeyCode::Esc => (2, 4),
1800            KeyCode::Left => (3, 0),
1801            KeyCode::Right => (3, 1),
1802            KeyCode::Up => (3, 2),
1803            KeyCode::Down => (3, 3),
1804            KeyCode::Home => (3, 4),
1805            KeyCode::End => (3, 5),
1806            KeyCode::PageUp => (3, 6),
1807            KeyCode::PageDown => (3, 7),
1808            _ => (255, 0),
1809        }
1810    }
1811
1812    /// Find the mnemonic character for a menu (based on Alt+letter keybindings)
1813    /// Returns the character that should be underlined in the menu label
1814    pub fn find_menu_mnemonic(&self, menu_name: &str) -> Option<char> {
1815        // Search in custom bindings first, then default bindings
1816        let search_maps = vec![
1817            self.bindings.get(&KeyContext::Normal),
1818            self.bindings.get(&KeyContext::Global),
1819            self.default_bindings.get(&KeyContext::Normal),
1820            self.default_bindings.get(&KeyContext::Global),
1821        ];
1822
1823        for map in search_maps.into_iter().flatten() {
1824            for ((key_code, modifiers), action) in map {
1825                // Check if this is an Alt+letter binding for MenuOpen with matching name
1826                if let Action::MenuOpen(name) = action {
1827                    if name.eq_ignore_ascii_case(menu_name) && *modifiers == KeyModifiers::ALT {
1828                        // Return the character for Alt+letter bindings
1829                        if let KeyCode::Char(c) = key_code {
1830                            return Some(c.to_ascii_lowercase());
1831                        }
1832                    }
1833                }
1834            }
1835        }
1836
1837        None
1838    }
1839
1840    /// Parse a key string to KeyCode
1841    fn parse_key(key: &str) -> Option<KeyCode> {
1842        let lower = key.to_lowercase();
1843        match lower.as_str() {
1844            "enter" => Some(KeyCode::Enter),
1845            "backspace" => Some(KeyCode::Backspace),
1846            "delete" | "del" => Some(KeyCode::Delete),
1847            "tab" => Some(KeyCode::Tab),
1848            "backtab" => Some(KeyCode::BackTab),
1849            "esc" | "escape" => Some(KeyCode::Esc),
1850            "space" => Some(KeyCode::Char(' ')),
1851
1852            "left" => Some(KeyCode::Left),
1853            "right" => Some(KeyCode::Right),
1854            "up" => Some(KeyCode::Up),
1855            "down" => Some(KeyCode::Down),
1856            "home" => Some(KeyCode::Home),
1857            "end" => Some(KeyCode::End),
1858            "pageup" => Some(KeyCode::PageUp),
1859            "pagedown" => Some(KeyCode::PageDown),
1860
1861            s if s.len() == 1 => s.chars().next().map(KeyCode::Char),
1862            // Handle function keys like "f1", "f2", ..., "f12"
1863            s if s.starts_with('f') && s.len() >= 2 => s[1..].parse::<u8>().ok().map(KeyCode::F),
1864            _ => None,
1865        }
1866    }
1867
1868    /// Parse modifiers from strings
1869    fn parse_modifiers(modifiers: &[String]) -> KeyModifiers {
1870        let mut result = KeyModifiers::empty();
1871        for m in modifiers {
1872            match m.to_lowercase().as_str() {
1873                "ctrl" | "control" => result |= KeyModifiers::CONTROL,
1874                "shift" => result |= KeyModifiers::SHIFT,
1875                "alt" => result |= KeyModifiers::ALT,
1876                "super" | "cmd" | "command" | "meta" => result |= KeyModifiers::SUPER,
1877                _ => {}
1878            }
1879        }
1880        result
1881    }
1882
1883    /// Create default keybindings organized by context
1884    /// Get all keybindings (for help display)
1885    /// Returns a Vec of (key_description, action_description)
1886    pub fn get_all_bindings(&self) -> Vec<(String, String)> {
1887        let mut bindings = Vec::new();
1888
1889        // Collect all bindings from all contexts
1890        for context in &[
1891            KeyContext::Normal,
1892            KeyContext::Prompt,
1893            KeyContext::Popup,
1894            KeyContext::FileExplorer,
1895            KeyContext::Menu,
1896        ] {
1897            let mut all_keys: HashMap<(KeyCode, KeyModifiers), Action> = HashMap::new();
1898
1899            // Start with defaults for this context
1900            if let Some(context_defaults) = self.default_bindings.get(context) {
1901                for (key, action) in context_defaults {
1902                    all_keys.insert(*key, action.clone());
1903                }
1904            }
1905
1906            // Override with custom bindings for this context
1907            if let Some(context_bindings) = self.bindings.get(context) {
1908                for (key, action) in context_bindings {
1909                    all_keys.insert(*key, action.clone());
1910                }
1911            }
1912
1913            // Convert to readable format with context prefix
1914            let context_str = if *context != KeyContext::Normal {
1915                format!("[{}] ", context.to_when_clause())
1916            } else {
1917                String::new()
1918            };
1919
1920            for ((key_code, modifiers), action) in all_keys {
1921                let key_str = Self::format_key(key_code, modifiers);
1922                let action_str = format!("{}{}", context_str, Self::format_action(&action));
1923                bindings.push((key_str, action_str));
1924            }
1925        }
1926
1927        // Sort by action description for easier browsing
1928        bindings.sort_by(|a, b| a.1.cmp(&b.1));
1929
1930        bindings
1931    }
1932
1933    /// Format a key combination as a readable string
1934    fn format_key(key_code: KeyCode, modifiers: KeyModifiers) -> String {
1935        format_keybinding(&key_code, &modifiers)
1936    }
1937
1938    /// Format an action as a readable description
1939    fn format_action(action: &Action) -> String {
1940        match action {
1941            Action::InsertChar(c) => t!("action.insert_char", char = c),
1942            Action::InsertNewline => t!("action.insert_newline"),
1943            Action::InsertTab => t!("action.insert_tab"),
1944            Action::MoveLeft => t!("action.move_left"),
1945            Action::MoveRight => t!("action.move_right"),
1946            Action::MoveUp => t!("action.move_up"),
1947            Action::MoveDown => t!("action.move_down"),
1948            Action::MoveWordLeft => t!("action.move_word_left"),
1949            Action::MoveWordRight => t!("action.move_word_right"),
1950            Action::MoveWordEnd => t!("action.move_word_end"),
1951            Action::ViMoveWordEnd => t!("action.move_word_end"),
1952            Action::MoveLeftInLine => t!("action.move_left"),
1953            Action::MoveRightInLine => t!("action.move_right"),
1954            Action::MoveLineStart => t!("action.move_line_start"),
1955            Action::MoveLineEnd => t!("action.move_line_end"),
1956            Action::MoveLineUp => t!("action.move_line_up"),
1957            Action::MoveLineDown => t!("action.move_line_down"),
1958            Action::MovePageUp => t!("action.move_page_up"),
1959            Action::MovePageDown => t!("action.move_page_down"),
1960            Action::MoveDocumentStart => t!("action.move_document_start"),
1961            Action::MoveDocumentEnd => t!("action.move_document_end"),
1962            Action::SelectLeft => t!("action.select_left"),
1963            Action::SelectRight => t!("action.select_right"),
1964            Action::SelectUp => t!("action.select_up"),
1965            Action::SelectDown => t!("action.select_down"),
1966            Action::SelectToParagraphUp => t!("action.select_to_paragraph_up"),
1967            Action::SelectToParagraphDown => t!("action.select_to_paragraph_down"),
1968            Action::SelectWordLeft => t!("action.select_word_left"),
1969            Action::SelectWordRight => t!("action.select_word_right"),
1970            Action::SelectWordEnd => t!("action.select_word_end"),
1971            Action::ViSelectWordEnd => t!("action.select_word_end"),
1972            Action::SelectLineStart => t!("action.select_line_start"),
1973            Action::SelectLineEnd => t!("action.select_line_end"),
1974            Action::SelectDocumentStart => t!("action.select_document_start"),
1975            Action::SelectDocumentEnd => t!("action.select_document_end"),
1976            Action::SelectPageUp => t!("action.select_page_up"),
1977            Action::SelectPageDown => t!("action.select_page_down"),
1978            Action::SelectAll => t!("action.select_all"),
1979            Action::SelectWord => t!("action.select_word"),
1980            Action::SelectLine => t!("action.select_line"),
1981            Action::ExpandSelection => t!("action.expand_selection"),
1982            Action::BlockSelectLeft => t!("action.block_select_left"),
1983            Action::BlockSelectRight => t!("action.block_select_right"),
1984            Action::BlockSelectUp => t!("action.block_select_up"),
1985            Action::BlockSelectDown => t!("action.block_select_down"),
1986            Action::DeleteBackward => t!("action.delete_backward"),
1987            Action::DeleteForward => t!("action.delete_forward"),
1988            Action::DeleteWordBackward => t!("action.delete_word_backward"),
1989            Action::DeleteWordForward => t!("action.delete_word_forward"),
1990            Action::DeleteLine => t!("action.delete_line"),
1991            Action::DeleteToLineEnd => t!("action.delete_to_line_end"),
1992            Action::DeleteToLineStart => t!("action.delete_to_line_start"),
1993            Action::DeleteViWordEnd => t!("action.delete_word_forward"),
1994            Action::TransposeChars => t!("action.transpose_chars"),
1995            Action::OpenLine => t!("action.open_line"),
1996            Action::DuplicateLine => t!("action.duplicate_line"),
1997            Action::Recenter => t!("action.recenter"),
1998            Action::SetMark => t!("action.set_mark"),
1999            Action::Copy => t!("action.copy"),
2000            Action::CopyWithTheme(theme) if theme.is_empty() => t!("action.copy_with_formatting"),
2001            Action::CopyWithTheme(theme) => t!("action.copy_with_theme", theme = theme),
2002            Action::Cut => t!("action.cut"),
2003            Action::Paste => t!("action.paste"),
2004            Action::YankWordForward => t!("action.yank_word_forward"),
2005            Action::YankWordBackward => t!("action.yank_word_backward"),
2006            Action::YankToLineEnd => t!("action.yank_to_line_end"),
2007            Action::YankToLineStart => t!("action.yank_to_line_start"),
2008            Action::YankViWordEnd => t!("action.yank_word_forward"),
2009            Action::AddCursorAbove => t!("action.add_cursor_above"),
2010            Action::AddCursorBelow => t!("action.add_cursor_below"),
2011            Action::AddCursorNextMatch => t!("action.add_cursor_next_match"),
2012            Action::RemoveSecondaryCursors => t!("action.remove_secondary_cursors"),
2013            Action::Save => t!("action.save"),
2014            Action::SaveAs => t!("action.save_as"),
2015            Action::Open => t!("action.open"),
2016            Action::SwitchProject => t!("action.switch_project"),
2017            Action::New => t!("action.new"),
2018            Action::Close => t!("action.close"),
2019            Action::CloseTab => t!("action.close_tab"),
2020            Action::Quit => t!("action.quit"),
2021            Action::ForceQuit => t!("action.force_quit"),
2022            Action::Detach => t!("action.detach"),
2023            Action::Revert => t!("action.revert"),
2024            Action::ToggleAutoRevert => t!("action.toggle_auto_revert"),
2025            Action::FormatBuffer => t!("action.format_buffer"),
2026            Action::TrimTrailingWhitespace => t!("action.trim_trailing_whitespace"),
2027            Action::EnsureFinalNewline => t!("action.ensure_final_newline"),
2028            Action::GotoLine => t!("action.goto_line"),
2029            Action::ScanLineIndex => t!("action.scan_line_index"),
2030            Action::GoToMatchingBracket => t!("action.goto_matching_bracket"),
2031            Action::JumpToNextError => t!("action.jump_to_next_error"),
2032            Action::JumpToPreviousError => t!("action.jump_to_previous_error"),
2033            Action::SmartHome => t!("action.smart_home"),
2034            Action::DedentSelection => t!("action.dedent_selection"),
2035            Action::ToggleComment => t!("action.toggle_comment"),
2036            Action::DabbrevExpand => std::borrow::Cow::Borrowed("Expand abbreviation (dabbrev)"),
2037            Action::ToggleFold => t!("action.toggle_fold"),
2038            Action::SetBookmark(c) => t!("action.set_bookmark", key = c),
2039            Action::JumpToBookmark(c) => t!("action.jump_to_bookmark", key = c),
2040            Action::ClearBookmark(c) => t!("action.clear_bookmark", key = c),
2041            Action::ListBookmarks => t!("action.list_bookmarks"),
2042            Action::ToggleSearchCaseSensitive => t!("action.toggle_search_case_sensitive"),
2043            Action::ToggleSearchWholeWord => t!("action.toggle_search_whole_word"),
2044            Action::ToggleSearchRegex => t!("action.toggle_search_regex"),
2045            Action::ToggleSearchConfirmEach => t!("action.toggle_search_confirm_each"),
2046            Action::StartMacroRecording => t!("action.start_macro_recording"),
2047            Action::StopMacroRecording => t!("action.stop_macro_recording"),
2048            Action::PlayMacro(c) => t!("action.play_macro", key = c),
2049            Action::ToggleMacroRecording(c) => t!("action.toggle_macro_recording", key = c),
2050            Action::ShowMacro(c) => t!("action.show_macro", key = c),
2051            Action::ListMacros => t!("action.list_macros"),
2052            Action::PromptRecordMacro => t!("action.prompt_record_macro"),
2053            Action::PromptPlayMacro => t!("action.prompt_play_macro"),
2054            Action::PlayLastMacro => t!("action.play_last_macro"),
2055            Action::PromptSetBookmark => t!("action.prompt_set_bookmark"),
2056            Action::PromptJumpToBookmark => t!("action.prompt_jump_to_bookmark"),
2057            Action::Undo => t!("action.undo"),
2058            Action::Redo => t!("action.redo"),
2059            Action::ScrollUp => t!("action.scroll_up"),
2060            Action::ScrollDown => t!("action.scroll_down"),
2061            Action::ShowHelp => t!("action.show_help"),
2062            Action::ShowKeyboardShortcuts => t!("action.show_keyboard_shortcuts"),
2063            Action::ShowWarnings => t!("action.show_warnings"),
2064            Action::ShowStatusLog => t!("action.show_status_log"),
2065            Action::ShowLspStatus => t!("action.show_lsp_status"),
2066            Action::ClearWarnings => t!("action.clear_warnings"),
2067            Action::CommandPalette => t!("action.command_palette"),
2068            Action::QuickOpen => t!("action.quick_open"),
2069            Action::InspectThemeAtCursor => t!("action.inspect_theme_at_cursor"),
2070            Action::ToggleLineWrap => t!("action.toggle_line_wrap"),
2071            Action::ToggleCurrentLineHighlight => t!("action.toggle_current_line_highlight"),
2072            Action::ToggleReadOnly => t!("action.toggle_read_only"),
2073            Action::TogglePageView => t!("action.toggle_page_view"),
2074            Action::SetPageWidth => t!("action.set_page_width"),
2075            Action::NextBuffer => t!("action.next_buffer"),
2076            Action::PrevBuffer => t!("action.prev_buffer"),
2077            Action::NavigateBack => t!("action.navigate_back"),
2078            Action::NavigateForward => t!("action.navigate_forward"),
2079            Action::SplitHorizontal => t!("action.split_horizontal"),
2080            Action::SplitVertical => t!("action.split_vertical"),
2081            Action::CloseSplit => t!("action.close_split"),
2082            Action::NextSplit => t!("action.next_split"),
2083            Action::PrevSplit => t!("action.prev_split"),
2084            Action::IncreaseSplitSize => t!("action.increase_split_size"),
2085            Action::DecreaseSplitSize => t!("action.decrease_split_size"),
2086            Action::ToggleMaximizeSplit => t!("action.toggle_maximize_split"),
2087            Action::PromptConfirm => t!("action.prompt_confirm"),
2088            Action::PromptConfirmWithText(ref text) => {
2089                format!("{} ({})", t!("action.prompt_confirm"), text).into()
2090            }
2091            Action::PromptCancel => t!("action.prompt_cancel"),
2092            Action::PromptBackspace => t!("action.prompt_backspace"),
2093            Action::PromptDelete => t!("action.prompt_delete"),
2094            Action::PromptMoveLeft => t!("action.prompt_move_left"),
2095            Action::PromptMoveRight => t!("action.prompt_move_right"),
2096            Action::PromptMoveStart => t!("action.prompt_move_start"),
2097            Action::PromptMoveEnd => t!("action.prompt_move_end"),
2098            Action::PromptSelectPrev => t!("action.prompt_select_prev"),
2099            Action::PromptSelectNext => t!("action.prompt_select_next"),
2100            Action::PromptPageUp => t!("action.prompt_page_up"),
2101            Action::PromptPageDown => t!("action.prompt_page_down"),
2102            Action::PromptAcceptSuggestion => t!("action.prompt_accept_suggestion"),
2103            Action::PromptMoveWordLeft => t!("action.prompt_move_word_left"),
2104            Action::PromptMoveWordRight => t!("action.prompt_move_word_right"),
2105            Action::PromptDeleteWordForward => t!("action.prompt_delete_word_forward"),
2106            Action::PromptDeleteWordBackward => t!("action.prompt_delete_word_backward"),
2107            Action::PromptDeleteToLineEnd => t!("action.prompt_delete_to_line_end"),
2108            Action::PromptCopy => t!("action.prompt_copy"),
2109            Action::PromptCut => t!("action.prompt_cut"),
2110            Action::PromptPaste => t!("action.prompt_paste"),
2111            Action::PromptMoveLeftSelecting => t!("action.prompt_move_left_selecting"),
2112            Action::PromptMoveRightSelecting => t!("action.prompt_move_right_selecting"),
2113            Action::PromptMoveHomeSelecting => t!("action.prompt_move_home_selecting"),
2114            Action::PromptMoveEndSelecting => t!("action.prompt_move_end_selecting"),
2115            Action::PromptSelectWordLeft => t!("action.prompt_select_word_left"),
2116            Action::PromptSelectWordRight => t!("action.prompt_select_word_right"),
2117            Action::PromptSelectAll => t!("action.prompt_select_all"),
2118            Action::FileBrowserToggleHidden => t!("action.file_browser_toggle_hidden"),
2119            Action::FileBrowserToggleDetectEncoding => {
2120                t!("action.file_browser_toggle_detect_encoding")
2121            }
2122            Action::PopupSelectNext => t!("action.popup_select_next"),
2123            Action::PopupSelectPrev => t!("action.popup_select_prev"),
2124            Action::PopupPageUp => t!("action.popup_page_up"),
2125            Action::PopupPageDown => t!("action.popup_page_down"),
2126            Action::PopupConfirm => t!("action.popup_confirm"),
2127            Action::PopupCancel => t!("action.popup_cancel"),
2128            Action::ToggleFileExplorer => t!("action.toggle_file_explorer"),
2129            Action::ToggleMenuBar => t!("action.toggle_menu_bar"),
2130            Action::ToggleTabBar => t!("action.toggle_tab_bar"),
2131            Action::ToggleStatusBar => t!("action.toggle_status_bar"),
2132            Action::TogglePromptLine => t!("action.toggle_prompt_line"),
2133            Action::ToggleVerticalScrollbar => t!("action.toggle_vertical_scrollbar"),
2134            Action::ToggleHorizontalScrollbar => t!("action.toggle_horizontal_scrollbar"),
2135            Action::FocusFileExplorer => t!("action.focus_file_explorer"),
2136            Action::FocusEditor => t!("action.focus_editor"),
2137            Action::FileExplorerUp => t!("action.file_explorer_up"),
2138            Action::FileExplorerDown => t!("action.file_explorer_down"),
2139            Action::FileExplorerPageUp => t!("action.file_explorer_page_up"),
2140            Action::FileExplorerPageDown => t!("action.file_explorer_page_down"),
2141            Action::FileExplorerExpand => t!("action.file_explorer_expand"),
2142            Action::FileExplorerCollapse => t!("action.file_explorer_collapse"),
2143            Action::FileExplorerOpen => t!("action.file_explorer_open"),
2144            Action::FileExplorerRefresh => t!("action.file_explorer_refresh"),
2145            Action::FileExplorerNewFile => t!("action.file_explorer_new_file"),
2146            Action::FileExplorerNewDirectory => t!("action.file_explorer_new_directory"),
2147            Action::FileExplorerDelete => t!("action.file_explorer_delete"),
2148            Action::FileExplorerRename => t!("action.file_explorer_rename"),
2149            Action::FileExplorerToggleHidden => t!("action.file_explorer_toggle_hidden"),
2150            Action::FileExplorerToggleGitignored => t!("action.file_explorer_toggle_gitignored"),
2151            Action::FileExplorerSearchClear => t!("action.file_explorer_search_clear"),
2152            Action::FileExplorerSearchBackspace => t!("action.file_explorer_search_backspace"),
2153            Action::LspCompletion => t!("action.lsp_completion"),
2154            Action::LspGotoDefinition => t!("action.lsp_goto_definition"),
2155            Action::LspReferences => t!("action.lsp_references"),
2156            Action::LspRename => t!("action.lsp_rename"),
2157            Action::LspHover => t!("action.lsp_hover"),
2158            Action::LspSignatureHelp => t!("action.lsp_signature_help"),
2159            Action::LspCodeActions => t!("action.lsp_code_actions"),
2160            Action::LspRestart => t!("action.lsp_restart"),
2161            Action::LspStop => t!("action.lsp_stop"),
2162            Action::LspToggleForBuffer => t!("action.lsp_toggle_for_buffer"),
2163            Action::ToggleInlayHints => t!("action.toggle_inlay_hints"),
2164            Action::ToggleMouseHover => t!("action.toggle_mouse_hover"),
2165            Action::ToggleLineNumbers => t!("action.toggle_line_numbers"),
2166            Action::ToggleScrollSync => t!("action.toggle_scroll_sync"),
2167            Action::ToggleMouseCapture => t!("action.toggle_mouse_capture"),
2168            Action::ToggleDebugHighlights => t!("action.toggle_debug_highlights"),
2169            Action::SetBackground => t!("action.set_background"),
2170            Action::SetBackgroundBlend => t!("action.set_background_blend"),
2171            Action::AddRuler => t!("action.add_ruler"),
2172            Action::RemoveRuler => t!("action.remove_ruler"),
2173            Action::SetTabSize => t!("action.set_tab_size"),
2174            Action::SetLineEnding => t!("action.set_line_ending"),
2175            Action::SetEncoding => t!("action.set_encoding"),
2176            Action::ReloadWithEncoding => t!("action.reload_with_encoding"),
2177            Action::SetLanguage => t!("action.set_language"),
2178            Action::ToggleIndentationStyle => t!("action.toggle_indentation_style"),
2179            Action::ToggleTabIndicators => t!("action.toggle_tab_indicators"),
2180            Action::ToggleWhitespaceIndicators => t!("action.toggle_whitespace_indicators"),
2181            Action::ResetBufferSettings => t!("action.reset_buffer_settings"),
2182            Action::DumpConfig => t!("action.dump_config"),
2183            Action::Search => t!("action.search"),
2184            Action::FindInSelection => t!("action.find_in_selection"),
2185            Action::FindNext => t!("action.find_next"),
2186            Action::FindPrevious => t!("action.find_previous"),
2187            Action::FindSelectionNext => t!("action.find_selection_next"),
2188            Action::FindSelectionPrevious => t!("action.find_selection_previous"),
2189            Action::Replace => t!("action.replace"),
2190            Action::QueryReplace => t!("action.query_replace"),
2191            Action::MenuActivate => t!("action.menu_activate"),
2192            Action::MenuClose => t!("action.menu_close"),
2193            Action::MenuLeft => t!("action.menu_left"),
2194            Action::MenuRight => t!("action.menu_right"),
2195            Action::MenuUp => t!("action.menu_up"),
2196            Action::MenuDown => t!("action.menu_down"),
2197            Action::MenuExecute => t!("action.menu_execute"),
2198            Action::MenuOpen(name) => t!("action.menu_open", name = name),
2199            Action::SwitchKeybindingMap(map) => t!("action.switch_keybinding_map", map = map),
2200            Action::PluginAction(name) => t!("action.plugin_action", name = name),
2201            Action::ScrollTabsLeft => t!("action.scroll_tabs_left"),
2202            Action::ScrollTabsRight => t!("action.scroll_tabs_right"),
2203            Action::SelectTheme => t!("action.select_theme"),
2204            Action::SelectKeybindingMap => t!("action.select_keybinding_map"),
2205            Action::SelectCursorStyle => t!("action.select_cursor_style"),
2206            Action::SelectLocale => t!("action.select_locale"),
2207            Action::SwitchToPreviousTab => t!("action.switch_to_previous_tab"),
2208            Action::SwitchToTabByName => t!("action.switch_to_tab_by_name"),
2209            Action::OpenTerminal => t!("action.open_terminal"),
2210            Action::CloseTerminal => t!("action.close_terminal"),
2211            Action::FocusTerminal => t!("action.focus_terminal"),
2212            Action::TerminalEscape => t!("action.terminal_escape"),
2213            Action::ToggleKeyboardCapture => t!("action.toggle_keyboard_capture"),
2214            Action::TerminalPaste => t!("action.terminal_paste"),
2215            Action::OpenSettings => t!("action.open_settings"),
2216            Action::CloseSettings => t!("action.close_settings"),
2217            Action::SettingsSave => t!("action.settings_save"),
2218            Action::SettingsReset => t!("action.settings_reset"),
2219            Action::SettingsToggleFocus => t!("action.settings_toggle_focus"),
2220            Action::SettingsActivate => t!("action.settings_activate"),
2221            Action::SettingsSearch => t!("action.settings_search"),
2222            Action::SettingsHelp => t!("action.settings_help"),
2223            Action::SettingsIncrement => t!("action.settings_increment"),
2224            Action::SettingsDecrement => t!("action.settings_decrement"),
2225            Action::ShellCommand => t!("action.shell_command"),
2226            Action::ShellCommandReplace => t!("action.shell_command_replace"),
2227            Action::ToUpperCase => t!("action.to_uppercase"),
2228            Action::ToLowerCase => t!("action.to_lowercase"),
2229            Action::ToggleCase => t!("action.to_uppercase"),
2230            Action::SortLines => t!("action.sort_lines"),
2231            Action::CalibrateInput => t!("action.calibrate_input"),
2232            Action::EventDebug => t!("action.event_debug"),
2233            Action::LoadPluginFromBuffer => "Load Plugin from Buffer".into(),
2234            Action::OpenKeybindingEditor => "Keybinding Editor".into(),
2235            Action::None => t!("action.none"),
2236        }
2237        .to_string()
2238    }
2239
2240    /// Public wrapper for parse_key (for keybinding editor)
2241    pub fn parse_key_public(key: &str) -> Option<KeyCode> {
2242        Self::parse_key(key)
2243    }
2244
2245    /// Public wrapper for parse_modifiers (for keybinding editor)
2246    pub fn parse_modifiers_public(modifiers: &[String]) -> KeyModifiers {
2247        Self::parse_modifiers(modifiers)
2248    }
2249
2250    /// Format an action name string as a human-readable description.
2251    /// Used by the keybinding editor to display action names without needing
2252    /// a full Action enum parse.
2253    pub fn format_action_from_str(action_name: &str) -> String {
2254        // Try to parse as Action enum first
2255        if let Some(action) = Action::from_str(action_name, &std::collections::HashMap::new()) {
2256            Self::format_action(&action)
2257        } else {
2258            // Fallback: convert snake_case to Title Case
2259            action_name
2260                .split('_')
2261                .map(|word| {
2262                    let mut chars = word.chars();
2263                    match chars.next() {
2264                        Some(c) => {
2265                            let upper: String = c.to_uppercase().collect();
2266                            format!("{}{}", upper, chars.as_str())
2267                        }
2268                        None => String::new(),
2269                    }
2270                })
2271                .collect::<Vec<_>>()
2272                .join(" ")
2273        }
2274    }
2275
2276    /// Return a sorted list of all valid action name strings.
2277    /// Delegates to `Action::all_action_names()` which is generated by the
2278    /// `define_action_str_mapping!` macro (same source of truth as `Action::from_str`).
2279    pub fn all_action_names() -> Vec<String> {
2280        Action::all_action_names()
2281    }
2282
2283    /// Get the keybinding string for an action in a specific context
2284    /// Returns the first keybinding found (prioritizing custom bindings over defaults)
2285    /// When multiple keybindings exist for the same action, prefers canonical keys over
2286    /// terminal equivalents (e.g., "Space" over "@")
2287    /// Returns None if no binding is found
2288    pub fn get_keybinding_for_action(
2289        &self,
2290        action: &Action,
2291        context: KeyContext,
2292    ) -> Option<String> {
2293        // Helper to collect all matching keybindings from a map and pick the best one
2294        fn find_best_keybinding(
2295            bindings: &HashMap<(KeyCode, KeyModifiers), Action>,
2296            action: &Action,
2297        ) -> Option<(KeyCode, KeyModifiers)> {
2298            let matches: Vec<_> = bindings
2299                .iter()
2300                .filter(|(_, a)| *a == action)
2301                .map(|((k, m), _)| (*k, *m))
2302                .collect();
2303
2304            if matches.is_empty() {
2305                return None;
2306            }
2307
2308            // Sort to prefer canonical keys over terminal equivalents
2309            // Terminal equivalents like '@' (for space), '7' (for '/'), etc. should be deprioritized
2310            let mut sorted = matches;
2311            sorted.sort_by(|(k1, m1), (k2, m2)| {
2312                let score1 = keybinding_priority_score(k1);
2313                let score2 = keybinding_priority_score(k2);
2314                // Lower score = higher priority
2315                match score1.cmp(&score2) {
2316                    std::cmp::Ordering::Equal => {
2317                        // Tie-break by formatted string for full determinism
2318                        let s1 = format_keybinding(k1, m1);
2319                        let s2 = format_keybinding(k2, m2);
2320                        s1.cmp(&s2)
2321                    }
2322                    other => other,
2323                }
2324            });
2325
2326            sorted.into_iter().next()
2327        }
2328
2329        // Check custom bindings first (higher priority)
2330        if let Some(context_bindings) = self.bindings.get(&context) {
2331            if let Some((keycode, modifiers)) = find_best_keybinding(context_bindings, action) {
2332                return Some(format_keybinding(&keycode, &modifiers));
2333            }
2334        }
2335
2336        // Check default bindings for this context
2337        if let Some(context_bindings) = self.default_bindings.get(&context) {
2338            if let Some((keycode, modifiers)) = find_best_keybinding(context_bindings, action) {
2339                return Some(format_keybinding(&keycode, &modifiers));
2340            }
2341        }
2342
2343        // For certain contexts, also check Normal context for application-wide actions
2344        if context != KeyContext::Normal && Self::is_application_wide_action(action) {
2345            // Check custom normal bindings
2346            if let Some(normal_bindings) = self.bindings.get(&KeyContext::Normal) {
2347                if let Some((keycode, modifiers)) = find_best_keybinding(normal_bindings, action) {
2348                    return Some(format_keybinding(&keycode, &modifiers));
2349                }
2350            }
2351
2352            // Check default normal bindings
2353            if let Some(normal_bindings) = self.default_bindings.get(&KeyContext::Normal) {
2354                if let Some((keycode, modifiers)) = find_best_keybinding(normal_bindings, action) {
2355                    return Some(format_keybinding(&keycode, &modifiers));
2356                }
2357            }
2358        }
2359
2360        None
2361    }
2362
2363    /// Reload bindings from config (for hot reload)
2364    pub fn reload(&mut self, config: &Config) {
2365        self.bindings.clear();
2366        for binding in &config.keybindings {
2367            if let Some(key_code) = Self::parse_key(&binding.key) {
2368                let modifiers = Self::parse_modifiers(&binding.modifiers);
2369                if let Some(action) = Action::from_str(&binding.action, &binding.args) {
2370                    // Determine context from "when" clause
2371                    let context = if let Some(ref when) = binding.when {
2372                        KeyContext::from_when_clause(when).unwrap_or(KeyContext::Normal)
2373                    } else {
2374                        KeyContext::Normal
2375                    };
2376
2377                    self.bindings
2378                        .entry(context)
2379                        .or_default()
2380                        .insert((key_code, modifiers), action);
2381                }
2382            }
2383        }
2384    }
2385}
2386
2387#[cfg(test)]
2388mod tests {
2389    use super::*;
2390
2391    #[test]
2392    fn test_parse_key() {
2393        assert_eq!(KeybindingResolver::parse_key("enter"), Some(KeyCode::Enter));
2394        assert_eq!(
2395            KeybindingResolver::parse_key("backspace"),
2396            Some(KeyCode::Backspace)
2397        );
2398        assert_eq!(KeybindingResolver::parse_key("tab"), Some(KeyCode::Tab));
2399        assert_eq!(
2400            KeybindingResolver::parse_key("backtab"),
2401            Some(KeyCode::BackTab)
2402        );
2403        assert_eq!(
2404            KeybindingResolver::parse_key("BackTab"),
2405            Some(KeyCode::BackTab)
2406        );
2407        assert_eq!(KeybindingResolver::parse_key("a"), Some(KeyCode::Char('a')));
2408    }
2409
2410    #[test]
2411    fn test_parse_modifiers() {
2412        let mods = vec!["ctrl".to_string()];
2413        assert_eq!(
2414            KeybindingResolver::parse_modifiers(&mods),
2415            KeyModifiers::CONTROL
2416        );
2417
2418        let mods = vec!["ctrl".to_string(), "shift".to_string()];
2419        assert_eq!(
2420            KeybindingResolver::parse_modifiers(&mods),
2421            KeyModifiers::CONTROL | KeyModifiers::SHIFT
2422        );
2423    }
2424
2425    #[test]
2426    fn test_resolve_basic() {
2427        let config = Config::default();
2428        let resolver = KeybindingResolver::new(&config);
2429
2430        let event = KeyEvent::new(KeyCode::Left, KeyModifiers::empty());
2431        assert_eq!(
2432            resolver.resolve(&event, KeyContext::Normal),
2433            Action::MoveLeft
2434        );
2435
2436        let event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
2437        assert_eq!(
2438            resolver.resolve(&event, KeyContext::Normal),
2439            Action::InsertChar('a')
2440        );
2441    }
2442
2443    #[test]
2444    fn test_action_from_str() {
2445        let args = HashMap::new();
2446        assert_eq!(Action::from_str("move_left", &args), Some(Action::MoveLeft));
2447        assert_eq!(Action::from_str("save", &args), Some(Action::Save));
2448        // Unknown action names are treated as plugin actions
2449        assert_eq!(
2450            Action::from_str("unknown", &args),
2451            Some(Action::PluginAction("unknown".to_string()))
2452        );
2453
2454        // Test new context-specific actions
2455        assert_eq!(
2456            Action::from_str("keyboard_shortcuts", &args),
2457            Some(Action::ShowKeyboardShortcuts)
2458        );
2459        assert_eq!(
2460            Action::from_str("prompt_confirm", &args),
2461            Some(Action::PromptConfirm)
2462        );
2463        assert_eq!(
2464            Action::from_str("popup_cancel", &args),
2465            Some(Action::PopupCancel)
2466        );
2467
2468        // Test calibrate_input action
2469        assert_eq!(
2470            Action::from_str("calibrate_input", &args),
2471            Some(Action::CalibrateInput)
2472        );
2473    }
2474
2475    #[test]
2476    fn test_key_context_from_when_clause() {
2477        assert_eq!(
2478            KeyContext::from_when_clause("normal"),
2479            Some(KeyContext::Normal)
2480        );
2481        assert_eq!(
2482            KeyContext::from_when_clause("prompt"),
2483            Some(KeyContext::Prompt)
2484        );
2485        assert_eq!(
2486            KeyContext::from_when_clause("popup"),
2487            Some(KeyContext::Popup)
2488        );
2489        assert_eq!(KeyContext::from_when_clause("help"), None);
2490        assert_eq!(KeyContext::from_when_clause("  help  "), None); // Test trimming
2491        assert_eq!(KeyContext::from_when_clause("unknown"), None);
2492        assert_eq!(KeyContext::from_when_clause(""), None);
2493    }
2494
2495    #[test]
2496    fn test_key_context_to_when_clause() {
2497        assert_eq!(KeyContext::Normal.to_when_clause(), "normal");
2498        assert_eq!(KeyContext::Prompt.to_when_clause(), "prompt");
2499        assert_eq!(KeyContext::Popup.to_when_clause(), "popup");
2500    }
2501
2502    #[test]
2503    fn test_context_specific_bindings() {
2504        let config = Config::default();
2505        let resolver = KeybindingResolver::new(&config);
2506
2507        // Test prompt context bindings
2508        let enter_event = KeyEvent::new(KeyCode::Enter, KeyModifiers::empty());
2509        assert_eq!(
2510            resolver.resolve(&enter_event, KeyContext::Prompt),
2511            Action::PromptConfirm
2512        );
2513        assert_eq!(
2514            resolver.resolve(&enter_event, KeyContext::Normal),
2515            Action::InsertNewline
2516        );
2517
2518        // Test popup context bindings
2519        let up_event = KeyEvent::new(KeyCode::Up, KeyModifiers::empty());
2520        assert_eq!(
2521            resolver.resolve(&up_event, KeyContext::Popup),
2522            Action::PopupSelectPrev
2523        );
2524        assert_eq!(
2525            resolver.resolve(&up_event, KeyContext::Normal),
2526            Action::MoveUp
2527        );
2528    }
2529
2530    #[test]
2531    fn test_context_fallback_to_normal() {
2532        let config = Config::default();
2533        let resolver = KeybindingResolver::new(&config);
2534
2535        // Ctrl+S should work in all contexts (falls back to normal)
2536        let save_event = KeyEvent::new(KeyCode::Char('s'), KeyModifiers::CONTROL);
2537        assert_eq!(
2538            resolver.resolve(&save_event, KeyContext::Normal),
2539            Action::Save
2540        );
2541        assert_eq!(
2542            resolver.resolve(&save_event, KeyContext::Popup),
2543            Action::Save
2544        );
2545        // Note: Prompt context might handle this differently in practice
2546    }
2547
2548    #[test]
2549    fn test_context_priority_resolution() {
2550        use crate::config::Keybinding;
2551
2552        // Create a config with a custom binding that overrides default in help context
2553        let mut config = Config::default();
2554        config.keybindings.push(Keybinding {
2555            key: "esc".to_string(),
2556            modifiers: vec![],
2557            keys: vec![],
2558            action: "quit".to_string(), // Override Esc in popup context to quit
2559            args: HashMap::new(),
2560            when: Some("popup".to_string()),
2561        });
2562
2563        let resolver = KeybindingResolver::new(&config);
2564        let esc_event = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty());
2565
2566        // In popup context, custom binding should override default PopupCancel
2567        assert_eq!(
2568            resolver.resolve(&esc_event, KeyContext::Popup),
2569            Action::Quit
2570        );
2571
2572        // In normal context, should still be RemoveSecondaryCursors
2573        assert_eq!(
2574            resolver.resolve(&esc_event, KeyContext::Normal),
2575            Action::RemoveSecondaryCursors
2576        );
2577    }
2578
2579    #[test]
2580    fn test_character_input_in_contexts() {
2581        let config = Config::default();
2582        let resolver = KeybindingResolver::new(&config);
2583
2584        let char_event = KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty());
2585
2586        // Character input should work in Normal and Prompt contexts
2587        assert_eq!(
2588            resolver.resolve(&char_event, KeyContext::Normal),
2589            Action::InsertChar('a')
2590        );
2591        assert_eq!(
2592            resolver.resolve(&char_event, KeyContext::Prompt),
2593            Action::InsertChar('a')
2594        );
2595
2596        // But not in Popup contexts (returns None)
2597        assert_eq!(
2598            resolver.resolve(&char_event, KeyContext::Popup),
2599            Action::None
2600        );
2601    }
2602
2603    #[test]
2604    fn test_custom_keybinding_loading() {
2605        use crate::config::Keybinding;
2606
2607        let mut config = Config::default();
2608
2609        // Add a custom keybinding for normal context
2610        config.keybindings.push(Keybinding {
2611            key: "f".to_string(),
2612            modifiers: vec!["ctrl".to_string()],
2613            keys: vec![],
2614            action: "command_palette".to_string(),
2615            args: HashMap::new(),
2616            when: None, // Default to normal context
2617        });
2618
2619        let resolver = KeybindingResolver::new(&config);
2620
2621        // Test normal context custom binding
2622        let ctrl_f = KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL);
2623        assert_eq!(
2624            resolver.resolve(&ctrl_f, KeyContext::Normal),
2625            Action::CommandPalette
2626        );
2627
2628        // Test prompt context custom binding
2629        let ctrl_k = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL);
2630        assert_eq!(
2631            resolver.resolve(&ctrl_k, KeyContext::Prompt),
2632            Action::PromptDeleteToLineEnd
2633        );
2634        assert_eq!(
2635            resolver.resolve(&ctrl_k, KeyContext::Normal),
2636            Action::DeleteToLineEnd
2637        );
2638    }
2639
2640    #[test]
2641    fn test_all_context_default_bindings_exist() {
2642        let config = Config::default();
2643        let resolver = KeybindingResolver::new(&config);
2644
2645        // Verify that default bindings exist for all contexts
2646        assert!(resolver.default_bindings.contains_key(&KeyContext::Normal));
2647        assert!(resolver.default_bindings.contains_key(&KeyContext::Prompt));
2648        assert!(resolver.default_bindings.contains_key(&KeyContext::Popup));
2649        assert!(resolver
2650            .default_bindings
2651            .contains_key(&KeyContext::FileExplorer));
2652        assert!(resolver.default_bindings.contains_key(&KeyContext::Menu));
2653
2654        // Verify each context has some bindings
2655        assert!(!resolver.default_bindings[&KeyContext::Normal].is_empty());
2656        assert!(!resolver.default_bindings[&KeyContext::Prompt].is_empty());
2657        assert!(!resolver.default_bindings[&KeyContext::Popup].is_empty());
2658        assert!(!resolver.default_bindings[&KeyContext::FileExplorer].is_empty());
2659        assert!(!resolver.default_bindings[&KeyContext::Menu].is_empty());
2660    }
2661
2662    /// Validate that every action name in every built-in keymap resolves to a
2663    /// known built-in action, not a `PluginAction`.  This catches typos like
2664    /// `"prompt_delete_to_end"` (should be `"prompt_delete_to_line_end"`).
2665    #[test]
2666    fn test_all_builtin_keymaps_have_valid_action_names() {
2667        let known_actions: std::collections::HashSet<String> =
2668            Action::all_action_names().into_iter().collect();
2669
2670        let config = Config::default();
2671
2672        for map_name in crate::config::KeybindingMapName::BUILTIN_OPTIONS {
2673            let bindings = config.resolve_keymap(map_name);
2674            for binding in &bindings {
2675                assert!(
2676                    known_actions.contains(&binding.action),
2677                    "Keymap '{}' contains unknown action '{}' (key: '{}', when: {:?}). \
2678                     This will be treated as a plugin action at runtime. \
2679                     Check for typos in the keymap JSON file.",
2680                    map_name,
2681                    binding.action,
2682                    binding.key,
2683                    binding.when,
2684                );
2685            }
2686        }
2687    }
2688
2689    #[test]
2690    fn test_resolve_determinism() {
2691        // Property: Resolving the same key in the same context should always return the same action
2692        let config = Config::default();
2693        let resolver = KeybindingResolver::new(&config);
2694
2695        let test_cases = vec![
2696            (KeyCode::Left, KeyModifiers::empty(), KeyContext::Normal),
2697            (
2698                KeyCode::Esc,
2699                KeyModifiers::empty(),
2700                KeyContext::FileExplorer,
2701            ),
2702            (KeyCode::Enter, KeyModifiers::empty(), KeyContext::Prompt),
2703            (KeyCode::Down, KeyModifiers::empty(), KeyContext::Popup),
2704        ];
2705
2706        for (key_code, modifiers, context) in test_cases {
2707            let event = KeyEvent::new(key_code, modifiers);
2708            let action1 = resolver.resolve(&event, context.clone());
2709            let action2 = resolver.resolve(&event, context.clone());
2710            let action3 = resolver.resolve(&event, context);
2711
2712            assert_eq!(action1, action2, "Resolve should be deterministic");
2713            assert_eq!(action2, action3, "Resolve should be deterministic");
2714        }
2715    }
2716
2717    #[test]
2718    fn test_modifier_combinations() {
2719        let config = Config::default();
2720        let resolver = KeybindingResolver::new(&config);
2721
2722        // Test that modifier combinations are distinguished correctly
2723        let char_s = KeyCode::Char('s');
2724
2725        let no_mod = KeyEvent::new(char_s, KeyModifiers::empty());
2726        let ctrl = KeyEvent::new(char_s, KeyModifiers::CONTROL);
2727        let shift = KeyEvent::new(char_s, KeyModifiers::SHIFT);
2728        let ctrl_shift = KeyEvent::new(char_s, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
2729
2730        let action_no_mod = resolver.resolve(&no_mod, KeyContext::Normal);
2731        let action_ctrl = resolver.resolve(&ctrl, KeyContext::Normal);
2732        let action_shift = resolver.resolve(&shift, KeyContext::Normal);
2733        let action_ctrl_shift = resolver.resolve(&ctrl_shift, KeyContext::Normal);
2734
2735        // These should all be different actions (or at least distinguishable)
2736        assert_eq!(action_no_mod, Action::InsertChar('s'));
2737        assert_eq!(action_ctrl, Action::Save);
2738        assert_eq!(action_shift, Action::InsertChar('s')); // Shift alone is still character input
2739                                                           // Ctrl+Shift+S is not bound by default, should return None
2740        assert_eq!(action_ctrl_shift, Action::None);
2741    }
2742
2743    #[test]
2744    fn test_scroll_keybindings() {
2745        let config = Config::default();
2746        let resolver = KeybindingResolver::new(&config);
2747
2748        // Test Ctrl+Up -> ScrollUp
2749        let ctrl_up = KeyEvent::new(KeyCode::Up, KeyModifiers::CONTROL);
2750        assert_eq!(
2751            resolver.resolve(&ctrl_up, KeyContext::Normal),
2752            Action::ScrollUp,
2753            "Ctrl+Up should resolve to ScrollUp"
2754        );
2755
2756        // Test Ctrl+Down -> ScrollDown
2757        let ctrl_down = KeyEvent::new(KeyCode::Down, KeyModifiers::CONTROL);
2758        assert_eq!(
2759            resolver.resolve(&ctrl_down, KeyContext::Normal),
2760            Action::ScrollDown,
2761            "Ctrl+Down should resolve to ScrollDown"
2762        );
2763    }
2764
2765    #[test]
2766    fn test_lsp_completion_keybinding() {
2767        let config = Config::default();
2768        let resolver = KeybindingResolver::new(&config);
2769
2770        // Test Ctrl+Space -> LspCompletion
2771        let ctrl_space = KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL);
2772        assert_eq!(
2773            resolver.resolve(&ctrl_space, KeyContext::Normal),
2774            Action::LspCompletion,
2775            "Ctrl+Space should resolve to LspCompletion"
2776        );
2777    }
2778
2779    #[test]
2780    fn test_terminal_key_equivalents() {
2781        // Test that terminal_key_equivalents returns correct mappings
2782        let ctrl = KeyModifiers::CONTROL;
2783
2784        // Ctrl+/ <-> Ctrl+7
2785        let slash_equivs = terminal_key_equivalents(KeyCode::Char('/'), ctrl);
2786        assert_eq!(slash_equivs, vec![(KeyCode::Char('7'), ctrl)]);
2787
2788        let seven_equivs = terminal_key_equivalents(KeyCode::Char('7'), ctrl);
2789        assert_eq!(seven_equivs, vec![(KeyCode::Char('/'), ctrl)]);
2790
2791        // Ctrl+Backspace <-> Ctrl+H
2792        let backspace_equivs = terminal_key_equivalents(KeyCode::Backspace, ctrl);
2793        assert_eq!(backspace_equivs, vec![(KeyCode::Char('h'), ctrl)]);
2794
2795        let h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl);
2796        assert_eq!(h_equivs, vec![(KeyCode::Backspace, ctrl)]);
2797
2798        // No equivalents for regular keys
2799        let a_equivs = terminal_key_equivalents(KeyCode::Char('a'), ctrl);
2800        assert!(a_equivs.is_empty());
2801
2802        // No equivalents without Ctrl
2803        let slash_no_ctrl = terminal_key_equivalents(KeyCode::Char('/'), KeyModifiers::empty());
2804        assert!(slash_no_ctrl.is_empty());
2805    }
2806
2807    #[test]
2808    fn test_terminal_key_equivalents_auto_binding() {
2809        let config = Config::default();
2810        let resolver = KeybindingResolver::new(&config);
2811
2812        // Ctrl+/ should be bound to toggle_comment
2813        let ctrl_slash = KeyEvent::new(KeyCode::Char('/'), KeyModifiers::CONTROL);
2814        let action_slash = resolver.resolve(&ctrl_slash, KeyContext::Normal);
2815        assert_eq!(
2816            action_slash,
2817            Action::ToggleComment,
2818            "Ctrl+/ should resolve to ToggleComment"
2819        );
2820
2821        // Ctrl+7 should also be bound to toggle_comment (auto-generated equivalent)
2822        let ctrl_7 = KeyEvent::new(KeyCode::Char('7'), KeyModifiers::CONTROL);
2823        let action_7 = resolver.resolve(&ctrl_7, KeyContext::Normal);
2824        assert_eq!(
2825            action_7,
2826            Action::ToggleComment,
2827            "Ctrl+7 should resolve to ToggleComment (terminal equivalent of Ctrl+/)"
2828        );
2829    }
2830
2831    #[test]
2832    fn test_terminal_key_equivalents_normalization() {
2833        // This test verifies that all terminal key equivalents are correctly mapped
2834        // These mappings exist because terminals send different key codes for certain
2835        // key combinations due to historical terminal emulation reasons.
2836
2837        let ctrl = KeyModifiers::CONTROL;
2838
2839        // === Ctrl+/ <-> Ctrl+7 ===
2840        // Most terminals send Ctrl+7 (0x1F) when user presses Ctrl+/
2841        let slash_equivs = terminal_key_equivalents(KeyCode::Char('/'), ctrl);
2842        assert_eq!(
2843            slash_equivs,
2844            vec![(KeyCode::Char('7'), ctrl)],
2845            "Ctrl+/ should map to Ctrl+7"
2846        );
2847        let seven_equivs = terminal_key_equivalents(KeyCode::Char('7'), ctrl);
2848        assert_eq!(
2849            seven_equivs,
2850            vec![(KeyCode::Char('/'), ctrl)],
2851            "Ctrl+7 should map back to Ctrl+/"
2852        );
2853
2854        // === Ctrl+Backspace <-> Ctrl+H ===
2855        // Many terminals send Ctrl+H (0x08, ASCII backspace) for Ctrl+Backspace
2856        let backspace_equivs = terminal_key_equivalents(KeyCode::Backspace, ctrl);
2857        assert_eq!(
2858            backspace_equivs,
2859            vec![(KeyCode::Char('h'), ctrl)],
2860            "Ctrl+Backspace should map to Ctrl+H"
2861        );
2862        let h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl);
2863        assert_eq!(
2864            h_equivs,
2865            vec![(KeyCode::Backspace, ctrl)],
2866            "Ctrl+H should map back to Ctrl+Backspace"
2867        );
2868
2869        // === Ctrl+Space <-> Ctrl+@ ===
2870        // Ctrl+Space sends NUL (0x00), same as Ctrl+@
2871        let space_equivs = terminal_key_equivalents(KeyCode::Char(' '), ctrl);
2872        assert_eq!(
2873            space_equivs,
2874            vec![(KeyCode::Char('@'), ctrl)],
2875            "Ctrl+Space should map to Ctrl+@"
2876        );
2877        let at_equivs = terminal_key_equivalents(KeyCode::Char('@'), ctrl);
2878        assert_eq!(
2879            at_equivs,
2880            vec![(KeyCode::Char(' '), ctrl)],
2881            "Ctrl+@ should map back to Ctrl+Space"
2882        );
2883
2884        // === Ctrl+- <-> Ctrl+_ ===
2885        // Ctrl+- and Ctrl+_ both send 0x1F in some terminals
2886        let minus_equivs = terminal_key_equivalents(KeyCode::Char('-'), ctrl);
2887        assert_eq!(
2888            minus_equivs,
2889            vec![(KeyCode::Char('_'), ctrl)],
2890            "Ctrl+- should map to Ctrl+_"
2891        );
2892        let underscore_equivs = terminal_key_equivalents(KeyCode::Char('_'), ctrl);
2893        assert_eq!(
2894            underscore_equivs,
2895            vec![(KeyCode::Char('-'), ctrl)],
2896            "Ctrl+_ should map back to Ctrl+-"
2897        );
2898
2899        // === No equivalents for regular keys ===
2900        assert!(
2901            terminal_key_equivalents(KeyCode::Char('a'), ctrl).is_empty(),
2902            "Ctrl+A should have no terminal equivalents"
2903        );
2904        assert!(
2905            terminal_key_equivalents(KeyCode::Char('z'), ctrl).is_empty(),
2906            "Ctrl+Z should have no terminal equivalents"
2907        );
2908        assert!(
2909            terminal_key_equivalents(KeyCode::Enter, ctrl).is_empty(),
2910            "Ctrl+Enter should have no terminal equivalents"
2911        );
2912
2913        // === No equivalents without Ctrl modifier ===
2914        assert!(
2915            terminal_key_equivalents(KeyCode::Char('/'), KeyModifiers::empty()).is_empty(),
2916            "/ without Ctrl should have no equivalents"
2917        );
2918        assert!(
2919            terminal_key_equivalents(KeyCode::Char('7'), KeyModifiers::SHIFT).is_empty(),
2920            "Shift+7 should have no equivalents"
2921        );
2922        assert!(
2923            terminal_key_equivalents(KeyCode::Char('h'), KeyModifiers::ALT).is_empty(),
2924            "Alt+H should have no equivalents"
2925        );
2926
2927        // === Ctrl+H only maps to Backspace when ONLY Ctrl is pressed ===
2928        // Ctrl+Shift+H or Ctrl+Alt+H should NOT map to Backspace
2929        let ctrl_shift = KeyModifiers::CONTROL | KeyModifiers::SHIFT;
2930        let ctrl_shift_h_equivs = terminal_key_equivalents(KeyCode::Char('h'), ctrl_shift);
2931        assert!(
2932            ctrl_shift_h_equivs.is_empty(),
2933            "Ctrl+Shift+H should NOT map to Ctrl+Shift+Backspace"
2934        );
2935    }
2936
2937    #[test]
2938    fn test_no_duplicate_keybindings_in_keymaps() {
2939        // Load all keymaps and check for duplicate bindings within the same context
2940        // A duplicate is when the same key+modifiers+context is defined more than once
2941        use std::collections::HashMap;
2942
2943        let keymaps: &[(&str, &str)] = &[
2944            ("default", include_str!("../../keymaps/default.json")),
2945            ("macos", include_str!("../../keymaps/macos.json")),
2946        ];
2947
2948        for (keymap_name, json_content) in keymaps {
2949            let keymap: crate::config::KeymapConfig = serde_json::from_str(json_content)
2950                .unwrap_or_else(|e| panic!("Failed to parse keymap '{}': {}", keymap_name, e));
2951
2952            // Track seen bindings per context: (key, modifiers, context) -> action
2953            let mut seen: HashMap<(String, Vec<String>, String), String> = HashMap::new();
2954            let mut duplicates: Vec<String> = Vec::new();
2955
2956            for binding in &keymap.bindings {
2957                let when = binding.when.clone().unwrap_or_default();
2958                let key_id = (binding.key.clone(), binding.modifiers.clone(), when.clone());
2959
2960                if let Some(existing_action) = seen.get(&key_id) {
2961                    duplicates.push(format!(
2962                        "Duplicate in '{}': key='{}', modifiers={:?}, when='{}' -> '{}' vs '{}'",
2963                        keymap_name,
2964                        binding.key,
2965                        binding.modifiers,
2966                        when,
2967                        existing_action,
2968                        binding.action
2969                    ));
2970                } else {
2971                    seen.insert(key_id, binding.action.clone());
2972                }
2973            }
2974
2975            assert!(
2976                duplicates.is_empty(),
2977                "Found duplicate keybindings:\n{}",
2978                duplicates.join("\n")
2979            );
2980        }
2981    }
2982}