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}