reovim_module_vim/resolvers/insert.rs
1//! Vim insert mode key resolver.
2//!
3//! In insert mode, most keys insert characters directly. Special keys
4//! like Escape exit insert mode, and control sequences trigger commands.
5
6use {
7 reovim_driver_input::{
8 ExtensionMap, KeyCode, KeyEvent, KeyLookupState, KeySequence, ModeKeyResolver, ModeState,
9 Modifiers, ResolveContext, ResolveInput, ResolveResult,
10 },
11 reovim_kernel::api::v1::ModeId,
12};
13
14use crate::{VimSessionState, modes::VimMode};
15
16/// Vim insert mode key resolver.
17///
18/// Insert mode is primarily for text input:
19/// - Most printable characters are inserted directly
20/// - Escape exits to normal mode
21/// - Some control sequences trigger commands (Ctrl+H for backspace, etc.)
22/// - Arrow keys and special keys are handled via keymap lookup
23///
24/// # Example
25///
26/// ```ignore
27/// let resolver = VimInsertResolver::new();
28///
29/// // Regular character - insert it
30/// let result = resolver.resolve(&key('a'), &mut state);
31/// assert!(matches!(result, ResolveResult::InsertChar('a')));
32///
33/// // Escape - exit to normal mode
34/// let result = resolver.resolve(&KeyEvent::new(KeyCode::Escape), &mut state);
35/// assert!(matches!(result, ResolveResult::ModeTransition(..)));
36/// ```
37pub struct VimInsertResolver {
38 /// Mode ID for insert mode.
39 mode_id: ModeId,
40}
41
42impl VimInsertResolver {
43 /// Create a new insert mode resolver.
44 #[must_use]
45 pub const fn new() -> Self {
46 Self {
47 mode_id: VimMode::INSERT_ID,
48 }
49 }
50
51 /// Check if a key should insert a character.
52 const fn is_insertable(key: &KeyEvent) -> Option<char> {
53 // Only consider keys without control/alt modifiers for insertion
54 // Shift is allowed (for uppercase letters)
55 if key.modifiers.contains(Modifiers::CTRL) || key.modifiers.contains(Modifiers::ALT) {
56 return None;
57 }
58
59 match key.code {
60 KeyCode::Char(c) => Some(c),
61 KeyCode::Tab => Some('\t'),
62 KeyCode::Enter => Some('\n'),
63 _ => None,
64 }
65 }
66}
67
68impl Default for VimInsertResolver {
69 fn default() -> Self {
70 Self::new()
71 }
72}
73
74impl ModeKeyResolver for VimInsertResolver {
75 /// Insert mode key resolution with keymap lookup.
76 ///
77 /// This method handles keymap lookup for non-insertable keys like Escape,
78 /// Backspace, and arrow keys. When a key is not insertable, we query the
79 /// keymap to find a bound command.
80 ///
81 /// # Architecture
82 ///
83 /// Insert mode differs from normal mode:
84 /// - Insertable characters (letters, numbers, etc.) return `InsertChar`
85 /// - Non-insertable keys (Escape, Backspace, arrows) query the keymap
86 ///
87 /// This enables Escape to trigger the `vim:exit-insert` command, which
88 /// properly ends undo batching before switching to normal mode.
89 #[cfg_attr(coverage_nightly, coverage(off))]
90 fn resolve_with_keymap(
91 &self,
92 key: &KeyEvent,
93 _state: &mut ModeState,
94 input: &ResolveInput<'_>,
95 ) -> ResolveResult {
96 // Check for insertable character first
97 if let Some(c) = Self::is_insertable(key) {
98 return ResolveResult::insert_char(c);
99 }
100
101 // Non-insertable key - query keymap for binding
102 // Use single-key lookup (insert mode has no multi-key sequences)
103 let keys = KeySequence::from_keys(&[*key]);
104 let lookup_state = input.keymap.query(input.mode, &keys);
105
106 match lookup_state {
107 KeyLookupState::ExactOnly(cmd) | KeyLookupState::ExactWithLonger { exact: cmd, .. } => {
108 // Found a binding - execute it
109 ResolveResult::Execute(cmd, ResolveContext::new())
110 }
111 KeyLookupState::PrefixOnly => {
112 // Waiting for more keys (unlikely in insert mode)
113 ResolveResult::Pending
114 }
115 KeyLookupState::NotFound => {
116 // No binding - let runner handle (may be ignored)
117 ResolveResult::NotHandled
118 }
119 }
120 }
121
122 /// Insert mode key resolution with extension tracking for dot repeat.
123 ///
124 /// This method tracks all inserted characters in `VimSessionState.insert_buffer`
125 /// for dot repeat support (Epic #465).
126 #[cfg_attr(coverage_nightly, coverage(off))]
127 fn resolve_with_extensions(
128 &self,
129 key: &KeyEvent,
130 state: &mut ModeState,
131 input: &ResolveInput<'_>,
132 _shared_extensions: &mut ExtensionMap,
133 client_extensions: &mut ExtensionMap,
134 ) -> ResolveResult {
135 // #577: Record key for dot repeat (before any early returns)
136 if let Some(vim) = client_extensions.get_mut::<VimSessionState>() {
137 vim.record_repeat_key(*key);
138 }
139
140 // Check for insertable character first
141 if let Some(c) = Self::is_insertable(key) {
142 // Track inserted character for dot repeat (Epic #465)
143 if let Some(vim) = client_extensions.get_mut::<VimSessionState>() {
144 vim.insert_buffer.push(c);
145 }
146 return ResolveResult::insert_char(c);
147 }
148
149 // Non-insertable key - delegate to keymap lookup
150 self.resolve_with_keymap(key, state, input)
151 }
152
153 fn mode_id(&self) -> &ModeId {
154 &self.mode_id
155 }
156
157 fn inherits_from(&self) -> Option<&ModeId> {
158 // Insert mode doesn't inherit from normal mode
159 // (different key interpretation)
160 None
161 }
162
163 fn reset(&mut self) {
164 // No state to reset in insert mode
165 }
166}