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