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    /// Counted Visual-mode `i<ch>` / `a<ch>` extension.
659    ///
660    /// Defaults to the uncounted method so existing downstream implementations
661    /// remain source-compatible.
662    fn visual_text_obj_extend_counted(&mut self, ch: char, inner: bool, count: usize) {
663        let _ = count;
664        self.visual_text_obj_extend(ch, inner);
665    }
666
667    // ─── Insert-mode primitives ────────────────────────────────────────────
668    //
669    // Each wraps a `crate::vim::insert_*_bridge` and, when the bridge
670    // reports a mutation, runs the post-mutation sync (dirty mark, insert-row
671    // widening, scrolloff correction). Callers must ensure the editor is in
672    // Insert (or Replace) mode first.
673
674    /// Insert `ch` at the cursor. In Replace mode, overstrike the cell under
675    /// the cursor instead; at end-of-line, always appends. With `smartindent`,
676    /// closing brackets trigger a one-unit dedent on an otherwise-whitespace
677    /// line.
678    fn insert_char(&mut self, ch: char);
679    /// Insert a newline, applying autoindent / smartindent.
680    fn insert_newline(&mut self);
681    /// Insert a tab (or spaces to the next `softtabstop` boundary under
682    /// `expandtab`).
683    fn insert_tab(&mut self);
684    /// Backspace. Deletes a whole soft-tab run at an aligned boundary under
685    /// `softtabstop`; joins with the previous line at column 0.
686    fn insert_backspace(&mut self);
687    /// Delete the char under the cursor; joins with the next line at EOL.
688    fn insert_delete(&mut self);
689    /// Arrow-key motion in Insert, breaking the undo group per
690    /// `undo_break_on_motion`.
691    fn insert_arrow(&mut self, dir: InsertDir);
692    /// Home in Insert.
693    fn insert_home(&mut self);
694    /// End in Insert.
695    fn insert_end(&mut self);
696    /// PageUp in Insert.
697    fn insert_pageup(&mut self, viewport_h: u16);
698    /// PageDown in Insert.
699    fn insert_pagedown(&mut self, viewport_h: u16);
700    /// `Ctrl-W` — delete the word before the cursor.
701    fn insert_ctrl_w(&mut self);
702    /// `Ctrl-U` — delete to the start of the line.
703    fn insert_ctrl_u(&mut self);
704    /// `Ctrl-H` — backspace equivalent.
705    fn insert_ctrl_h(&mut self);
706    /// `Ctrl-O` — arm a one-shot Normal-mode command.
707    fn insert_ctrl_o_arm(&mut self);
708    /// `Ctrl-R` — arm register paste; the next char names the register.
709    fn insert_ctrl_r_arm(&mut self);
710    /// `Ctrl-T` — indent the current line one `shiftwidth`.
711    fn insert_ctrl_t(&mut self);
712    /// `Ctrl-D` — dedent the current line one `shiftwidth`.
713    fn insert_ctrl_d(&mut self);
714    /// `Ctrl-A` — insert the text typed during the most recent insert
715    /// session (vim's "." register).
716    fn insert_ctrl_a(&mut self);
717    /// `Ctrl-E` — insert the char in the same column of the line below.
718    fn insert_ctrl_e(&mut self);
719    /// `Ctrl-Y` — insert the char in the same column of the line above.
720    fn insert_ctrl_y(&mut self);
721    /// Paste register `reg` at the cursor (the `Ctrl-R` follow-up).
722    fn insert_paste_register(&mut self, reg: char);
723    /// `Ctrl-[` — expand any pending abbreviation (Esc-equivalent trigger).
724    fn insert_ctrl_bracket(&mut self);
725    /// Esc from Insert — end the insert session and return to Normal.
726    fn leave_insert_to_normal(&mut self);
727
728    // ─── Insert-mode entry ─────────────────────────────────────────────────
729
730    /// `i` — insert before the cursor, `count` times on commit.
731    fn enter_insert_i(&mut self, count: usize);
732    /// `I` — insert at the first non-blank of the line.
733    fn enter_insert_shift_i(&mut self, count: usize);
734    /// `a` — append after the cursor.
735    fn enter_insert_a(&mut self, count: usize);
736    /// `A` — append at end-of-line.
737    fn enter_insert_shift_a(&mut self, count: usize);
738    /// `o` — open a new line below and insert.
739    fn open_line_below(&mut self, count: usize);
740    /// `O` — open a new line above and insert.
741    fn open_line_above(&mut self, count: usize);
742    /// `R` — enter Replace mode.
743    fn enter_replace_mode(&mut self, count: usize);
744
745    // ─── Normal-mode edit primitives ───────────────────────────────────────
746
747    /// `x` — delete `count` chars forward.
748    fn delete_char_forward(&mut self, count: usize);
749    /// `X` — delete `count` chars backward.
750    fn delete_char_backward(&mut self, count: usize);
751    /// `s` — substitute `count` chars (delete then insert).
752    fn substitute_char(&mut self, count: usize);
753    /// `S` — substitute whole lines.
754    fn substitute_line(&mut self, count: usize);
755    /// `D` — delete to end-of-line (`[count]D` extends down count-1 lines).
756    fn delete_to_eol(&mut self, count: usize);
757    /// `C` — change to end-of-line (`[count]C` extends down count-1 lines).
758    fn change_to_eol(&mut self, count: usize);
759    /// `Y` — yank to end-of-line.
760    fn yank_to_eol(&mut self, count: usize);
761    /// `J` — join `count` lines.
762    fn join_line(&mut self, count: usize);
763    /// `~` — toggle case of `count` chars, advancing right.
764    fn toggle_case_at_cursor(&mut self, count: usize);
765
766    // ─── Vim mark commands ─────────────────────────────────────────────────
767    //
768    // Mark *storage* (`Editor::mark` / `set_mark` / `marks()` / `file_marks()`
769    // / the global-mark map) stays on the engine: a mark is a positional
770    // bookmark, which is an editor concern that other seams already consume
771    // (hjkl-ex backs `:marks` and `'a` line addressing with it, and LSP /
772    // quickfix / bookmark features could too).
773    //
774    // What lives here is the vim *command* layer on top of that storage — the
775    // `m` / `'` / `` ` `` keybindings, which decide linewise vs charwise jump
776    // and push the jumplist. That is vim semantics, not bookmark storage.
777
778    /// `.` — dot-repeat: replay the last buffered change at the cursor. A
779    /// non-zero `count` *replaces* the change's stored count (`:h .` — `3x`
780    /// then `2.` deletes 2, not 6); `count == 0` means no explicit count.
781    fn replay_last_change(&mut self, count: usize);
782
783    /// `m{ch}` — record a mark named `ch` at the current cursor position.
784    /// Invalid chars are silently ignored.
785    fn set_mark_at_cursor(&mut self, ch: char);
786
787    /// `'{ch}` — jump to mark `ch`, linewise (row, first non-blank). Pushes the
788    /// pre-jump position onto the jumplist if the cursor actually moved.
789    fn goto_mark_line(&mut self, ch: char);
790
791    /// `` `{ch} `` — jump to mark `ch`, charwise (exact row + col). Pushes the
792    /// pre-jump position onto the jumplist if the cursor actually moved.
793    fn goto_mark_char(&mut self, ch: char);
794
795    /// Like [`VimEditorExt::goto_mark_line`], but reports cross-buffer jumps:
796    /// uppercase marks (`'A'`–`'Z'`) living in another buffer return
797    /// [`MarkJump::CrossBuffer`] so the app can switch slots first.
798    fn try_goto_mark_line(&mut self, ch: char) -> MarkJump;
799
800    /// Charwise counterpart of [`VimEditorExt::try_goto_mark_line`].
801    fn try_goto_mark_char(&mut self, ch: char) -> MarkJump;
802
803    // ─── Vim FSM state accessors (pending chord, count, mode, macros) ──────
804    //
805    // The FSM in this crate reads and writes VimState through these. They are
806    // pure vim state, so they belong here rather than on the mode-agnostic
807    // engine core (#267).
808
809    /// Return a clone of the current pending chord state.
810    fn pending(&self) -> crate::vim::Pending;
811
812    /// Overwrite the pending chord state.
813    fn set_pending(&mut self, p: crate::vim::Pending);
814
815    /// Atomically take the pending chord, replacing it with `Pending::None`.
816    fn take_pending(&mut self) -> crate::vim::Pending;
817
818    /// Return the raw digit-prefix count (`0` = no prefix typed yet).
819    fn count(&self) -> usize;
820
821    /// Overwrite the digit-prefix count directly. Clamped at
822    /// [`crate::vim::MAX_COUNT`] (vim's documented count ceiling, `:h count`).
823    fn set_count(&mut self, c: usize);
824
825    /// Accumulate one more digit into the count prefix (mirrors `count * 10 + digit`).
826    fn accumulate_count_digit(&mut self, digit: usize);
827
828    /// Reset the count prefix to zero (no pending count).
829    fn reset_count(&mut self);
830
831    /// Consume the count and return it; resets to zero. Returns `1` when no
832    /// prefix was typed (mirrors `take_count` in vim.rs).
833    fn take_count(&mut self) -> usize;
834
835    /// Return the FSM-internal mode (Normal / Insert / Visual / …).
836    fn fsm_mode(&self) -> crate::vim::Mode;
837
838    /// Overwrite the FSM-internal mode without side-effects. Prefer the
839    /// semantic primitives (`enter_insert_i`, `enter_visual_char`, …).
840    fn set_fsm_mode(&mut self, m: crate::vim::Mode);
841
842    /// `true` while the `.` dot-repeat replay is running.
843    fn is_replaying(&self) -> bool;
844
845    /// Set or clear the dot-replay flag.
846    fn set_replaying(&mut self, v: bool);
847
848    /// `true` when we entered Normal from Insert via `Ctrl-o` and will return
849    /// to Insert after the next complete command.
850    fn is_one_shot_normal(&self) -> bool;
851
852    /// Set or clear the Ctrl-o one-shot-normal flag.
853    fn set_one_shot_normal(&mut self, v: bool);
854
855    /// Return the last `f`/`F`/`t`/`T` target as `(char, forward, till)`, or
856    /// `None` before any find command was executed.
857    fn last_find(&self) -> Option<(char, bool, bool)>;
858
859    /// Overwrite the stored last-find target.
860    fn set_last_find(&mut self, target: Option<(char, bool, bool)>);
861
862    /// Perform a vim-sneak style two-char digraph jump. Scans the buffer
863    /// from the current cursor for the `count`-th occurrence of `c1+c2`.
864    /// `forward=true` searches ahead; `forward=false` searches backward.
865    /// Respects `Settings::motion_sneak` — callers (hjkl-vim FSM) should
866    /// already gate on the setting; this method always executes the sneak.
867    fn sneak(&mut self, c1: char, c2: char, forward: bool, count: usize);
868
869    /// Apply an operator over a sneak digraph range. Charwise exclusive —
870    /// deletes from cursor up to (not including) the first char of the match.
871    fn apply_op_sneak(
872        &mut self,
873        op: crate::vim::Operator,
874        c1: char,
875        c2: char,
876        forward: bool,
877        total_count: usize,
878    );
879
880    /// Return the last sneak digraph and direction stored after a sneak motion.
881    /// `Some(((c1, c2), forward))` when a sneak has been performed this session;
882    /// `None` before any sneak. Used by `;`/`,` repeat and tests.
883    fn last_sneak(&self) -> Option<((char, char), bool)>;
884
885    /// Return a clone of the last recorded mutating change, or `None` before
886    /// any change has been made.
887    fn last_change(&self) -> Option<crate::vim::LastChange>;
888
889    /// Overwrite the stored last-change record.
890    fn set_last_change(&mut self, lc: Option<crate::vim::LastChange>);
891
892    /// Borrow the last-change record mutably (e.g. to fill in an `inserted`
893    /// field after the insert session completes).
894    fn last_change_mut(&mut self) -> Option<&mut crate::vim::LastChange>;
895
896    /// Borrow the active insert session, or `None` when not in Insert mode.
897    fn insert_session(&self) -> Option<&crate::vim::InsertSession>;
898
899    /// Borrow the active insert session mutably.
900    fn insert_session_mut(&mut self) -> Option<&mut crate::vim::InsertSession>;
901
902    /// Atomically take the insert session out, leaving `None`.
903    fn take_insert_session(&mut self) -> Option<crate::vim::InsertSession>;
904
905    /// Install a new insert session, replacing any existing one.
906    fn set_insert_session(&mut self, s: Option<crate::vim::InsertSession>);
907
908    // ─── Register selection / chord status / macro controller ──────────────
909
910    /// Return the user's pending register selection (set via `"<reg>` chord
911    /// before an operator). `None` if no register was selected — caller should
912    /// use the unnamed register `"`.
913    ///
914    /// Read-only — does not consume / clear the pending selection. The
915    /// register is cleared by the engine after the next operator fires.
916    ///
917    /// Promoted in 0.6.X for Phase 4e to let the App's visual-op dispatch arm
918    /// honor `"a` + visual op chord sequences.
919    fn pending_register(&self) -> Option<char>;
920
921    /// True when the user's pending register selector is `+` or `*`.
922    /// the host peeks this so it can refresh `sync_clipboard_register`
923    /// only when a clipboard read is actually about to happen.
924    fn pending_register_is_clipboard(&self) -> bool;
925
926    /// Register currently being recorded into via `q{reg}`. `None` when
927    /// no recording is active. Hosts use this to surface a "recording @r"
928    /// indicator in the status line.
929    fn recording_register(&self) -> Option<char>;
930
931    /// Pending repeat count the user has typed but not yet resolved
932    /// (e.g. pressing `5` before `d`). `None` when nothing is pending.
933    /// Hosts surface this in a "showcmd" area.
934    fn pending_count(&self) -> Option<u32>;
935
936    /// The operator character for any in-flight operator that is waiting
937    /// for a motion (e.g. `d` after the user types `d` but before a
938    /// motion). Returns `None` when no operator is pending.
939    fn pending_op(&self) -> Option<char>;
940
941    /// `true` when the engine is in any pending chord state — waiting for
942    /// the next key to complete a command (e.g. `r<char>` replace,
943    /// `f<char>` find, `m<a>` set-mark, `'<a>` goto-mark, operator-pending
944    /// after `d` / `c` / `y`, `g`-prefix continuation, `z`-prefix continuation,
945    /// register selection `"<reg>`, macro recording target, etc).
946    ///
947    /// Hosts use this to bypass their own chord dispatch (keymap tries, etc.)
948    /// and forward keys directly to the engine so in-flight commands can
949    /// complete without the host eating their continuation keys.
950    fn is_chord_pending(&self) -> bool;
951
952    /// `true` when `insert_ctrl_r_arm()` has been called and the dispatcher
953    /// is waiting for the next typed character to name the register to paste.
954    /// The dispatcher should call `insert_paste_register(c)` instead of
955    /// `insert_char(c)` for the next printable key, then the flag auto-clears.
956    ///
957    /// Phase 6.5: exposed so the app-level `dispatch_insert_key` can branch
958    /// without having to drive the full FSM.
959    fn is_insert_register_pending(&self) -> bool;
960
961    /// Clear the `Ctrl-R` register-paste pending flag. Call this immediately
962    /// before `insert_paste_register(c)` in app-level dispatchers so that the
963    /// flag does not persist into the next key. Call before
964    /// `insert_paste_register_bridge` (which `hjkl_vim::insert` does).
965    ///
966    /// Phase 6.5: used by `dispatch_insert_key` in the app crate.
967    fn clear_insert_register_pending(&mut self);
968
969    /// Set `vim.pending_register` to `Some(reg)` if `reg` is a valid register
970    /// selector (`a`–`z`, `A`–`Z`, `0`–`9`, `"`, `+`, `*`, `_`). Invalid
971    /// chars are silently ignored (no-op), matching the engine FSM's
972    /// `handle_select_register` behaviour.
973    ///
974    /// Promoted to the public surface in 0.5.17 so the hjkl-vim
975    /// `PendingState::SelectRegister` reducer can dispatch `SetPendingRegister`
976    /// without re-entering the engine FSM. `handle_select_register` (engine FSM
977    /// path for macro-replay / defensive coverage) delegates here to avoid
978    /// logic duplication.
979    fn set_pending_register(&mut self, reg: char);
980
981    /// Begin recording keystrokes into register `reg`. The caller (app) is
982    /// responsible for stopping the recording via `stop_macro_record` when the
983    /// user presses bare `q`.
984    ///
985    /// - Uppercase `reg` (e.g. `'A'`) appends to the existing lowercase
986    ///   recording by pre-seeding `recording_keys` with the decoded text of the
987    ///   matching lowercase register, matching vim's capital-register append
988    ///   semantics.
989    /// - Lowercase `reg` clears `recording_keys` (fresh recording).
990    /// - Invalid chars (non-alphabetic, non-digit) are silently ignored.
991    ///
992    /// Promoted to the public surface in Phase 5b so the app's
993    /// `route_chord_key` can start a recording without re-entering the engine
994    /// FSM. `handle_record_macro_target` (engine FSM path for macro-replay
995    /// defensive coverage) continues to use the same logic via delegation.
996    fn start_macro_record(&mut self, reg: char);
997
998    /// Finalize the active recording: encode `recording_keys` as text and write
999    /// to the matching (lowercase) named register. Clears both `recording_macro`
1000    /// and `recording_keys`. No-ops if no recording is active.
1001    ///
1002    /// Promoted to the public surface in Phase 5b so the app's `QChord` action
1003    /// can stop a recording when the user presses bare `q` without re-entering
1004    /// the engine FSM.
1005    fn stop_macro_record(&mut self);
1006
1007    /// Returns `true` while a `q{reg}` recording is in progress.
1008    /// Hosts use this to show a "recording @r" status indicator and to decide
1009    /// whether bare `q` should stop the recording or open the `RecordMacroTarget`
1010    /// chord.
1011    fn is_recording_macro(&self) -> bool;
1012
1013    /// Returns `true` while a macro is being replayed. The app sets this flag
1014    /// (via `play_macro`) and clears it (via `end_macro_replay`) around the
1015    /// re-feed loop so the recorder hook can skip double-capture.
1016    fn is_replaying_macro(&self) -> bool;
1017
1018    /// Decode the named register `reg` into a `Vec<hjkl_engine::input::Input>` and
1019    /// prepare for replay, returning ONE iteration of the inputs the app
1020    /// should re-feed through `route_chord_key`.
1021    ///
1022    /// Count semantics live in the HOST: `3@a` replays the returned keys
1023    /// three times by looping (or re-splicing a work queue), never by
1024    /// materializing `keys × count` up front — an unclamped `999999999@a`
1025    /// would otherwise allocate multi-GB before the first key plays
1026    /// (audit R2).
1027    ///
1028    /// Resolves `reg`:
1029    /// - `'@'` → use `vim.last_macro`; returns empty vec if none.
1030    /// - Any other char → lowercase it, read the register, decode.
1031    ///
1032    /// Side-effects:
1033    /// - Sets `vim.last_macro` to the resolved register.
1034    /// - Sets `vim.replaying_macro = true` so the recorder hook skips during
1035    ///   replay. The app calls `end_macro_replay` after the loop finishes.
1036    ///
1037    /// Returns an empty vec (and no side-effects for `'@'`) if the register is
1038    /// unset or empty.
1039    fn play_macro(&mut self, reg: char) -> Vec<hjkl_engine::input::Input>;
1040
1041    /// Clear the `replaying_macro` flag. Called by the app after the
1042    /// re-feed loop in the `PlayMacro` commit arm completes (or aborts).
1043    fn end_macro_replay(&mut self);
1044
1045    /// Append `input` to the active recording (`recording_keys`) if and only
1046    /// if a recording is in progress AND we are not currently replaying.
1047    /// Called by the app's `route_chord_key` recorder hook so that user
1048    /// keystrokes captured through the app-level chord path are recorded
1049    /// (rather than relying solely on the engine FSM's in-step hook).
1050    fn record_input(&mut self, input: hjkl_engine::input::Input);
1051
1052    // ─── Mode reset / mouse-driven selection / operator range probe ────────
1053
1054    /// Force back to Normal mode (used when dismissing completions etc.).
1055    fn force_normal(&mut self);
1056
1057    /// Handle a left-button click at doc-space `(row, col)`. Exits Visual mode
1058    /// if active, breaks the insert-mode undo group (vim parity for
1059    /// `undo_break_on_motion`), then moves the cursor. The EOL clamp is
1060    /// mode-aware (neovim parity): Normal/Visual cap at `len - 1`, Insert
1061    /// allows the one-past-EOL position.
1062    fn mouse_click_doc(&mut self, row: usize, col: usize);
1063
1064    /// Begin a mouse-drag selection: anchor at the cursor and enter
1065    /// Visual-char mode. Idempotent if already in Visual-char.
1066    fn mouse_begin_drag(&mut self);
1067
1068    /// Dry-run `motion_key` and return the `(min_row, max_row)` span between
1069    /// the cursor row and the motion's target row, restoring the cursor
1070    /// afterwards. `None` when `motion_key` is not a known motion.
1071    fn range_for_op_motion(
1072        &mut self,
1073        motion_key: char,
1074        total_count: usize,
1075    ) -> Option<(usize, usize)>;
1076
1077    // ─── Motion dispatch / operator range probes ───────────────────────────
1078
1079    /// Execute a named cursor motion `kind`, repeated `count` times. Maps the
1080    /// keymap-layer `MotionKind` onto the vim motion primitives, bypassing the
1081    /// FSM. Identical cursor semantics to the FSM path — sticky column, scroll
1082    /// sync and big-jump tracking all apply.
1083    fn apply_motion(&mut self, kind: MotionKind, count: usize);
1084
1085    /// Dry-run a `g`-prefixed motion and return `(min_row, max_row)` — for
1086    /// `=gg` / `=gj` etc. `None` for unknown `ch`. The cursor is restored.
1087    fn range_for_op_g(&mut self, ch: char, total_count: usize) -> Option<(usize, usize)>;
1088
1089    /// Dry-run a text object and return `(min_row, max_row)` — for `=iw` /
1090    /// `=ap` etc. `None` for unknown `ch`.
1091    fn range_for_op_text_obj(
1092        &self,
1093        ch: char,
1094        inner: bool,
1095        total_count: usize,
1096    ) -> Option<(usize, usize)>;
1097}
1098
1099impl<H: Host> VimEditorExt for Editor<hjkl_buffer::View, H> {
1100    fn visual_block_bounds(&self) -> (usize, usize, usize, usize) {
1101        let (ar, ac) = crate::vim_state::vim(self).block_anchor;
1102        let (cr, _) = self.cursor();
1103        let cc = crate::vim_state::vim(self).block_vcol;
1104        (ar.min(cr), ar.max(cr), ac.min(cc), ac.max(cc))
1105    }
1106
1107    fn block_highlight(&self) -> Option<(usize, usize, usize, usize)> {
1108        if self.vim_mode() != VimMode::VisualBlock {
1109            return None;
1110        }
1111        let (ar, ac) = crate::vim_state::vim(self).block_anchor;
1112        let cr = self.cursor().0;
1113        let cc = crate::vim_state::vim(self).block_vcol;
1114        Some((ar.min(cr), ar.max(cr), ac.min(cc), ac.max(cc)))
1115    }
1116
1117    fn char_highlight(&self) -> Option<((usize, usize), (usize, usize))> {
1118        if self.vim_mode() != VimMode::Visual {
1119            return None;
1120        }
1121        let anchor = crate::vim_state::vim(self).visual_anchor;
1122        let cursor = self.cursor();
1123        let (start, end) = if anchor <= cursor {
1124            (anchor, cursor)
1125        } else {
1126            (cursor, anchor)
1127        };
1128        if self.settings().selection_exclusive {
1129            // Half-open: start..end (end excluded). Empty when start == end.
1130            if start == end {
1131                return None;
1132            }
1133            Some((start, end))
1134        } else {
1135            // Inclusive (vim default): both endpoints are selected.
1136            Some((start, end))
1137        }
1138    }
1139
1140    fn visual_char_range_exclusive(&self) -> Option<((usize, usize), (usize, usize))> {
1141        if self.vim_mode() != VimMode::Visual {
1142            return None;
1143        }
1144        let anchor = crate::vim_state::vim(self).visual_anchor;
1145        let cursor = self.cursor();
1146        if anchor == cursor {
1147            return None;
1148        }
1149        let (start, end) = if anchor <= cursor {
1150            (anchor, cursor)
1151        } else {
1152            (cursor, anchor)
1153        };
1154        Some((start, end))
1155    }
1156
1157    fn line_highlight(&self) -> Option<(usize, usize)> {
1158        if self.vim_mode() != VimMode::VisualLine {
1159            return None;
1160        }
1161        let anchor = crate::vim_state::vim(self).visual_line_anchor;
1162        let cursor = self.cursor().0;
1163        Some((anchor.min(cursor), anchor.max(cursor)))
1164    }
1165
1166    fn buffer_selection(&self) -> Option<hjkl_buffer::Selection> {
1167        use hjkl_buffer::{Position, Selection};
1168        let (cr, cc) = self.cursor();
1169        match self.vim_mode() {
1170            VimMode::Visual => {
1171                let (ar, ac) = crate::vim_state::vim(self).visual_anchor;
1172                let head = Position::new(cr, cc);
1173                if self.settings().selection_exclusive {
1174                    // Exclusive (VSCode bar-caret): render the half-open char set
1175                    // [start, end) so the cell under the caret is NOT highlighted.
1176                    // The buffer-tui renderer paints `row_span` inclusively, so
1177                    // drop one char off the max end. Empty selection → no
1178                    // highlight (caller is effectively in Insert).
1179                    let anchor_pos = Position::new(ar, ac);
1180                    if anchor_pos == head {
1181                        return None;
1182                    }
1183                    let (start, end) = if (ar, ac) <= (head.row, head.col) {
1184                        (anchor_pos, head)
1185                    } else {
1186                        (head, anchor_pos)
1187                    };
1188                    return Some(Selection::Char {
1189                        anchor: start,
1190                        head: dec_pos_one_char(self, end),
1191                    });
1192                }
1193                Some(Selection::Char {
1194                    anchor: Position::new(ar, ac),
1195                    head,
1196                })
1197            }
1198            VimMode::VisualLine => Some(Selection::Line {
1199                anchor_row: crate::vim_state::vim(self).visual_line_anchor,
1200                head_row: cr,
1201            }),
1202            VimMode::VisualBlock => {
1203                let (ar, ac) = crate::vim_state::vim(self).block_anchor;
1204                let vcol = crate::vim_state::vim(self).block_vcol;
1205                if crate::vim_state::vim(self).block_to_eol {
1206                    // Ragged (`$` — `:h v_b_$`): `Selection::Block::row_span`
1207                    // only ever resolves one fixed `(left, right)` pair for
1208                    // every row. Reuse the SAME `usize::MAX` "cap at the
1209                    // row's actual length" convention `Selection::Line`
1210                    // already uses (see `hjkl_buffer::selection::RowSpan`)
1211                    // by forcing the right corner's col to `usize::MAX` —
1212                    // the renderer then extends every row to its own EOL.
1213                    // Normalise which corner carries `MAX` so it's always
1214                    // the "right" one regardless of anchor/cursor order.
1215                    let left = ac.min(vcol);
1216                    return Some(Selection::Block {
1217                        anchor: Position::new(ar, left),
1218                        head: Position::new(cr, usize::MAX),
1219                    });
1220                }
1221                Some(Selection::Block {
1222                    anchor: Position::new(ar, ac),
1223                    head: Position::new(cr, vcol),
1224                })
1225            }
1226            _ => None,
1227        }
1228    }
1229
1230    fn selection_highlight(&self) -> Option<Highlight> {
1231        let sel = self.buffer_selection()?;
1232        let (start, end) = match sel {
1233            hjkl_buffer::Selection::Char { anchor, head } => {
1234                let a = (anchor.row, anchor.col);
1235                let h = (head.row, head.col);
1236                if a <= h { (a, h) } else { (h, a) }
1237            }
1238            hjkl_buffer::Selection::Line {
1239                anchor_row,
1240                head_row,
1241            } => {
1242                let (top, bot) = if anchor_row <= head_row {
1243                    (anchor_row, head_row)
1244                } else {
1245                    (head_row, anchor_row)
1246                };
1247                let last_col = self.line(bot).map_or(0, |l| l.len());
1248                ((top, 0), (bot, last_col))
1249            }
1250            hjkl_buffer::Selection::Block { anchor, head } => {
1251                let (top, bot) = if anchor.row <= head.row {
1252                    (anchor.row, head.row)
1253                } else {
1254                    (head.row, anchor.row)
1255                };
1256                let (left, right) = if anchor.col <= head.col {
1257                    (anchor.col, head.col)
1258                } else {
1259                    (head.col, anchor.col)
1260                };
1261                ((top, left), (bot, right))
1262            }
1263        };
1264        Some(Highlight {
1265            range: Pos {
1266                line: start.0 as u32,
1267                col: start.1 as u32,
1268            }..Pos {
1269                line: end.0 as u32,
1270                col: end.1 as u32,
1271            },
1272            kind: HighlightKind::Selection,
1273        })
1274    }
1275
1276    // ─── Text-object resolution ────────────────────────────────────────────
1277
1278    fn text_object_inner_word(&self) -> Option<((usize, usize), (usize, usize))> {
1279        crate::vim::text_object_inner_word_bridge(self)
1280    }
1281
1282    fn text_object_around_word(&self) -> Option<((usize, usize), (usize, usize))> {
1283        crate::vim::text_object_around_word_bridge(self)
1284    }
1285
1286    fn text_object_inner_big_word(&self) -> Option<((usize, usize), (usize, usize))> {
1287        crate::vim::text_object_inner_big_word_bridge(self)
1288    }
1289
1290    fn text_object_around_big_word(&self) -> Option<((usize, usize), (usize, usize))> {
1291        crate::vim::text_object_around_big_word_bridge(self)
1292    }
1293
1294    fn text_object_inner_quote(&self, quote: char) -> Option<((usize, usize), (usize, usize))> {
1295        crate::vim::text_object_inner_quote_bridge(self, quote)
1296    }
1297
1298    fn text_object_around_quote(&self, quote: char) -> Option<((usize, usize), (usize, usize))> {
1299        crate::vim::text_object_around_quote_bridge(self, quote)
1300    }
1301
1302    fn text_object_inner_bracket(&self, open: char) -> Option<((usize, usize), (usize, usize))> {
1303        crate::vim::text_object_inner_bracket_bridge(self, open)
1304    }
1305
1306    fn text_object_around_bracket(&self, open: char) -> Option<((usize, usize), (usize, usize))> {
1307        crate::vim::text_object_around_bracket_bridge(self, open)
1308    }
1309
1310    fn text_object_inner_sentence(&self) -> Option<((usize, usize), (usize, usize))> {
1311        crate::vim::text_object_inner_sentence_bridge(self)
1312    }
1313
1314    fn text_object_around_sentence(&self) -> Option<((usize, usize), (usize, usize))> {
1315        crate::vim::text_object_around_sentence_bridge(self)
1316    }
1317
1318    fn text_object_inner_paragraph(&self) -> Option<((usize, usize), (usize, usize))> {
1319        crate::vim::text_object_inner_paragraph_bridge(self)
1320    }
1321
1322    fn text_object_around_paragraph(&self) -> Option<((usize, usize), (usize, usize))> {
1323        crate::vim::text_object_around_paragraph_bridge(self)
1324    }
1325
1326    fn text_object_inner_tag(&self) -> Option<((usize, usize), (usize, usize))> {
1327        crate::vim::text_object_inner_tag_bridge(self)
1328    }
1329
1330    fn text_object_around_tag(&self) -> Option<((usize, usize), (usize, usize))> {
1331        crate::vim::text_object_around_tag_bridge(self)
1332    }
1333
1334    // ─── Range-mutation primitives ─────────────────────────────────────────
1335
1336    fn delete_range(
1337        &mut self,
1338        start: (usize, usize),
1339        end: (usize, usize),
1340        kind: RangeKind,
1341        register: char,
1342    ) {
1343        crate::vim::delete_range_bridge(self, start, end, kind, register);
1344    }
1345
1346    fn yank_range(
1347        &mut self,
1348        start: (usize, usize),
1349        end: (usize, usize),
1350        kind: RangeKind,
1351        register: char,
1352    ) {
1353        crate::vim::yank_range_bridge(self, start, end, kind, register);
1354    }
1355
1356    fn change_range(
1357        &mut self,
1358        start: (usize, usize),
1359        end: (usize, usize),
1360        kind: RangeKind,
1361        register: char,
1362    ) {
1363        crate::vim::change_range_bridge(self, start, end, kind, register);
1364    }
1365
1366    fn indent_range(
1367        &mut self,
1368        start: (usize, usize),
1369        end: (usize, usize),
1370        count: i32,
1371        shiftwidth: u32,
1372    ) {
1373        crate::vim::indent_range_bridge(self, start, end, count, shiftwidth);
1374    }
1375
1376    fn case_range(
1377        &mut self,
1378        start: (usize, usize),
1379        end: (usize, usize),
1380        kind: RangeKind,
1381        op: Operator,
1382    ) {
1383        crate::vim::case_range_bridge(self, start, end, kind, op);
1384    }
1385
1386    // ─── Block-shape range-mutation primitives ─────────────────────────────
1387
1388    fn delete_block(
1389        &mut self,
1390        top_row: usize,
1391        bot_row: usize,
1392        left_col: usize,
1393        right_col: usize,
1394        register: char,
1395    ) {
1396        crate::vim::delete_block_bridge(self, top_row, bot_row, left_col, right_col, register);
1397    }
1398
1399    fn yank_block(
1400        &mut self,
1401        top_row: usize,
1402        bot_row: usize,
1403        left_col: usize,
1404        right_col: usize,
1405        register: char,
1406    ) {
1407        crate::vim::yank_block_bridge(self, top_row, bot_row, left_col, right_col, register);
1408    }
1409
1410    fn change_block(
1411        &mut self,
1412        top_row: usize,
1413        bot_row: usize,
1414        left_col: usize,
1415        right_col: usize,
1416        register: char,
1417    ) {
1418        crate::vim::change_block_bridge(self, top_row, bot_row, left_col, right_col, register);
1419    }
1420
1421    fn indent_block(
1422        &mut self,
1423        top_row: usize,
1424        bot_row: usize,
1425        _left_col: usize,
1426        _right_col: usize,
1427        count: i32,
1428    ) {
1429        crate::vim::indent_block_bridge(self, top_row, bot_row, count);
1430    }
1431
1432    fn auto_indent_range(&mut self, start: (usize, usize), end: (usize, usize)) {
1433        crate::vim::auto_indent_range_bridge(self, start, end);
1434    }
1435
1436    // ─── Paste ─────────────────────────────────────────────────────────────
1437
1438    fn paste_after(&mut self, count: usize) {
1439        crate::vim::paste_after_bridge(self, count);
1440    }
1441
1442    fn paste_before(&mut self, count: usize) {
1443        crate::vim::paste_before_bridge(self, count);
1444    }
1445
1446    fn paste_cursor_after(&mut self, before: bool, count: usize) {
1447        crate::vim::paste_bridge(self, before, count, true, false);
1448    }
1449
1450    fn paste_reindent(&mut self, before: bool, count: usize) {
1451        crate::vim::paste_bridge(self, before, count, false, true);
1452    }
1453
1454    fn visual_paste(&mut self, before: bool) {
1455        crate::vim::visual_paste(self, before);
1456    }
1457
1458    // ─── Visual-mode operators ─────────────────────────────────────────────
1459
1460    fn adjust_number_visual(&mut self, delta: i64, sequential: bool) {
1461        crate::vim::adjust_number_visual(self, delta, sequential);
1462    }
1463
1464    fn ampersand_repeat(&mut self) {
1465        crate::vim::ampersand_repeat(self);
1466    }
1467
1468    fn visual_join(&mut self, with_space: bool) {
1469        crate::vim::visual_join(self, with_space);
1470    }
1471
1472    fn goto_percent(&mut self, count: usize) {
1473        crate::vim::goto_percent(self, count);
1474    }
1475
1476    // ─── Jumplist motion ───────────────────────────────────────────────────
1477
1478    fn jump_back(&mut self, count: usize) {
1479        crate::vim::jump_back_bridge(self, count);
1480    }
1481
1482    fn jump_forward(&mut self, count: usize) {
1483        crate::vim::jump_forward_bridge(self, count);
1484    }
1485
1486    // ─── Search ────────────────────────────────────────────────────────────
1487
1488    fn search_repeat(&mut self, forward: bool, count: usize) {
1489        crate::vim::search_repeat_bridge(self, forward, count);
1490    }
1491
1492    fn word_search(&mut self, forward: bool, whole_word: bool, count: usize) {
1493        crate::vim::word_search_bridge(self, forward, whole_word, count);
1494    }
1495
1496    // ─── Chord appliers ────────────────────────────────────────────────────
1497
1498    fn replace_char_at(&mut self, ch: char, count: usize) {
1499        crate::vim::replace_char(self, ch, count);
1500    }
1501
1502    fn find_char(&mut self, ch: char, forward: bool, till: bool, count: usize) {
1503        crate::vim::apply_find_char(self, ch, forward, till, count.max(1));
1504    }
1505
1506    fn after_g(&mut self, ch: char, count: usize) {
1507        crate::vim::apply_after_g(self, ch, count);
1508    }
1509
1510    fn after_z(&mut self, ch: char, count: usize) {
1511        crate::vim::apply_after_z(self, ch, count);
1512    }
1513
1514    // ─── Operator dispatch ─────────────────────────────────────────────────
1515
1516    fn apply_op_motion(&mut self, op: Operator, motion_key: char, total_count: usize) {
1517        crate::vim::apply_op_motion_key(self, op, motion_key, total_count);
1518    }
1519
1520    fn apply_op_double(&mut self, op: Operator, total_count: usize) {
1521        crate::vim::apply_op_double(self, op, total_count);
1522    }
1523
1524    fn apply_op_find(&mut self, op: Operator, ch: char, forward: bool, till: bool, count: usize) {
1525        crate::vim::apply_op_find_motion(self, op, ch, forward, till, count);
1526    }
1527
1528    fn apply_op_text_obj(&mut self, op: Operator, ch: char, inner: bool, total_count: usize) {
1529        crate::vim::apply_op_text_obj_inner(self, op, ch, inner, total_count);
1530    }
1531
1532    fn apply_op_g(&mut self, op: Operator, ch: char, total_count: usize) {
1533        crate::vim::apply_op_g_inner(self, op, ch, total_count);
1534    }
1535
1536    // ─── Mode transitions ──────────────────────────────────────────────────
1537
1538    fn vim_mode(&self) -> VimMode {
1539        crate::vim_state::vim(self).current_mode
1540    }
1541
1542    fn enter_visual_char(&mut self) {
1543        crate::vim::enter_visual_char_bridge(self);
1544    }
1545
1546    fn enter_visual_line(&mut self) {
1547        crate::vim::enter_visual_line_bridge(self);
1548    }
1549
1550    fn enter_visual_block(&mut self) {
1551        crate::vim::enter_visual_block_bridge(self);
1552    }
1553
1554    fn exit_visual_to_normal(&mut self) {
1555        crate::vim::exit_visual_to_normal_bridge(self);
1556    }
1557
1558    fn visual_o_toggle(&mut self) {
1559        crate::vim::visual_o_toggle_bridge(self);
1560    }
1561
1562    fn reenter_last_visual(&mut self) {
1563        crate::vim::reenter_last_visual_bridge(self);
1564    }
1565
1566    fn set_mode(&mut self, mode: VimMode) {
1567        crate::vim::set_mode_bridge(self, mode);
1568    }
1569
1570    // ─── Visual anchors ────────────────────────────────────────────────────
1571
1572    fn visual_anchor(&self) -> (usize, usize) {
1573        crate::vim_state::vim(self).visual_anchor
1574    }
1575    fn set_visual_anchor(&mut self, anchor: (usize, usize)) {
1576        crate::vim_state::vim_mut(self).visual_anchor = anchor;
1577    }
1578    fn visual_line_anchor(&self) -> usize {
1579        crate::vim_state::vim(self).visual_line_anchor
1580    }
1581    fn set_visual_line_anchor(&mut self, row: usize) {
1582        crate::vim_state::vim_mut(self).visual_line_anchor = row;
1583    }
1584    fn block_anchor(&self) -> (usize, usize) {
1585        crate::vim_state::vim(self).block_anchor
1586    }
1587    fn set_block_anchor(&mut self, anchor: (usize, usize)) {
1588        crate::vim_state::vim_mut(self).block_anchor = anchor;
1589    }
1590    fn block_vcol(&self) -> usize {
1591        crate::vim_state::vim(self).block_vcol
1592    }
1593    fn set_block_vcol(&mut self, vcol: usize) {
1594        crate::vim_state::vim_mut(self).block_vcol = vcol;
1595    }
1596    fn block_to_eol(&self) -> bool {
1597        crate::vim_state::vim(self).block_to_eol
1598    }
1599    fn set_block_to_eol(&mut self, to_eol: bool) {
1600        crate::vim_state::vim_mut(self).block_to_eol = to_eol;
1601    }
1602
1603    // ─── Yank / register staging ───────────────────────────────────────────
1604
1605    fn set_pending_register_raw(&mut self, reg: Option<char>) {
1606        crate::vim_state::vim_mut(self).pending_register = reg;
1607    }
1608    fn take_pending_register_raw(&mut self) -> Option<char> {
1609        crate::vim_state::vim_mut(self).pending_register.take()
1610    }
1611
1612    // ─── Macro recording / replay ──────────────────────────────────────────
1613
1614    fn recording_macro(&self) -> Option<char> {
1615        crate::vim_state::vim(self).recording_macro
1616    }
1617    fn set_recording_macro(&mut self, reg: Option<char>) {
1618        crate::vim_state::vim_mut(self).recording_macro = reg;
1619    }
1620    fn push_recording_key(&mut self, input: Input) {
1621        crate::vim_state::vim_mut(self).recording_keys.push(input);
1622    }
1623    fn take_recording_keys(&mut self) -> Vec<Input> {
1624        std::mem::take(&mut crate::vim_state::vim_mut(self).recording_keys)
1625    }
1626    fn set_recording_keys(&mut self, keys: Vec<Input>) {
1627        crate::vim_state::vim_mut(self).recording_keys = keys;
1628    }
1629    fn recording_keys_len(&self) -> usize {
1630        crate::vim_state::vim(self).recording_keys.len()
1631    }
1632    fn is_replaying_macro_raw(&self) -> bool {
1633        crate::vim_state::vim(self).replaying_macro
1634    }
1635    fn set_replaying_macro_raw(&mut self, v: bool) {
1636        crate::vim_state::vim_mut(self).replaying_macro = v;
1637    }
1638    fn last_macro(&self) -> Option<char> {
1639        crate::vim_state::vim(self).last_macro
1640    }
1641    fn set_last_macro(&mut self, reg: Option<char>) {
1642        crate::vim_state::vim_mut(self).last_macro = reg;
1643    }
1644
1645    // ─── Last insert / visual / viewport ───────────────────────────────────
1646
1647    fn last_insert_pos(&self) -> Option<(usize, usize)> {
1648        crate::vim_state::vim(self).last_insert_pos
1649    }
1650    fn set_last_insert_pos(&mut self, pos: Option<(usize, usize)>) {
1651        crate::vim_state::vim_mut(self).last_insert_pos = pos;
1652    }
1653    fn last_visual(&self) -> Option<LastVisual> {
1654        crate::vim_state::vim(self).last_visual
1655    }
1656    fn set_last_visual(&mut self, snap: Option<LastVisual>) {
1657        crate::vim_state::vim_mut(self).last_visual = snap;
1658    }
1659    fn insert_pending_register(&self) -> bool {
1660        crate::vim_state::vim(self).insert_pending_register
1661    }
1662    fn set_insert_pending_register(&mut self, v: bool) {
1663        crate::vim_state::vim_mut(self).insert_pending_register = v;
1664    }
1665
1666    // ─── Change-mark start ─────────────────────────────────────────────────
1667
1668    fn change_mark_start(&self) -> Option<(usize, usize)> {
1669        crate::vim_state::vim(self).change_mark_start
1670    }
1671    fn take_change_mark_start(&mut self) -> Option<(usize, usize)> {
1672        crate::vim_state::vim_mut(self).change_mark_start.take()
1673    }
1674    fn set_change_mark_start(&mut self, pos: Option<(usize, usize)>) {
1675        crate::vim_state::vim_mut(self).change_mark_start = pos;
1676    }
1677
1678    // ─── Visual / motion / search primitives ───────────────────────────────
1679
1680    fn is_visual(&self) -> bool {
1681        matches!(
1682            crate::vim_state::vim(self).mode,
1683            FsmMode::Visual | FsmMode::VisualLine | FsmMode::VisualBlock
1684        )
1685    }
1686
1687    fn apply_op_with_motion_direct(&mut self, op: Operator, motion: &Motion, count: usize) {
1688        crate::vim::apply_op_with_motion(self, op, motion, count);
1689    }
1690
1691    fn adjust_number(&mut self, delta: i64) {
1692        crate::vim::adjust_number(self, delta);
1693    }
1694
1695    fn enter_search(&mut self, forward: bool) {
1696        crate::vim::enter_search(self, forward);
1697    }
1698
1699    fn enter_search_op(&mut self, forward: bool, op: Operator, count: usize) {
1700        crate::vim::enter_search_op(self, forward, op, count);
1701    }
1702
1703    fn apply_op_search_range(&mut self, op: Operator, origin: (usize, usize)) {
1704        crate::vim::apply_op_search_range(self, op, origin);
1705    }
1706
1707    fn visual_block_insert_at_left(&mut self, top: usize, bot: usize, col: usize, count: usize) {
1708        self.jump_cursor(top, col);
1709        crate::vim_state::vim_mut(self).mode = FsmMode::Normal;
1710        let undo_depth_before = self.undo_stack_len();
1711        crate::vim::begin_insert(
1712            self,
1713            count,
1714            InsertReason::BlockEdge {
1715                top,
1716                bot,
1717                col,
1718                pad: false,
1719                // `I`'s insertion point already IS the block's left edge —
1720                // but `leave_insert_to_normal_bridge` unconditionally steps
1721                // the cursor back one column after Esc (vim's generic
1722                // leave-insert adjustment), so store one PAST the target
1723                // and let that step-back land exactly on `col`. Verified
1724                // against real nvim at a non-zero left edge (the only
1725                // existing case started at col 0, where the step-back's
1726                // `col > 0` guard happened to no-op and masked this).
1727                cursor_col: col + 1,
1728                // `I` never pads (`pad: false`), so both fields are dead
1729                // here; record the honest values anyway.
1730                pre_pad_len: hjkl_engine::buf_helpers::buf_line_chars(self.buffer(), top),
1731                undo_depth_before,
1732            },
1733        );
1734    }
1735
1736    fn visual_block_append_at_right(
1737        &mut self,
1738        top: usize,
1739        bot: usize,
1740        col: usize,
1741        left: usize,
1742        count: usize,
1743    ) {
1744        // vim `v_b_A`: pad the top row to `col` with spaces before the
1745        // cursor lands there, same as `replicate_block_text` does for
1746        // every other row on Esc. Without this, `jump_cursor` clamps
1747        // `col` down to the row's current length and the typed text
1748        // lands inside the block instead of past its right edge.
1749        //
1750        // The pad must land in the SAME undo group as the insert session
1751        // (vim's block `A` is one `u` step — pad, typed text, and the
1752        // replicated rows all revert together). So push the undo
1753        // checkpoint ourselves before padding, then use
1754        // `begin_insert_noundo` — mirrors the `Operator::Change` block
1755        // path (push_undo, mutate, begin_insert_noundo) in `visual_ops`.
1756        // Record the undo depth just before the push: an EMPTY block-`A`
1757        // leaves that checkpoint as a no-op boundary, and
1758        // `finish_insert_session` consumes it on Esc (when it is still the
1759        // most recent one) so a no-op command leaves the undo tree
1760        // untouched.
1761        let undo_depth_before = self.undo_stack_len();
1762        self.push_undo();
1763        let line_len = hjkl_engine::buf_helpers::buf_line_chars(self.buffer(), top);
1764        if col > line_len {
1765            let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1766            self.mutate_edit(hjkl_buffer::Edit::InsertStr {
1767                at: hjkl_buffer::Position::new(top, line_len),
1768                text: pad,
1769            });
1770        }
1771        self.jump_cursor(top, col);
1772        crate::vim_state::vim_mut(self).mode = FsmMode::Normal;
1773        crate::vim::begin_insert_noundo(
1774            self,
1775            count,
1776            InsertReason::BlockEdge {
1777                top,
1778                bot,
1779                col,
1780                pad: true,
1781                // Same "one past the target, let the generic Esc step-back
1782                // land exactly there" convention as `visual_block_insert_
1783                // at_left` — see its comment.
1784                cursor_col: left + 1,
1785                // The top row's length before padding — the pad occupies
1786                // `pre_pad_len..col` of the row, which is exactly the range
1787                // to remove on an empty Esc.
1788                pre_pad_len: line_len,
1789                undo_depth_before,
1790            },
1791        );
1792    }
1793
1794    fn execute_motion(&mut self, motion: Motion, count: usize) {
1795        crate::vim::execute_motion(self, motion, count);
1796    }
1797
1798    fn update_block_vcol(&mut self, motion: &Motion) {
1799        crate::vim::update_block_vcol(self, motion);
1800    }
1801
1802    fn apply_visual_operator(&mut self, op: Operator, count: usize) {
1803        crate::vim::apply_visual_operator(self, op, count);
1804    }
1805
1806    fn replace_block_char(&mut self, ch: char) {
1807        crate::vim::block_replace(self, ch);
1808    }
1809
1810    fn visual_replace_char(&mut self, ch: char) {
1811        crate::vim::visual_replace_char(self, ch);
1812    }
1813
1814    fn visual_text_obj_extend(&mut self, ch: char, inner: bool) {
1815        self.visual_text_obj_extend_counted(ch, inner, 1);
1816    }
1817
1818    fn visual_text_obj_extend_counted(&mut self, ch: char, inner: bool, count: usize) {
1819        let count = count.max(1);
1820        let Some(obj) = crate::vim::text_object_from_char(ch) else {
1821            return;
1822        };
1823        let reverse_block_sentence = obj == TextObject::Sentence
1824            && crate::vim_state::vim(self).mode == FsmMode::VisualBlock
1825            && self.cursor().0 < crate::vim_state::vim(self).block_anchor.0;
1826        let reverse_landing = if reverse_block_sentence {
1827            let (cur_row, _) = self.cursor();
1828            let block_vcol = crate::vim_state::vim(self).block_vcol;
1829            let probe_col = self.line(cur_row).map_or(0, |line| {
1830                block_vcol.min(line.chars().count().saturating_sub(1))
1831            });
1832            let saved_cursor = self.cursor();
1833            // `H` moves the viewport head to its first nonblank while the
1834            // block retains its logical column. Probe that column for vim's
1835            // reverse sentence lookup, then restore even when it finds none.
1836            self.jump_cursor(cur_row, probe_col);
1837            let landing = crate::vim::reverse_visual_block_sentence_landing(self, inner, count);
1838            self.jump_cursor(saved_cursor.0, saved_cursor.1);
1839            landing
1840        } else {
1841            None
1842        };
1843        let block_sentence_forward = obj == TextObject::Sentence
1844            && inner
1845            && crate::vim_state::vim(self).mode == FsmMode::VisualBlock
1846            && self.cursor().0 > crate::vim_state::vim(self).block_anchor.0;
1847        let range = if let Some(landing) = reverse_landing {
1848            Some((landing, landing, RangeKind::Exclusive))
1849        } else if block_sentence_forward {
1850            // In a forward VisualBlock selection, `is` walks sentence bodies
1851            // and same-line separators as distinct count units; the shared
1852            // counted-sentence scan already implements that alternation.
1853            crate::vim::text_object_range(self, obj, inner, count)
1854        } else {
1855            crate::vim::text_object_range(self, obj, inner, count)
1856        };
1857        let Some((start, end, kind)) = range else {
1858            return;
1859        };
1860        // B6: `:h v_ip` — when the selection ALREADY exactly equals this
1861        // text object's natural bounds (the user is re-applying `ip`/`ap`/
1862        // etc. to a selection it already produced, e.g. `vipip`), the
1863        // object GROWS instead of re-selecting the identical (so
1864        // no-op-looking) range. `ip`/`ap` alternate paragraph and
1865        // blank-run units this way; growth is implemented generically here
1866        // by probing the SAME text object one row past the current end and
1867        // unioning the two ranges, rather than hand-rolling paragraph-
1868        // specific alternation logic.
1869        // Compare the SELECTION'S END (which the cursor always tracks, by
1870        // this function's own construction below) against the freshly
1871        // computed object's end — NOT the anchor. After a grow, the anchor
1872        // stays pinned at the FIRST application's start while the end keeps
1873        // moving, so an anchor-based check would only ever match once
1874        // (verified against real nvim: `vipipipd`, three applications,
1875        // grows a second time too — the anchor-based check breaks that).
1876        let already_matches = match kind {
1877            RangeKind::Linewise => {
1878                crate::vim_state::vim(self).mode == FsmMode::VisualLine && self.cursor().0 == end.0
1879            }
1880            _ => {
1881                crate::vim_state::vim(self).mode == FsmMode::Visual
1882                    && self.cursor() == crate::vim::retreat_one(self, end)
1883            }
1884        };
1885        // When growing, keep the EXISTING anchor (the first application's
1886        // start) — `start` above is the freshly computed single-object's
1887        // start (e.g. the blank run's own start on a second `ip`), which is
1888        // NOT where the accumulated selection began.
1889        //
1890        // The probe position differs by kind: Linewise units are probed one
1891        // ROW past the current end (`ip`/`ap` grow onto the next line);
1892        // charwise units (`iw`, quotes, brackets, …) are probed at `end`
1893        // directly — `end` for an Exclusive object already points ONE PAST
1894        // the last selected char, i.e. exactly where the next same-line unit
1895        // begins (verified against real nvim: `viwiw` on "foo bar baz"
1896        // grows "foo" to "foo " — the following WHITESPACE run on the SAME
1897        // row, not a jump to the next line).
1898        let (start, end) = if already_matches {
1899            let existing_start = match kind {
1900                RangeKind::Linewise => (crate::vim_state::vim(self).visual_line_anchor, 0),
1901                _ => crate::vim_state::vim(self).visual_anchor,
1902            };
1903            let probe = match kind {
1904                RangeKind::Linewise => (end.0 + 1, 0),
1905                _ => end,
1906            };
1907            let saved_cursor = self.cursor();
1908            self.jump_cursor(probe.0, probe.1);
1909            let grown = crate::vim::text_object_range(self, obj, inner, 1)
1910                .filter(|&(_, _, grown_kind)| grown_kind == kind)
1911                .map(|(_, grown_end, _)| grown_end);
1912            self.jump_cursor(saved_cursor.0, saved_cursor.1);
1913            match grown {
1914                Some(grown_end) if grown_end > end => (existing_start, grown_end),
1915                _ => (existing_start, end),
1916            }
1917        } else {
1918            (start, end)
1919        };
1920        // NOTE: only the WORD objects stay blockwise and EXTEND the block
1921        // (handled above — the block keeps its rows and the cursor extends
1922        // its columns to the object end); BRACKET and TAG objects are
1923        // selection no-ops, and paragraph / sentence also stay blockwise,
1924        // landing the cursor at the object-extend position. Measured on
1925        // neovim 0.12.4 from a `<C-v>j` block:
1926        //
1927        //   - `iw` / `aw` / `iW` / `aW` / `ip` / `is` stay BLOCKWISE and just
1928        //     extend the cursor (so the block keeps its rows and takes the
1929        //     object's columns);
1930        //   - `ib` / `ab` / `iB` / `it` leave the block EXACTLY as the block
1931        //     motion made it — mode stays visual_block, cursor keeps the
1932        //     post-motion position; the object is found but the selection
1933        //     does not change (`<C-v>jib~` flips the same cells as
1934        //     `<C-v>j~`). hjkl used to collapse these to charwise;
1935        //   - `i"` does nothing at all (the object is not found from a block
1936        //     at the measured position).
1937        //
1938        // Sentence objects with the anchor below the cursor use vim's reverse
1939        // `findsent` landing: `is` returns to the current sentence start and
1940        // `as` includes only its immediately preceding same-line separator.
1941        // Quotes already no-op in hjkl too. The word objects additionally
1942        // write `block_vcol` so `block_bounds` sees the new column.
1943        if crate::vim_state::vim(self).mode == FsmMode::VisualBlock {
1944            // Word objects keep the selection blockwise: the block spans
1945            // anchor-column..object-end-column, matching nvim.
1946            if let TextObject::Word { .. } = obj {
1947                let (er, ec) = crate::vim::retreat_one(self, end);
1948                self.jump_cursor(er, ec);
1949                crate::vim_state::vim_mut(self).block_vcol = ec;
1950                crate::vim_state::vim_mut(self).block_to_eol = false;
1951                return;
1952            }
1953            // Bracket and tag objects are selection no-ops in blockwise
1954            // visual (nvim-measured): return without touching mode or
1955            // cursor. `end` was computed above but is deliberately unused —
1956            // the block keeps the post-motion geometry.
1957            if let TextObject::Bracket(_) | TextObject::XmlTag = obj {
1958                return;
1959            }
1960            // Paragraph (`ip`) and sentence (`is`) objects EXTEND the block
1961            // (nvim-measured): mode and anchor stay, the cursor lands at
1962            // the object-extend position, and `block_vcol` syncs to the
1963            // landing column so `block_bounds` sees the new geometry. A
1964            // single-row block falls through to the collapse below (vim
1965            // takes its normal object path there).
1966            if let TextObject::Paragraph = obj {
1967                let (cur_row, _) = self.cursor();
1968                let anchor_row = crate::vim_state::vim(self).block_anchor.0;
1969                if cur_row != anchor_row {
1970                    if let Some(land_row) =
1971                        crate::vim::paragraph_extend_landing(self, cur_row, anchor_row, inner)
1972                    {
1973                        self.jump_cursor(land_row, 0);
1974                        crate::vim_state::vim_mut(self).block_vcol = 0;
1975                        crate::vim_state::vim_mut(self).block_to_eol = false;
1976                        return;
1977                    }
1978                    // Buffer edge: vim's `current_par` FAILs (a no-op).
1979                    return;
1980                }
1981            }
1982            if let TextObject::Sentence = obj {
1983                let (cur_row, _) = self.cursor();
1984                let anchor_row = crate::vim_state::vim(self).block_anchor.0;
1985                if cur_row != anchor_row {
1986                    if cur_row < anchor_row {
1987                        let landing = reverse_landing.unwrap_or_else(|| {
1988                            if inner || start.1 == 0 {
1989                                start
1990                            } else {
1991                                self.line(start.0).map_or(start, |line| {
1992                                    let chars: Vec<char> = line.chars().collect();
1993                                    let mut col = start.1;
1994                                    while col > 0 && chars[col - 1].is_whitespace() {
1995                                        col -= 1;
1996                                    }
1997                                    (start.0, col)
1998                                })
1999                            }
2000                        });
2001                        self.jump_cursor(landing.0, landing.1);
2002                        crate::vim_state::vim_mut(self).block_vcol = landing.1;
2003                        crate::vim_state::vim_mut(self).block_to_eol = false;
2004                        return;
2005                    }
2006                    let (er, ec) = crate::vim::retreat_one(self, end);
2007                    self.jump_cursor(er, ec);
2008                    crate::vim_state::vim_mut(self).block_vcol = ec;
2009                    crate::vim_state::vim_mut(self).block_to_eol = false;
2010                    return;
2011                }
2012            }
2013            match kind {
2014                RangeKind::Linewise => {
2015                    crate::vim_state::vim_mut(self).visual_line_anchor = start.0;
2016                    crate::vim_state::vim_mut(self).mode = FsmMode::VisualLine;
2017                    crate::vim_state::vim_mut(self).current_mode = VimMode::VisualLine;
2018                    self.jump_cursor(end.0, 0);
2019                }
2020                _ => {
2021                    crate::vim_state::vim_mut(self).mode = FsmMode::Visual;
2022                    crate::vim_state::vim_mut(self).current_mode = VimMode::Visual;
2023                    crate::vim_state::vim_mut(self).visual_anchor = (start.0, start.1);
2024                    let (er, ec) = crate::vim::retreat_one(self, end);
2025                    self.jump_cursor(er, ec);
2026                }
2027            }
2028            return;
2029        }
2030        match kind {
2031            RangeKind::Linewise => {
2032                crate::vim_state::vim_mut(self).visual_line_anchor = start.0;
2033                crate::vim_state::vim_mut(self).mode = FsmMode::VisualLine;
2034                crate::vim_state::vim_mut(self).current_mode = VimMode::VisualLine;
2035                self.jump_cursor(end.0, 0);
2036            }
2037            _ => {
2038                crate::vim_state::vim_mut(self).mode = FsmMode::Visual;
2039                crate::vim_state::vim_mut(self).current_mode = VimMode::Visual;
2040                crate::vim_state::vim_mut(self).visual_anchor = (start.0, start.1);
2041                let (er, ec) = crate::vim::retreat_one(self, end);
2042                self.jump_cursor(er, ec);
2043            }
2044        }
2045    }
2046
2047    // ─── Insert-mode primitives ────────────────────────────────────────────
2048
2049    fn insert_char(&mut self, ch: char) {
2050        if crate::vim::insert_char_bridge(self, ch) {
2051            after_insert_mutation(self);
2052        }
2053    }
2054
2055    fn insert_newline(&mut self) {
2056        if crate::vim::insert_newline_bridge(self) {
2057            after_insert_mutation(self);
2058        }
2059    }
2060
2061    fn insert_tab(&mut self) {
2062        if crate::vim::insert_tab_bridge(self) {
2063            after_insert_mutation(self);
2064        }
2065    }
2066
2067    fn insert_backspace(&mut self) {
2068        if crate::vim::insert_backspace_bridge(self) {
2069            after_insert_mutation(self);
2070        }
2071    }
2072
2073    fn insert_delete(&mut self) {
2074        if crate::vim::insert_delete_bridge(self) {
2075            after_insert_mutation(self);
2076        }
2077    }
2078
2079    fn insert_arrow(&mut self, dir: InsertDir) {
2080        crate::vim::insert_arrow_bridge(self, dir);
2081        after_insert_motion(self);
2082    }
2083
2084    fn insert_home(&mut self) {
2085        crate::vim::insert_home_bridge(self);
2086        after_insert_motion(self);
2087    }
2088
2089    fn insert_end(&mut self) {
2090        crate::vim::insert_end_bridge(self);
2091        after_insert_motion(self);
2092    }
2093
2094    fn insert_pageup(&mut self, viewport_h: u16) {
2095        crate::vim::insert_pageup_bridge(self, viewport_h);
2096        after_insert_motion(self);
2097    }
2098
2099    fn insert_pagedown(&mut self, viewport_h: u16) {
2100        crate::vim::insert_pagedown_bridge(self, viewport_h);
2101        after_insert_motion(self);
2102    }
2103
2104    fn insert_ctrl_w(&mut self) {
2105        if crate::vim::insert_ctrl_w_bridge(self) {
2106            after_insert_mutation(self);
2107        }
2108    }
2109
2110    fn insert_ctrl_u(&mut self) {
2111        if crate::vim::insert_ctrl_u_bridge(self) {
2112            after_insert_mutation(self);
2113        }
2114    }
2115
2116    fn insert_ctrl_h(&mut self) {
2117        if crate::vim::insert_ctrl_h_bridge(self) {
2118            after_insert_mutation(self);
2119        }
2120    }
2121
2122    fn insert_ctrl_o_arm(&mut self) {
2123        crate::vim::insert_ctrl_o_bridge(self);
2124    }
2125
2126    fn insert_ctrl_r_arm(&mut self) {
2127        crate::vim::insert_ctrl_r_bridge(self);
2128    }
2129
2130    fn insert_ctrl_t(&mut self) {
2131        // Indent-only: no scrolloff re-check (the cursor row does not move).
2132        let mutated = crate::vim::insert_ctrl_t_bridge(self);
2133        if mutated {
2134            self.mark_content_dirty();
2135            let (row, _) = self.cursor();
2136            crate::vim_state::vim_mut(self).widen_insert_row(row);
2137        }
2138    }
2139
2140    fn insert_ctrl_d(&mut self) {
2141        let mutated = crate::vim::insert_ctrl_d_bridge(self);
2142        if mutated {
2143            self.mark_content_dirty();
2144            let (row, _) = self.cursor();
2145            crate::vim_state::vim_mut(self).widen_insert_row(row);
2146        }
2147    }
2148
2149    fn insert_ctrl_a(&mut self) {
2150        if crate::vim::insert_ctrl_a_bridge(self) {
2151            after_insert_mutation(self);
2152        }
2153    }
2154
2155    fn insert_ctrl_e(&mut self) {
2156        if crate::vim::insert_ctrl_e_bridge(self) {
2157            after_insert_mutation(self);
2158        }
2159    }
2160
2161    fn insert_ctrl_y(&mut self) {
2162        if crate::vim::insert_ctrl_y_bridge(self) {
2163            after_insert_mutation(self);
2164        }
2165    }
2166
2167    fn insert_paste_register(&mut self, reg: char) {
2168        crate::vim::insert_paste_register_bridge(self, reg);
2169        let (row, _) = self.cursor();
2170        crate::vim_state::vim_mut(self).widen_insert_row(row);
2171    }
2172
2173    fn insert_ctrl_bracket(&mut self) {
2174        if crate::vim::check_and_apply_abbrev(self, AbbrevTrigger::CtrlBracket) {
2175            after_insert_mutation(self);
2176        }
2177    }
2178
2179    fn leave_insert_to_normal(&mut self) {
2180        crate::vim::leave_insert_to_normal_bridge(self);
2181    }
2182
2183    // ─── Insert-mode entry ─────────────────────────────────────────────────
2184
2185    fn enter_insert_i(&mut self, count: usize) {
2186        crate::vim::enter_insert_i_bridge(self, count);
2187    }
2188
2189    fn enter_insert_shift_i(&mut self, count: usize) {
2190        crate::vim::enter_insert_shift_i_bridge(self, count);
2191    }
2192
2193    fn enter_insert_a(&mut self, count: usize) {
2194        crate::vim::enter_insert_a_bridge(self, count);
2195    }
2196
2197    fn enter_insert_shift_a(&mut self, count: usize) {
2198        crate::vim::enter_insert_shift_a_bridge(self, count);
2199    }
2200
2201    fn open_line_below(&mut self, count: usize) {
2202        crate::vim::open_line_below_bridge(self, count);
2203    }
2204
2205    fn open_line_above(&mut self, count: usize) {
2206        crate::vim::open_line_above_bridge(self, count);
2207    }
2208
2209    fn enter_replace_mode(&mut self, count: usize) {
2210        crate::vim::enter_replace_mode_bridge(self, count);
2211    }
2212
2213    // ─── Normal-mode edit primitives ───────────────────────────────────────
2214
2215    fn delete_char_forward(&mut self, count: usize) {
2216        crate::vim::delete_char_forward_bridge(self, count);
2217    }
2218
2219    fn delete_char_backward(&mut self, count: usize) {
2220        crate::vim::delete_char_backward_bridge(self, count);
2221    }
2222
2223    fn substitute_char(&mut self, count: usize) {
2224        crate::vim::substitute_char_bridge(self, count);
2225    }
2226
2227    fn substitute_line(&mut self, count: usize) {
2228        crate::vim::substitute_line_bridge(self, count);
2229    }
2230
2231    fn delete_to_eol(&mut self, count: usize) {
2232        crate::vim::delete_to_eol_bridge(self, count);
2233    }
2234
2235    fn change_to_eol(&mut self, count: usize) {
2236        crate::vim::change_to_eol_bridge(self, count);
2237    }
2238
2239    fn yank_to_eol(&mut self, count: usize) {
2240        crate::vim::yank_to_eol_bridge(self, count);
2241    }
2242
2243    fn join_line(&mut self, count: usize) {
2244        crate::vim::join_line_bridge(self, count);
2245    }
2246
2247    fn toggle_case_at_cursor(&mut self, count: usize) {
2248        crate::vim::toggle_case_at_cursor_bridge(self, count);
2249    }
2250
2251    // ─── Vim mark commands ─────────────────────────────────────────────────
2252
2253    fn replay_last_change(&mut self, count: usize) {
2254        crate::vim::replay_last_change(self, count);
2255    }
2256
2257    fn set_mark_at_cursor(&mut self, ch: char) {
2258        crate::vim::set_mark_at_cursor(self, ch);
2259    }
2260
2261    fn goto_mark_line(&mut self, ch: char) {
2262        crate::vim::goto_mark(self, ch, true);
2263    }
2264
2265    fn goto_mark_char(&mut self, ch: char) {
2266        crate::vim::goto_mark(self, ch, false);
2267    }
2268
2269    fn try_goto_mark_line(&mut self, ch: char) -> MarkJump {
2270        crate::vim::try_goto_mark(self, ch, true)
2271    }
2272
2273    fn try_goto_mark_char(&mut self, ch: char) -> MarkJump {
2274        crate::vim::try_goto_mark(self, ch, false)
2275    }
2276
2277    // ─── Vim FSM state accessors ───────────────────────────────────────────
2278
2279    fn pending(&self) -> crate::vim::Pending {
2280        crate::vim_state::vim(self).pending.clone()
2281    }
2282
2283    fn set_pending(&mut self, p: crate::vim::Pending) {
2284        crate::vim_state::vim_mut(self).pending = p;
2285    }
2286
2287    fn take_pending(&mut self) -> crate::vim::Pending {
2288        std::mem::take(&mut crate::vim_state::vim_mut(self).pending)
2289    }
2290
2291    fn count(&self) -> usize {
2292        crate::vim_state::vim(self).count
2293    }
2294
2295    fn set_count(&mut self, c: usize) {
2296        crate::vim_state::vim_mut(self).count = c.min(crate::vim::MAX_COUNT);
2297    }
2298
2299    fn accumulate_count_digit(&mut self, digit: usize) {
2300        // Saturate the add too: once the multiply has saturated at
2301        // `usize::MAX`, a plain `+ digit` overflows (panic in debug builds)
2302        // after ~20 typed digits. Then clamp at vim's documented count
2303        // ceiling (`:h count`) so no apply loop can iterate more than
2304        // 999,999,999 times regardless of how many digits were typed.
2305        let v = crate::vim_state::vim_mut(self);
2306        v.count = v
2307            .count
2308            .saturating_mul(10)
2309            .saturating_add(digit)
2310            .min(crate::vim::MAX_COUNT);
2311    }
2312
2313    fn reset_count(&mut self) {
2314        crate::vim_state::vim_mut(self).count = 0;
2315    }
2316
2317    fn take_count(&mut self) -> usize {
2318        if crate::vim_state::vim(self).count > 0 {
2319            let n = crate::vim_state::vim(self).count;
2320            crate::vim_state::vim_mut(self).count = 0;
2321            n
2322        } else {
2323            1
2324        }
2325    }
2326
2327    fn fsm_mode(&self) -> crate::vim::Mode {
2328        crate::vim_state::vim(self).mode
2329    }
2330
2331    fn set_fsm_mode(&mut self, m: crate::vim::Mode) {
2332        crate::vim_state::vim_mut(self).mode = m;
2333        crate::vim_state::vim_mut(self).current_mode =
2334            crate::vim_state::vim_mut(self).public_mode();
2335    }
2336
2337    fn is_replaying(&self) -> bool {
2338        crate::vim_state::vim(self).replaying
2339    }
2340
2341    fn set_replaying(&mut self, v: bool) {
2342        crate::vim_state::vim_mut(self).replaying = v;
2343    }
2344
2345    fn is_one_shot_normal(&self) -> bool {
2346        crate::vim_state::vim(self).one_shot_normal
2347    }
2348
2349    fn set_one_shot_normal(&mut self, v: bool) {
2350        crate::vim_state::vim_mut(self).one_shot_normal = v;
2351    }
2352
2353    fn last_find(&self) -> Option<(char, bool, bool)> {
2354        crate::vim_state::vim(self).last_find
2355    }
2356
2357    fn set_last_find(&mut self, target: Option<(char, bool, bool)>) {
2358        crate::vim_state::vim_mut(self).last_find = target;
2359    }
2360
2361    fn sneak(&mut self, c1: char, c2: char, forward: bool, count: usize) {
2362        crate::vim::apply_sneak(self, c1, c2, forward, count.max(1));
2363    }
2364
2365    fn apply_op_sneak(
2366        &mut self,
2367        op: crate::vim::Operator,
2368        c1: char,
2369        c2: char,
2370        forward: bool,
2371        total_count: usize,
2372    ) {
2373        crate::vim::apply_op_sneak(self, op, c1, c2, forward, total_count);
2374    }
2375
2376    fn last_sneak(&self) -> Option<((char, char), bool)> {
2377        crate::vim_state::vim(self).last_sneak
2378    }
2379
2380    fn last_change(&self) -> Option<crate::vim::LastChange> {
2381        crate::vim_state::vim(self).last_change.clone()
2382    }
2383
2384    fn set_last_change(&mut self, lc: Option<crate::vim::LastChange>) {
2385        crate::vim_state::vim_mut(self).last_change = lc;
2386    }
2387
2388    fn last_change_mut(&mut self) -> Option<&mut crate::vim::LastChange> {
2389        crate::vim_state::vim_mut(self).last_change.as_mut()
2390    }
2391
2392    fn insert_session(&self) -> Option<&crate::vim::InsertSession> {
2393        crate::vim_state::vim(self).insert_session.as_ref()
2394    }
2395
2396    fn insert_session_mut(&mut self) -> Option<&mut crate::vim::InsertSession> {
2397        crate::vim_state::vim_mut(self).insert_session.as_mut()
2398    }
2399
2400    fn take_insert_session(&mut self) -> Option<crate::vim::InsertSession> {
2401        crate::vim_state::vim_mut(self).insert_session.take()
2402    }
2403
2404    fn set_insert_session(&mut self, s: Option<crate::vim::InsertSession>) {
2405        crate::vim_state::vim_mut(self).insert_session = s;
2406    }
2407
2408    // ─── Register selection / chord status / macro controller ──────────────
2409
2410    fn pending_register(&self) -> Option<char> {
2411        crate::vim_state::vim(self).pending_register
2412    }
2413
2414    fn pending_register_is_clipboard(&self) -> bool {
2415        matches!(
2416            crate::vim_state::vim(self).pending_register,
2417            Some('+') | Some('*')
2418        )
2419    }
2420
2421    fn recording_register(&self) -> Option<char> {
2422        crate::vim_state::vim(self).recording_macro
2423    }
2424
2425    fn pending_count(&self) -> Option<u32> {
2426        crate::vim_state::vim(self).pending_count_val()
2427    }
2428
2429    fn pending_op(&self) -> Option<char> {
2430        crate::vim_state::vim(self).pending_op_char()
2431    }
2432
2433    fn is_chord_pending(&self) -> bool {
2434        crate::vim_state::vim(self).is_chord_pending()
2435    }
2436
2437    fn is_insert_register_pending(&self) -> bool {
2438        crate::vim_state::vim(self).insert_pending_register
2439    }
2440
2441    fn clear_insert_register_pending(&mut self) {
2442        crate::vim_state::vim_mut(self).insert_pending_register = false;
2443    }
2444
2445    fn set_pending_register(&mut self, reg: char) {
2446        // `-` is the small-delete register (readable/pasteable, e.g. `"-p`).
2447        if reg.is_ascii_alphanumeric() || matches!(reg, '"' | '+' | '*' | '_' | '-') {
2448            crate::vim_state::vim_mut(self).pending_register = Some(reg);
2449        }
2450        // Invalid chars silently no-op (matches engine FSM behavior).
2451    }
2452
2453    fn start_macro_record(&mut self, reg: char) {
2454        if !(reg.is_ascii_alphabetic() || reg.is_ascii_digit()) {
2455            return;
2456        }
2457        crate::vim_state::vim_mut(self).recording_macro = Some(reg);
2458        if reg.is_ascii_uppercase() {
2459            // Seed recording_keys with the existing lowercase register's text
2460            // decoded back to inputs so capital-register append continues from
2461            // where the previous recording left off.
2462            let lower = reg.to_ascii_lowercase();
2463            let text = self
2464                .with_registers(|r| r.read(lower).map(|s| s.text.clone()))
2465                .unwrap_or_default();
2466            crate::vim_state::vim_mut(self).recording_keys =
2467                hjkl_engine::input::decode_macro(&text);
2468        } else {
2469            crate::vim_state::vim_mut(self).recording_keys.clear();
2470        }
2471    }
2472
2473    fn stop_macro_record(&mut self) {
2474        let Some(reg) = crate::vim_state::vim_mut(self).recording_macro.take() else {
2475            return;
2476        };
2477        let keys = std::mem::take(&mut crate::vim_state::vim_mut(self).recording_keys);
2478        let text = hjkl_engine::input::encode_macro(&keys);
2479        self.set_named_register_text(reg.to_ascii_lowercase(), text);
2480    }
2481
2482    fn is_recording_macro(&self) -> bool {
2483        crate::vim_state::vim(self).recording_macro.is_some()
2484    }
2485
2486    fn is_replaying_macro(&self) -> bool {
2487        crate::vim_state::vim(self).replaying_macro
2488    }
2489
2490    fn play_macro(&mut self, reg: char) -> Vec<hjkl_engine::input::Input> {
2491        let resolved = if reg == '@' {
2492            match crate::vim_state::vim(self).last_macro {
2493                Some(r) => r,
2494                None => return vec![],
2495            }
2496        } else {
2497            reg.to_ascii_lowercase()
2498        };
2499        let text = match self.with_registers(|regs| regs.read(resolved).cloned()) {
2500            Some(slot) if !slot.text.is_empty() => slot.text,
2501            _ => return vec![],
2502        };
2503        let keys = hjkl_engine::input::decode_macro(&text);
2504        crate::vim_state::vim_mut(self).last_macro = Some(resolved);
2505        crate::vim_state::vim_mut(self).replaying_macro = true;
2506        // ONE iteration only — the host loops the count (audit R2). The old
2507        // `keys.repeat(count)` materialized count × keys.len() Inputs up
2508        // front, so `999999999@a` allocated multi-GB before playing a key.
2509        keys
2510    }
2511
2512    fn end_macro_replay(&mut self) {
2513        crate::vim_state::vim_mut(self).replaying_macro = false;
2514    }
2515
2516    fn record_input(&mut self, input: hjkl_engine::input::Input) {
2517        if crate::vim_state::vim(self).recording_macro.is_some()
2518            && !crate::vim_state::vim(self).replaying_macro
2519        {
2520            crate::vim_state::vim_mut(self).recording_keys.push(input);
2521        }
2522    }
2523
2524    // ─── Mode reset / mouse-driven selection / operator range probe ────────
2525
2526    fn force_normal(&mut self) {
2527        crate::vim::force_normal_bridge(self);
2528    }
2529
2530    fn mouse_click_doc(&mut self, row: usize, col: usize) {
2531        crate::vim::mouse_click_doc_bridge(self, row, col);
2532    }
2533
2534    fn mouse_begin_drag(&mut self) {
2535        crate::vim::mouse_begin_drag_bridge(self);
2536    }
2537
2538    fn range_for_op_motion(
2539        &mut self,
2540        motion_key: char,
2541        total_count: usize,
2542    ) -> Option<(usize, usize)> {
2543        crate::vim::range_for_op_motion_bridge(self, motion_key, total_count)
2544    }
2545
2546    // ─── Motion dispatch / operator range probes ───────────────────────────
2547
2548    fn apply_motion(&mut self, kind: MotionKind, count: usize) {
2549        crate::vim::apply_motion_kind(self, kind, count);
2550    }
2551
2552    fn range_for_op_g(&mut self, ch: char, total_count: usize) -> Option<(usize, usize)> {
2553        crate::vim::range_for_op_g_bridge(self, ch, total_count)
2554    }
2555
2556    fn range_for_op_text_obj(
2557        &self,
2558        ch: char,
2559        inner: bool,
2560        total_count: usize,
2561    ) -> Option<(usize, usize)> {
2562        crate::vim::range_for_op_text_obj_bridge(self, ch, inner, total_count)
2563    }
2564}