vorto 0.13.0-beta.2

A Vim-flavored modal terminal editor with batteries included: tree-sitter, LSP, fuzzy pickers, vim-surround, multi-cursor, and optional Copilot.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
use crate::finder::FuzzyKind;
use crate::mode::Mode;

// ════════════════════════════════════════════════════════════════════════
// Tokens (syntactic level)
// ════════════════════════════════════════════════════════════════════════
//
// A `Token` is what a single key press resolves to in the current parsing
// context. The token list accumulates until `classify` decides it forms a
// complete vim-style command, at which point `build_expr` turns it into
// the semantic AST (`Expr`).

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Token {
    /// A digit being typed as a count prefix (e.g. `2`, `0` for "20").
    /// Multiple consecutive `Count` tokens combine into one number.
    Count(u32),
    /// Operator pending: `d`, `y`, `c`.
    Op(Operator),
    /// The same operator key pressed again immediately — vim's
    /// `dd` / `yy` "operator on current line" shortcut.
    SelfDouble(Operator),
    /// A motion / cursor-move (h, j, w, $, G, etc.).
    Motion(MotionKind),
    /// A standalone action that fires immediately (i, o, :, /, p, u, …).
    Direct(DirectKind),
    /// Text-object scope marker, only valid after an operator (i / a).
    Scope(Scope),
    /// Text-object body marker, valid after a Scope (w, ", (, …).
    Object(Object),
    /// `<space>` leader — by itself transitions tokenization context but
    /// is otherwise dropped during `build_expr`.
    LeaderPrefix,
    /// `g` prefix — for two-key sequences like `gg` (goto file start).
    GotoPrefix,
    /// `z` prefix — for `zz`/`zt`/`zb` viewport-scroll actions.
    ZPrefix,
    /// `f`/`F`/`t`/`T` waiting for the literal target character. The
    /// next key press in `FindCharPending` context resolves to a
    /// `Motion(FindChar { … })` carrying this prefix's direction/till
    /// flags plus the typed character.
    FindCharPrefix { forward: bool, till: bool },
    /// `r` — waiting for the replacement char.
    ReplaceCharPrefix,
    /// `<space>w` — the "window" sub-leader. Tokenization context
    /// transitions to a window-pending state so the next key resolves
    /// against `WINDOW_BINDINGS` (split / close / focus / cycle).
    WindowPrefix,
    /// `Ctrl-W` — vim's window-prefix chord. Same role as
    /// [`WindowPrefix`] but with a separate binding table because
    /// vim's `Ctrl-W h` is "focus left" while `<space>w h` is
    /// "horizontal split" in this app.
    ///
    /// [`WindowPrefix`]: Token::WindowPrefix
    CtrlWPrefix,
    /// `<space>m` — the harpoon "bookmark" sub-leader. Tokenization
    /// transitions to a bookmark-pending state so the next key resolves
    /// against `BOOKMARK_BINDINGS` (`a` add / `d` remove-here / `m`
    /// picker). Same shape as [`WindowPrefix`](Token::WindowPrefix).
    BookmarkPrefix,
    /// `]` / `[` — bracket-prefix for next/previous-style jumps
    /// (`]d` / `[d` for diagnostics, room for `]c` / `[c` etc.).
    /// Direction is baked into the prefix so the body bindings can
    /// stay direction-free.
    BracketPrefix { forward: bool },
    /// `ys` — vim-surround "add". Emitted in OpPending after a Yank
    /// when `s` is pressed; the trailing `Op(Yank)` on the stack is
    /// kept as the parse anchor (see `build_expr`).
    SurroundAddPrefix,
    /// `cs` — vim-surround "change". Same anchor pattern as
    /// [`Token::SurroundAddPrefix`] but emitted after a Change op.
    SurroundChangePrefix,
    /// `ds` — vim-surround "delete". Same anchor pattern but after a
    /// Delete op.
    SurroundDeletePrefix,
    /// A literal surround char (`"`, `(`, `t` …) captured in
    /// `SurroundCharPending` context. Carries the raw key so the
    /// evaluator can map it to a pair / text-object kind.
    SurroundChar(char),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Operator {
    Delete,
    Yank,
    Change,
    /// `>` — shift target lines right by one indent level.
    Indent,
    /// `<` — shift target lines left by one indent level.
    Dedent,
    /// `gc` — toggle line comments over the target range (or rows
    /// touched by the target, for non-line-wise targets). `gcc` is
    /// the self-double shortcut for the current line; `<count>gcc`
    /// covers `count` lines. With multiple cursors active and a
    /// line-wise target, fans out to every cursor's row.
    Comment,
    /// `gb` — toggle a block comment around the target range using
    /// the language's `block_comment_token` pair (e.g. `/* … */`).
    /// Falls back to `Comment` (per-row line comment) when the active
    /// language has no block tokens configured. `gbc` is the
    /// self-double shortcut for the current line.
    BlockComment,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MotionKind {
    Left,
    Right,
    Up,
    Down,
    LineStart,
    LineEnd,
    /// `^` — first non-whitespace char on the line.
    LineFirstNonBlank,
    /// `g_` — last non-whitespace char on the line.
    LineLastNonBlank,
    WordForward,
    WordBack,
    /// `e` — end of the current/next word (char-class).
    WordEnd,
    /// `W` — WORD forward (whitespace-delimited, no punctuation split).
    BigWordForward,
    /// `B` — WORD back.
    BigWordBack,
    /// `E` — WORD end forward.
    BigWordEnd,
    /// `ge` — backward word end.
    WordEndBack,
    /// `gE` — backward WORD end.
    BigWordEndBack,
    /// `f{c}` / `F{c}` / `t{c}` / `T{c}` — find/till a literal char.
    /// `forward=false` is the uppercase backward variant; `till=true`
    /// places the cursor one char short of the target.
    FindChar {
        ch: char,
        forward: bool,
        till: bool,
    },
    /// `;` / `,` — repeat the last find-char motion, optionally with
    /// direction reversed (`,`).
    RepeatFind {
        reverse: bool,
    },
    FileStart,
    FileEnd,
    /// `%` — jump to the matching bracket of the pair under (or just
    /// after) the cursor. Treats `()`, `[]`, `{}` as pairs.
    BracketMatch,
    /// `*` — search forward for the word under the cursor.
    SearchWordForward,
    /// `#` — search backward for the word under the cursor.
    SearchWordBack,
    /// `H` — top of the visible viewport (count = offset from top).
    ViewportTop,
    /// `M` — middle of the visible viewport.
    ViewportMiddle,
    /// `L` — bottom of the visible viewport (count = offset from bottom).
    ViewportBottom,
    /// `<C-d>` — half-page down. Count multiplies the half-height
    /// step (so `2<C-d>` covers a full page when supported).
    HalfPageDown,
    /// `<C-u>` — half-page up.
    HalfPageUp,
    /// `<C-f>` — full page down.
    PageDown,
    /// `<C-b>` — full page up.
    PageUp,
    SearchNext,
    SearchPrev,
    /// `{` — move to the previous blank line (or file start). Treats
    /// the buffer as paragraphs separated by all-whitespace lines.
    ParagraphBack,
    /// `}` — move to the next blank line (or file end).
    ParagraphForward,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Scope {
    Inner,
    Around,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Object {
    Word,
    /// `W` — vim's WORD: a whitespace-bounded run. Differs from
    /// [`Object::Word`] in that punctuation does *not* split the run,
    /// so `foo.bar(baz)` is one WORD but three words.
    #[allow(clippy::upper_case_acronyms, reason = "vim spells this `WORD`")]
    WORD,
    DoubleQuote,
    SingleQuote,
    Backtick,
    Paren,
    Brace,
    Bracket,
    AngleBracket,
    // Syntactic objects resolved through tree-sitter `textobjects.scm`.
    // `Inner` / `Around` map to the query capture suffixes `.inner` /
    // `.outer` respectively.
    Function,
    Class,
    Parameter,
    /// `T` — type definitions / aliases (TS `type Foo = ...`, Rust
    /// `type Foo = ...`, Go `type Foo ...`). Resolved through
    /// `textobjects.scm` like [`Object::Function`].
    Type,
    /// `p` — vim's paragraph: a contiguous run of non-blank lines
    /// bordered by blank lines (or file start/end). Char-class
    /// equivalent at the line level: `is_blank` vs not.
    Paragraph,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DirectKind {
    EnterMode(Mode),
    OpenPrompt(PromptKind),
    OpenLineBelow,
    OpenLineAbove,
    /// `a` — move past the cursor and enter Insert.
    AppendAfterCursor,
    /// `A` — jump to end-of-line and enter Insert.
    AppendAtLineEnd,
    /// `I` — jump to first non-blank of the line and enter Insert.
    InsertAtLineStart,
    /// `C` — change from cursor to end of line.
    ChangeToEol,
    /// `D` — delete from cursor to end of line.
    DeleteToEol,
    /// `Y` — yank the current line (vim's classic `yy` semantics).
    YankLine,
    /// `J` — join the next line into this one, replacing the line
    /// break with a single space (or nothing when joining onto an
    /// empty line).
    JoinLines,
    /// `~` — toggle case of the character under the cursor, then
    /// advance one column.
    ToggleCase,
    /// `s` — delete the char under the cursor and enter Insert.
    SubstituteChar,
    /// `S` — clear the current line and enter Insert at col 0.
    SubstituteLine,
    /// `zz` — center the cursor's line in the viewport.
    ViewportCenter,
    /// `zt` — scroll so the cursor's line is the top of the viewport.
    ViewportTopAtCursor,
    /// `zb` — scroll so the cursor's line is the bottom of the viewport.
    ViewportBottomAtCursor,
    /// `r<c>` — replace the char under the cursor with `c`.
    ReplaceChar {
        ch: char,
    },
    Paste,
    Undo,
    Redo,
    DeleteCharUnderCursor,
    Quit,
    QuitForce,
    /// `:bn` / `:bnext` — switch to the next buffer in MRU order.
    BufferNext,
    /// `:bp` / `:bprev` — switch to the previous buffer in MRU order.
    BufferPrev,
    /// `:bd` / `:bdelete` — drop the current buffer (refuse if dirty).
    BufferDelete,
    /// `:bd!` / `:bc` — force-drop the current buffer (discards unsaved edits).
    BufferDeleteForce,
    /// `:bca` — force-drop every buffer and land on a fresh scratch.
    /// Unsaved edits are discarded.
    BufferDeleteAll,
    /// `:ls` / `:buffers` — open the buffer picker.
    BufferList,
    /// `:new` — switch to the unnamed scratch buffer. If a sleeping
    /// scratch exists (previously visited and stashed) it's thawed;
    /// otherwise a fresh empty one is installed. No-op when already
    /// on the scratch buffer.
    NewScratchBuffer,
    SaveAndQuit,
    Save,
    /// `:w!` — save, creating any missing parent directories. Useful
    /// when the buffer's path points into a directory that doesn't
    /// exist yet (a plain `:w` errors out in that case).
    SaveForce,
    Open,
    /// `:log` — open the debug log file (resolved the same way the
    /// logger writes it: `$VORTO_LOG`, else `$XDG_STATE_HOME/vorto/…`).
    OpenLog,
    /// `:reload` — reread the active buffer's backing file from disk.
    /// Always reloads; the pre-reload state is recoverable via undo.
    Reload,
    /// `:reload-all` — re-read every file-backed buffer (active,
    /// parked, sleeping). Active and parked entries take an undo
    /// snapshot so their pre-reload state is recoverable; sleeping
    /// entries with unsaved edits are left alone (those edits aren't
    /// backed by disk and would be unrecoverable if dropped).
    ReloadAll,
    GotoLine,
    /// `gd` — `textDocument/definition` for the symbol under the cursor.
    GotoDefinition,
    /// `gD` — `textDocument/declaration` (distinct from definition in
    /// languages like C/C++; most others alias the two).
    GotoDeclaration,
    /// `gi` — `textDocument/implementation` (jump from trait method /
    /// interface decl to a concrete impl).
    GotoImplementation,
    /// `gr` — `textDocument/references` for the symbol under the cursor.
    FindReferences,
    /// `<space>r` — open a prompt to enter the new name, then send
    /// `textDocument/rename` and apply the returned `WorkspaceEdit`.
    Rename,
    /// `<space>a` — request `textDocument/codeAction` at the cursor
    /// and surface the results in a picker.
    CodeAction,
    /// `K` — request `textDocument/hover` for the symbol under the
    /// cursor and display the result in a scrollable popup.
    Hover,
    /// `:lsp` — open a read-only modal listing every language with
    /// an LSP configured plus its current running state.
    LspStatus,
    /// `.` — replay the last buffer-modifying change. Intercepted in
    /// `App::evaluate` before reaching the normal dispatch path, so this
    /// variant never appears in `handle_direct`'s match arms.
    RepeatLast,
    /// `gn` / `gN` — find the next/previous match of the current search
    /// pattern, enter Visual mode, and select the match. `reverse`
    /// flips against the stored search direction (so `gN` after a `/`
    /// becomes `reverse: true`).
    SearchSelectNext {
        reverse: bool,
    },
    /// `g*` / `g#` — seed the search pattern from the word under the
    /// cursor (same extraction as `*` / `#`) without jumping. Useful
    /// when you want to highlight or set up for `n` / `gn` without
    /// losing your position.
    SearchWordKeep {
        forward: bool,
    },
    /// `:noh` — clear the active search pattern so `hlsearch` stops
    /// painting matches. The pattern goes back to empty; `n` / `N`
    /// after this do nothing until a new search is performed.
    ClearSearch,
    /// `:s/pat/repl/[g]` / `:%s/pat/repl/[g]` — substitute. The full
    /// command line (e.g. `s/foo/bar/g` or `%s/foo/bar/g`) is passed
    /// through `Ctx::rest`; parsing and execution happen in the
    /// handler.
    Substitute,
    /// `+` — multi-cursor: find the next occurrence of the word under
    /// the cursor, push the current primary into `extra_cursors`, and
    /// jump primary to the match. Also seeds the search pattern so
    /// `n` / `N` walk the same matches.
    MultiCursorAddNext,
    /// `Shift+Down` — multi-cursor: push the current primary into
    /// `extra_cursors` and move primary one row down (same column,
    /// clamped to the new line's length). No-op on the last line.
    /// Requires a terminal that reports Shift+Down distinctly from a
    /// plain Down (Kitty `DISAMBIGUATE_ESCAPE_CODES`, enabled at boot).
    MultiCursorAddBelow,
    /// `-` — pop the most recently added extra cursor and move primary
    /// back to its position. No-op when there are no extras.
    MultiCursorPop,
    /// `<space>,` — drop every extra cursor and keep only primary.
    MultiCursorClear,
    /// `<space>ma` — bookmark the active buffer at the cursor's row
    /// (harpoon-style). De-dups by buffer; persisted for file-backed
    /// buffers, session-only for scratch.
    BookmarkAdd,
    /// `<space>md` — remove the active buffer's bookmark, if any.
    BookmarkRemoveCurrent,
    /// `gw` — easymotion / hop-style two-character label jump. Computes
    /// jump targets at every visible word start, overlays a 2-char label
    /// on each, and waits for the user to type the label. After the
    /// first character only matching labels remain (showing their
    /// second char); a unique first-char prefix jumps immediately. Esc
    /// or any non-label key cancels.
    JumpLabel,
    /// `gA` — select the whole buffer (alias for `ggVG`). Drops anchor
    /// at (0, 0), enters Visual-line mode, parks the cursor on the last
    /// line so operators apply to every line.
    SelectWholeBuffer,
    /// `:split` / `<space>w h` — open a new pane below the active one.
    SplitWindowHorizontal,
    /// `:vsplit` / `<space>w v` — open a new pane to the right.
    SplitWindowVertical,
    /// `:close` / `<space>w c` — close the active pane (refuses on
    /// the last remaining pane; `:q` is the right tool there).
    CloseWindow,
    /// `]d` / `[d` — jump to the next / previous LSP diagnostic in the
    /// current buffer. Wraps around at the end / start of the buffer.
    GotoDiagnostic {
        forward: bool,
    },
    /// Move focus to the pane lying in the given cardinal direction.
    /// Used by `Ctrl-W h/j/k/l` and `<space>w` arrow keys.
    FocusWindow {
        dir: FocusDir,
    },
    /// Cycle to the next pane in tree-traversal order. `Ctrl-W w` /
    /// `<space>w o`.
    CycleWindow,
    /// `Ctrl-O` — step back through the jump history (vim's jumplist).
    /// Count walks N entries at once.
    JumpBack,
    /// `Ctrl-I` / `Tab` — step forward through the jump history.
    JumpForward,
    /// `:jumps` / `<space>j` — open the fuzzy picker over the jump
    /// history so the user can jump straight to any past position.
    JumpList,
}

/// Cardinal direction for [`DirectKind::FocusWindow`]. Re-export of the
/// pane-module enum so the action AST doesn't depend on `app::pane`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FocusDir {
    Left,
    Right,
    Up,
    Down,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PromptKind {
    Command,
    Search {
        forward: bool,
    },
    Fuzzy(FuzzyKind),
    /// `<space>e` — tree file explorer. Has its own prompt state and
    /// key handling, separate from the fuzzy-finder widget.
    Explorer,
}

// ════════════════════════════════════════════════════════════════════════
// AST (semantic level)
// ════════════════════════════════════════════════════════════════════════

#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
    /// Standalone action, optionally repeated (e.g. `5p` could be "5 pastes").
    Direct { kind: DirectKind, count: u32 },
    /// Cursor movement only — no operator wrapping it.
    Motion(MotionExpr),
    /// Operator applied to a target.
    Op {
        op: Operator,
        target: Target,
        /// Outer count: `3d2w` → 3. Multiplied with any inner motion count.
        outer_count: u32,
    },
    /// `ys{target}{ch}` — wrap a motion/object range with the pair
    /// implied by `ch`.
    SurroundAdd { target: Target, ch: char },
    /// `cs{from}{to}` — replace the surrounding pair identified by
    /// `from` with the pair implied by `to`.
    SurroundChange { from: char, to: char },
    /// `ds{ch}` — remove the surrounding pair identified by `ch`.
    SurroundDelete { ch: char },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MotionExpr {
    pub motion: MotionKind,
    pub count: u32,
}

/// Remembered parameters of a `MotionKind::FindChar` — what `;` and `,`
/// replay. Structurally a flattened `FindChar`, but kept as its own type
/// so the "last find" slot on `App` is clearly typed. Lives in the
/// grammar layer so `effect::Cmd::SetLastFind` and any downstream
/// consumers can reference it without inverting module dependencies.
#[derive(Debug, Clone, Copy)]
pub struct LastFind {
    pub ch: char,
    pub forward: bool,
    pub till: bool,
}

/// What `.` replays. Either a one-shot Expr (e.g. `dw`, `x`, `p`, `r<c>`)
/// or an Insert-mode session — the trigger that entered Insert plus the
/// keystrokes typed before Esc.
#[derive(Debug, Clone)]
pub enum LastChange {
    Expr(Expr),
    Insert { trigger: Expr, keys: Vec<InsertKey> },
}

/// Replay-able Insert-mode keystrokes. Cursor motions (arrow keys) end
/// the recording in vim; we follow that by simply not recording them.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InsertKey {
    Char(char),
    Newline,
    Backspace,
    Dedent,
    /// A bracketed-paste payload — replayed as raw text without firing
    /// auto-indent or auto-pair, matching how it was originally inserted.
    Paste(String),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Target {
    /// d3w → motion `w` with count 3.
    Motion(MotionExpr),
    /// dib → text object (inner block).
    TextObject { scope: Scope, object: Object },
    /// dd, yy — operator applied to the current line line-wise.
    LineWise,
    /// `dgn` / `cgn` / `ygn` — operator applied to the next/previous
    /// match of the current search pattern. Differs from a normal
    /// motion target: the range starts at the match's first char (not
    /// the cursor), so it can't be expressed as `Target::Motion`.
    /// `reverse` flips against the stored search direction, same
    /// convention as `DirectKind::SearchSelectNext`.
    SearchMatch { reverse: bool },
}

// ════════════════════════════════════════════════════════════════════════
// Dispatch context
// ════════════════════════════════════════════════════════════════════════

/// Context passed to evaluators when an Expr fires. Carries the runtime
/// argument from `:` command lines (`rest`) and the count from the parse.
/// The count is *not* the same as `Expr`'s count fields — those are part
/// of the parsed AST. `Ctx::count` is here for command-line commands that
/// take a count separately (currently only `:goto`).
#[derive(Debug, Clone, Copy)]
pub struct Ctx<'a> {
    pub rest: &'a str,
    #[allow(dead_code)]
    pub count: u32,
}

impl<'a> Ctx<'a> {
    pub fn with_rest(rest: &'a str) -> Self {
        Self { rest, count: 1 }
    }
}

impl Default for Ctx<'_> {
    fn default() -> Self {
        Self { rest: "", count: 1 }
    }
}