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