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