A Vim compatibility layer for the ttt
editor, implemented as a single Lua plugin. Modal editing, the operator × motion
× text-object grammar, visual modes, registers, marks, macros, dot-repeat, a
: command line, and / search.
Originally built in ttt#386.
Requires ttt 1.1.0 or newer. The plugin depends on core additions in 1.1.0 — the key interceptor's precedence over Escape (needed to leave insert mode) and the
ttt.command_lineAPI (needed for:and/). On older builds Esc will not return you to normal mode and the command line will not open. The manifest declaresapi: 2, so an older editor refuses to load it rather than running it half-broken.
See REFERENCE.md for the full list of supported keys, the substitution regex subset, design notes, and known gaps.
Copy this repo's plugin files into a plugin directory ttt scans — either
~/.config/ttt/plugins/vim/ or <workspace>/plugins/vim/:
plugin.ttt.json
init.lua
On first launch ttt will prompt to approve the plugin's permissions
(keybindings, editor.read, editor.write, commands, fs.write,
settings).
Settings live under a vim object in settings.json:
| Key | Default | Effect |
|---|---|---|
vim.enabled |
true |
Start with Vim mode active. |
vim.clipboard |
false |
Mirror the unnamed register to the system clipboard. |
The test suite drives the real ttt binary via its --exec debug harness. It
does not download a release — it builds ttt from source so the binary always
matches the ttt it is tested against.
npm install
npm test # builds ttt, then runs the vitest suitenpm test runs scripts/prepare-ttt.sh, which resolves a binary into
.ttt-bin/ttt in this order:
TTT_BIN=<path>— use an existing binary as-is.- A sibling
../tttcheckout — build it in place. - Otherwise clone
tttatTTT_REF(defaultmain) and build it.
So with a local ../ttt checkout, npm test just works. In CI the same script
clones and builds ttt (see .github/workflows/test.yml).
The plugin is one large init.lua by necessity: the ttt plugin sandbox cannot
require sibling .lua files, so everything lives in one file, organized into
do … end blocks (which also keep each scope under Lua 5.1's 200-local limit).
tests/fuzz/ holds a seeded differential fuzzer that checks ttt-vim against
real Vim as the oracle: it generates a random key sequence from a seed,
replays it through both editors, and compares the saved files. Identical output
⇒ spec-compliant; a divergence is a ttt-vim bug or a documented gap. See
tests/fuzz/DESIGN.md for the architecture and
tests/fuzz/FINDINGS.md for what it has found.
npm run fuzz # seeds 1..200, ~8 actions each
FUZZ_START=1 FUZZ_COUNT=500 npm run fuzz
FUZZ_SEED=1337 npm run fuzz # one seed, verbose (prints tokens + diff)Requires vim on PATH (the oracle). Divergences are written as reproducible
JSON reports under tests/fuzz/reports/ (git-ignored) — each carries the seed,
token stream, both outputs, and a diff, so a failure reproduces from its seed
alone. The fuzzer is on-demand and not part of npm test; the deterministic
fixed-sequence parity check (tests/fuzz/differential.test.js) is.
To reduce a failing seed to a minimal repro:
node tests/fuzz/repro.mjs 40 # rerun one seed, print the diff
npm run fuzz:shrink 40 # delta-debug it to a minimal token sequence