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