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