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;