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