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}