Skip to main content

hjkl_vim_types/
lib.rs

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