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