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 },
320 /// `x`, `X` with a count.
321 CharDel { forward: bool, count: usize },
322 /// `r<ch>` with a count.
323 ReplaceChar { ch: char, count: usize },
324 /// `~` with a count.
325 ToggleCase { count: usize },
326 /// `J` with a count.
327 JoinLine { count: usize },
328 /// `p` / `P` (and `gp`/`gP`, `]p`/`[p`) with a count.
329 Paste {
330 before: bool,
331 count: usize,
332 /// `gp` / `gP` — leave the cursor just after the pasted text.
333 cursor_after: bool,
334 /// `]p` / `[p` — reindent the pasted block to the current line.
335 reindent: bool,
336 },
337 /// `D` (delete to EOL).
338 DeleteToEol { inserted: Option<String> },
339 /// `o` / `O` + the inserted text.
340 OpenLine { above: bool, inserted: String },
341 /// `i`/`I`/`a`/`A` + inserted text.
342 InsertAt {
343 entry: InsertEntry,
344 inserted: String,
345 count: usize,
346 },
347 /// `dgn` / `cgn` (and `gN` forms) — operate on the next search match.
348 /// `inserted` is filled on Esc for the `cgn` change form so `.` retypes it.
349 GnOp {
350 op: Operator,
351 forward: bool,
352 inserted: Option<String>,
353 },
354 /// `R{text}<Esc>` — replace (overstrike) mode. `.` re-overtypes `text`.
355 ReplaceMode { text: String },
356}
357
358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
359pub enum InsertEntry {
360 I,
361 A,
362 ShiftI,
363 ShiftA,
364}
365
366/// Tracks which kind of horizontal jump was last performed so `;` / `,`
367/// can dispatch to the correct repeat handler.
368///
369/// - `FindChar` — last horizontal motion was `f`/`F`/`t`/`T`; `;`/`,`
370/// repeats via `Motion::FindRepeat`.
371/// - `Sneak` — last horizontal motion was `s`/`S` sneak; `;`/`,` repeats
372/// via `apply_sneak` with the stored digraph.
373/// - `None` — no horizontal motion yet; `;`/`,` are no-ops for both.
374#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
375pub enum LastHorizontalMotion {
376 #[default]
377 None,
378 FindChar,
379 Sneak,
380}
381
382#[derive(Debug, Clone)]
383pub struct InsertSession {
384 pub count: usize,
385 /// Min/max row visited during this session. Widens on every key.
386 pub row_min: usize,
387 pub row_max: usize,
388 /// O(1) rope snapshot of the full buffer at session entry. Used to
389 /// diff the affected row window at finish without being fooled by
390 /// cursor navigation through rows the user never edited.
391 /// `ropey::Rope::clone` is Arc-clone — no byte copying.
392 pub before_rope: ropey::Rope,
393 pub reason: InsertReason,
394 /// (row, col) where the insert session began (char-indexed). Abbreviation
395 /// expansion uses `start_col` as `mincol` — only chars at or after this
396 /// column on `start_row` are eligible as part of the `lhs` match, so
397 /// pre-existing buffer text is never consumed by expansion.
398 pub start_row: usize,
399 pub start_col: usize,
400}
401
402#[derive(Debug, Clone)]
403pub enum InsertReason {
404 /// Plain entry via i/I/a/A — recorded as `InsertAt`.
405 Enter(InsertEntry),
406 /// Entry via `o`/`O` — records OpenLine on Esc.
407 Open { above: bool },
408 /// Entry via an operator's change side-effect. Retro-fills the
409 /// stored last-change's `inserted` field on Esc.
410 AfterChange,
411 /// Entry via `C` (delete to EOL + insert).
412 DeleteToEol,
413 /// Entry via an insert triggered during dot-replay — don't touch
414 /// last_change because the outer replay will restore it.
415 ReplayOnly,
416 /// `I` or `A` from VisualBlock: insert the typed text at `col` on
417 /// every row in `top..=bot`. `col` is the start column for `I`, the
418 /// one-past-block-end column for `A`.
419 BlockEdge { top: usize, bot: usize, col: usize },
420 /// `c` from VisualBlock: block content deleted, then user types
421 /// replacement text replicated across all block rows on Esc. Cursor
422 /// advances to the last typed char after replication (unlike BlockEdge
423 /// which leaves cursor at the insertion column).
424 BlockChange { top: usize, bot: usize, col: usize },
425 /// `R` — Replace mode. Each typed char overwrites the cell under
426 /// the cursor instead of inserting; at end-of-line the session
427 /// falls through to insert (same as vim).
428 Replace,
429}
430
431/// Saved visual-mode anchor + cursor for `gv` (re-enters the last
432/// visual selection). `mode` carries which visual flavour to
433/// restore; `anchor` / `cursor` mean different things per flavour:
434///
435/// - `Visual` — `anchor` is the char-wise visual anchor.
436/// - `VisualLine` — `anchor.0` is the `visual_line_anchor` row;
437/// `anchor.1` is unused.
438/// - `VisualBlock`— `anchor` is `block_anchor`, `block_vcol` is the
439/// sticky vcol that survives j/k clamping.
440#[derive(Debug, Clone, Copy)]
441pub struct LastVisual {
442 pub mode: Mode,
443 pub anchor: (usize, usize),
444 pub cursor: (usize, usize),
445 pub block_vcol: usize,
446}