Skip to main content

reovim_module_vim/
fallback.rs

1//! Vim fallback handler for character insertion.
2//!
3//! This handler provides the policy for unmatched keys:
4//! - In Insert mode: Insert the character into the buffer
5//! - In Normal mode: Beep (invalid key)
6//!
7//! # Epic #372 - Mode Ownership
8//!
9//! This handler uses `VimMode::*_ID` constants directly to check the current
10//! mode, which is why it belongs in the vim module rather than the generic
11//! editor module.
12//!
13//! # Design Philosophy
14//!
15//! The fallback handler is a **policy** component. The event loop (mechanism)
16//! doesn't know about Insert mode or character insertion - it just delegates
17//! to this handler when a key doesn't match any binding.
18//!
19//! Tab, Enter, Backspace, and Delete are NOT handled here because they have
20//! explicit keybindings to commands in insert mode (see keymap/insert.rs).
21
22use reovim_driver_input::{
23    FallbackContext, FallbackResult, InputFallbackHandler, KeyCode, KeyEvent, Modifiers,
24};
25
26use crate::modes::VimMode;
27
28/// Vim-specific fallback handler.
29///
30/// Implements character insertion for Insert mode and beeps for
31/// unmatched keys in Normal mode.
32///
33/// # Design Philosophy
34///
35/// This is a **policy** implementation. The event loop (mechanism) doesn't
36/// know about Insert mode or character insertion - it just delegates to
37/// this handler when a key doesn't match any binding.
38///
39/// # Example
40///
41/// ```ignore
42/// use reovim_module_vim::VimFallbackHandler;
43/// use runner::EventLoop;
44///
45/// let fallback = VimFallbackHandler;
46/// let event_loop = EventLoop::new(app, modes, commands, keymaps, fallback);
47/// ```
48#[derive(Debug, Clone, Copy, Default)]
49pub struct VimFallbackHandler;
50
51impl<C: FallbackContext> InputFallbackHandler<C> for VimFallbackHandler {
52    #[cfg_attr(coverage_nightly, coverage(off))]
53    fn handle_unmatched(&self, key: KeyEvent, ctx: &mut C) -> FallbackResult {
54        let mode_id = ctx.current_mode();
55
56        // Check if we're in Insert mode
57        if *mode_id == VimMode::INSERT_ID {
58            // Try to extract a printable character
59            if let Some(ch) = key_to_char(&key) {
60                // Insert the character into the active buffer
61                if let Some(buffer_id) = ctx.active_buffer()
62                    && let Some(cursor_before) = ctx.cursor_position()
63                    && let Some(buffer_arc) = ctx.get_buffer(buffer_id)
64                {
65                    let mut buffer = buffer_arc.write();
66                    // Insert character at cursor position
67                    let text = ch.to_string();
68                    buffer.insert_at(cursor_before, &text);
69                    drop(buffer);
70
71                    // Calculate cursor after insert (advance by text length)
72                    let cursor_after = if ch == '\n' {
73                        reovim_kernel::api::v1::Position::new(cursor_before.line + 1, 0)
74                    } else {
75                        reovim_kernel::api::v1::Position::new(
76                            cursor_before.line,
77                            cursor_before.column + 1,
78                        )
79                    };
80
81                    // Update cursor position
82                    ctx.set_cursor_position(cursor_after);
83
84                    // Accumulate edit for batched undo tracking
85                    // (consecutive inserts become single undo node)
86                    let edit = reovim_kernel::api::v1::Edit::insert(cursor_before, &text);
87                    ctx.accumulate_edit(buffer_id, edit, cursor_before, cursor_after);
88
89                    return FallbackResult::Handled;
90                }
91                return FallbackResult::Handled;
92            }
93
94            // Non-printable key in Insert mode - ignore it
95            return FallbackResult::Ignored;
96        }
97
98        // In Normal mode, unmatched keys should beep
99        if *mode_id == VimMode::NORMAL_ID {
100            return FallbackResult::Beep;
101        }
102
103        // Unknown mode - ignore
104        FallbackResult::Ignored
105    }
106}
107
108/// Extract a printable character from a key event.
109///
110/// Returns `Some(char)` if the key is a printable character without
111/// modifiers (or with just Shift for uppercase).
112fn key_to_char(key: &KeyEvent) -> Option<char> {
113    // Only handle key press events
114    if !key.is_press() {
115        return None;
116    }
117
118    // Check for printable character
119    match key.code {
120        KeyCode::Char(ch) => {
121            // Allow character with no modifiers or just shift
122            if key.modifiers.is_empty() || key.modifiers == Modifiers::SHIFT {
123                Some(ch)
124            } else {
125                None
126            }
127        }
128        KeyCode::Tab => Some('\t'),
129        KeyCode::Enter => Some('\n'),
130        _ => None,
131    }
132}
133
134#[cfg(test)]
135#[path = "fallback_tests.rs"]
136mod tests;