A composable vim engine in Rust. It is a library, not an editor: you bring a document and a place to draw, it owns the modes, the grammar, and the cursors.
Status: early. The contract crate is being frozen; nothing here is released.
Most vim implementations are welded to one buffer type — a rope, a textarea,
a specific editor's model. That makes them unusable anywhere else, and it makes
vim-surround a special case inside the core instead of an ordinary extension.
This one separates three things that are usually tangled:
- the grammar (
count operator count motion) is a pure state machine over keys, with no idea what a document is; - the document is a trait the host implements, so a rope and a Notion-style block tree are both first-class, not one plus an adapter;
- the vocabulary — motions, operators, text objects, actions — is a
registry. Everything stock lives in
vim-builtinsand uses only the public API, so an extension is not a second-class citizen.vim-surroundis the proof: it is written against the same API you get, and core has nosurroundbranch anywhere.
These are the ones that usually get discovered too late.
The host owns the document. The engine reads it and proposes. A host
implements Document, and that trait is the whole read surface: identity,
document order, per-line text, byte length, metadata, caps(). Its invariants
are stated on the trait (at least one line, ids stable across every edit that
does not remove the line, no '\n' inside a line, len_of answerable without
materialising the text). The engine never writes through it. It proposes an
EditBatch, the host applies what it will and reports back as Deltas, and
those deltas are the only thing that moves an Anchor. A native embedding
hands over its real document and there is no second copy anywhere. A copy
appears only where the engine sits behind a boundary that cannot answer a
synchronous read, and it is then a non-authoritative mirror serving the same
trait, with the delta stream as its one writer. Either way the rule is the
same, and docs/CONTRACT.md states it as a guarantee: one writer, never the
engine.
Positions are anchors, not offsets. A collaborator's edit arriving mid-command must move your cursor, not corrupt it. Deltas remap anchors; the confirmation protocol defines what happens when an edit the engine proposed races an edit the host already applied.
WASM is a target, not the interface. wit/vim.wit describes the
interface, and it is load-bearing as a design forcing function: an interface
that must survive being described in WIT cannot cheat with shared mutable
state. But the component model has no native browser support today and jco
calls its browser support experimental, so betting a keystroke path on it would
be a bad trade. The plain Rust crate is the primary target and the only one
that runs: no crate here builds a wasm artifact.
The WIT is a build input for exactly one thing. ts/ is a TypeScript package
whose types are generated from it by @bytecodealliance/jco@1.31.0, pinned
exactly. The command is jco types, the host mode: it emits the surface a
TypeScript host gets from a transpiled component, which is the engine
export as callable functions plus the shape of the host import it has to
supply. (jco guest-types is the other mode and is the wrong one here, since
it emits ambient modules for writing the guest in TypeScript, and the guest is
Rust.) The generated tree is committed, and the ts-types job in CI
regenerates it and fails if anything differs, so the TypeScript spelling of the
interface cannot drift from the WIT. ts/src/index.ts is a thin shim over
that: nominal brands for the handles WIT flattens to u64 and u32, two
constructors that enforce the line-id minting rule, and the docs/CONTRACT.md
invariants as doc comments. The same job runs tsc --noEmit, so the shim
cannot outlive a type the WIT drops.
Be precise about what that buys: shape, in one direction. The generator makes
the TypeScript match the WIT and nothing more. It does not check that the WIT
and vim-types still mean the same thing, and neither one is generated from
the other; keeping those two in step is still review, and a disagreement is a
bug in whichever one moved (docs/CONTRACT.md). A browser module and the
component itself are planned as separate targets behind the same description.
Until one exists, read the world as a specification of a component nobody
builds yet.
Extensions are compile-time crates, not loadable plugins. wac compose is
a build step and browsers cannot dynamically link components. The extension
API is the valuable part and it is real; the distribution story is "add a
dependency". Saying so here so nobody designs a plugin registry around a
capability that does not exist.
Dot-repeat, macros, and marks are in core from day one. They cannot be
retrofitted: . forces every command to be a replayable key-intent and forces
an action to be a pure function of its context and arguments. That constrains
the extension API, so it is decided before there are extensions to break.
Document shape is declared, not guessed. Caps is four flags, and each
one names what changes when it is false:
rectangular:Ctrl-Vrefuses with a bell and no blockwise state is entered. A rectangle needs a monospaced grid, and there is no column 7 that means the same thing in a heading and a code block. Half-working blockwise visual is worse than none.split_lines: noEdit::SplitLineand noEdit::JoinWithNextare ever emitted;Enterin insert andJin normal bell instead.reorder_lines: noEdit::MoveLineis ever emitted;ddpand a visual block move bell instead.set_depth:>>and<<splice leading whitespace rather than emittingEdit::SetDepthand reparenting.
Caps::RAW_TEXT and Caps::OUTLINE are the two preset shapes. A capability
with no stated consequence does not belong in the struct: it would be a knob
nobody reads, and hosts would set it by guess.
Multi-cursor is first-class, and real vim cannot be its oracle. Stock
behavior is settled by differential test against Neovim rather than by memory
of it: vim-oracle drives a real nvim over msgpack-rpc and commits what it
did, 210 vectors so far, and the engine-versus-golden comparison is written and
waiting for an engine to run it against. Vim has no multi-cursor, so that
oracle can say nothing about it; multi-cursor correctness rests on stated
invariants and a property suite instead. That gap is acknowledged rather than
papered over.
In the tree today:
crates/
vim-types the frozen contract, and the only crate everything else
depends on. `no_std` + `alloc`, zero dependencies.
positions LineId, Pos, Bias, Anchor, Span
lines LineKind, LineMeta, HydrateLine, IndexLine
document the Document trait, Caps
change Text, Edit, EditBatch, Delta, RemoteCause
remapping remap_anchor, the normative rule as code
keys KeyCode, Key
grammar Mode, ModeSet, VisualKind, MotionId, ObjectId,
OperatorId, ActionId, ArgSpec, Args, InsertAt,
Target, Cmd, Program, Feed
output CursorView, Status, Clip, PromptKind, Effect,
FeedResult
test double VecDoc: applies an EditBatch, emits the Deltas a
real host would report, and remaps anchors
through them, so a lane can drive the whole
edit/confirm/remap loop with no host at all
vim-core the registry, and the Motion / TextObject / Operator / Action /
Extension traits. Signatures only: the bodies are
`unimplemented!()` on purpose, and the crate is explicitly not
frozen.
vim-oracle dev-only, depends on nothing else here: drives real Neovim over
msgpack-rpc, 210 committed golden vectors, plus the
engine-versus-golden comparison and its divergence report.
Outside crates/:
ts/ `@indexable/vim`, the TypeScript view of `wit/vim.wit`.
generated/ emitted by `jco types`, committed, regenerated and diffed in
CI. Never hand-edited.
src/ the shim: brands for the flattened handles, the line-id
constructors, and the contract invariants as doc comments.
test/ type-level checks. `tsc --noEmit` is the whole suite, and every
`@ts-expect-error` in it is a control that fires if a brand
stops biting.
Named, designed against, not yet written:
vim-machine the pure state machine: keymap trie, grammar, dot/macro recording
vim-engine Engine<D>: delta ingestion, anchor remapping, registers, cursors
vim-builtins stock vim, written against the public registry API only
vim-surround the proof extension
vim-text raw-text host over a rope; the native reference embedding
vim-blocks block-tree host
vim-component WASM component guest + JS host example
vim facade: `Engine::with_defaults()`
The behavior of vim-surround is reimplemented from tpope/vim-surround's
documented behavior. No VimScript from that project is vendored or
transliterated; it is Vim-license charityware and its code is not compatible
with this repository's licensing.
MIT OR Apache-2.0, at your option.