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 /// `g0` — first character of the *screen* line: the start of the wrap
243 /// segment holding the cursor under wrap, the first visible column
244 /// without it.
245 ScreenLineStart,
246 /// `g^` — first non-blank character from the screen line's start on
247 /// (the line's last character when only blanks follow).
248 ScreenLineFirstNonBlank,
249 /// `g$` — last character of the screen line; inclusive under an
250 /// operator, and the NUL past the line's end in Visual when the line
251 /// ends on that screen line.
252 ScreenLineEnd,
253 /// `{` — previous paragraph (preceding blank line, or top).
254 ParagraphPrev,
255 /// `}` — next paragraph (following blank line, or bottom).
256 ParagraphNext,
257 /// `(` — previous sentence boundary.
258 SentencePrev,
259 /// `)` — next sentence boundary.
260 SentenceNext,
261 /// `gj` — `count` visual rows down (one screen segment per step
262 /// under `:set wrap`; falls back to `Down` otherwise).
263 ScreenDown,
264 /// `gk` — `count` visual rows up; mirror of [`Motion::ScreenDown`].
265 ScreenUp,
266 /// `[[` — backward to the previous `{` at column 0 (C section header).
267 /// Charwise exclusive; count-aware.
268 SectionBackward,
269 /// `]]` — forward to the next `{` at column 0. Charwise exclusive.
270 SectionForward,
271 /// `[]` — backward to the previous `}` at column 0 (C section end).
272 /// Charwise exclusive; count-aware.
273 SectionEndBackward,
274 /// `][` — forward to the next `}` at column 0. Charwise exclusive.
275 SectionEndForward,
276 /// `+` / `<CR>` — first non-blank of the next line. Linewise.
277 FirstNonBlankNextLine,
278 /// `-` — first non-blank of the previous line. Linewise.
279 FirstNonBlankPrevLine,
280 /// `_` — first non-blank of `count-1` lines down (count=1 = current line). Linewise.
281 FirstNonBlankLine,
282 /// `{count}|` — jump to column `count` on the current line (1-based;
283 /// no count or count=0 → column 1 → index 0). Clamped to line length.
284 GotoColumn,
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub enum TextObject {
289 Word {
290 big: bool,
291 },
292 Quote(char),
293 Bracket(char),
294 Paragraph,
295 /// `it` / `at` — XML/HTML-style tag pair. `inner = true` covers
296 /// content between `>` and `</`; `inner = false` covers the open
297 /// tag through the close tag inclusive.
298 XmlTag,
299 /// `is` / `as` — sentence: a run ending at `.`, `?`, or `!`
300 /// followed by whitespace or end-of-line. `inner = true` covers
301 /// the sentence text only; `inner = false` includes trailing
302 /// whitespace.
303 Sentence,
304}
305
306/// Classification determines how operators treat the range end.
307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
308pub enum RangeKind {
309 /// Range end is exclusive (end column not included). Typical: h, l, w, 0, $.
310 Exclusive,
311 /// Range end is inclusive. Typical: e, f, t, %.
312 Inclusive,
313 /// Whole lines from top row to bottom row. Typical: j, k, gg, G.
314 Linewise,
315}
316
317// ─── Dot-repeat storage ────────────────────────────────────────────────────
318
319/// Information needed to replay a mutating change via `.`.
320#[derive(Debug, Clone)]
321pub enum LastChange {
322 /// Operator over a motion.
323 OpMotion {
324 op: Operator,
325 motion: Motion,
326 count: usize,
327 inserted: Option<String>,
328 /// The explicit register the original change used (`"adw`), if any.
329 /// See [`LastChange::LineOp::register`].
330 register: Option<char>,
331 },
332 /// Operator over a text-object.
333 OpTextObj {
334 op: Operator,
335 obj: TextObject,
336 inner: bool,
337 inserted: Option<String>,
338 /// The explicit register the original change used (`"adiw`), if any.
339 /// See [`LastChange::LineOp::register`].
340 register: Option<char>,
341 },
342 /// Operator over the motion to mark `ch` (`d'a` when `linewise`, else
343 /// ``d`a``). Replays against the mark's position at repeat time.
344 OpMark {
345 op: Operator,
346 ch: char,
347 linewise: bool,
348 inserted: Option<String>,
349 /// See [`LastChange::LineOp::register`].
350 register: Option<char>,
351 },
352 /// `dd`, `cc`, `yy` with a count.
353 LineOp {
354 op: Operator,
355 count: usize,
356 inserted: Option<String>,
357 /// The explicit register (`"add`, `"ayy`, ...) the original change
358 /// used, if any. `.` must reuse it (`:h redo-register`) rather than
359 /// falling back to the unnamed register.
360 register: Option<char>,
361 },
362 /// `x`, `X` with a count.
363 CharDel {
364 forward: bool,
365 count: usize,
366 /// The explicit register the original change used (`"ax`), if any.
367 /// See [`LastChange::LineOp::register`].
368 register: Option<char>,
369 },
370 /// `r<ch>` with a count.
371 ReplaceChar { ch: char, count: usize },
372 /// `~` with a count.
373 ToggleCase { count: usize },
374 /// `J` with a count.
375 JoinLine { count: usize },
376 /// `p` / `P` (and `gp`/`gP`, `]p`/`[p`) with a count.
377 Paste {
378 before: bool,
379 count: usize,
380 /// `gp` / `gP` — leave the cursor just after the pasted text.
381 cursor_after: bool,
382 /// `]p` / `[p` — reindent the pasted block to the current line.
383 reindent: bool,
384 /// The explicit register the original paste read (`"ap`), if any.
385 /// See [`LastChange::LineOp::register`].
386 register: Option<char>,
387 },
388 /// `D` (delete to EOL).
389 DeleteToEol {
390 inserted: Option<String>,
391 /// The explicit register the original delete wrote (`"aD`), if any.
392 /// See [`LastChange::LineOp::register`].
393 register: Option<char>,
394 },
395 /// `o` / `O` + the inserted text.
396 OpenLine {
397 above: bool,
398 inserted: String,
399 /// `[count]o` opens `count` lines, each with `inserted`.
400 count: usize,
401 },
402 /// `i`/`I`/`a`/`A` + inserted text.
403 InsertAt {
404 entry: InsertEntry,
405 inserted: String,
406 count: usize,
407 },
408 /// `dgn` / `cgn` (and `gN` forms) — operate on the next search match.
409 /// `inserted` is filled on Esc for the `cgn` change form so `.` retypes it.
410 GnOp {
411 op: Operator,
412 forward: bool,
413 inserted: Option<String>,
414 /// The explicit register the original change used (`"acgn`), if any.
415 /// See [`LastChange::LineOp::register`].
416 register: Option<char>,
417 },
418 /// `R{text}<Esc>` — replace (overstrike) mode. `.` re-overtypes `text`.
419 ReplaceMode { text: String },
420 /// A visual-mode operator (`v`/`V` + `d`/`c`/`</>`/`~`/`u`/`U`/`?`).
421 /// vim (`:h v_.`) replays over a same-SIZE region anchored at the
422 /// current cursor rather than the original absolute range — `extent`
423 /// captures that size. `inserted` is filled on Esc for the `c` form so
424 /// `.` retypes it (same `AfterChange` patch site as `OpMotion` /
425 /// `OpTextObj` / `LineOp`).
426 ///
427 /// `d` / `c` / `~`/`u`/`U`/`g?` from Visual — charwise, linewise, AND
428 /// blockwise (`VisualExtent::Block`). Block `c` fills `inserted` at its
429 /// own `BlockChange` finish site (`comment.rs`), mirroring the charwise
430 /// `AfterChange` patch.
431 VisualOp {
432 op: Operator,
433 extent: VisualExtent,
434 inserted: Option<String>,
435 /// The explicit register the original change used (`vll"ad`), if
436 /// any. See [`LastChange::LineOp::register`].
437 register: Option<char>,
438 },
439 /// Charwise (`v`) / linewise (`V`) `r{ch}` — dot-repeat re-replaces a
440 /// same-SIZE region anchored at the cursor (`:h v_.`), mirroring
441 /// `VisualBlockReplace` for the block case. `r` has no `Operator`, so it
442 /// rides its own variant instead of `VisualOp`. `extent` is a
443 /// `VisualExtent::Char` or `VisualExtent::Line` captured from the live
444 /// selection.
445 VisualReplace { ch: char, extent: VisualExtent },
446 /// Visual-BLOCK `r{ch}` — dot-repeat re-replaces a same-size rectangle
447 /// anchored TOP-LEFT at the cursor. `r` has no `Operator`, so it rides
448 /// its own variant instead of `VisualOp`. `to_eol` preserves a `$`-ragged
449 /// right edge (`:h v_b_$`).
450 VisualBlockReplace {
451 ch: char,
452 rows: usize,
453 cols: usize,
454 to_eol: bool,
455 },
456 /// Visual-BLOCK `I` / `A` — dot-repeat re-inserts `text` at the block's
457 /// left (`append == false`) or right (`append == true`) edge over a
458 /// same-size rectangle anchored TOP-LEFT at the cursor. `cols` is the
459 /// block width (`A` appends `cols` columns past the cursor); `to_eol`
460 /// preserves a `$`-ragged right edge (`A` only, `:h v_b_$`).
461 VisualBlockInsert {
462 text: String,
463 rows: usize,
464 cols: usize,
465 to_eol: bool,
466 append: bool,
467 },
468}
469
470/// Size of a visual-mode selection, captured for `LastChange::VisualOp`
471/// dot-repeat (`:h v_.`). Vim's rule: characterwise replays over the same
472/// number of lines, with the same character width on the last line;
473/// linewise replays over the same number of lines.
474#[derive(Debug, Clone, Copy, PartialEq, Eq)]
475pub enum VisualExtent {
476 /// Charwise (`v`). `lines == 1`: `width` is the raw selected char count,
477 /// and replay selects exactly `width` chars starting at the cursor.
478 /// `lines > 1`: the first replay line runs from the cursor's column to
479 /// ITS OWN end of line, middle lines are taken whole, and the last line
480 /// takes its first `width` chars (measured from column 0, matching the
481 /// original selection's last-line char count).
482 Char { lines: usize, width: usize },
483 /// Linewise (`V`). Replay is exactly `[count]dd`-equivalent: `lines`
484 /// rows starting at the cursor's row.
485 Line { lines: usize },
486 /// Blockwise (`<C-v>`). Replay reconstructs a `rows` × `cols` rectangle
487 /// with its TOP-LEFT corner at the cursor (`:h v_.` for blocks), then
488 /// re-runs the operator. `to_eol` preserves a `$`-ragged right edge
489 /// (`:h v_b_$`) — every row then resolves its own EOL instead of the
490 /// fixed `cols` width.
491 Block {
492 rows: usize,
493 cols: usize,
494 to_eol: bool,
495 },
496}
497
498#[derive(Debug, Clone, Copy, PartialEq, Eq)]
499pub enum InsertEntry {
500 I,
501 A,
502 ShiftI,
503 ShiftA,
504 /// `gI` — insert at column 0, before any indent.
505 GShiftI,
506}
507
508/// Tracks which kind of horizontal jump was last performed so `;` / `,`
509/// can dispatch to the correct repeat handler.
510///
511/// - `FindChar` — last horizontal motion was `f`/`F`/`t`/`T`; `;`/`,`
512/// repeats via `Motion::FindRepeat`.
513/// - `Sneak` — last horizontal motion was `s`/`S` sneak; `;`/`,` repeats
514/// via `apply_sneak` with the stored digraph.
515/// - `None` — no horizontal motion yet; `;`/`,` are no-ops for both.
516#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
517pub enum LastHorizontalMotion {
518 #[default]
519 None,
520 FindChar,
521 Sneak,
522}
523
524#[derive(Debug, Clone)]
525pub struct InsertSession {
526 pub count: usize,
527 /// Min/max row visited during this session. Widens on every key.
528 pub row_min: usize,
529 pub row_max: usize,
530 /// O(1) rope snapshot of the full buffer at session entry. Used to
531 /// diff the affected row window at finish without being fooled by
532 /// cursor navigation through rows the user never edited.
533 /// `ropey::Rope::clone` is Arc-clone — no byte copying.
534 pub before_rope: ropey::Rope,
535 pub reason: InsertReason,
536 /// (row, col) where the insert session began (char-indexed). Abbreviation
537 /// expansion uses `start_col` as `mincol` — only chars at or after this
538 /// column on `start_row` are eligible as part of the `lhs` match, so
539 /// pre-existing buffer text is never consumed by expansion.
540 pub start_row: usize,
541 pub start_col: usize,
542}
543
544#[derive(Debug, Clone)]
545pub enum InsertReason {
546 /// Plain entry via i/I/a/A — recorded as `InsertAt`.
547 Enter(InsertEntry),
548 /// Entry via `o`/`O` — records OpenLine on Esc.
549 Open { above: bool },
550 /// Entry via an operator's change side-effect. Retro-fills the
551 /// stored last-change's `inserted` field on Esc.
552 AfterChange,
553 /// Entry via `C` (delete to EOL + insert).
554 DeleteToEol,
555 /// Entry via an insert triggered during dot-replay — don't touch
556 /// last_change because the outer replay will restore it.
557 ReplayOnly,
558 /// `I` or `A` from VisualBlock: insert the typed text at `col` on
559 /// rows in `top..=bot`. `col` is the start column for `I`, the
560 /// one-past-block-end column for `A`.
561 ///
562 /// `pad` distinguishes the two vim behaviours at rows shorter than
563 /// `col` (`:h v_b_I` vs `:h v_b_A`): `A` pads short rows with spaces
564 /// so the appended text still lines up (`pad: true`); `I` skips rows
565 /// that don't reach `col` entirely — no padding, no insert on that
566 /// row (`pad: false`).
567 ///
568 /// `cursor_col` is where the cursor lands on Esc — the block's LEFT
569 /// edge for both `I` and `A` (verified against real nvim: `A`'s
570 /// cursor does NOT land at the append/typed position, `col`, once
571 /// the block is more than one column wide). For `I`, `cursor_col ==
572 /// col` (its insertion point already IS the left edge); for `A` they
573 /// differ whenever the block spans more than one column.
574 BlockEdge {
575 top: usize,
576 bot: usize,
577 col: usize,
578 pad: bool,
579 cursor_col: usize,
580 /// Top row's char count BEFORE the `A` pad was applied. Recorded at
581 /// construction so `finish_insert_session` can remove exactly the
582 /// pad's bytes (`pre_pad_len..col`) on an empty Esc — the pad itself
583 /// is never recorded as an undo step, so its range must be stashed
584 /// here rather than derived from the (already padded) live buffer.
585 pre_pad_len: usize,
586 /// Undo-stack depth recorded just before the caller's `push_undo` for
587 /// this block command. An empty block-`A` pushes a no-op undo
588 /// boundary; when it is still the most recent one (depth == this + 1
589 /// at Esc — nothing else pushed mid-session, and the push was not
590 /// suppressed by an enclosing undo group), `finish_insert_session`
591 /// consumes it so the no-op command leaves the undo tree untouched.
592 undo_depth_before: usize,
593 },
594 /// `c` from VisualBlock: block content deleted, then user types
595 /// replacement text replicated across all block rows on Esc. Cursor
596 /// advances to the last typed char after replication (unlike BlockEdge
597 /// which leaves cursor at the insertion column).
598 BlockChange { top: usize, bot: usize, col: usize },
599 /// `R` — Replace mode. Each typed char overwrites the cell under
600 /// the cursor instead of inserting; at end-of-line the session
601 /// falls through to insert (same as vim).
602 Replace,
603}
604
605/// Saved visual-mode anchor + cursor for `gv` (re-enters the last
606/// visual selection). `mode` carries which visual flavour to
607/// restore; `anchor` / `cursor` mean different things per flavour:
608///
609/// - `Visual` — `anchor` is the char-wise visual anchor.
610/// - `VisualLine` — `anchor.0` is the `visual_line_anchor` row;
611/// `anchor.1` is unused.
612/// - `VisualBlock`— `anchor` is `block_anchor`, `block_vcol` is the
613/// sticky vcol that survives j/k clamping.
614#[derive(Debug, Clone, Copy)]
615pub struct LastVisual {
616 pub mode: Mode,
617 pub anchor: (usize, usize),
618 pub cursor: (usize, usize),
619 pub block_vcol: usize,
620}