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