Skip to main content

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}