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