Skip to main content

hjkl_vim/
editor_ext.rs

1//! [`VimEditorExt`] — vim-discipline accessor methods on the engine
2//! [`Editor`], migrated out of `hjkl-engine` (#267 / #265 G3).
3//!
4//! These read the vim FSM state (`Editor::vim`) to answer render/selection
5//! questions. They belong to the vim *discipline*, not the mode-agnostic
6//! engine core, so they live here — a blanket trait impl on
7//! `Editor<View, H>`. As `VimState` finishes relocating into this crate,
8//! more of the engine's vim accessors move onto this trait; call sites pick
9//! them up with `use hjkl_vim::VimEditorExt`.
10
11use crate::vim::{
12    AbbrevTrigger, InsertDir, InsertReason, LastVisual, Motion, Operator, RangeKind, TextObject,
13};
14use hjkl_engine::input::Input;
15use hjkl_engine::types::{Highlight, HighlightKind, Host, Pos};
16use hjkl_engine::{Editor, FsmMode, MarkJump, MotionKind, VimMode};
17
18/// Move a position back by one character, wrapping to the end of the previous
19/// line when at column 0. Clamps at the buffer start `(0, 0)`. Used to render
20/// exclusive (VSCode) char selections via the inclusive buffer-tui paint path.
21///
22/// Was `Editor::dec_pos_one_char` in the engine; it exists only to serve
23/// [`VimEditorExt::buffer_selection`], so it moved here with it.
24fn dec_pos_one_char<H: Host>(
25    ed: &Editor<hjkl_buffer::View, H>,
26    p: hjkl_buffer::Position,
27) -> hjkl_buffer::Position {
28    use hjkl_buffer::Position;
29    if p.col > 0 {
30        return Position::new(p.row, p.col - 1);
31    }
32    if p.row > 0 {
33        let prev = p.row - 1;
34        let len = ed.line(prev).map_or(0, |l| l.chars().count());
35        return Position::new(prev, len);
36    }
37    Position::new(0, 0)
38}
39
40/// Common post-mutation sync for the `insert_*` primitives.
41///
42/// The vim FSM's `step` runs `ensure_cursor_in_scrolloff` at the end of every
43/// normal/visual motion; insert-mode primitives bypass `step` and must
44/// self-correct or the cursor scrolls off the viewport (held Enter, multi-line
45/// backspace at BOL, arrow keys at edge, etc.).
46///
47/// Marks the content dirty, widens the insert row's autoindent tracking, and
48/// re-checks scrolloff. Was `Editor::after_insert_mutation` (#267) — it exists
49/// only to serve the insert primitives, so it moved here with them.
50fn after_insert_mutation<H: Host>(ed: &mut Editor<hjkl_buffer::View, H>) {
51    ed.mark_content_dirty();
52    let (row, _) = ed.cursor();
53    crate::vim_state::vim_mut(ed).widen_insert_row(row);
54    ed.ensure_cursor_in_scrolloff();
55}
56
57/// Like [`after_insert_mutation`] but for cursor-only insert ops that do not
58/// change content (arrows, Home/End, PageUp/Down). Skips the dirty mark.
59fn after_insert_motion<H: Host>(ed: &mut Editor<hjkl_buffer::View, H>) {
60    let (row, _) = ed.cursor();
61    crate::vim_state::vim_mut(ed).widen_insert_row(row);
62    ed.ensure_cursor_in_scrolloff();
63}
64
65/// Vim-discipline read accessors layered onto every `Editor<View, H>`.
66///
67/// Blanket-implemented below; bring it into scope with
68/// `use hjkl_vim::VimEditorExt` to call these on an `Editor`.
69pub trait VimEditorExt {
70    /// VisualBlock selection bounds as `(top, bot, left, right)` — inclusive
71    /// rows and inclusive columns, derived from the block anchor and the
72    /// cursor's sticky column. Meaningful only while in VisualBlock mode;
73    /// callers that need the "are we in block mode?" guard use
74    /// [`VimEditorExt::block_highlight`] instead.
75    fn visual_block_bounds(&self) -> (usize, usize, usize, usize);
76
77    /// The VisualBlock highlight rectangle `(top, bot, left, right)`, or
78    /// `None` when the editor is not in VisualBlock mode.
79    fn block_highlight(&self) -> Option<(usize, usize, usize, usize)>;
80
81    /// Start/end `(row, col)` of the active char-wise Visual selection,
82    /// positionally ordered. `None` when not in Visual mode.
83    ///
84    /// When [`hjkl_engine::editor::Settings::selection_exclusive`] is `false`
85    /// (default, vim behaviour): both endpoints are **inclusive** — the cells
86    /// at `start` and `end` are both selected.
87    ///
88    /// When it is `true` (VSCode bar-cursor behaviour): the range is
89    /// **half-open** — `start` is included but `end` is the first cell that is
90    /// NOT selected (the caret sits before it). If the selection is empty
91    /// (`anchor == cursor`) `None` is returned so callers do not need to check
92    /// for zero-length ranges.
93    fn char_highlight(&self) -> Option<((usize, usize), (usize, usize))>;
94
95    /// Return the half-open exclusive char-visual range `(start, end)` where
96    /// `end` is the first cell NOT selected (the caret position). `None`
97    /// when not in Visual mode or the selection is empty.
98    ///
99    /// Convenience accessor for the VSCode dispatcher; avoids duplicating
100    /// the anchor/cursor ordering logic at the call site.
101    fn visual_char_range_exclusive(&self) -> Option<((usize, usize), (usize, usize))>;
102
103    /// Top/bottom rows of the active VisualLine selection (inclusive).
104    /// `None` when we're not in VisualLine mode.
105    fn line_highlight(&self) -> Option<(usize, usize)>;
106
107    /// Active selection in `hjkl_buffer::Selection` shape. `None` when not in
108    /// a Visual mode. The host hands this straight to `BufferView`.
109    fn buffer_selection(&self) -> Option<hjkl_buffer::Selection>;
110
111    /// Active visual selection as a SPEC [`Highlight`] with
112    /// [`HighlightKind::Selection`].
113    ///
114    /// Returns `None` when the editor isn't in a Visual mode. Visual-line and
115    /// visual-block selections collapse to the bounding char range of the
116    /// selection — the SPEC `Selection` kind doesn't carry sub-line info
117    /// today; hosts that need full line / block geometry continue to read
118    /// [`VimEditorExt::buffer_selection`] (the legacy `hjkl_buffer::Selection`
119    /// shape).
120    fn selection_highlight(&self) -> Option<Highlight>;
121
122    // ─── Text-object resolution (hjkl#70) ──────────────────────────────────
123    //
124    // Pure functions — no cursor mutation, no mode change, no register write.
125    // Each delegates to the `crate::vim::text_object_*_bridge` resolvers,
126    // which remain in the engine until vim.rs itself relocates (#267).
127    //
128    // Return value: `Some((start, end))` where both positions are `(row, col)`
129    // char-column pairs and `end` is *exclusive* (one past the last char to act
130    // on), matching the convention used by `delete_range` / `yank_range` / etc.
131    //
132    // Quote methods take the quote char itself (`'"'`, `'\''`, `` '`' ``).
133    // Bracket methods take the OPEN bracket char (`'('`, `'{'`, `'['`, `'<'`);
134    // close-bracket variants are NOT accepted — the grammar layer normalises
135    // close→open before calling these.
136
137    /// Resolve the range of `iw` (inner word) at the cursor.
138    ///
139    /// An inner word is the contiguous run of keyword characters (or
140    /// punctuation characters if the cursor is on punctuation) under the
141    /// cursor, without surrounding whitespace. Whitespace-only positions
142    /// return `None`.
143    fn text_object_inner_word(&self) -> Option<((usize, usize), (usize, usize))>;
144
145    /// Resolve the range of `aw` (around word) at the cursor.
146    ///
147    /// Like `iw` but extends the range to include trailing whitespace after
148    /// the word. If no trailing whitespace exists, leading whitespace before
149    /// the word is absorbed instead (vim `:help text-objects` behaviour).
150    fn text_object_around_word(&self) -> Option<((usize, usize), (usize, usize))>;
151
152    /// Resolve the range of `iW` (inner WORD) at the cursor.
153    ///
154    /// A WORD is any contiguous run of non-whitespace characters — punctuation
155    /// is not a word boundary.
156    fn text_object_inner_big_word(&self) -> Option<((usize, usize), (usize, usize))>;
157
158    /// Resolve the range of `aW` (around WORD) at the cursor.
159    fn text_object_around_big_word(&self) -> Option<((usize, usize), (usize, usize))>;
160
161    /// Resolve the range of `i<quote>` (inner quote) at the cursor.
162    ///
163    /// Excludes the quote characters themselves. `None` when the cursor's line
164    /// contains fewer than two occurrences of `quote`, or no matching pair can
165    /// be found around or ahead of the cursor.
166    fn text_object_inner_quote(&self, quote: char) -> Option<((usize, usize), (usize, usize))>;
167
168    /// Resolve the range of `a<quote>` (around quote) at the cursor.
169    ///
170    /// Like `i<quote>` but includes the quote characters plus surrounding
171    /// whitespace on one side: trailing after the closing quote if any exists,
172    /// otherwise leading before the opening quote.
173    fn text_object_around_quote(&self, quote: char) -> Option<((usize, usize), (usize, usize))>;
174
175    /// Resolve the range of `i<bracket>` (inner bracket pair) at the cursor.
176    ///
177    /// The cursor may be anywhere inside the pair or on a bracket character.
178    /// When not inside any pair the resolver falls back to a forward scan
179    /// (targets.vim-style: `ci(` works when the cursor is before `(`).
180    /// Multi-line pairs are supported.
181    fn text_object_inner_bracket(&self, open: char) -> Option<((usize, usize), (usize, usize))>;
182
183    /// Resolve the range of `a<bracket>` (around bracket pair) at the cursor.
184    ///
185    /// Like `i<bracket>` but includes the bracket characters themselves.
186    fn text_object_around_bracket(&self, open: char) -> Option<((usize, usize), (usize, usize))>;
187
188    /// Resolve `is` (inner sentence) at the cursor.
189    ///
190    /// Excludes trailing whitespace. Sentence boundaries follow vim's `is`
191    /// semantics (period / `?` / `!` followed by whitespace or
192    /// end-of-paragraph).
193    fn text_object_inner_sentence(&self) -> Option<((usize, usize), (usize, usize))>;
194
195    /// Resolve `as` (around sentence) at the cursor.
196    ///
197    /// Like `is` but includes trailing whitespace after the terminator.
198    fn text_object_around_sentence(&self) -> Option<((usize, usize), (usize, usize))>;
199
200    /// Resolve `ip` (inner paragraph) at the cursor.
201    ///
202    /// A paragraph is a block of non-blank lines bounded by blank lines or
203    /// buffer edges. `None` when the cursor is on a blank line.
204    fn text_object_inner_paragraph(&self) -> Option<((usize, usize), (usize, usize))>;
205
206    /// Resolve `ap` (around paragraph) at the cursor.
207    ///
208    /// Like `ip` but includes one trailing blank line when present.
209    fn text_object_around_paragraph(&self) -> Option<((usize, usize), (usize, usize))>;
210
211    /// Resolve `it` (inner tag) at the cursor.
212    ///
213    /// Matches XML/HTML-style `<tag>...</tag>` pairs, returning the content
214    /// between the open and close tags (excluding the tags themselves).
215    fn text_object_inner_tag(&self) -> Option<((usize, usize), (usize, usize))>;
216
217    /// Resolve `at` (around tag) at the cursor.
218    ///
219    /// Like `it` but includes the open and close tag delimiters.
220    fn text_object_around_tag(&self) -> Option<((usize, usize), (usize, usize))>;
221
222    // ─── Range-mutation primitives (hjkl#70) ───────────────────────────────
223    //
224    // These do not consume input — the caller (the visual-mode operator path)
225    // has already resolved the range from the visual selection before calling
226    // in. Normal-mode op dispatch continues to use `apply_op_motion` /
227    // `apply_op_double` / `apply_op_find` / `apply_op_text_obj`.
228
229    /// Delete the region `[start, end)` and stash the removed text in
230    /// `register`. `'"'` selects the unnamed register (vim default);
231    /// `'a'`–`'z'` select named registers.
232    fn delete_range(
233        &mut self,
234        start: (usize, usize),
235        end: (usize, usize),
236        kind: RangeKind,
237        register: char,
238    );
239
240    /// Yank (copy) the region `[start, end)` into `register` without mutating
241    /// the buffer. `'"'` selects the unnamed register; `'0'` the yank-only
242    /// register; `'a'`–`'z'` select named registers.
243    fn yank_range(
244        &mut self,
245        start: (usize, usize),
246        end: (usize, usize),
247        kind: RangeKind,
248        register: char,
249    );
250
251    /// Delete the region `[start, end)` and transition to Insert mode (vim `c`
252    /// operator). The deleted text is stashed in `register`. On return the
253    /// editor is in Insert mode; the caller must not issue further normal-mode
254    /// ops until the insert session ends.
255    fn change_range(
256        &mut self,
257        start: (usize, usize),
258        end: (usize, usize),
259        kind: RangeKind,
260        register: char,
261    );
262
263    /// Indent (`count > 0`) or outdent (`count < 0`) the row span
264    /// `[start.0, end.0]`. Column components are ignored — indent is always
265    /// linewise. `shiftwidth` overrides the editor's configured shiftwidth for
266    /// this call; pass `0` to use the current editor setting. `count == 0` is
267    /// a no-op.
268    fn indent_range(
269        &mut self,
270        start: (usize, usize),
271        end: (usize, usize),
272        count: i32,
273        shiftwidth: u32,
274    );
275
276    /// Apply a case transformation (`Operator::Uppercase` /
277    /// `Operator::Lowercase` / `Operator::ToggleCase`) to the region
278    /// `[start, end)`. Other `Operator` variants are silently ignored (no-op).
279    /// Registers are left untouched — vim's case operators do not write to
280    /// registers.
281    fn case_range(
282        &mut self,
283        start: (usize, usize),
284        end: (usize, usize),
285        kind: RangeKind,
286        op: Operator,
287    );
288
289    // ─── Block-shape range-mutation primitives (hjkl#70) ───────────────────
290    //
291    // Rectangular VisualBlock operations. `top_row`/`bot_row` are inclusive
292    // line indices; `left_col`/`right_col` are inclusive char-column bounds.
293    // Ragged-edge handling (short lines not reaching `right_col`) matches the
294    // engine FSM's `apply_block_operator` path — short lines lose only the
295    // chars that exist. `register` is the target; `'"'` selects unnamed.
296
297    /// Delete a rectangular VisualBlock selection.
298    fn delete_block(
299        &mut self,
300        top_row: usize,
301        bot_row: usize,
302        left_col: usize,
303        right_col: usize,
304        register: char,
305    );
306
307    /// Yank a rectangular VisualBlock selection into `register` without
308    /// mutating the buffer.
309    fn yank_block(
310        &mut self,
311        top_row: usize,
312        bot_row: usize,
313        left_col: usize,
314        right_col: usize,
315        register: char,
316    );
317
318    /// Delete a rectangular VisualBlock selection and enter Insert mode (`c`
319    /// operator). Mode is Insert on return.
320    fn change_block(
321        &mut self,
322        top_row: usize,
323        bot_row: usize,
324        left_col: usize,
325        right_col: usize,
326        register: char,
327    );
328
329    /// Indent (`count > 0`) or outdent (`count < 0`) rows `top_row..=bot_row`.
330    /// Column bounds are ignored — vim's block indent is always linewise.
331    /// `count == 0` is a no-op.
332    fn indent_block(
333        &mut self,
334        top_row: usize,
335        bot_row: usize,
336        left_col: usize,
337        right_col: usize,
338        count: i32,
339    );
340
341    /// Auto-indent (v1 dumb shiftwidth) the row span `[start.0, end.0]`.
342    /// Column components are ignored — auto-indent is always linewise.
343    ///
344    /// The algorithm is a naive bracket-depth counter: it scans the buffer
345    /// from row 0 to compute the correct depth at `start.0`, then for each
346    /// line in the target range strips existing leading whitespace and
347    /// prepends `depth × indent_unit`. Lines whose first non-whitespace
348    /// character is a close bracket get one fewer indent level. Empty /
349    /// whitespace-only lines are cleared. After the operation the cursor lands
350    /// on the first non-whitespace character of `start_row` (vim parity `==`).
351    ///
352    /// **v1 limitation**: the bracket scan does not detect brackets inside
353    /// string literals or comments.
354    fn auto_indent_range(&mut self, start: (usize, usize), end: (usize, usize));
355
356    // ─── Paste ─────────────────────────────────────────────────────────────
357
358    /// `p` — paste the unnamed register (or the register selected via `"r`)
359    /// after the cursor. Linewise content opens a new line below; charwise
360    /// content is inserted inline. Records `Paste { before: false }` for `.`.
361    fn paste_after(&mut self, count: usize);
362
363    /// `P` — paste the unnamed register (or the `"r` register) before the
364    /// cursor. Linewise content opens a new line above; charwise is inline.
365    /// Records `Paste { before: true }` for dot-repeat.
366    fn paste_before(&mut self, count: usize);
367
368    /// `gp` / `gP` — paste like `p`/`P` but leave the cursor just after the
369    /// pasted text. `before = true` for `gP`.
370    fn paste_cursor_after(&mut self, before: bool, count: usize);
371
372    /// `]p` / `[p` — linewise paste with the pasted block reindented to match
373    /// the current line. `before = true` for `[p`.
374    fn paste_reindent(&mut self, before: bool, count: usize);
375
376    /// Visual-mode `p` / `P` — replace the active selection with the register.
377    /// `before = true` for `P` (preserves the source register).
378    fn visual_paste(&mut self, before: bool);
379
380    // ─── Visual-mode operators ─────────────────────────────────────────────
381
382    /// Visual-mode `<C-a>`/`<C-x>` (uniform) and `g<C-a>`/`g<C-x>`
383    /// (`sequential`) — adjust the first number on each selected line.
384    fn adjust_number_visual(&mut self, delta: i64, sequential: bool);
385
386    /// Normal-mode `&` — repeat the last `:s` on the current line (no flags).
387    fn ampersand_repeat(&mut self);
388
389    /// Visual-mode `J` (`with_space = true`) / `gJ` (`false`) — join the
390    /// selected lines into one.
391    fn visual_join(&mut self, with_space: bool);
392
393    /// `[count]%` — jump to the line at `count` percent of the file.
394    fn goto_percent(&mut self, count: usize);
395
396    // ─── Jumplist motion ───────────────────────────────────────────────────
397
398    /// `<C-o>` — jump back `count` entries in the jumplist, saving the current
399    /// position on the forward stack so `<C-i>` can return.
400    fn jump_back(&mut self, count: usize);
401
402    /// `<C-i>` / `Tab` — redo `count` entries on the forward jumplist stack,
403    /// saving the current position on the backward stack.
404    fn jump_forward(&mut self, count: usize);
405
406    // ─── Search ────────────────────────────────────────────────────────────
407
408    /// `n` — repeat the last `/` or `?` search `count` times in its original
409    /// direction. `forward = true` keeps the direction; `false` inverts (`N`).
410    fn search_repeat(&mut self, forward: bool, count: usize);
411
412    /// `*` / `#` / `g*` / `g#` — search for the word under the cursor.
413    /// `forward` chooses direction; `whole_word` wraps the pattern in `\b`
414    /// anchors (true for `*` / `#`, false for `g*` / `g#`). `count` repeats.
415    fn word_search(&mut self, forward: bool, whole_word: bool, count: usize);
416
417    // ─── Chord appliers ────────────────────────────────────────────────────
418    //
419    // Each applies a completed chord with a pre-captured count, so the
420    // pending-state reducers can dispatch without re-entering the engine FSM.
421
422    /// `r<x>` — replace the char under the cursor with `ch`, `count` times.
423    /// Cursor ends on the last replaced char; one undo snapshot at start.
424    fn replace_char_at(&mut self, ch: char, count: usize);
425
426    /// `f`/`F`/`t`/`T` — find `ch` on the current line. `forward` chooses
427    /// direction, `till` stops one char short. Records `last_find` for `;`/`,`.
428    fn find_char(&mut self, ch: char, forward: bool, till: bool, count: usize);
429
430    /// Apply the g-chord effect for `g<ch>` with a pre-captured `count`.
431    fn after_g(&mut self, ch: char, count: usize);
432
433    /// Apply the z-chord effect for `z<ch>` with a pre-captured `count` —
434    /// `zz`/`zt`/`zb` (scroll-cursor), the fold ops, and `zf`.
435    fn after_z(&mut self, ch: char, count: usize);
436
437    // ─── Operator dispatch ─────────────────────────────────────────────────
438
439    /// Apply an operator over a single-key motion (e.g. `dw`, `d$`, `dG`).
440    /// The engine resolves `motion_key` to a `Motion` via `parse_motion`.
441    /// `total_count` is the folded product of prefix and inner counts. No-op
442    /// when `motion_key` is not a known motion (vim cancels the operator).
443    fn apply_op_motion(&mut self, op: Operator, motion_key: char, total_count: usize);
444
445    /// Apply a doubled-letter line op (`dd` / `yy` / `cc` / `>>` / `<<`).
446    fn apply_op_double(&mut self, op: Operator, total_count: usize);
447
448    /// Apply an operator over a find motion (`df<x>` / `dF<x>` / `dt<x>` /
449    /// `dT<x>`). Records `last_find` for `;` / `,` repeat and updates
450    /// `last_change` when `op` is Change (dot-repeat).
451    fn apply_op_find(&mut self, op: Operator, ch: char, forward: bool, till: bool, count: usize);
452
453    /// Apply an operator over a text-object range (`diw` / `daw` / `di"` …).
454    /// Unknown `ch` values are silently ignored, matching the FSM.
455    fn apply_op_text_obj(&mut self, op: Operator, ch: char, inner: bool, total_count: usize);
456
457    /// Apply an operator over a g-chord motion or case-op linewise form
458    /// (`dgg` / `dge` / `dgE` / `dgj` / `dgk` / `gUgU` …).
459    fn apply_op_g(&mut self, op: Operator, ch: char, total_count: usize);
460
461    // ─── Mode transitions ──────────────────────────────────────────────────
462    //
463    // Both the FSM and these wrappers write `current_mode`, so `vim_mode()`
464    // returns correct values regardless of which path performed the
465    // transition.
466
467    /// The current vim mode (Normal / Insert / Visual / VisualLine / VisualBlock).
468    fn vim_mode(&self) -> VimMode;
469
470    /// `v` from Normal — enter charwise Visual mode, anchoring the selection
471    /// at the current cursor position.
472    fn enter_visual_char(&mut self);
473
474    /// `V` from Normal — enter linewise Visual mode, anchoring on the current
475    /// line. Motions extend the selection by whole lines.
476    fn enter_visual_line(&mut self);
477
478    /// `<C-v>` from Normal — enter Visual-block mode. The selection is a
479    /// rectangle whose corners are the anchor and the live cursor.
480    fn enter_visual_block(&mut self);
481
482    /// Esc from any visual mode — set `<` / `>` marks, stash the selection for
483    /// `gv` re-entry, then return to Normal mode.
484    fn exit_visual_to_normal(&mut self);
485
486    /// `o` in Visual / VisualLine / VisualBlock — swap the cursor and anchor so
487    /// the user can extend the other end of the selection. Does NOT mutate the
488    /// selection range; only the active endpoint changes.
489    fn visual_o_toggle(&mut self);
490
491    /// `gv` — restore the last visual selection (mode + anchor + cursor
492    /// position). No-op when no visual selection has been exited yet.
493    fn reenter_last_visual(&mut self);
494
495    /// Direct mode-transition entry point. Sets both the internal FSM mode and
496    /// the stable `current_mode` field read by `vim_mode()`.
497    ///
498    /// Prefer the semantic primitives (`enter_visual_char`, `enter_insert_i`,
499    /// …) which also set up required bookkeeping (anchors, sessions, …). Use
500    /// `set_mode` only when you need a raw mode flip without side-effects.
501    fn set_mode(&mut self, mode: VimMode);
502
503    // ─── Visual anchors ────────────────────────────────────────────────────
504
505    /// The charwise Visual-mode anchor `(row, col)`.
506    fn visual_anchor(&self) -> (usize, usize);
507    /// Set the charwise Visual-mode anchor.
508    fn set_visual_anchor(&mut self, anchor: (usize, usize));
509    /// The linewise Visual-mode anchor row.
510    fn visual_line_anchor(&self) -> usize;
511    /// Set the linewise Visual-mode anchor row.
512    fn set_visual_line_anchor(&mut self, row: usize);
513    /// The VisualBlock anchor `(row, col)`.
514    fn block_anchor(&self) -> (usize, usize);
515    /// Set the VisualBlock anchor.
516    fn set_block_anchor(&mut self, anchor: (usize, usize));
517    /// The VisualBlock sticky (virtual) column.
518    fn block_vcol(&self) -> usize;
519    /// Set the VisualBlock sticky (virtual) column.
520    fn set_block_vcol(&mut self, vcol: usize);
521    /// Whether the VisualBlock selection is "ragged" (`$` was pressed —
522    /// `:h v_b_$`): every row resolves its own right edge to its own EOL
523    /// instead of the block's fixed `right` column.
524    fn block_to_eol(&self) -> bool;
525    /// Set the VisualBlock ragged (`$`) flag.
526    fn set_block_to_eol(&mut self, to_eol: bool);
527
528    // ─── Yank / register staging ───────────────────────────────────────────
529
530    /// Set the pending `"r` register selector without consuming it.
531    fn set_pending_register_raw(&mut self, reg: Option<char>);
532    /// Take (and clear) the pending `"r` register selector.
533    fn take_pending_register_raw(&mut self) -> Option<char>;
534
535    // ─── Macro recording / replay ──────────────────────────────────────────
536
537    /// Register currently being recorded into via `q{reg}`, if any.
538    fn recording_macro(&self) -> Option<char>;
539    /// Set (or clear) the register being recorded into.
540    fn set_recording_macro(&mut self, reg: Option<char>);
541    /// Append an input to the in-flight macro recording.
542    fn push_recording_key(&mut self, input: Input);
543    /// Take (and clear) the recorded macro keys.
544    fn take_recording_keys(&mut self) -> Vec<Input>;
545    /// Replace the recorded macro keys wholesale.
546    fn set_recording_keys(&mut self, keys: Vec<Input>);
547    /// Number of keys recorded so far.
548    fn recording_keys_len(&self) -> usize;
549    /// Whether a macro is currently being replayed.
550    fn is_replaying_macro_raw(&self) -> bool;
551    /// Set the macro-replay flag.
552    fn set_replaying_macro_raw(&mut self, v: bool);
553    /// The last macro register played, for `@@`.
554    fn last_macro(&self) -> Option<char>;
555    /// Set the last macro register played.
556    fn set_last_macro(&mut self, reg: Option<char>);
557
558    // ─── Last insert / visual / viewport ───────────────────────────────────
559
560    /// Position where the last insert session ended (`gi`).
561    fn last_insert_pos(&self) -> Option<(usize, usize)>;
562    /// Set the last insert-session end position.
563    fn set_last_insert_pos(&mut self, pos: Option<(usize, usize)>);
564    /// Snapshot of the last visual selection, for `gv`.
565    fn last_visual(&self) -> Option<LastVisual>;
566    /// Set the last-visual snapshot.
567    fn set_last_visual(&mut self, snap: Option<LastVisual>);
568    /// Whether `Ctrl-R` is armed and awaiting a register name.
569    fn insert_pending_register(&self) -> bool;
570    /// Set the `Ctrl-R` pending-register flag.
571    fn set_insert_pending_register(&mut self, v: bool);
572
573    // ─── Change-mark start ─────────────────────────────────────────────────
574
575    /// The stashed `[` mark start for a Change operation, or `None`.
576    fn change_mark_start(&self) -> Option<(usize, usize)>;
577    /// Take (and clear) the stashed `[` mark start.
578    fn take_change_mark_start(&mut self) -> Option<(usize, usize)>;
579    /// Set the stashed `[` mark start.
580    fn set_change_mark_start(&mut self, pos: Option<(usize, usize)>);
581
582    // ─── Visual / motion / search primitives ───────────────────────────────
583    //
584    // Vim *semantics* — motions, operators over selections, block-edge insert,
585    // search entry. These do not belong on a mode-agnostic rope editor; the
586    // engine keeps the raw buffer primitives (cursor, line reads, edits) and
587    // the vim discipline layers meaning on top (#265 / #267).
588
589    /// `true` when the editor is in any visual mode (Visual / VisualLine /
590    /// VisualBlock).
591    fn is_visual(&self) -> bool;
592
593    /// Apply `op` over `motion` with `count` repetitions, taking the full
594    /// vim-quirks path (operator context for `l`, clamping, etc.).
595    fn apply_op_with_motion_direct(&mut self, op: Operator, motion: &Motion, count: usize);
596
597    /// `Ctrl-a` / `Ctrl-x` — adjust the number under or after the cursor.
598    /// `delta = 1` increments, `-1` decrements; larger deltas multiply as in
599    /// vim's `5<C-a>`.
600    fn adjust_number(&mut self, delta: i64);
601
602    /// Open the `/` or `?` search prompt. `forward = true` for `/`.
603    fn enter_search(&mut self, forward: bool);
604
605    /// `d/pat` / `c/pat` / `y/pat` — open the search prompt in operator-pending
606    /// mode so the operator applies over the range to the match on commit.
607    fn enter_search_op(&mut self, forward: bool, op: Operator, count: usize);
608
609    /// Apply a pending operator-search over the exclusive charwise range from
610    /// `origin` to the current cursor (the just-found match position).
611    fn apply_op_search_range(&mut self, op: Operator, origin: (usize, usize));
612
613    /// VisualBlock `I` — enter Insert at the left edge of the block.
614    /// `count` repeats the typed text on every row (`[count]I`).
615    fn visual_block_insert_at_left(&mut self, top: usize, bot: usize, col: usize, count: usize);
616
617    /// VisualBlock `A` — enter Insert at the right edge of the block.
618    /// `col` is the append/typed column (one past the block's right edge,
619    /// or the ragged `$` column); `left` is the block's own left edge,
620    /// where the cursor lands on Esc (verified against real nvim — `A`'s
621    /// post-Esc cursor is NOT `col` on a block wider than one column).
622    /// `count` repeats the typed text on every row (`[count]A`).
623    fn visual_block_append_at_right(
624        &mut self,
625        top: usize,
626        bot: usize,
627        col: usize,
628        left: usize,
629        count: usize,
630    );
631
632    /// Execute a motion, pushing to the jumplist for big jumps and updating the
633    /// sticky column.
634    fn execute_motion(&mut self, motion: Motion, count: usize);
635
636    /// Update the VisualBlock virtual column after a motion. Horizontal motions
637    /// sync `block_vcol` to the cursor column; vertical motions leave it alone
638    /// so the intended column survives clamping to shorter rows.
639    fn update_block_vcol(&mut self, motion: &Motion);
640
641    /// Apply `op` over the current visual selection (char-wise, linewise, or
642    /// block).
643    fn apply_visual_operator(&mut self, op: Operator, count: usize);
644
645    /// VisualBlock `r<ch>` — replace every character cell in the block with
646    /// `ch`.
647    fn replace_block_char(&mut self, ch: char);
648
649    /// Charwise (`v`) / linewise (`V`) Visual-mode `r<ch>` — replace every
650    /// character in the selection with `ch` (B2). Newlines are preserved;
651    /// registers are untouched.
652    fn visual_replace_char(&mut self, ch: char);
653
654    /// Visual-mode `i<ch>` / `a<ch>` — extend the selection to cover the text
655    /// object identified by `ch`.
656    fn visual_text_obj_extend(&mut self, ch: char, inner: bool);
657
658    // ─── Insert-mode primitives ────────────────────────────────────────────
659    //
660    // Each wraps a `crate::vim::insert_*_bridge` and, when the bridge
661    // reports a mutation, runs the post-mutation sync (dirty mark, insert-row
662    // widening, scrolloff correction). Callers must ensure the editor is in
663    // Insert (or Replace) mode first.
664
665    /// Insert `ch` at the cursor. In Replace mode, overstrike the cell under
666    /// the cursor instead; at end-of-line, always appends. With `smartindent`,
667    /// closing brackets trigger a one-unit dedent on an otherwise-whitespace
668    /// line.
669    fn insert_char(&mut self, ch: char);
670    /// Insert a newline, applying autoindent / smartindent.
671    fn insert_newline(&mut self);
672    /// Insert a tab (or spaces to the next `softtabstop` boundary under
673    /// `expandtab`).
674    fn insert_tab(&mut self);
675    /// Backspace. Deletes a whole soft-tab run at an aligned boundary under
676    /// `softtabstop`; joins with the previous line at column 0.
677    fn insert_backspace(&mut self);
678    /// Delete the char under the cursor; joins with the next line at EOL.
679    fn insert_delete(&mut self);
680    /// Arrow-key motion in Insert, breaking the undo group per
681    /// `undo_break_on_motion`.
682    fn insert_arrow(&mut self, dir: InsertDir);
683    /// Home in Insert.
684    fn insert_home(&mut self);
685    /// End in Insert.
686    fn insert_end(&mut self);
687    /// PageUp in Insert.
688    fn insert_pageup(&mut self, viewport_h: u16);
689    /// PageDown in Insert.
690    fn insert_pagedown(&mut self, viewport_h: u16);
691    /// `Ctrl-W` — delete the word before the cursor.
692    fn insert_ctrl_w(&mut self);
693    /// `Ctrl-U` — delete to the start of the line.
694    fn insert_ctrl_u(&mut self);
695    /// `Ctrl-H` — backspace equivalent.
696    fn insert_ctrl_h(&mut self);
697    /// `Ctrl-O` — arm a one-shot Normal-mode command.
698    fn insert_ctrl_o_arm(&mut self);
699    /// `Ctrl-R` — arm register paste; the next char names the register.
700    fn insert_ctrl_r_arm(&mut self);
701    /// `Ctrl-T` — indent the current line one `shiftwidth`.
702    fn insert_ctrl_t(&mut self);
703    /// `Ctrl-D` — dedent the current line one `shiftwidth`.
704    fn insert_ctrl_d(&mut self);
705    /// `Ctrl-A` — insert the text typed during the most recent insert
706    /// session (vim's "." register).
707    fn insert_ctrl_a(&mut self);
708    /// `Ctrl-E` — insert the char in the same column of the line below.
709    fn insert_ctrl_e(&mut self);
710    /// `Ctrl-Y` — insert the char in the same column of the line above.
711    fn insert_ctrl_y(&mut self);
712    /// Paste register `reg` at the cursor (the `Ctrl-R` follow-up).
713    fn insert_paste_register(&mut self, reg: char);
714    /// `Ctrl-[` — expand any pending abbreviation (Esc-equivalent trigger).
715    fn insert_ctrl_bracket(&mut self);
716    /// Esc from Insert — end the insert session and return to Normal.
717    fn leave_insert_to_normal(&mut self);
718
719    // ─── Insert-mode entry ─────────────────────────────────────────────────
720
721    /// `i` — insert before the cursor, `count` times on commit.
722    fn enter_insert_i(&mut self, count: usize);
723    /// `I` — insert at the first non-blank of the line.
724    fn enter_insert_shift_i(&mut self, count: usize);
725    /// `a` — append after the cursor.
726    fn enter_insert_a(&mut self, count: usize);
727    /// `A` — append at end-of-line.
728    fn enter_insert_shift_a(&mut self, count: usize);
729    /// `o` — open a new line below and insert.
730    fn open_line_below(&mut self, count: usize);
731    /// `O` — open a new line above and insert.
732    fn open_line_above(&mut self, count: usize);
733    /// `R` — enter Replace mode.
734    fn enter_replace_mode(&mut self, count: usize);
735
736    // ─── Normal-mode edit primitives ───────────────────────────────────────
737
738    /// `x` — delete `count` chars forward.
739    fn delete_char_forward(&mut self, count: usize);
740    /// `X` — delete `count` chars backward.
741    fn delete_char_backward(&mut self, count: usize);
742    /// `s` — substitute `count` chars (delete then insert).
743    fn substitute_char(&mut self, count: usize);
744    /// `S` — substitute whole lines.
745    fn substitute_line(&mut self, count: usize);
746    /// `D` — delete to end-of-line (`[count]D` extends down count-1 lines).
747    fn delete_to_eol(&mut self, count: usize);
748    /// `C` — change to end-of-line (`[count]C` extends down count-1 lines).
749    fn change_to_eol(&mut self, count: usize);
750    /// `Y` — yank to end-of-line.
751    fn yank_to_eol(&mut self, count: usize);
752    /// `J` — join `count` lines.
753    fn join_line(&mut self, count: usize);
754    /// `~` — toggle case of `count` chars, advancing right.
755    fn toggle_case_at_cursor(&mut self, count: usize);
756
757    // ─── Vim mark commands ─────────────────────────────────────────────────
758    //
759    // Mark *storage* (`Editor::mark` / `set_mark` / `marks()` / `file_marks()`
760    // / the global-mark map) stays on the engine: a mark is a positional
761    // bookmark, which is an editor concern that other seams already consume
762    // (hjkl-ex backs `:marks` and `'a` line addressing with it, and LSP /
763    // quickfix / bookmark features could too).
764    //
765    // What lives here is the vim *command* layer on top of that storage — the
766    // `m` / `'` / `` ` `` keybindings, which decide linewise vs charwise jump
767    // and push the jumplist. That is vim semantics, not bookmark storage.
768
769    /// `.` — dot-repeat: replay the last buffered change at the cursor. A
770    /// non-zero `count` *replaces* the change's stored count (`:h .` — `3x`
771    /// then `2.` deletes 2, not 6); `count == 0` means no explicit count.
772    fn replay_last_change(&mut self, count: usize);
773
774    /// `m{ch}` — record a mark named `ch` at the current cursor position.
775    /// Invalid chars are silently ignored.
776    fn set_mark_at_cursor(&mut self, ch: char);
777
778    /// `'{ch}` — jump to mark `ch`, linewise (row, first non-blank). Pushes the
779    /// pre-jump position onto the jumplist if the cursor actually moved.
780    fn goto_mark_line(&mut self, ch: char);
781
782    /// `` `{ch} `` — jump to mark `ch`, charwise (exact row + col). Pushes the
783    /// pre-jump position onto the jumplist if the cursor actually moved.
784    fn goto_mark_char(&mut self, ch: char);
785
786    /// Like [`VimEditorExt::goto_mark_line`], but reports cross-buffer jumps:
787    /// uppercase marks (`'A'`–`'Z'`) living in another buffer return
788    /// [`MarkJump::CrossBuffer`] so the app can switch slots first.
789    fn try_goto_mark_line(&mut self, ch: char) -> MarkJump;
790
791    /// Charwise counterpart of [`VimEditorExt::try_goto_mark_line`].
792    fn try_goto_mark_char(&mut self, ch: char) -> MarkJump;
793
794    // ─── Vim FSM state accessors (pending chord, count, mode, macros) ──────
795    //
796    // The FSM in this crate reads and writes VimState through these. They are
797    // pure vim state, so they belong here rather than on the mode-agnostic
798    // engine core (#267).
799
800    /// Return a clone of the current pending chord state.
801    fn pending(&self) -> crate::vim::Pending;
802
803    /// Overwrite the pending chord state.
804    fn set_pending(&mut self, p: crate::vim::Pending);
805
806    /// Atomically take the pending chord, replacing it with `Pending::None`.
807    fn take_pending(&mut self) -> crate::vim::Pending;
808
809    /// Return the raw digit-prefix count (`0` = no prefix typed yet).
810    fn count(&self) -> usize;
811
812    /// Overwrite the digit-prefix count directly. Clamped at
813    /// [`crate::vim::MAX_COUNT`] (vim's documented count ceiling, `:h count`).
814    fn set_count(&mut self, c: usize);
815
816    /// Accumulate one more digit into the count prefix (mirrors `count * 10 + digit`).
817    fn accumulate_count_digit(&mut self, digit: usize);
818
819    /// Reset the count prefix to zero (no pending count).
820    fn reset_count(&mut self);
821
822    /// Consume the count and return it; resets to zero. Returns `1` when no
823    /// prefix was typed (mirrors `take_count` in vim.rs).
824    fn take_count(&mut self) -> usize;
825
826    /// Return the FSM-internal mode (Normal / Insert / Visual / …).
827    fn fsm_mode(&self) -> crate::vim::Mode;
828
829    /// Overwrite the FSM-internal mode without side-effects. Prefer the
830    /// semantic primitives (`enter_insert_i`, `enter_visual_char`, …).
831    fn set_fsm_mode(&mut self, m: crate::vim::Mode);
832
833    /// `true` while the `.` dot-repeat replay is running.
834    fn is_replaying(&self) -> bool;
835
836    /// Set or clear the dot-replay flag.
837    fn set_replaying(&mut self, v: bool);
838
839    /// `true` when we entered Normal from Insert via `Ctrl-o` and will return
840    /// to Insert after the next complete command.
841    fn is_one_shot_normal(&self) -> bool;
842
843    /// Set or clear the Ctrl-o one-shot-normal flag.
844    fn set_one_shot_normal(&mut self, v: bool);
845
846    /// Return the last `f`/`F`/`t`/`T` target as `(char, forward, till)`, or
847    /// `None` before any find command was executed.
848    fn last_find(&self) -> Option<(char, bool, bool)>;
849
850    /// Overwrite the stored last-find target.
851    fn set_last_find(&mut self, target: Option<(char, bool, bool)>);
852
853    /// Perform a vim-sneak style two-char digraph jump. Scans the buffer
854    /// from the current cursor for the `count`-th occurrence of `c1+c2`.
855    /// `forward=true` searches ahead; `forward=false` searches backward.
856    /// Respects `Settings::motion_sneak` — callers (hjkl-vim FSM) should
857    /// already gate on the setting; this method always executes the sneak.
858    fn sneak(&mut self, c1: char, c2: char, forward: bool, count: usize);
859
860    /// Apply an operator over a sneak digraph range. Charwise exclusive —
861    /// deletes from cursor up to (not including) the first char of the match.
862    fn apply_op_sneak(
863        &mut self,
864        op: crate::vim::Operator,
865        c1: char,
866        c2: char,
867        forward: bool,
868        total_count: usize,
869    );
870
871    /// Return the last sneak digraph and direction stored after a sneak motion.
872    /// `Some(((c1, c2), forward))` when a sneak has been performed this session;
873    /// `None` before any sneak. Used by `;`/`,` repeat and tests.
874    fn last_sneak(&self) -> Option<((char, char), bool)>;
875
876    /// Return a clone of the last recorded mutating change, or `None` before
877    /// any change has been made.
878    fn last_change(&self) -> Option<crate::vim::LastChange>;
879
880    /// Overwrite the stored last-change record.
881    fn set_last_change(&mut self, lc: Option<crate::vim::LastChange>);
882
883    /// Borrow the last-change record mutably (e.g. to fill in an `inserted`
884    /// field after the insert session completes).
885    fn last_change_mut(&mut self) -> Option<&mut crate::vim::LastChange>;
886
887    /// Borrow the active insert session, or `None` when not in Insert mode.
888    fn insert_session(&self) -> Option<&crate::vim::InsertSession>;
889
890    /// Borrow the active insert session mutably.
891    fn insert_session_mut(&mut self) -> Option<&mut crate::vim::InsertSession>;
892
893    /// Atomically take the insert session out, leaving `None`.
894    fn take_insert_session(&mut self) -> Option<crate::vim::InsertSession>;
895
896    /// Install a new insert session, replacing any existing one.
897    fn set_insert_session(&mut self, s: Option<crate::vim::InsertSession>);
898
899    // ─── Register selection / chord status / macro controller ──────────────
900
901    /// Return the user's pending register selection (set via `"<reg>` chord
902    /// before an operator). `None` if no register was selected — caller should
903    /// use the unnamed register `"`.
904    ///
905    /// Read-only — does not consume / clear the pending selection. The
906    /// register is cleared by the engine after the next operator fires.
907    ///
908    /// Promoted in 0.6.X for Phase 4e to let the App's visual-op dispatch arm
909    /// honor `"a` + visual op chord sequences.
910    fn pending_register(&self) -> Option<char>;
911
912    /// True when the user's pending register selector is `+` or `*`.
913    /// the host peeks this so it can refresh `sync_clipboard_register`
914    /// only when a clipboard read is actually about to happen.
915    fn pending_register_is_clipboard(&self) -> bool;
916
917    /// Register currently being recorded into via `q{reg}`. `None` when
918    /// no recording is active. Hosts use this to surface a "recording @r"
919    /// indicator in the status line.
920    fn recording_register(&self) -> Option<char>;
921
922    /// Pending repeat count the user has typed but not yet resolved
923    /// (e.g. pressing `5` before `d`). `None` when nothing is pending.
924    /// Hosts surface this in a "showcmd" area.
925    fn pending_count(&self) -> Option<u32>;
926
927    /// The operator character for any in-flight operator that is waiting
928    /// for a motion (e.g. `d` after the user types `d` but before a
929    /// motion). Returns `None` when no operator is pending.
930    fn pending_op(&self) -> Option<char>;
931
932    /// `true` when the engine is in any pending chord state — waiting for
933    /// the next key to complete a command (e.g. `r<char>` replace,
934    /// `f<char>` find, `m<a>` set-mark, `'<a>` goto-mark, operator-pending
935    /// after `d` / `c` / `y`, `g`-prefix continuation, `z`-prefix continuation,
936    /// register selection `"<reg>`, macro recording target, etc).
937    ///
938    /// Hosts use this to bypass their own chord dispatch (keymap tries, etc.)
939    /// and forward keys directly to the engine so in-flight commands can
940    /// complete without the host eating their continuation keys.
941    fn is_chord_pending(&self) -> bool;
942
943    /// `true` when `insert_ctrl_r_arm()` has been called and the dispatcher
944    /// is waiting for the next typed character to name the register to paste.
945    /// The dispatcher should call `insert_paste_register(c)` instead of
946    /// `insert_char(c)` for the next printable key, then the flag auto-clears.
947    ///
948    /// Phase 6.5: exposed so the app-level `dispatch_insert_key` can branch
949    /// without having to drive the full FSM.
950    fn is_insert_register_pending(&self) -> bool;
951
952    /// Clear the `Ctrl-R` register-paste pending flag. Call this immediately
953    /// before `insert_paste_register(c)` in app-level dispatchers so that the
954    /// flag does not persist into the next key. Call before
955    /// `insert_paste_register_bridge` (which `hjkl_vim::insert` does).
956    ///
957    /// Phase 6.5: used by `dispatch_insert_key` in the app crate.
958    fn clear_insert_register_pending(&mut self);
959
960    /// Set `vim.pending_register` to `Some(reg)` if `reg` is a valid register
961    /// selector (`a`–`z`, `A`–`Z`, `0`–`9`, `"`, `+`, `*`, `_`). Invalid
962    /// chars are silently ignored (no-op), matching the engine FSM's
963    /// `handle_select_register` behaviour.
964    ///
965    /// Promoted to the public surface in 0.5.17 so the hjkl-vim
966    /// `PendingState::SelectRegister` reducer can dispatch `SetPendingRegister`
967    /// without re-entering the engine FSM. `handle_select_register` (engine FSM
968    /// path for macro-replay / defensive coverage) delegates here to avoid
969    /// logic duplication.
970    fn set_pending_register(&mut self, reg: char);
971
972    /// Begin recording keystrokes into register `reg`. The caller (app) is
973    /// responsible for stopping the recording via `stop_macro_record` when the
974    /// user presses bare `q`.
975    ///
976    /// - Uppercase `reg` (e.g. `'A'`) appends to the existing lowercase
977    ///   recording by pre-seeding `recording_keys` with the decoded text of the
978    ///   matching lowercase register, matching vim's capital-register append
979    ///   semantics.
980    /// - Lowercase `reg` clears `recording_keys` (fresh recording).
981    /// - Invalid chars (non-alphabetic, non-digit) are silently ignored.
982    ///
983    /// Promoted to the public surface in Phase 5b so the app's
984    /// `route_chord_key` can start a recording without re-entering the engine
985    /// FSM. `handle_record_macro_target` (engine FSM path for macro-replay
986    /// defensive coverage) continues to use the same logic via delegation.
987    fn start_macro_record(&mut self, reg: char);
988
989    /// Finalize the active recording: encode `recording_keys` as text and write
990    /// to the matching (lowercase) named register. Clears both `recording_macro`
991    /// and `recording_keys`. No-ops if no recording is active.
992    ///
993    /// Promoted to the public surface in Phase 5b so the app's `QChord` action
994    /// can stop a recording when the user presses bare `q` without re-entering
995    /// the engine FSM.
996    fn stop_macro_record(&mut self);
997
998    /// Returns `true` while a `q{reg}` recording is in progress.
999    /// Hosts use this to show a "recording @r" status indicator and to decide
1000    /// whether bare `q` should stop the recording or open the `RecordMacroTarget`
1001    /// chord.
1002    fn is_recording_macro(&self) -> bool;
1003
1004    /// Returns `true` while a macro is being replayed. The app sets this flag
1005    /// (via `play_macro`) and clears it (via `end_macro_replay`) around the
1006    /// re-feed loop so the recorder hook can skip double-capture.
1007    fn is_replaying_macro(&self) -> bool;
1008
1009    /// Decode the named register `reg` into a `Vec<hjkl_engine::input::Input>` and
1010    /// prepare for replay, returning ONE iteration of the inputs the app
1011    /// should re-feed through `route_chord_key`.
1012    ///
1013    /// Count semantics live in the HOST: `3@a` replays the returned keys
1014    /// three times by looping (or re-splicing a work queue), never by
1015    /// materializing `keys × count` up front — an unclamped `999999999@a`
1016    /// would otherwise allocate multi-GB before the first key plays
1017    /// (audit R2).
1018    ///
1019    /// Resolves `reg`:
1020    /// - `'@'` → use `vim.last_macro`; returns empty vec if none.
1021    /// - Any other char → lowercase it, read the register, decode.
1022    ///
1023    /// Side-effects:
1024    /// - Sets `vim.last_macro` to the resolved register.
1025    /// - Sets `vim.replaying_macro = true` so the recorder hook skips during
1026    ///   replay. The app calls `end_macro_replay` after the loop finishes.
1027    ///
1028    /// Returns an empty vec (and no side-effects for `'@'`) if the register is
1029    /// unset or empty.
1030    fn play_macro(&mut self, reg: char) -> Vec<hjkl_engine::input::Input>;
1031
1032    /// Clear the `replaying_macro` flag. Called by the app after the
1033    /// re-feed loop in the `PlayMacro` commit arm completes (or aborts).
1034    fn end_macro_replay(&mut self);
1035
1036    /// Append `input` to the active recording (`recording_keys`) if and only
1037    /// if a recording is in progress AND we are not currently replaying.
1038    /// Called by the app's `route_chord_key` recorder hook so that user
1039    /// keystrokes captured through the app-level chord path are recorded
1040    /// (rather than relying solely on the engine FSM's in-step hook).
1041    fn record_input(&mut self, input: hjkl_engine::input::Input);
1042
1043    // ─── Mode reset / mouse-driven selection / operator range probe ────────
1044
1045    /// Force back to Normal mode (used when dismissing completions etc.).
1046    fn force_normal(&mut self);
1047
1048    /// Handle a left-button click at doc-space `(row, col)`. Exits Visual mode
1049    /// if active, breaks the insert-mode undo group (vim parity for
1050    /// `undo_break_on_motion`), then moves the cursor. The EOL clamp is
1051    /// mode-aware (neovim parity): Normal/Visual cap at `len - 1`, Insert
1052    /// allows the one-past-EOL position.
1053    fn mouse_click_doc(&mut self, row: usize, col: usize);
1054
1055    /// Begin a mouse-drag selection: anchor at the cursor and enter
1056    /// Visual-char mode. Idempotent if already in Visual-char.
1057    fn mouse_begin_drag(&mut self);
1058
1059    /// Dry-run `motion_key` and return the `(min_row, max_row)` span between
1060    /// the cursor row and the motion's target row, restoring the cursor
1061    /// afterwards. `None` when `motion_key` is not a known motion.
1062    fn range_for_op_motion(
1063        &mut self,
1064        motion_key: char,
1065        total_count: usize,
1066    ) -> Option<(usize, usize)>;
1067
1068    // ─── Motion dispatch / operator range probes ───────────────────────────
1069
1070    /// Execute a named cursor motion `kind`, repeated `count` times. Maps the
1071    /// keymap-layer `MotionKind` onto the vim motion primitives, bypassing the
1072    /// FSM. Identical cursor semantics to the FSM path — sticky column, scroll
1073    /// sync and big-jump tracking all apply.
1074    fn apply_motion(&mut self, kind: MotionKind, count: usize);
1075
1076    /// Dry-run a `g`-prefixed motion and return `(min_row, max_row)` — for
1077    /// `=gg` / `=gj` etc. `None` for unknown `ch`. The cursor is restored.
1078    fn range_for_op_g(&mut self, ch: char, total_count: usize) -> Option<(usize, usize)>;
1079
1080    /// Dry-run a text object and return `(min_row, max_row)` — for `=iw` /
1081    /// `=ap` etc. `None` for unknown `ch`.
1082    fn range_for_op_text_obj(
1083        &self,
1084        ch: char,
1085        inner: bool,
1086        total_count: usize,
1087    ) -> Option<(usize, usize)>;
1088}
1089
1090impl<H: Host> VimEditorExt for Editor<hjkl_buffer::View, H> {
1091    fn visual_block_bounds(&self) -> (usize, usize, usize, usize) {
1092        let (ar, ac) = crate::vim_state::vim(self).block_anchor;
1093        let (cr, _) = self.cursor();
1094        let cc = crate::vim_state::vim(self).block_vcol;
1095        (ar.min(cr), ar.max(cr), ac.min(cc), ac.max(cc))
1096    }
1097
1098    fn block_highlight(&self) -> Option<(usize, usize, usize, usize)> {
1099        if self.vim_mode() != VimMode::VisualBlock {
1100            return None;
1101        }
1102        let (ar, ac) = crate::vim_state::vim(self).block_anchor;
1103        let cr = self.cursor().0;
1104        let cc = crate::vim_state::vim(self).block_vcol;
1105        Some((ar.min(cr), ar.max(cr), ac.min(cc), ac.max(cc)))
1106    }
1107
1108    fn char_highlight(&self) -> Option<((usize, usize), (usize, usize))> {
1109        if self.vim_mode() != VimMode::Visual {
1110            return None;
1111        }
1112        let anchor = crate::vim_state::vim(self).visual_anchor;
1113        let cursor = self.cursor();
1114        let (start, end) = if anchor <= cursor {
1115            (anchor, cursor)
1116        } else {
1117            (cursor, anchor)
1118        };
1119        if self.settings().selection_exclusive {
1120            // Half-open: start..end (end excluded). Empty when start == end.
1121            if start == end {
1122                return None;
1123            }
1124            Some((start, end))
1125        } else {
1126            // Inclusive (vim default): both endpoints are selected.
1127            Some((start, end))
1128        }
1129    }
1130
1131    fn visual_char_range_exclusive(&self) -> Option<((usize, usize), (usize, usize))> {
1132        if self.vim_mode() != VimMode::Visual {
1133            return None;
1134        }
1135        let anchor = crate::vim_state::vim(self).visual_anchor;
1136        let cursor = self.cursor();
1137        if anchor == cursor {
1138            return None;
1139        }
1140        let (start, end) = if anchor <= cursor {
1141            (anchor, cursor)
1142        } else {
1143            (cursor, anchor)
1144        };
1145        Some((start, end))
1146    }
1147
1148    fn line_highlight(&self) -> Option<(usize, usize)> {
1149        if self.vim_mode() != VimMode::VisualLine {
1150            return None;
1151        }
1152        let anchor = crate::vim_state::vim(self).visual_line_anchor;
1153        let cursor = self.cursor().0;
1154        Some((anchor.min(cursor), anchor.max(cursor)))
1155    }
1156
1157    fn buffer_selection(&self) -> Option<hjkl_buffer::Selection> {
1158        use hjkl_buffer::{Position, Selection};
1159        let (cr, cc) = self.cursor();
1160        match self.vim_mode() {
1161            VimMode::Visual => {
1162                let (ar, ac) = crate::vim_state::vim(self).visual_anchor;
1163                let head = Position::new(cr, cc);
1164                if self.settings().selection_exclusive {
1165                    // Exclusive (VSCode bar-caret): render the half-open char set
1166                    // [start, end) so the cell under the caret is NOT highlighted.
1167                    // The buffer-tui renderer paints `row_span` inclusively, so
1168                    // drop one char off the max end. Empty selection → no
1169                    // highlight (caller is effectively in Insert).
1170                    let anchor_pos = Position::new(ar, ac);
1171                    if anchor_pos == head {
1172                        return None;
1173                    }
1174                    let (start, end) = if (ar, ac) <= (head.row, head.col) {
1175                        (anchor_pos, head)
1176                    } else {
1177                        (head, anchor_pos)
1178                    };
1179                    return Some(Selection::Char {
1180                        anchor: start,
1181                        head: dec_pos_one_char(self, end),
1182                    });
1183                }
1184                Some(Selection::Char {
1185                    anchor: Position::new(ar, ac),
1186                    head,
1187                })
1188            }
1189            VimMode::VisualLine => Some(Selection::Line {
1190                anchor_row: crate::vim_state::vim(self).visual_line_anchor,
1191                head_row: cr,
1192            }),
1193            VimMode::VisualBlock => {
1194                let (ar, ac) = crate::vim_state::vim(self).block_anchor;
1195                let vcol = crate::vim_state::vim(self).block_vcol;
1196                if crate::vim_state::vim(self).block_to_eol {
1197                    // Ragged (`$` — `:h v_b_$`): `Selection::Block::row_span`
1198                    // only ever resolves one fixed `(left, right)` pair for
1199                    // every row. Reuse the SAME `usize::MAX` "cap at the
1200                    // row's actual length" convention `Selection::Line`
1201                    // already uses (see `hjkl_buffer::selection::RowSpan`)
1202                    // by forcing the right corner's col to `usize::MAX` —
1203                    // the renderer then extends every row to its own EOL.
1204                    // Normalise which corner carries `MAX` so it's always
1205                    // the "right" one regardless of anchor/cursor order.
1206                    let left = ac.min(vcol);
1207                    return Some(Selection::Block {
1208                        anchor: Position::new(ar, left),
1209                        head: Position::new(cr, usize::MAX),
1210                    });
1211                }
1212                Some(Selection::Block {
1213                    anchor: Position::new(ar, ac),
1214                    head: Position::new(cr, vcol),
1215                })
1216            }
1217            _ => None,
1218        }
1219    }
1220
1221    fn selection_highlight(&self) -> Option<Highlight> {
1222        let sel = self.buffer_selection()?;
1223        let (start, end) = match sel {
1224            hjkl_buffer::Selection::Char { anchor, head } => {
1225                let a = (anchor.row, anchor.col);
1226                let h = (head.row, head.col);
1227                if a <= h { (a, h) } else { (h, a) }
1228            }
1229            hjkl_buffer::Selection::Line {
1230                anchor_row,
1231                head_row,
1232            } => {
1233                let (top, bot) = if anchor_row <= head_row {
1234                    (anchor_row, head_row)
1235                } else {
1236                    (head_row, anchor_row)
1237                };
1238                let last_col = self.line(bot).map_or(0, |l| l.len());
1239                ((top, 0), (bot, last_col))
1240            }
1241            hjkl_buffer::Selection::Block { anchor, head } => {
1242                let (top, bot) = if anchor.row <= head.row {
1243                    (anchor.row, head.row)
1244                } else {
1245                    (head.row, anchor.row)
1246                };
1247                let (left, right) = if anchor.col <= head.col {
1248                    (anchor.col, head.col)
1249                } else {
1250                    (head.col, anchor.col)
1251                };
1252                ((top, left), (bot, right))
1253            }
1254        };
1255        Some(Highlight {
1256            range: Pos {
1257                line: start.0 as u32,
1258                col: start.1 as u32,
1259            }..Pos {
1260                line: end.0 as u32,
1261                col: end.1 as u32,
1262            },
1263            kind: HighlightKind::Selection,
1264        })
1265    }
1266
1267    // ─── Text-object resolution ────────────────────────────────────────────
1268
1269    fn text_object_inner_word(&self) -> Option<((usize, usize), (usize, usize))> {
1270        crate::vim::text_object_inner_word_bridge(self)
1271    }
1272
1273    fn text_object_around_word(&self) -> Option<((usize, usize), (usize, usize))> {
1274        crate::vim::text_object_around_word_bridge(self)
1275    }
1276
1277    fn text_object_inner_big_word(&self) -> Option<((usize, usize), (usize, usize))> {
1278        crate::vim::text_object_inner_big_word_bridge(self)
1279    }
1280
1281    fn text_object_around_big_word(&self) -> Option<((usize, usize), (usize, usize))> {
1282        crate::vim::text_object_around_big_word_bridge(self)
1283    }
1284
1285    fn text_object_inner_quote(&self, quote: char) -> Option<((usize, usize), (usize, usize))> {
1286        crate::vim::text_object_inner_quote_bridge(self, quote)
1287    }
1288
1289    fn text_object_around_quote(&self, quote: char) -> Option<((usize, usize), (usize, usize))> {
1290        crate::vim::text_object_around_quote_bridge(self, quote)
1291    }
1292
1293    fn text_object_inner_bracket(&self, open: char) -> Option<((usize, usize), (usize, usize))> {
1294        crate::vim::text_object_inner_bracket_bridge(self, open)
1295    }
1296
1297    fn text_object_around_bracket(&self, open: char) -> Option<((usize, usize), (usize, usize))> {
1298        crate::vim::text_object_around_bracket_bridge(self, open)
1299    }
1300
1301    fn text_object_inner_sentence(&self) -> Option<((usize, usize), (usize, usize))> {
1302        crate::vim::text_object_inner_sentence_bridge(self)
1303    }
1304
1305    fn text_object_around_sentence(&self) -> Option<((usize, usize), (usize, usize))> {
1306        crate::vim::text_object_around_sentence_bridge(self)
1307    }
1308
1309    fn text_object_inner_paragraph(&self) -> Option<((usize, usize), (usize, usize))> {
1310        crate::vim::text_object_inner_paragraph_bridge(self)
1311    }
1312
1313    fn text_object_around_paragraph(&self) -> Option<((usize, usize), (usize, usize))> {
1314        crate::vim::text_object_around_paragraph_bridge(self)
1315    }
1316
1317    fn text_object_inner_tag(&self) -> Option<((usize, usize), (usize, usize))> {
1318        crate::vim::text_object_inner_tag_bridge(self)
1319    }
1320
1321    fn text_object_around_tag(&self) -> Option<((usize, usize), (usize, usize))> {
1322        crate::vim::text_object_around_tag_bridge(self)
1323    }
1324
1325    // ─── Range-mutation primitives ─────────────────────────────────────────
1326
1327    fn delete_range(
1328        &mut self,
1329        start: (usize, usize),
1330        end: (usize, usize),
1331        kind: RangeKind,
1332        register: char,
1333    ) {
1334        crate::vim::delete_range_bridge(self, start, end, kind, register);
1335    }
1336
1337    fn yank_range(
1338        &mut self,
1339        start: (usize, usize),
1340        end: (usize, usize),
1341        kind: RangeKind,
1342        register: char,
1343    ) {
1344        crate::vim::yank_range_bridge(self, start, end, kind, register);
1345    }
1346
1347    fn change_range(
1348        &mut self,
1349        start: (usize, usize),
1350        end: (usize, usize),
1351        kind: RangeKind,
1352        register: char,
1353    ) {
1354        crate::vim::change_range_bridge(self, start, end, kind, register);
1355    }
1356
1357    fn indent_range(
1358        &mut self,
1359        start: (usize, usize),
1360        end: (usize, usize),
1361        count: i32,
1362        shiftwidth: u32,
1363    ) {
1364        crate::vim::indent_range_bridge(self, start, end, count, shiftwidth);
1365    }
1366
1367    fn case_range(
1368        &mut self,
1369        start: (usize, usize),
1370        end: (usize, usize),
1371        kind: RangeKind,
1372        op: Operator,
1373    ) {
1374        crate::vim::case_range_bridge(self, start, end, kind, op);
1375    }
1376
1377    // ─── Block-shape range-mutation primitives ─────────────────────────────
1378
1379    fn delete_block(
1380        &mut self,
1381        top_row: usize,
1382        bot_row: usize,
1383        left_col: usize,
1384        right_col: usize,
1385        register: char,
1386    ) {
1387        crate::vim::delete_block_bridge(self, top_row, bot_row, left_col, right_col, register);
1388    }
1389
1390    fn yank_block(
1391        &mut self,
1392        top_row: usize,
1393        bot_row: usize,
1394        left_col: usize,
1395        right_col: usize,
1396        register: char,
1397    ) {
1398        crate::vim::yank_block_bridge(self, top_row, bot_row, left_col, right_col, register);
1399    }
1400
1401    fn change_block(
1402        &mut self,
1403        top_row: usize,
1404        bot_row: usize,
1405        left_col: usize,
1406        right_col: usize,
1407        register: char,
1408    ) {
1409        crate::vim::change_block_bridge(self, top_row, bot_row, left_col, right_col, register);
1410    }
1411
1412    fn indent_block(
1413        &mut self,
1414        top_row: usize,
1415        bot_row: usize,
1416        _left_col: usize,
1417        _right_col: usize,
1418        count: i32,
1419    ) {
1420        crate::vim::indent_block_bridge(self, top_row, bot_row, count);
1421    }
1422
1423    fn auto_indent_range(&mut self, start: (usize, usize), end: (usize, usize)) {
1424        crate::vim::auto_indent_range_bridge(self, start, end);
1425    }
1426
1427    // ─── Paste ─────────────────────────────────────────────────────────────
1428
1429    fn paste_after(&mut self, count: usize) {
1430        crate::vim::paste_after_bridge(self, count);
1431    }
1432
1433    fn paste_before(&mut self, count: usize) {
1434        crate::vim::paste_before_bridge(self, count);
1435    }
1436
1437    fn paste_cursor_after(&mut self, before: bool, count: usize) {
1438        crate::vim::paste_bridge(self, before, count, true, false);
1439    }
1440
1441    fn paste_reindent(&mut self, before: bool, count: usize) {
1442        crate::vim::paste_bridge(self, before, count, false, true);
1443    }
1444
1445    fn visual_paste(&mut self, before: bool) {
1446        crate::vim::visual_paste(self, before);
1447    }
1448
1449    // ─── Visual-mode operators ─────────────────────────────────────────────
1450
1451    fn adjust_number_visual(&mut self, delta: i64, sequential: bool) {
1452        crate::vim::adjust_number_visual(self, delta, sequential);
1453    }
1454
1455    fn ampersand_repeat(&mut self) {
1456        crate::vim::ampersand_repeat(self);
1457    }
1458
1459    fn visual_join(&mut self, with_space: bool) {
1460        crate::vim::visual_join(self, with_space);
1461    }
1462
1463    fn goto_percent(&mut self, count: usize) {
1464        crate::vim::goto_percent(self, count);
1465    }
1466
1467    // ─── Jumplist motion ───────────────────────────────────────────────────
1468
1469    fn jump_back(&mut self, count: usize) {
1470        crate::vim::jump_back_bridge(self, count);
1471    }
1472
1473    fn jump_forward(&mut self, count: usize) {
1474        crate::vim::jump_forward_bridge(self, count);
1475    }
1476
1477    // ─── Search ────────────────────────────────────────────────────────────
1478
1479    fn search_repeat(&mut self, forward: bool, count: usize) {
1480        crate::vim::search_repeat_bridge(self, forward, count);
1481    }
1482
1483    fn word_search(&mut self, forward: bool, whole_word: bool, count: usize) {
1484        crate::vim::word_search_bridge(self, forward, whole_word, count);
1485    }
1486
1487    // ─── Chord appliers ────────────────────────────────────────────────────
1488
1489    fn replace_char_at(&mut self, ch: char, count: usize) {
1490        crate::vim::replace_char(self, ch, count);
1491    }
1492
1493    fn find_char(&mut self, ch: char, forward: bool, till: bool, count: usize) {
1494        crate::vim::apply_find_char(self, ch, forward, till, count.max(1));
1495    }
1496
1497    fn after_g(&mut self, ch: char, count: usize) {
1498        crate::vim::apply_after_g(self, ch, count);
1499    }
1500
1501    fn after_z(&mut self, ch: char, count: usize) {
1502        crate::vim::apply_after_z(self, ch, count);
1503    }
1504
1505    // ─── Operator dispatch ─────────────────────────────────────────────────
1506
1507    fn apply_op_motion(&mut self, op: Operator, motion_key: char, total_count: usize) {
1508        crate::vim::apply_op_motion_key(self, op, motion_key, total_count);
1509    }
1510
1511    fn apply_op_double(&mut self, op: Operator, total_count: usize) {
1512        crate::vim::apply_op_double(self, op, total_count);
1513    }
1514
1515    fn apply_op_find(&mut self, op: Operator, ch: char, forward: bool, till: bool, count: usize) {
1516        crate::vim::apply_op_find_motion(self, op, ch, forward, till, count);
1517    }
1518
1519    fn apply_op_text_obj(&mut self, op: Operator, ch: char, inner: bool, total_count: usize) {
1520        crate::vim::apply_op_text_obj_inner(self, op, ch, inner, total_count);
1521    }
1522
1523    fn apply_op_g(&mut self, op: Operator, ch: char, total_count: usize) {
1524        crate::vim::apply_op_g_inner(self, op, ch, total_count);
1525    }
1526
1527    // ─── Mode transitions ──────────────────────────────────────────────────
1528
1529    fn vim_mode(&self) -> VimMode {
1530        crate::vim_state::vim(self).current_mode
1531    }
1532
1533    fn enter_visual_char(&mut self) {
1534        crate::vim::enter_visual_char_bridge(self);
1535    }
1536
1537    fn enter_visual_line(&mut self) {
1538        crate::vim::enter_visual_line_bridge(self);
1539    }
1540
1541    fn enter_visual_block(&mut self) {
1542        crate::vim::enter_visual_block_bridge(self);
1543    }
1544
1545    fn exit_visual_to_normal(&mut self) {
1546        crate::vim::exit_visual_to_normal_bridge(self);
1547    }
1548
1549    fn visual_o_toggle(&mut self) {
1550        crate::vim::visual_o_toggle_bridge(self);
1551    }
1552
1553    fn reenter_last_visual(&mut self) {
1554        crate::vim::reenter_last_visual_bridge(self);
1555    }
1556
1557    fn set_mode(&mut self, mode: VimMode) {
1558        crate::vim::set_mode_bridge(self, mode);
1559    }
1560
1561    // ─── Visual anchors ────────────────────────────────────────────────────
1562
1563    fn visual_anchor(&self) -> (usize, usize) {
1564        crate::vim_state::vim(self).visual_anchor
1565    }
1566    fn set_visual_anchor(&mut self, anchor: (usize, usize)) {
1567        crate::vim_state::vim_mut(self).visual_anchor = anchor;
1568    }
1569    fn visual_line_anchor(&self) -> usize {
1570        crate::vim_state::vim(self).visual_line_anchor
1571    }
1572    fn set_visual_line_anchor(&mut self, row: usize) {
1573        crate::vim_state::vim_mut(self).visual_line_anchor = row;
1574    }
1575    fn block_anchor(&self) -> (usize, usize) {
1576        crate::vim_state::vim(self).block_anchor
1577    }
1578    fn set_block_anchor(&mut self, anchor: (usize, usize)) {
1579        crate::vim_state::vim_mut(self).block_anchor = anchor;
1580    }
1581    fn block_vcol(&self) -> usize {
1582        crate::vim_state::vim(self).block_vcol
1583    }
1584    fn set_block_vcol(&mut self, vcol: usize) {
1585        crate::vim_state::vim_mut(self).block_vcol = vcol;
1586    }
1587    fn block_to_eol(&self) -> bool {
1588        crate::vim_state::vim(self).block_to_eol
1589    }
1590    fn set_block_to_eol(&mut self, to_eol: bool) {
1591        crate::vim_state::vim_mut(self).block_to_eol = to_eol;
1592    }
1593
1594    // ─── Yank / register staging ───────────────────────────────────────────
1595
1596    fn set_pending_register_raw(&mut self, reg: Option<char>) {
1597        crate::vim_state::vim_mut(self).pending_register = reg;
1598    }
1599    fn take_pending_register_raw(&mut self) -> Option<char> {
1600        crate::vim_state::vim_mut(self).pending_register.take()
1601    }
1602
1603    // ─── Macro recording / replay ──────────────────────────────────────────
1604
1605    fn recording_macro(&self) -> Option<char> {
1606        crate::vim_state::vim(self).recording_macro
1607    }
1608    fn set_recording_macro(&mut self, reg: Option<char>) {
1609        crate::vim_state::vim_mut(self).recording_macro = reg;
1610    }
1611    fn push_recording_key(&mut self, input: Input) {
1612        crate::vim_state::vim_mut(self).recording_keys.push(input);
1613    }
1614    fn take_recording_keys(&mut self) -> Vec<Input> {
1615        std::mem::take(&mut crate::vim_state::vim_mut(self).recording_keys)
1616    }
1617    fn set_recording_keys(&mut self, keys: Vec<Input>) {
1618        crate::vim_state::vim_mut(self).recording_keys = keys;
1619    }
1620    fn recording_keys_len(&self) -> usize {
1621        crate::vim_state::vim(self).recording_keys.len()
1622    }
1623    fn is_replaying_macro_raw(&self) -> bool {
1624        crate::vim_state::vim(self).replaying_macro
1625    }
1626    fn set_replaying_macro_raw(&mut self, v: bool) {
1627        crate::vim_state::vim_mut(self).replaying_macro = v;
1628    }
1629    fn last_macro(&self) -> Option<char> {
1630        crate::vim_state::vim(self).last_macro
1631    }
1632    fn set_last_macro(&mut self, reg: Option<char>) {
1633        crate::vim_state::vim_mut(self).last_macro = reg;
1634    }
1635
1636    // ─── Last insert / visual / viewport ───────────────────────────────────
1637
1638    fn last_insert_pos(&self) -> Option<(usize, usize)> {
1639        crate::vim_state::vim(self).last_insert_pos
1640    }
1641    fn set_last_insert_pos(&mut self, pos: Option<(usize, usize)>) {
1642        crate::vim_state::vim_mut(self).last_insert_pos = pos;
1643    }
1644    fn last_visual(&self) -> Option<LastVisual> {
1645        crate::vim_state::vim(self).last_visual
1646    }
1647    fn set_last_visual(&mut self, snap: Option<LastVisual>) {
1648        crate::vim_state::vim_mut(self).last_visual = snap;
1649    }
1650    fn insert_pending_register(&self) -> bool {
1651        crate::vim_state::vim(self).insert_pending_register
1652    }
1653    fn set_insert_pending_register(&mut self, v: bool) {
1654        crate::vim_state::vim_mut(self).insert_pending_register = v;
1655    }
1656
1657    // ─── Change-mark start ─────────────────────────────────────────────────
1658
1659    fn change_mark_start(&self) -> Option<(usize, usize)> {
1660        crate::vim_state::vim(self).change_mark_start
1661    }
1662    fn take_change_mark_start(&mut self) -> Option<(usize, usize)> {
1663        crate::vim_state::vim_mut(self).change_mark_start.take()
1664    }
1665    fn set_change_mark_start(&mut self, pos: Option<(usize, usize)>) {
1666        crate::vim_state::vim_mut(self).change_mark_start = pos;
1667    }
1668
1669    // ─── Visual / motion / search primitives ───────────────────────────────
1670
1671    fn is_visual(&self) -> bool {
1672        matches!(
1673            crate::vim_state::vim(self).mode,
1674            FsmMode::Visual | FsmMode::VisualLine | FsmMode::VisualBlock
1675        )
1676    }
1677
1678    fn apply_op_with_motion_direct(&mut self, op: Operator, motion: &Motion, count: usize) {
1679        crate::vim::apply_op_with_motion(self, op, motion, count);
1680    }
1681
1682    fn adjust_number(&mut self, delta: i64) {
1683        crate::vim::adjust_number(self, delta);
1684    }
1685
1686    fn enter_search(&mut self, forward: bool) {
1687        crate::vim::enter_search(self, forward);
1688    }
1689
1690    fn enter_search_op(&mut self, forward: bool, op: Operator, count: usize) {
1691        crate::vim::enter_search_op(self, forward, op, count);
1692    }
1693
1694    fn apply_op_search_range(&mut self, op: Operator, origin: (usize, usize)) {
1695        crate::vim::apply_op_search_range(self, op, origin);
1696    }
1697
1698    fn visual_block_insert_at_left(&mut self, top: usize, bot: usize, col: usize, count: usize) {
1699        self.jump_cursor(top, col);
1700        crate::vim_state::vim_mut(self).mode = FsmMode::Normal;
1701        crate::vim::begin_insert(
1702            self,
1703            count,
1704            InsertReason::BlockEdge {
1705                top,
1706                bot,
1707                col,
1708                pad: false,
1709                // `I`'s insertion point already IS the block's left edge —
1710                // but `leave_insert_to_normal_bridge` unconditionally steps
1711                // the cursor back one column after Esc (vim's generic
1712                // leave-insert adjustment), so store one PAST the target
1713                // and let that step-back land exactly on `col`. Verified
1714                // against real nvim at a non-zero left edge (the only
1715                // existing case started at col 0, where the step-back's
1716                // `col > 0` guard happened to no-op and masked this).
1717                cursor_col: col + 1,
1718            },
1719        );
1720    }
1721
1722    fn visual_block_append_at_right(
1723        &mut self,
1724        top: usize,
1725        bot: usize,
1726        col: usize,
1727        left: usize,
1728        count: usize,
1729    ) {
1730        // vim `v_b_A`: pad the top row to `col` with spaces before the
1731        // cursor lands there, same as `replicate_block_text` does for
1732        // every other row on Esc. Without this, `jump_cursor` clamps
1733        // `col` down to the row's current length and the typed text
1734        // lands inside the block instead of past its right edge.
1735        //
1736        // The pad must land in the SAME undo group as the insert session
1737        // (vim's block `A` is one `u` step — pad, typed text, and the
1738        // replicated rows all revert together). So push the undo
1739        // checkpoint ourselves before padding, then use
1740        // `begin_insert_noundo` — mirrors the `Operator::Change` block
1741        // path (push_undo, mutate, begin_insert_noundo) in `visual_ops`.
1742        self.push_undo();
1743        let line_len = hjkl_engine::buf_helpers::buf_line_chars(self.buffer(), top);
1744        if col > line_len {
1745            let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1746            self.mutate_edit(hjkl_buffer::Edit::InsertStr {
1747                at: hjkl_buffer::Position::new(top, line_len),
1748                text: pad,
1749            });
1750        }
1751        self.jump_cursor(top, col);
1752        crate::vim_state::vim_mut(self).mode = FsmMode::Normal;
1753        crate::vim::begin_insert_noundo(
1754            self,
1755            count,
1756            InsertReason::BlockEdge {
1757                top,
1758                bot,
1759                col,
1760                pad: true,
1761                // Same "one past the target, let the generic Esc step-back
1762                // land exactly there" convention as `visual_block_insert_
1763                // at_left` — see its comment.
1764                cursor_col: left + 1,
1765            },
1766        );
1767    }
1768
1769    fn execute_motion(&mut self, motion: Motion, count: usize) {
1770        crate::vim::execute_motion(self, motion, count);
1771    }
1772
1773    fn update_block_vcol(&mut self, motion: &Motion) {
1774        crate::vim::update_block_vcol(self, motion);
1775    }
1776
1777    fn apply_visual_operator(&mut self, op: Operator, count: usize) {
1778        crate::vim::apply_visual_operator(self, op, count);
1779    }
1780
1781    fn replace_block_char(&mut self, ch: char) {
1782        crate::vim::block_replace(self, ch);
1783    }
1784
1785    fn visual_replace_char(&mut self, ch: char) {
1786        crate::vim::visual_replace_char(self, ch);
1787    }
1788
1789    fn visual_text_obj_extend(&mut self, ch: char, inner: bool) {
1790        let obj = match ch {
1791            'w' => TextObject::Word { big: false },
1792            'W' => TextObject::Word { big: true },
1793            '"' | '\'' | '`' => TextObject::Quote(ch),
1794            '(' | ')' | 'b' => TextObject::Bracket('('),
1795            '[' | ']' => TextObject::Bracket('['),
1796            '{' | '}' | 'B' => TextObject::Bracket('{'),
1797            '<' | '>' => TextObject::Bracket('<'),
1798            'p' => TextObject::Paragraph,
1799            't' => TextObject::XmlTag,
1800            's' => TextObject::Sentence,
1801            _ => return,
1802        };
1803        let Some((start, end, kind)) = crate::vim::text_object_range(self, obj, inner, 1) else {
1804            return;
1805        };
1806        // B6: `:h v_ip` — when the selection ALREADY exactly equals this
1807        // text object's natural bounds (the user is re-applying `ip`/`ap`/
1808        // etc. to a selection it already produced, e.g. `vipip`), the
1809        // object GROWS instead of re-selecting the identical (so
1810        // no-op-looking) range. `ip`/`ap` alternate paragraph and
1811        // blank-run units this way; growth is implemented generically here
1812        // by probing the SAME text object one row past the current end and
1813        // unioning the two ranges, rather than hand-rolling paragraph-
1814        // specific alternation logic.
1815        // Compare the SELECTION'S END (which the cursor always tracks, by
1816        // this function's own construction below) against the freshly
1817        // computed object's end — NOT the anchor. After a grow, the anchor
1818        // stays pinned at the FIRST application's start while the end keeps
1819        // moving, so an anchor-based check would only ever match once
1820        // (verified against real nvim: `vipipipd`, three applications,
1821        // grows a second time too — the anchor-based check breaks that).
1822        let already_matches = match kind {
1823            RangeKind::Linewise => {
1824                crate::vim_state::vim(self).mode == FsmMode::VisualLine && self.cursor().0 == end.0
1825            }
1826            _ => {
1827                crate::vim_state::vim(self).mode == FsmMode::Visual
1828                    && self.cursor() == crate::vim::retreat_one(self, end)
1829            }
1830        };
1831        // When growing, keep the EXISTING anchor (the first application's
1832        // start) — `start` above is the freshly computed single-object's
1833        // start (e.g. the blank run's own start on a second `ip`), which is
1834        // NOT where the accumulated selection began.
1835        //
1836        // The probe position differs by kind: Linewise units are probed one
1837        // ROW past the current end (`ip`/`ap` grow onto the next line);
1838        // charwise units (`iw`, quotes, brackets, …) are probed at `end`
1839        // directly — `end` for an Exclusive object already points ONE PAST
1840        // the last selected char, i.e. exactly where the next same-line unit
1841        // begins (verified against real nvim: `viwiw` on "foo bar baz"
1842        // grows "foo" to "foo " — the following WHITESPACE run on the SAME
1843        // row, not a jump to the next line).
1844        let (start, end) = if already_matches {
1845            let existing_start = match kind {
1846                RangeKind::Linewise => (crate::vim_state::vim(self).visual_line_anchor, 0),
1847                _ => crate::vim_state::vim(self).visual_anchor,
1848            };
1849            let probe = match kind {
1850                RangeKind::Linewise => (end.0 + 1, 0),
1851                _ => end,
1852            };
1853            let saved_cursor = self.cursor();
1854            self.jump_cursor(probe.0, probe.1);
1855            let grown = crate::vim::text_object_range(self, obj, inner, 1)
1856                .filter(|&(_, _, grown_kind)| grown_kind == kind)
1857                .map(|(_, grown_end, _)| grown_end);
1858            self.jump_cursor(saved_cursor.0, saved_cursor.1);
1859            match grown {
1860                Some(grown_end) if grown_end > end => (existing_start, grown_end),
1861                _ => (existing_start, end),
1862            }
1863        } else {
1864            (start, end)
1865        };
1866        // vim switches from visual-block to the text-object's native mode
1867        // (charwise for iw/aw/iquote/ibracket, linewise for ip/ap).  The
1868        // block anchor is meaningless for the resulting charwise/linewise
1869        // selection — replace it with the text object's computed start.
1870        if crate::vim_state::vim(self).mode == FsmMode::VisualBlock {
1871            match kind {
1872                RangeKind::Linewise => {
1873                    crate::vim_state::vim_mut(self).visual_line_anchor = start.0;
1874                    crate::vim_state::vim_mut(self).mode = FsmMode::VisualLine;
1875                    crate::vim_state::vim_mut(self).current_mode = VimMode::VisualLine;
1876                    self.jump_cursor(end.0, 0);
1877                }
1878                _ => {
1879                    crate::vim_state::vim_mut(self).mode = FsmMode::Visual;
1880                    crate::vim_state::vim_mut(self).current_mode = VimMode::Visual;
1881                    crate::vim_state::vim_mut(self).visual_anchor = (start.0, start.1);
1882                    let (er, ec) = crate::vim::retreat_one(self, end);
1883                    self.jump_cursor(er, ec);
1884                }
1885            }
1886            return;
1887        }
1888        match kind {
1889            RangeKind::Linewise => {
1890                crate::vim_state::vim_mut(self).visual_line_anchor = start.0;
1891                crate::vim_state::vim_mut(self).mode = FsmMode::VisualLine;
1892                crate::vim_state::vim_mut(self).current_mode = VimMode::VisualLine;
1893                self.jump_cursor(end.0, 0);
1894            }
1895            _ => {
1896                crate::vim_state::vim_mut(self).mode = FsmMode::Visual;
1897                crate::vim_state::vim_mut(self).current_mode = VimMode::Visual;
1898                crate::vim_state::vim_mut(self).visual_anchor = (start.0, start.1);
1899                let (er, ec) = crate::vim::retreat_one(self, end);
1900                self.jump_cursor(er, ec);
1901            }
1902        }
1903    }
1904
1905    // ─── Insert-mode primitives ────────────────────────────────────────────
1906
1907    fn insert_char(&mut self, ch: char) {
1908        if crate::vim::insert_char_bridge(self, ch) {
1909            after_insert_mutation(self);
1910        }
1911    }
1912
1913    fn insert_newline(&mut self) {
1914        if crate::vim::insert_newline_bridge(self) {
1915            after_insert_mutation(self);
1916        }
1917    }
1918
1919    fn insert_tab(&mut self) {
1920        if crate::vim::insert_tab_bridge(self) {
1921            after_insert_mutation(self);
1922        }
1923    }
1924
1925    fn insert_backspace(&mut self) {
1926        if crate::vim::insert_backspace_bridge(self) {
1927            after_insert_mutation(self);
1928        }
1929    }
1930
1931    fn insert_delete(&mut self) {
1932        if crate::vim::insert_delete_bridge(self) {
1933            after_insert_mutation(self);
1934        }
1935    }
1936
1937    fn insert_arrow(&mut self, dir: InsertDir) {
1938        crate::vim::insert_arrow_bridge(self, dir);
1939        after_insert_motion(self);
1940    }
1941
1942    fn insert_home(&mut self) {
1943        crate::vim::insert_home_bridge(self);
1944        after_insert_motion(self);
1945    }
1946
1947    fn insert_end(&mut self) {
1948        crate::vim::insert_end_bridge(self);
1949        after_insert_motion(self);
1950    }
1951
1952    fn insert_pageup(&mut self, viewport_h: u16) {
1953        crate::vim::insert_pageup_bridge(self, viewport_h);
1954        after_insert_motion(self);
1955    }
1956
1957    fn insert_pagedown(&mut self, viewport_h: u16) {
1958        crate::vim::insert_pagedown_bridge(self, viewport_h);
1959        after_insert_motion(self);
1960    }
1961
1962    fn insert_ctrl_w(&mut self) {
1963        if crate::vim::insert_ctrl_w_bridge(self) {
1964            after_insert_mutation(self);
1965        }
1966    }
1967
1968    fn insert_ctrl_u(&mut self) {
1969        if crate::vim::insert_ctrl_u_bridge(self) {
1970            after_insert_mutation(self);
1971        }
1972    }
1973
1974    fn insert_ctrl_h(&mut self) {
1975        if crate::vim::insert_ctrl_h_bridge(self) {
1976            after_insert_mutation(self);
1977        }
1978    }
1979
1980    fn insert_ctrl_o_arm(&mut self) {
1981        crate::vim::insert_ctrl_o_bridge(self);
1982    }
1983
1984    fn insert_ctrl_r_arm(&mut self) {
1985        crate::vim::insert_ctrl_r_bridge(self);
1986    }
1987
1988    fn insert_ctrl_t(&mut self) {
1989        // Indent-only: no scrolloff re-check (the cursor row does not move).
1990        let mutated = crate::vim::insert_ctrl_t_bridge(self);
1991        if mutated {
1992            self.mark_content_dirty();
1993            let (row, _) = self.cursor();
1994            crate::vim_state::vim_mut(self).widen_insert_row(row);
1995        }
1996    }
1997
1998    fn insert_ctrl_d(&mut self) {
1999        let mutated = crate::vim::insert_ctrl_d_bridge(self);
2000        if mutated {
2001            self.mark_content_dirty();
2002            let (row, _) = self.cursor();
2003            crate::vim_state::vim_mut(self).widen_insert_row(row);
2004        }
2005    }
2006
2007    fn insert_ctrl_a(&mut self) {
2008        if crate::vim::insert_ctrl_a_bridge(self) {
2009            after_insert_mutation(self);
2010        }
2011    }
2012
2013    fn insert_ctrl_e(&mut self) {
2014        if crate::vim::insert_ctrl_e_bridge(self) {
2015            after_insert_mutation(self);
2016        }
2017    }
2018
2019    fn insert_ctrl_y(&mut self) {
2020        if crate::vim::insert_ctrl_y_bridge(self) {
2021            after_insert_mutation(self);
2022        }
2023    }
2024
2025    fn insert_paste_register(&mut self, reg: char) {
2026        crate::vim::insert_paste_register_bridge(self, reg);
2027        let (row, _) = self.cursor();
2028        crate::vim_state::vim_mut(self).widen_insert_row(row);
2029    }
2030
2031    fn insert_ctrl_bracket(&mut self) {
2032        if crate::vim::check_and_apply_abbrev(self, AbbrevTrigger::CtrlBracket) {
2033            after_insert_mutation(self);
2034        }
2035    }
2036
2037    fn leave_insert_to_normal(&mut self) {
2038        crate::vim::leave_insert_to_normal_bridge(self);
2039    }
2040
2041    // ─── Insert-mode entry ─────────────────────────────────────────────────
2042
2043    fn enter_insert_i(&mut self, count: usize) {
2044        crate::vim::enter_insert_i_bridge(self, count);
2045    }
2046
2047    fn enter_insert_shift_i(&mut self, count: usize) {
2048        crate::vim::enter_insert_shift_i_bridge(self, count);
2049    }
2050
2051    fn enter_insert_a(&mut self, count: usize) {
2052        crate::vim::enter_insert_a_bridge(self, count);
2053    }
2054
2055    fn enter_insert_shift_a(&mut self, count: usize) {
2056        crate::vim::enter_insert_shift_a_bridge(self, count);
2057    }
2058
2059    fn open_line_below(&mut self, count: usize) {
2060        crate::vim::open_line_below_bridge(self, count);
2061    }
2062
2063    fn open_line_above(&mut self, count: usize) {
2064        crate::vim::open_line_above_bridge(self, count);
2065    }
2066
2067    fn enter_replace_mode(&mut self, count: usize) {
2068        crate::vim::enter_replace_mode_bridge(self, count);
2069    }
2070
2071    // ─── Normal-mode edit primitives ───────────────────────────────────────
2072
2073    fn delete_char_forward(&mut self, count: usize) {
2074        crate::vim::delete_char_forward_bridge(self, count);
2075    }
2076
2077    fn delete_char_backward(&mut self, count: usize) {
2078        crate::vim::delete_char_backward_bridge(self, count);
2079    }
2080
2081    fn substitute_char(&mut self, count: usize) {
2082        crate::vim::substitute_char_bridge(self, count);
2083    }
2084
2085    fn substitute_line(&mut self, count: usize) {
2086        crate::vim::substitute_line_bridge(self, count);
2087    }
2088
2089    fn delete_to_eol(&mut self, count: usize) {
2090        crate::vim::delete_to_eol_bridge(self, count);
2091    }
2092
2093    fn change_to_eol(&mut self, count: usize) {
2094        crate::vim::change_to_eol_bridge(self, count);
2095    }
2096
2097    fn yank_to_eol(&mut self, count: usize) {
2098        crate::vim::yank_to_eol_bridge(self, count);
2099    }
2100
2101    fn join_line(&mut self, count: usize) {
2102        crate::vim::join_line_bridge(self, count);
2103    }
2104
2105    fn toggle_case_at_cursor(&mut self, count: usize) {
2106        crate::vim::toggle_case_at_cursor_bridge(self, count);
2107    }
2108
2109    // ─── Vim mark commands ─────────────────────────────────────────────────
2110
2111    fn replay_last_change(&mut self, count: usize) {
2112        crate::vim::replay_last_change(self, count);
2113    }
2114
2115    fn set_mark_at_cursor(&mut self, ch: char) {
2116        crate::vim::set_mark_at_cursor(self, ch);
2117    }
2118
2119    fn goto_mark_line(&mut self, ch: char) {
2120        crate::vim::goto_mark(self, ch, true);
2121    }
2122
2123    fn goto_mark_char(&mut self, ch: char) {
2124        crate::vim::goto_mark(self, ch, false);
2125    }
2126
2127    fn try_goto_mark_line(&mut self, ch: char) -> MarkJump {
2128        crate::vim::try_goto_mark(self, ch, true)
2129    }
2130
2131    fn try_goto_mark_char(&mut self, ch: char) -> MarkJump {
2132        crate::vim::try_goto_mark(self, ch, false)
2133    }
2134
2135    // ─── Vim FSM state accessors ───────────────────────────────────────────
2136
2137    fn pending(&self) -> crate::vim::Pending {
2138        crate::vim_state::vim(self).pending.clone()
2139    }
2140
2141    fn set_pending(&mut self, p: crate::vim::Pending) {
2142        crate::vim_state::vim_mut(self).pending = p;
2143    }
2144
2145    fn take_pending(&mut self) -> crate::vim::Pending {
2146        std::mem::take(&mut crate::vim_state::vim_mut(self).pending)
2147    }
2148
2149    fn count(&self) -> usize {
2150        crate::vim_state::vim(self).count
2151    }
2152
2153    fn set_count(&mut self, c: usize) {
2154        crate::vim_state::vim_mut(self).count = c.min(crate::vim::MAX_COUNT);
2155    }
2156
2157    fn accumulate_count_digit(&mut self, digit: usize) {
2158        // Saturate the add too: once the multiply has saturated at
2159        // `usize::MAX`, a plain `+ digit` overflows (panic in debug builds)
2160        // after ~20 typed digits. Then clamp at vim's documented count
2161        // ceiling (`:h count`) so no apply loop can iterate more than
2162        // 999,999,999 times regardless of how many digits were typed.
2163        let v = crate::vim_state::vim_mut(self);
2164        v.count = v
2165            .count
2166            .saturating_mul(10)
2167            .saturating_add(digit)
2168            .min(crate::vim::MAX_COUNT);
2169    }
2170
2171    fn reset_count(&mut self) {
2172        crate::vim_state::vim_mut(self).count = 0;
2173    }
2174
2175    fn take_count(&mut self) -> usize {
2176        if crate::vim_state::vim(self).count > 0 {
2177            let n = crate::vim_state::vim(self).count;
2178            crate::vim_state::vim_mut(self).count = 0;
2179            n
2180        } else {
2181            1
2182        }
2183    }
2184
2185    fn fsm_mode(&self) -> crate::vim::Mode {
2186        crate::vim_state::vim(self).mode
2187    }
2188
2189    fn set_fsm_mode(&mut self, m: crate::vim::Mode) {
2190        crate::vim_state::vim_mut(self).mode = m;
2191        crate::vim_state::vim_mut(self).current_mode =
2192            crate::vim_state::vim_mut(self).public_mode();
2193    }
2194
2195    fn is_replaying(&self) -> bool {
2196        crate::vim_state::vim(self).replaying
2197    }
2198
2199    fn set_replaying(&mut self, v: bool) {
2200        crate::vim_state::vim_mut(self).replaying = v;
2201    }
2202
2203    fn is_one_shot_normal(&self) -> bool {
2204        crate::vim_state::vim(self).one_shot_normal
2205    }
2206
2207    fn set_one_shot_normal(&mut self, v: bool) {
2208        crate::vim_state::vim_mut(self).one_shot_normal = v;
2209    }
2210
2211    fn last_find(&self) -> Option<(char, bool, bool)> {
2212        crate::vim_state::vim(self).last_find
2213    }
2214
2215    fn set_last_find(&mut self, target: Option<(char, bool, bool)>) {
2216        crate::vim_state::vim_mut(self).last_find = target;
2217    }
2218
2219    fn sneak(&mut self, c1: char, c2: char, forward: bool, count: usize) {
2220        crate::vim::apply_sneak(self, c1, c2, forward, count.max(1));
2221    }
2222
2223    fn apply_op_sneak(
2224        &mut self,
2225        op: crate::vim::Operator,
2226        c1: char,
2227        c2: char,
2228        forward: bool,
2229        total_count: usize,
2230    ) {
2231        crate::vim::apply_op_sneak(self, op, c1, c2, forward, total_count);
2232    }
2233
2234    fn last_sneak(&self) -> Option<((char, char), bool)> {
2235        crate::vim_state::vim(self).last_sneak
2236    }
2237
2238    fn last_change(&self) -> Option<crate::vim::LastChange> {
2239        crate::vim_state::vim(self).last_change.clone()
2240    }
2241
2242    fn set_last_change(&mut self, lc: Option<crate::vim::LastChange>) {
2243        crate::vim_state::vim_mut(self).last_change = lc;
2244    }
2245
2246    fn last_change_mut(&mut self) -> Option<&mut crate::vim::LastChange> {
2247        crate::vim_state::vim_mut(self).last_change.as_mut()
2248    }
2249
2250    fn insert_session(&self) -> Option<&crate::vim::InsertSession> {
2251        crate::vim_state::vim(self).insert_session.as_ref()
2252    }
2253
2254    fn insert_session_mut(&mut self) -> Option<&mut crate::vim::InsertSession> {
2255        crate::vim_state::vim_mut(self).insert_session.as_mut()
2256    }
2257
2258    fn take_insert_session(&mut self) -> Option<crate::vim::InsertSession> {
2259        crate::vim_state::vim_mut(self).insert_session.take()
2260    }
2261
2262    fn set_insert_session(&mut self, s: Option<crate::vim::InsertSession>) {
2263        crate::vim_state::vim_mut(self).insert_session = s;
2264    }
2265
2266    // ─── Register selection / chord status / macro controller ──────────────
2267
2268    fn pending_register(&self) -> Option<char> {
2269        crate::vim_state::vim(self).pending_register
2270    }
2271
2272    fn pending_register_is_clipboard(&self) -> bool {
2273        matches!(
2274            crate::vim_state::vim(self).pending_register,
2275            Some('+') | Some('*')
2276        )
2277    }
2278
2279    fn recording_register(&self) -> Option<char> {
2280        crate::vim_state::vim(self).recording_macro
2281    }
2282
2283    fn pending_count(&self) -> Option<u32> {
2284        crate::vim_state::vim(self).pending_count_val()
2285    }
2286
2287    fn pending_op(&self) -> Option<char> {
2288        crate::vim_state::vim(self).pending_op_char()
2289    }
2290
2291    fn is_chord_pending(&self) -> bool {
2292        crate::vim_state::vim(self).is_chord_pending()
2293    }
2294
2295    fn is_insert_register_pending(&self) -> bool {
2296        crate::vim_state::vim(self).insert_pending_register
2297    }
2298
2299    fn clear_insert_register_pending(&mut self) {
2300        crate::vim_state::vim_mut(self).insert_pending_register = false;
2301    }
2302
2303    fn set_pending_register(&mut self, reg: char) {
2304        // `-` is the small-delete register (readable/pasteable, e.g. `"-p`).
2305        if reg.is_ascii_alphanumeric() || matches!(reg, '"' | '+' | '*' | '_' | '-') {
2306            crate::vim_state::vim_mut(self).pending_register = Some(reg);
2307        }
2308        // Invalid chars silently no-op (matches engine FSM behavior).
2309    }
2310
2311    fn start_macro_record(&mut self, reg: char) {
2312        if !(reg.is_ascii_alphabetic() || reg.is_ascii_digit()) {
2313            return;
2314        }
2315        crate::vim_state::vim_mut(self).recording_macro = Some(reg);
2316        if reg.is_ascii_uppercase() {
2317            // Seed recording_keys with the existing lowercase register's text
2318            // decoded back to inputs so capital-register append continues from
2319            // where the previous recording left off.
2320            let lower = reg.to_ascii_lowercase();
2321            let text = self
2322                .with_registers(|r| r.read(lower).map(|s| s.text.clone()))
2323                .unwrap_or_default();
2324            crate::vim_state::vim_mut(self).recording_keys =
2325                hjkl_engine::input::decode_macro(&text);
2326        } else {
2327            crate::vim_state::vim_mut(self).recording_keys.clear();
2328        }
2329    }
2330
2331    fn stop_macro_record(&mut self) {
2332        let Some(reg) = crate::vim_state::vim_mut(self).recording_macro.take() else {
2333            return;
2334        };
2335        let keys = std::mem::take(&mut crate::vim_state::vim_mut(self).recording_keys);
2336        let text = hjkl_engine::input::encode_macro(&keys);
2337        self.set_named_register_text(reg.to_ascii_lowercase(), text);
2338    }
2339
2340    fn is_recording_macro(&self) -> bool {
2341        crate::vim_state::vim(self).recording_macro.is_some()
2342    }
2343
2344    fn is_replaying_macro(&self) -> bool {
2345        crate::vim_state::vim(self).replaying_macro
2346    }
2347
2348    fn play_macro(&mut self, reg: char) -> Vec<hjkl_engine::input::Input> {
2349        let resolved = if reg == '@' {
2350            match crate::vim_state::vim(self).last_macro {
2351                Some(r) => r,
2352                None => return vec![],
2353            }
2354        } else {
2355            reg.to_ascii_lowercase()
2356        };
2357        let text = match self.with_registers(|regs| regs.read(resolved).cloned()) {
2358            Some(slot) if !slot.text.is_empty() => slot.text,
2359            _ => return vec![],
2360        };
2361        let keys = hjkl_engine::input::decode_macro(&text);
2362        crate::vim_state::vim_mut(self).last_macro = Some(resolved);
2363        crate::vim_state::vim_mut(self).replaying_macro = true;
2364        // ONE iteration only — the host loops the count (audit R2). The old
2365        // `keys.repeat(count)` materialized count × keys.len() Inputs up
2366        // front, so `999999999@a` allocated multi-GB before playing a key.
2367        keys
2368    }
2369
2370    fn end_macro_replay(&mut self) {
2371        crate::vim_state::vim_mut(self).replaying_macro = false;
2372    }
2373
2374    fn record_input(&mut self, input: hjkl_engine::input::Input) {
2375        if crate::vim_state::vim(self).recording_macro.is_some()
2376            && !crate::vim_state::vim(self).replaying_macro
2377        {
2378            crate::vim_state::vim_mut(self).recording_keys.push(input);
2379        }
2380    }
2381
2382    // ─── Mode reset / mouse-driven selection / operator range probe ────────
2383
2384    fn force_normal(&mut self) {
2385        crate::vim::force_normal_bridge(self);
2386    }
2387
2388    fn mouse_click_doc(&mut self, row: usize, col: usize) {
2389        crate::vim::mouse_click_doc_bridge(self, row, col);
2390    }
2391
2392    fn mouse_begin_drag(&mut self) {
2393        crate::vim::mouse_begin_drag_bridge(self);
2394    }
2395
2396    fn range_for_op_motion(
2397        &mut self,
2398        motion_key: char,
2399        total_count: usize,
2400    ) -> Option<(usize, usize)> {
2401        crate::vim::range_for_op_motion_bridge(self, motion_key, total_count)
2402    }
2403
2404    // ─── Motion dispatch / operator range probes ───────────────────────────
2405
2406    fn apply_motion(&mut self, kind: MotionKind, count: usize) {
2407        crate::vim::apply_motion_kind(self, kind, count);
2408    }
2409
2410    fn range_for_op_g(&mut self, ch: char, total_count: usize) -> Option<(usize, usize)> {
2411        crate::vim::range_for_op_g_bridge(self, ch, total_count)
2412    }
2413
2414    fn range_for_op_text_obj(
2415        &self,
2416        ch: char,
2417        inner: bool,
2418        total_count: usize,
2419    ) -> Option<(usize, usize)> {
2420        crate::vim::range_for_op_text_obj_bridge(self, ch, inner, total_count)
2421    }
2422}