Skip to main content

hjkl_vim_types/
lib.rs

1//! Vim vocabulary types for the hjkl editor.
2
3// ─── Modes & parser state ───────────────────────────────────────────────────
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
6pub enum Mode {
7    #[default]
8    Normal,
9    Insert,
10    Visual,
11    VisualLine,
12    /// Column-oriented selection (`Ctrl-V`). Unlike the other visual
13    /// modes this one doesn't use tui-textarea's single-range selection
14    /// — the block corners live in [`VimState::block_anchor`] and the
15    /// live cursor. Operators read the rectangle off those two points.
16    VisualBlock,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Default)]
20pub enum Pending {
21    #[default]
22    None,
23    /// Operator seen; still waiting for a motion / text-object / double-op.
24    /// `count1` is any count pressed before the operator.
25    Op { op: Operator, count1: usize },
26    /// Operator + 'i' or 'a' seen; waiting for the text-object character.
27    OpTextObj {
28        op: Operator,
29        count1: usize,
30        inner: bool,
31    },
32    /// Operator + 'g' seen (for `dgg`).
33    OpG { op: Operator, count1: usize },
34    /// Bare `g` seen in normal/visual — looking for `g`, `e`, `E`, …
35    G,
36    /// Bare `f`/`F`/`t`/`T` — looking for the target char.
37    Find { forward: bool, till: bool },
38    /// Operator + `f`/`F`/`t`/`T` — looking for target char.
39    OpFind {
40        op: Operator,
41        count1: usize,
42        forward: bool,
43        till: bool,
44    },
45    /// Operator + `'` (`linewise`) or `` ` `` — looking for the mark name.
46    OpMark { op: Operator, linewise: bool },
47    /// `r` pressed — waiting for the replacement char.
48    Replace,
49    /// Visual mode + `i` or `a` pressed — waiting for the text-object
50    /// character to extend the selection over.
51    VisualTextObj { inner: bool },
52    /// Bare `z` seen — looking for `z` (center), `t` (top), `b` (bottom).
53    Z,
54    /// `m` pressed — waiting for the mark letter to set.
55    SetMark,
56    /// `'` pressed — waiting for the mark letter to jump to its line
57    /// (lands on first non-blank, linewise for operators).
58    GotoMarkLine,
59    /// `` ` `` pressed — waiting for the mark letter to jump to the
60    /// exact `(row, col)` stored at set time (charwise for operators).
61    GotoMarkChar,
62    /// `"` pressed — waiting for the register selector. The next char
63    /// (`a`–`z`, `A`–`Z`, `0`–`9`, or `"`) sets `pending_register`.
64    SelectRegister,
65    /// `q` pressed (not currently recording) — waiting for the macro
66    /// register name. The macro records every key after the chord
67    /// resolves, until a bare `q` ends the recording.
68    RecordMacroTarget,
69    /// `@` pressed — waiting for the macro register name to play.
70    /// `count` is the prefix multiplier (`3@a` plays the macro 3
71    /// times); 0 means "no prefix" and is treated as 1.
72    PlayMacroTarget { count: usize },
73    /// `[` pressed in Normal/Visual mode — waiting for the second key.
74    /// Resolves `[[` → `SectionBackward`, `[]` → `SectionEndBackward`.
75    SquareBracketOpen,
76    /// `]` pressed in Normal/Visual mode — waiting for the second key.
77    /// Resolves `]]` → `SectionForward`, `][` → `SectionEndForward`.
78    SquareBracketClose,
79    /// Operator + `[` pending — waiting for second key to pick section motion.
80    OpSquareBracketOpen { op: Operator, count1: usize },
81    /// Operator + `]` pending — waiting for second key to pick section motion.
82    OpSquareBracketClose { op: Operator, count1: usize },
83    /// `s` / `S` in Normal mode with `motion_sneak=true` — waiting for
84    /// the first character of the two-char digraph.
85    /// `forward=true` → `s`; `forward=false` → `S` (backward).
86    SneakFirst { forward: bool, count: usize },
87    /// First sneak char captured; waiting for the second char to complete
88    /// the digraph and jump.
89    SneakSecond {
90        c1: char,
91        forward: bool,
92        count: usize,
93    },
94    /// Operator + `s` / `S` pending — waiting for the first char of the
95    /// two-char sneak digraph (e.g. `d` then `s` then `a` then `b` = `dsab`).
96    OpSneakFirst {
97        op: Operator,
98        count1: usize,
99        forward: bool,
100    },
101    /// Operator + sneak first char captured; waiting for the second char.
102    OpSneakSecond {
103        op: Operator,
104        count1: usize,
105        c1: char,
106        forward: bool,
107    },
108}
109
110// ─── Operator / Motion / TextObject ────────────────────────────────────────
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum Operator {
114    Delete,
115    Change,
116    Yank,
117    /// `gU{motion}` — uppercase the range. Entered via the `g` prefix
118    /// in normal mode or `U` in visual mode.
119    Uppercase,
120    /// `gu{motion}` — lowercase the range. `u` in visual mode.
121    Lowercase,
122    /// `g~{motion}` — toggle case of the range. `~` in visual mode
123    /// (character at the cursor for the single-char `~` command stays
124    /// its own code path in normal mode).
125    ToggleCase,
126    /// `>{motion}` — indent the line range by `shiftwidth` spaces.
127    /// Always linewise, even when the motion is char-wise — mirrors
128    /// vim's behaviour where `>w` indents the current line, not the
129    /// word on it.
130    Indent,
131    /// `<{motion}` — outdent the line range (remove up to
132    /// `shiftwidth` leading spaces per line).
133    Outdent,
134    /// `zf{motion}` / `zf{textobj}` / Visual `zf` — create a closed
135    /// fold spanning the row range. Doesn't mutate the buffer text;
136    /// cursor restores to the operator's start position.
137    Fold,
138    /// `gq{motion}` — reflow the row range to `settings.textwidth`.
139    /// Greedy word-wrap: collapses each paragraph (blank-line-bounded
140    /// run) into space-separated words, then re-emits lines whose
141    /// width stays under `textwidth`. Always linewise, like indent.
142    Reflow,
143    /// `gw{motion}` — same reflow as `gq` but cursor stays at the
144    /// pre-reflow `(row, col)`. If the reflow shrinks the line so the
145    /// original col is past the new EOL, the col is clamped to the last
146    /// char of the line (vim's behaviour). Always linewise.
147    ReflowKeepCursor,
148    /// `={motion}` — auto-indent the line range using shiftwidth-based
149    /// bracket depth counting (v1 dumb reindent). Always linewise.
150    /// See `auto_indent_range` for the algorithm and its limitations.
151    AutoIndent,
152    /// `!{motion}` — filter the line range through an external shell command.
153    /// The range text is piped to the command's stdin; stdout replaces the
154    /// range in the buffer. Non-zero exit or spawn failure returns an error
155    /// to the caller without mutating the buffer.
156    Filter,
157    /// `gc{motion}` / `gcc` — toggle line comments on the row range.
158    /// Dispatched through `Editor::toggle_comment_range` rather than the
159    /// normal `run_operator_over_range` pipeline (same pattern as `Filter`).
160    Comment,
161    /// `g?{motion}` / `g??` / visual `g?` — ROT13 the range. Same operator
162    /// shape as the case ops; only the per-char transform differs.
163    Rot13,
164}
165
166#[derive(Debug, Clone, PartialEq, Eq)]
167pub enum Motion {
168    Left,
169    Right,
170    /// `<Space>` — right-motion that wraps to the next line at EOL (vim's
171    /// default `whichwrap=b,s`). Distinct from `Right`/`l` which never wrap.
172    SpaceFwd,
173    /// `<BS>` — left-motion that wraps to the previous line's last char at BOL
174    /// (`whichwrap=b`). Distinct from `Left`/`h` which never wrap.
175    BackspaceBack,
176    Up,
177    Down,
178    WordFwd,
179    BigWordFwd,
180    WordBack,
181    BigWordBack,
182    WordEnd,
183    BigWordEnd,
184    /// The word-end motion `cw` / `cW` runs instead of `w` / `W` when the
185    /// cursor is on a non-blank (`:h cw`). It is NOT `e` / `E`: vim calls
186    /// `end_word()` with its `stop` flag set, so a first iteration that
187    /// starts on the last character of a word does not move — `cw` there
188    /// changes just that character, where `ce` runs on to the next word's
189    /// end. `big` picks `cW` over `cw`.
190    ChangeWordEnd {
191        big: bool,
192    },
193    /// `ge` — backward word end.
194    WordEndBack,
195    /// `gE` — backward WORD end.
196    BigWordEndBack,
197    LineStart,
198    FirstNonBlank,
199    LineEnd,
200    FileTop,
201    FileBottom,
202    Find {
203        ch: char,
204        forward: bool,
205        till: bool,
206    },
207    FindRepeat {
208        reverse: bool,
209    },
210    MatchBracket,
211    /// `[(` / `])` / `[{` / `]}` — jump to the previous/next unmatched bracket
212    /// of the given kind. `open` is the open char (`(` or `{`); `forward` picks
213    /// the close (`)`/`}`) when true, the open when false.
214    UnmatchedBracket {
215        forward: bool,
216        open: char,
217    },
218    WordAtCursor {
219        forward: bool,
220        /// `*` / `#` use `\bword\b` boundaries; `g*` / `g#` drop them so
221        /// the search hits substrings (e.g. `foo` matches inside `foobar`).
222        whole_word: bool,
223    },
224    /// `n` / `N` — repeat the last `/` or `?` search.
225    SearchNext {
226        reverse: bool,
227    },
228    /// `H` — cursor to viewport top (plus `count - 1` rows down).
229    ViewportTop,
230    /// `M` — cursor to viewport middle.
231    ViewportMiddle,
232    /// `L` — cursor to viewport bottom (minus `count - 1` rows up).
233    ViewportBottom,
234    /// `g_` — last non-blank char on the line.
235    LastNonBlank,
236    /// `gM` — cursor to the middle char column of the current line
237    /// (`floor(chars / 2)`). Vim's variant ignoring screen wrap.
238    LineMiddle,
239    /// `gm` — cursor to the middle of the *screen* line: column
240    /// `min(viewport_width / 2, last_col)`. Differs from `gM` (char-middle).
241    ScreenLineMiddle,
242    /// `{` — previous paragraph (preceding blank line, or top).
243    ParagraphPrev,
244    /// `}` — next paragraph (following blank line, or bottom).
245    ParagraphNext,
246    /// `(` — previous sentence boundary.
247    SentencePrev,
248    /// `)` — next sentence boundary.
249    SentenceNext,
250    /// `gj` — `count` visual rows down (one screen segment per step
251    /// under `:set wrap`; falls back to `Down` otherwise).
252    ScreenDown,
253    /// `gk` — `count` visual rows up; mirror of [`Motion::ScreenDown`].
254    ScreenUp,
255    /// `[[` — backward to the previous `{` at column 0 (C section header).
256    /// Charwise exclusive; count-aware.
257    SectionBackward,
258    /// `]]` — forward to the next `{` at column 0. Charwise exclusive.
259    SectionForward,
260    /// `[]` — backward to the previous `}` at column 0 (C section end).
261    /// Charwise exclusive; count-aware.
262    SectionEndBackward,
263    /// `][` — forward to the next `}` at column 0. Charwise exclusive.
264    SectionEndForward,
265    /// `+` / `<CR>` — first non-blank of the next line. Linewise.
266    FirstNonBlankNextLine,
267    /// `-` — first non-blank of the previous line. Linewise.
268    FirstNonBlankPrevLine,
269    /// `_` — first non-blank of `count-1` lines down (count=1 = current line). Linewise.
270    FirstNonBlankLine,
271    /// `{count}|` — jump to column `count` on the current line (1-based;
272    /// no count or count=0 → column 1 → index 0). Clamped to line length.
273    GotoColumn,
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Eq)]
277pub enum TextObject {
278    Word {
279        big: bool,
280    },
281    Quote(char),
282    Bracket(char),
283    Paragraph,
284    /// `it` / `at` — XML/HTML-style tag pair. `inner = true` covers
285    /// content between `>` and `</`; `inner = false` covers the open
286    /// tag through the close tag inclusive.
287    XmlTag,
288    /// `is` / `as` — sentence: a run ending at `.`, `?`, or `!`
289    /// followed by whitespace or end-of-line. `inner = true` covers
290    /// the sentence text only; `inner = false` includes trailing
291    /// whitespace.
292    Sentence,
293}
294
295/// Classification determines how operators treat the range end.
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297pub enum RangeKind {
298    /// Range end is exclusive (end column not included). Typical: h, l, w, 0, $.
299    Exclusive,
300    /// Range end is inclusive. Typical: e, f, t, %.
301    Inclusive,
302    /// Whole lines from top row to bottom row. Typical: j, k, gg, G.
303    Linewise,
304}
305
306// ─── Dot-repeat storage ────────────────────────────────────────────────────
307
308/// Information needed to replay a mutating change via `.`.
309#[derive(Debug, Clone)]
310pub enum LastChange {
311    /// Operator over a motion.
312    OpMotion {
313        op: Operator,
314        motion: Motion,
315        count: usize,
316        inserted: Option<String>,
317        /// The explicit register the original change used (`"adw`), if any.
318        /// See [`LastChange::LineOp::register`].
319        register: Option<char>,
320    },
321    /// Operator over a text-object.
322    OpTextObj {
323        op: Operator,
324        obj: TextObject,
325        inner: bool,
326        inserted: Option<String>,
327        /// The explicit register the original change used (`"adiw`), if any.
328        /// See [`LastChange::LineOp::register`].
329        register: Option<char>,
330    },
331    /// Operator over the motion to mark `ch` (`d'a` when `linewise`, else
332    /// ``d`a``). Replays against the mark's position at repeat time.
333    OpMark {
334        op: Operator,
335        ch: char,
336        linewise: bool,
337        inserted: Option<String>,
338        /// See [`LastChange::LineOp::register`].
339        register: Option<char>,
340    },
341    /// `dd`, `cc`, `yy` with a count.
342    LineOp {
343        op: Operator,
344        count: usize,
345        inserted: Option<String>,
346        /// The explicit register (`"add`, `"ayy`, ...) the original change
347        /// used, if any. `.` must reuse it (`:h redo-register`) rather than
348        /// falling back to the unnamed register.
349        register: Option<char>,
350    },
351    /// `x`, `X` with a count.
352    CharDel {
353        forward: bool,
354        count: usize,
355        /// The explicit register the original change used (`"ax`), if any.
356        /// See [`LastChange::LineOp::register`].
357        register: Option<char>,
358    },
359    /// `r<ch>` with a count.
360    ReplaceChar { ch: char, count: usize },
361    /// `~` with a count.
362    ToggleCase { count: usize },
363    /// `J` with a count.
364    JoinLine { count: usize },
365    /// `p` / `P` (and `gp`/`gP`, `]p`/`[p`) with a count.
366    Paste {
367        before: bool,
368        count: usize,
369        /// `gp` / `gP` — leave the cursor just after the pasted text.
370        cursor_after: bool,
371        /// `]p` / `[p` — reindent the pasted block to the current line.
372        reindent: bool,
373        /// The explicit register the original paste read (`"ap`), if any.
374        /// See [`LastChange::LineOp::register`].
375        register: Option<char>,
376    },
377    /// `D` (delete to EOL).
378    DeleteToEol {
379        inserted: Option<String>,
380        /// The explicit register the original delete wrote (`"aD`), if any.
381        /// See [`LastChange::LineOp::register`].
382        register: Option<char>,
383    },
384    /// `o` / `O` + the inserted text.
385    OpenLine {
386        above: bool,
387        inserted: String,
388        /// `[count]o` opens `count` lines, each with `inserted`.
389        count: usize,
390    },
391    /// `i`/`I`/`a`/`A` + inserted text.
392    InsertAt {
393        entry: InsertEntry,
394        inserted: String,
395        count: usize,
396    },
397    /// `dgn` / `cgn` (and `gN` forms) — operate on the next search match.
398    /// `inserted` is filled on Esc for the `cgn` change form so `.` retypes it.
399    GnOp {
400        op: Operator,
401        forward: bool,
402        inserted: Option<String>,
403        /// The explicit register the original change used (`"acgn`), if any.
404        /// See [`LastChange::LineOp::register`].
405        register: Option<char>,
406    },
407    /// `R{text}<Esc>` — replace (overstrike) mode. `.` re-overtypes `text`.
408    ReplaceMode { text: String },
409    /// A visual-mode operator (`v`/`V` + `d`/`c`/`</>`/`~`/`u`/`U`/`?`).
410    /// vim (`:h v_.`) replays over a same-SIZE region anchored at the
411    /// current cursor rather than the original absolute range — `extent`
412    /// captures that size. `inserted` is filled on Esc for the `c` form so
413    /// `.` retypes it (same `AfterChange` patch site as `OpMotion` /
414    /// `OpTextObj` / `LineOp`).
415    ///
416    /// `d` / `c` / `~`/`u`/`U`/`g?` from Visual — charwise, linewise, AND
417    /// blockwise (`VisualExtent::Block`). Block `c` fills `inserted` at its
418    /// own `BlockChange` finish site (`comment.rs`), mirroring the charwise
419    /// `AfterChange` patch.
420    VisualOp {
421        op: Operator,
422        extent: VisualExtent,
423        inserted: Option<String>,
424        /// The explicit register the original change used (`vll"ad`), if
425        /// any. See [`LastChange::LineOp::register`].
426        register: Option<char>,
427    },
428    /// Charwise (`v`) / linewise (`V`) `r{ch}` — dot-repeat re-replaces a
429    /// same-SIZE region anchored at the cursor (`:h v_.`), mirroring
430    /// `VisualBlockReplace` for the block case. `r` has no `Operator`, so it
431    /// rides its own variant instead of `VisualOp`. `extent` is a
432    /// `VisualExtent::Char` or `VisualExtent::Line` captured from the live
433    /// selection.
434    VisualReplace { ch: char, extent: VisualExtent },
435    /// Visual-BLOCK `r{ch}` — dot-repeat re-replaces a same-size rectangle
436    /// anchored TOP-LEFT at the cursor. `r` has no `Operator`, so it rides
437    /// its own variant instead of `VisualOp`. `to_eol` preserves a `$`-ragged
438    /// right edge (`:h v_b_$`).
439    VisualBlockReplace {
440        ch: char,
441        rows: usize,
442        cols: usize,
443        to_eol: bool,
444    },
445    /// Visual-BLOCK `I` / `A` — dot-repeat re-inserts `text` at the block's
446    /// left (`append == false`) or right (`append == true`) edge over a
447    /// same-size rectangle anchored TOP-LEFT at the cursor. `cols` is the
448    /// block width (`A` appends `cols` columns past the cursor); `to_eol`
449    /// preserves a `$`-ragged right edge (`A` only, `:h v_b_$`).
450    VisualBlockInsert {
451        text: String,
452        rows: usize,
453        cols: usize,
454        to_eol: bool,
455        append: bool,
456    },
457}
458
459/// Size of a visual-mode selection, captured for `LastChange::VisualOp`
460/// dot-repeat (`:h v_.`). Vim's rule: characterwise replays over the same
461/// number of lines, with the same character width on the last line;
462/// linewise replays over the same number of lines.
463#[derive(Debug, Clone, Copy, PartialEq, Eq)]
464pub enum VisualExtent {
465    /// Charwise (`v`). `lines == 1`: `width` is the raw selected char count,
466    /// and replay selects exactly `width` chars starting at the cursor.
467    /// `lines > 1`: the first replay line runs from the cursor's column to
468    /// ITS OWN end of line, middle lines are taken whole, and the last line
469    /// takes its first `width` chars (measured from column 0, matching the
470    /// original selection's last-line char count).
471    Char { lines: usize, width: usize },
472    /// Linewise (`V`). Replay is exactly `[count]dd`-equivalent: `lines`
473    /// rows starting at the cursor's row.
474    Line { lines: usize },
475    /// Blockwise (`<C-v>`). Replay reconstructs a `rows` × `cols` rectangle
476    /// with its TOP-LEFT corner at the cursor (`:h v_.` for blocks), then
477    /// re-runs the operator. `to_eol` preserves a `$`-ragged right edge
478    /// (`:h v_b_$`) — every row then resolves its own EOL instead of the
479    /// fixed `cols` width.
480    Block {
481        rows: usize,
482        cols: usize,
483        to_eol: bool,
484    },
485}
486
487#[derive(Debug, Clone, Copy, PartialEq, Eq)]
488pub enum InsertEntry {
489    I,
490    A,
491    ShiftI,
492    ShiftA,
493    /// `gI` — insert at column 0, before any indent.
494    GShiftI,
495}
496
497/// Tracks which kind of horizontal jump was last performed so `;` / `,`
498/// can dispatch to the correct repeat handler.
499///
500/// - `FindChar` — last horizontal motion was `f`/`F`/`t`/`T`; `;`/`,`
501///   repeats via `Motion::FindRepeat`.
502/// - `Sneak` — last horizontal motion was `s`/`S` sneak; `;`/`,` repeats
503///   via `apply_sneak` with the stored digraph.
504/// - `None` — no horizontal motion yet; `;`/`,` are no-ops for both.
505#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
506pub enum LastHorizontalMotion {
507    #[default]
508    None,
509    FindChar,
510    Sneak,
511}
512
513#[derive(Debug, Clone)]
514pub struct InsertSession {
515    pub count: usize,
516    /// Min/max row visited during this session. Widens on every key.
517    pub row_min: usize,
518    pub row_max: usize,
519    /// O(1) rope snapshot of the full buffer at session entry. Used to
520    /// diff the affected row window at finish without being fooled by
521    /// cursor navigation through rows the user never edited.
522    /// `ropey::Rope::clone` is Arc-clone — no byte copying.
523    pub before_rope: ropey::Rope,
524    pub reason: InsertReason,
525    /// (row, col) where the insert session began (char-indexed). Abbreviation
526    /// expansion uses `start_col` as `mincol` — only chars at or after this
527    /// column on `start_row` are eligible as part of the `lhs` match, so
528    /// pre-existing buffer text is never consumed by expansion.
529    pub start_row: usize,
530    pub start_col: usize,
531}
532
533#[derive(Debug, Clone)]
534pub enum InsertReason {
535    /// Plain entry via i/I/a/A — recorded as `InsertAt`.
536    Enter(InsertEntry),
537    /// Entry via `o`/`O` — records OpenLine on Esc.
538    Open { above: bool },
539    /// Entry via an operator's change side-effect. Retro-fills the
540    /// stored last-change's `inserted` field on Esc.
541    AfterChange,
542    /// Entry via `C` (delete to EOL + insert).
543    DeleteToEol,
544    /// Entry via an insert triggered during dot-replay — don't touch
545    /// last_change because the outer replay will restore it.
546    ReplayOnly,
547    /// `I` or `A` from VisualBlock: insert the typed text at `col` on
548    /// rows in `top..=bot`. `col` is the start column for `I`, the
549    /// one-past-block-end column for `A`.
550    ///
551    /// `pad` distinguishes the two vim behaviours at rows shorter than
552    /// `col` (`:h v_b_I` vs `:h v_b_A`): `A` pads short rows with spaces
553    /// so the appended text still lines up (`pad: true`); `I` skips rows
554    /// that don't reach `col` entirely — no padding, no insert on that
555    /// row (`pad: false`).
556    ///
557    /// `cursor_col` is where the cursor lands on Esc — the block's LEFT
558    /// edge for both `I` and `A` (verified against real nvim: `A`'s
559    /// cursor does NOT land at the append/typed position, `col`, once
560    /// the block is more than one column wide). For `I`, `cursor_col ==
561    /// col` (its insertion point already IS the left edge); for `A` they
562    /// differ whenever the block spans more than one column.
563    BlockEdge {
564        top: usize,
565        bot: usize,
566        col: usize,
567        pad: bool,
568        cursor_col: usize,
569        /// Top row's char count BEFORE the `A` pad was applied. Recorded at
570        /// construction so `finish_insert_session` can remove exactly the
571        /// pad's bytes (`pre_pad_len..col`) on an empty Esc — the pad itself
572        /// is never recorded as an undo step, so its range must be stashed
573        /// here rather than derived from the (already padded) live buffer.
574        pre_pad_len: usize,
575        /// Undo-stack depth recorded just before the caller's `push_undo` for
576        /// this block command. An empty block-`A` pushes a no-op undo
577        /// boundary; when it is still the most recent one (depth == this + 1
578        /// at Esc — nothing else pushed mid-session, and the push was not
579        /// suppressed by an enclosing undo group), `finish_insert_session`
580        /// consumes it so the no-op command leaves the undo tree untouched.
581        undo_depth_before: usize,
582    },
583    /// `c` from VisualBlock: block content deleted, then user types
584    /// replacement text replicated across all block rows on Esc. Cursor
585    /// advances to the last typed char after replication (unlike BlockEdge
586    /// which leaves cursor at the insertion column).
587    BlockChange { top: usize, bot: usize, col: usize },
588    /// `R` — Replace mode. Each typed char overwrites the cell under
589    /// the cursor instead of inserting; at end-of-line the session
590    /// falls through to insert (same as vim).
591    Replace,
592}
593
594/// Saved visual-mode anchor + cursor for `gv` (re-enters the last
595/// visual selection). `mode` carries which visual flavour to
596/// restore; `anchor` / `cursor` mean different things per flavour:
597///
598/// - `Visual`     — `anchor` is the char-wise visual anchor.
599/// - `VisualLine` — `anchor.0` is the `visual_line_anchor` row;
600///   `anchor.1` is unused.
601/// - `VisualBlock`— `anchor` is `block_anchor`, `block_vcol` is the
602///   sticky vcol that survives j/k clamping.
603#[derive(Debug, Clone, Copy)]
604pub struct LastVisual {
605    pub mode: Mode,
606    pub anchor: (usize, usize),
607    pub cursor: (usize, usize),
608    pub block_vcol: usize,
609}