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