Skip to main content

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}