hjkl_vim/insert.rs
1//! Phase 6.6d: insert-mode FSM body relocated from `hjkl-engine::vim`.
2//!
3//! Dispatched by [`crate::dispatch_input`] for insert mode.
4use crate::editor_ext::VimEditorExt;
5use hjkl_engine::{Host, Input, Key};
6
7/// Drive the insert-mode FSM for one keystroke.
8///
9/// Returns `true` (consumed) unconditionally — every key inside insert mode
10/// is swallowed regardless of whether it produced a visible effect.
11pub fn step_insert<H: Host>(
12 ed: &mut hjkl_engine::Editor<hjkl_buffer::View, H>,
13 input: Input,
14) -> bool {
15 // `Ctrl-R {reg}` paste — the previous keystroke armed the wait. Any
16 // non-char key cancels (matches vim, which beeps on selectors like
17 // Esc and re-emits the literal text otherwise).
18 if ed.insert_pending_register() {
19 ed.set_insert_pending_register(false);
20 if let Key::Char(c) = input.key
21 && !input.ctrl
22 {
23 ed.insert_paste_register(c);
24 }
25 return true;
26 }
27
28 if input.key == Key::Esc {
29 ed.leave_insert_to_normal();
30 return true;
31 }
32
33 // Ctrl-prefixed insert-mode shortcuts — thin dispatcher to public Editor methods.
34 if input.ctrl {
35 match input.key {
36 Key::Char('w') => {
37 ed.insert_ctrl_w();
38 return true;
39 }
40 Key::Char('u') => {
41 ed.insert_ctrl_u();
42 return true;
43 }
44 Key::Char('h') => {
45 ed.insert_ctrl_h();
46 return true;
47 }
48 Key::Char('o') => {
49 ed.insert_ctrl_o_arm();
50 return true;
51 }
52 Key::Char('r') => {
53 ed.insert_ctrl_r_arm();
54 return true;
55 }
56 Key::Char('t') => {
57 ed.insert_ctrl_t();
58 return true;
59 }
60 Key::Char('d') => {
61 ed.insert_ctrl_d();
62 return true;
63 }
64 Key::Char(']') => {
65 // `<C-]>` — expand abbreviation WITHOUT inserting any character.
66 ed.insert_ctrl_bracket();
67 return true;
68 }
69 // B1: `<C-a>` re-inserts the text typed during the last insert
70 // session, `<C-e>`/`<C-y>` copy the char below/above the cursor.
71 Key::Char('a') => {
72 ed.insert_ctrl_a();
73 return true;
74 }
75 Key::Char('e') => {
76 ed.insert_ctrl_e();
77 return true;
78 }
79 Key::Char('y') => {
80 ed.insert_ctrl_y();
81 return true;
82 }
83 // DELIBERATE vim divergence — any other ctrl-key combo has no
84 // dedicated insert-mode binding, and we consume it as a NO-OP.
85 //
86 // Real nvim instead inserts the raw control byte for unbound
87 // ctrl keys (verified against nvim 0.12: `i<C-b>x<Esc>` on
88 // "hello" produces "\x02xhello" — a literal ^B in the buffer).
89 // Reproducing that would put unprintable, un-typeable bytes in
90 // the document for keys that have no editing purpose in a TUI,
91 // so the no-op is intentional. Do NOT "fix" this to match nvim
92 // without deciding that trade-off deliberately; the choice is
93 // pinned by dispatch_input.rs::
94 // insert_unhandled_ctrl_key_is_noop_not_literal_letter.
95 //
96 // What this arm must never regress to: falling through to
97 // `handle_insert_key` below, which would type the ctrl LETTER
98 // itself as a literal character (the pre-fix bug: `<C-a>`
99 // inserted a plain "a").
100 Key::Char(_) => {
101 return true;
102 }
103 _ => {}
104 }
105 }
106
107 // Widen the session's visited row window *before* handling the key
108 // so navigation-only keystrokes (arrow keys) still extend the range.
109 let (row, _) = ed.cursor();
110 if let Some(session) = ed.insert_session_mut() {
111 session.row_min = session.row_min.min(row);
112 session.row_max = session.row_max.max(row);
113 }
114 let mutated = handle_insert_key(ed, input);
115 if mutated {
116 ed.mark_content_dirty();
117 let (row, _) = ed.cursor();
118 if let Some(session) = ed.insert_session_mut() {
119 session.row_min = session.row_min.min(row);
120 session.row_max = session.row_max.max(row);
121 }
122 }
123 true
124}
125
126/// Insert-mode key dispatcher — thin shim that routes each key to the
127/// corresponding public `Editor::*` method (Phase 6.6a). Returns `true`
128/// when the buffer mutated (editing keys), `false` for navigation-only keys.
129pub(crate) fn handle_insert_key<H: Host>(
130 ed: &mut hjkl_engine::Editor<hjkl_buffer::View, H>,
131 input: Input,
132) -> bool {
133 use hjkl_engine::InsertDir;
134 match input.key {
135 Key::Char(c) => {
136 ed.insert_char(c);
137 true
138 }
139 Key::Enter => {
140 ed.insert_newline();
141 true
142 }
143 Key::Tab => {
144 ed.insert_tab();
145 true
146 }
147 Key::Backspace => {
148 ed.insert_backspace();
149 true
150 }
151 Key::Delete => {
152 ed.insert_delete();
153 true
154 }
155 Key::Left => {
156 ed.insert_arrow(InsertDir::Left);
157 false
158 }
159 Key::Right => {
160 ed.insert_arrow(InsertDir::Right);
161 false
162 }
163 Key::Up => {
164 ed.insert_arrow(InsertDir::Up);
165 false
166 }
167 Key::Down => {
168 ed.insert_arrow(InsertDir::Down);
169 false
170 }
171 Key::Home => {
172 ed.insert_home();
173 false
174 }
175 Key::End => {
176 ed.insert_end();
177 false
178 }
179 Key::PageUp => {
180 let h = ed.viewport_height_value();
181 ed.insert_pageup(h);
182 false
183 }
184 Key::PageDown => {
185 let h = ed.viewport_height_value();
186 ed.insert_pagedown(h);
187 false
188 }
189 // F-keys, mouse scroll, copy/cut/paste virtual keys, Null —
190 // no insert-mode behaviour.
191 _ => false,
192 }
193}