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