01 / EDITING
Modal editing, faithfully
Normal · insert · visual (charwise, linewise, blockwise). Operators, text objects, marks,
registers, dot-repeat, undo tree, macros. The grammar you already speak.
w b eiw a"ciw@q
02 / SYNTAX
Tree-sitter, compiled in
Rust, TS / TSX / JSX, JS, JSON, Go, Python, C / C++, Java, Ruby, PHP, Lua, TOML, Svelte, Zig, Nix,
Elixir, Dockerfile, SQL, HTML, CSS, Markdown, C#, Razor, YAML, XML (incl. .csproj /
.fsproj / .props), Bash, plus byte-level passes for
.editorconfig and .gitignore. Grammars baked into the binary — upgrade
binvim and your parsers come with it.
rustts/tsxgohtmlcssmdc#razoryamlxml.editorconfig
03 / LSP
Real LSP, multi-server + multi-root
Completion, hover, diagnostics, goto-def, find-references, rename, code-actions, signature-help,
document & workspace symbols, code lens (virtual text above the anchor — server-driven,
with a synthesised fallback for "Run test" / "Debug test" rows on languages whose servers
don't ship the capability). Primary + auxiliary servers fan out per buffer — Tailwind and
Emmet layer on top of CSS / HTML / JSX / TSX / Vue / Svelte / Astro / Razor automatically. Two
dozen servers wired by extension, from rust-analyzer / tsserver / gopls to pyright / clangd /
jdtls. Multi-root: opening files from a sibling project root attaches the
new folder to the running server via workspace/didChangeWorkspaceFolders instead
of spawning a second process — one rust-analyzer can hold both halves of a monorepo, and
cross-workspace go-to-definition works. :workspaces dumps the attached folder
set per client. :messages surfaces server logs in a severity-coloured overlay;
:health flags stuck-at-init clients and shows per-kind pending-request + cache
counts.
rust-analyzertsservergoplspyrightclangdjdtlscsharp-lstailwindemmetsemanticTokens/fulldocumentHighlightcodeLensworkspaceFolders:workspaces:messages
04 / REFACTOR
Rename, code actions, snippets
<space>r renames a symbol across every file the server knows about — and
opens a modal preview overlay first, listing every edit with a per-row
checkbox and a file:line snippet. j/k navigate,
<Space> toggles, a/n flip all on/off,
o jumps to the edit site, <Enter> applies only the enabled
edits, <Esc> cancels — no more blind apply on a 40-file rename.
<space>a opens code actions — quick fixes, organise imports, extract method,
anything the LSP exposes. LSP snippet items get their $N /
${N:default} placeholders parsed and the cursor lands at $1. Multi-file
WorkspaceEdit applier with workspace/applyEdit round-trip.
<space>rpreview<space>agr$1 $2 $0
05 / AI ASSIST
AI assist, opt-in
Two paths, neither baked in: inline via GitHub Copilot, conversational via
terminal-native assistants. Copilot ships through
copilot-language-server as an aux LSP — set
[copilot] enabled = true in config.toml, device-flow auth surfaces in
the status line, ghost completions render as muted italic on a ~250 ms idle pause.
<Tab> accepts the ghost (wins over the LSP popup),
<Enter> accepts the LSP popup. For chat, :claude /
:codex / :opencode open the matching CLI in a right-side PTY pane.
Each invocation spawns a fresh tab — multiple sessions per tool are supported, switched via
the tab strip in the pane header. Shift-pair leader bindings extend this:
<space>jc opens a Claude tab as-is,
<space>jC opens Claude AND pre-types
@<active-buffer cwd-relative path> into the input field once it's ready
(same shift-pair for jx/jX Codex,
jo/jO opencode). <space>jf focuses the pane;
<space>jp hides/shows it without killing PTYs. binvim itself carries no
HTTP client; networking and auth stay in the language server / external tool.
copilot:claude:codex:opencodejC / jX / jO@path handoff⇥ acceptdevice-flow
06 / MULTI-CURSOR
Real multi-cursor
Sublime-style. In Visual-char, Ctrl-N finds the next literal occurrence and adds it
as a parallel selection — repeat to keep selecting. c deletes them all and drops a
mirrored cursor at every former start; typing, Backspace, and Enter all mirror at every site
simultaneously. Ctrl-click in Normal mode anchors extra cursors at click positions.
^N^clickc · d · y↵ mirror
07 / FORMAT
Format on save
One canonical formatter per language, dispatched by extension — biome, csharpier, gofmt, ruff,
prettier, taplo, stylua, ktfmt, and a dozen more. Prettier prefers project-local
node_modules/.bin before global. .editorconfig rules (final newline,
trailing whitespace) apply every save. :fmt or <space>f drives it
manually.
biomecsharpiergofmtruffclang-formatstyluaprettiertaploktfmt.editorconfig<space>f
08 / SURROUND
Surround ops
ds<char> strips the surrounding pair,
cs<old><new> swaps it, visual S<char> wraps the
selection. Pairs balance through nesting; quotes do nearest-enclosing on the line.
ds"cs'"cs(]VS{
09 / FOLDS
Code folding
Indent-based fold ranges, toggled with za/zo/zc and
zR/zM for everything. Folded blocks render as
⏷ N lines so you never lose context.
zazozczRzM
10 / SESSIONS
Sessions & tab bar
Open buffers + cursor + viewport persist on shutdown and restore on launch when no file argument
is passed. A tab row across the top shows every open buffer; H/L cycles,
click to switch, click × to close. Persistent undo too — each file's history
serialised per content hash.
H · Ltabsu / U
11 / SPLITS
Window splits, pick-on-split
<C-w>v / <C-w>s split vertically / horizontally
and open the file picker so the new pane lands on a different file in one keystroke.
Uppercase <C-w>V / <C-w>S keep Vim's same-buffer behaviour.
Per-tab split layouts — splitting one tab doesn't bleed into others. Focus moves geometrically
with <C-w>h/j/k/l; each pane keeps its own cursor, viewport, fold state, and
diagnostics. <C-w>T promotes a split companion to a real tab.
<C-w> v / s<C-w> V / S<C-w> h/j/k/l<C-w> q / o / =<C-w> Tper-tab layouts
12 / INLAY + SEMANTIC
Inlay hints & semantic tokens
Inlay hints render inline in dim italic — type hints on let bindings, parameter names
on call sites. Parameter hints lean a shade warmer than type hints so they scan apart. Semantic
tokens layer over the tree-sitter pass — LSP modifiers paint let mut foo red,
async fn lavender, std:: symbols sapphire, all without user config. Both
refetched per buffer-version with a one-in-flight cap so fast typing can't pile up. Toggleable via
[lsp].
: Typearg:let mutasync fnstd::[lsp]
13 / VISIBILITY
Visible structure
Every space, tab, NBSP, and EOL surfaces as a muted glyph (· → ⎵ ¬). Bracket and HTML-tag matching
highlights the partner as the cursor moves. Hover popup parses LSP markdown and runs tree-sitter
on fenced code blocks — signatures render the way they would in the buffer.
·→⎵¬{ }<tag>K hover
14 / ERGONOMICS
Quality of life
Smart indent on Enter (splits paired openers / closers). HTML tag auto-close
(<div>→<div>|</div>). macOS Insert backspace:
Alt+⌫ peels a word, Cmd+⌫ deletes to start of
line. Ctrl-A/Ctrl-X on numbers (decimal, hex, bin, oct). Double-click
selects a word; middle-click closes a tab. Auto-reload on external file changes.
⌥⌫ word⌘⌫ line^A / ^X<div>|</div>gt / gT
15 / DEBUG
Debuggers via DAP, four languages
Built-in adapter-agnostic DAP client. .NET via netcoredbg (auto-builds, reads
launchSettings.json profiles). Go via dlv dap (enumerates
package main dirs). Python via debugpy (active .py buffer
wins, else picks main.py / manage.py / app.py / …). Rust /
C / C++ via lldb-dap (parses Cargo.toml for
[[bin]] targets, prelaunch cargo build).
<space>ds (or F5) picks the adapter and drives initialize → launch
→ setBreakpoints → configurationDone. Live prelaunch output: the build
child (dotnet build / cargo build) is spawned async with piped
stdout/stderr, streamed line-by-line into the Console tab while it runs, and the real
initialize+launch only fires once the build exits successfully — no more "frozen
editor, no feedback" while a solution rebuilds. Conditional + hit-count
breakpoints: :dapb if <expr> attaches a condition,
:dapb hit <expr> attaches a hit count,
:dapb plain strips both — conditional breakpoints render as ◆ in
the gutter (plain stays ●) and the breakpoints pane lists each row's expression
inline. Bottom pane shows call stack + locals on the left, debug-console on the right.
VS / Rider F-keys (F5 / F9 / F10 / F11)
work in every mode. :debugtest (alias :dt) walks up from the cursor
for the enclosing test, then routes through the DAP layer instead of the test runner —
pytest (module: pytest) and go (delve mode: test) wired end-to-end.
Android JDWP attach lives behind <space>Ad — see ANDROID below.
<space>dsF5/F9/F10/F11netcoredbgdelvedebugpylldb-dap:dapb if/hit/plain:debugtest▶ ◆ ●
16 / PICKERS
Fuzzy with match highlighting
Files, recents, grep, document / workspace symbols, code actions, references, debug project /
profile — every picker uses the same fuzzy matcher with bonuses for word-boundary hits and
consecutive runs. Matched characters render Yellow + Bold so it's obvious which letters earned the
row's rank. Path rows prefix a Nerd Font file-type icon.
fuzzyfile iconsmatch hllive grep
17 / CLIPBOARD
System clipboard, both ways
Yanks mirror to the OS clipboard via arboard when the unnamed register is the target.
p / P read from the OS clipboard first — anything you
Cmd-C'd in another app wins over the in-memory yank. Visual p /
P swaps the selection with the register's contents. Named registers
("ay) stay local.
"+yp / Pv_parboard
18 / FILES
Sidebar file explorer
<space>e opens yazi by default — set
[file_explorer] tree = true to swap it for a built-in left-side tree pane rooted at
the cwd. j / k navigate, Enter /
l opens a file or expands a folder, h collapses or jumps to the parent,
g / G top / bottom, R rebuilds. File ops inside the
pane: a creates an entry under the cursor's parent dir (trailing
/ makes a folder, intermediate dirs auto-created);
r renames the cursor entry with a basename-prefilled prompt — if the renamed file
is open in a buffer, the buffer's path is rewritten so saves keep landing in the right file;
d arms a delete and the next key consumes the y/N confirmation (any non-y
cancels, so a stray double-tap doesn't unlink). Three-state toggle: closed → focused →
unfocused-but-visible → closed.
<space>ea / r / dy/N deletesidebarno plugin
19 / TESTS
Integrated test runner, five adapters
One adapter pattern, five toolchains. cargo (Rust), vitest (JS / TS), pytest (Python),
go test, and dotnet test (.NET) each ship as a TestAdapterSpec with a
streaming parser. :test opens a fuzzy picker of discovered tests;
:testnearest walks the buffer up for the enclosing test fn and runs it;
:testfile derives a file-scoped filter from the active path;
:testlast re-runs the most recent invocation. Results stream into a
:health-style scrollable overlay (pass / fail / ignored counts in the status
line on completion); failures populate the quickfix list with parsed file:line locations
so ]q / [q walks them.
cargovitestpytestgo testdotnet test<space>sn]q · [q
20 / SPELL
Spell check, no external library
:spell toggles spell-check on the active buffer; ]s /
[s jump between misspelled words; z= opens a suggestion picker
(single-edit neighbours filtered against the dictionary, capped at 12). Wordlist loads
from ~/.local/share/binvim/words (user override) or
/usr/share/dict/words (system default). The tokeniser splits camelCase /
snake_case / kebab-case so identifiers only trip on unknown constituents; pure-uppercase
abbreviations and tokens under 3 chars are skipped to keep the false-positive rate low
on source code. Per-buffer enable flag, version-keyed cache.
:spell]s · [sz=camelCase splitper-buffer
21 / PACKAGES
Package manager, four ecosystems
<space>p opens a per-ecosystem manifest picker that drives an
install / search / version flow through whichever package manager owns the active
workspace. NuGet via the dotnet CLI (parses
dotnet list package / dotnet package search --exact-match /
dotnet add package JSON, tolerates the first-run banner and honours
each project's nuget.config); npm via
npm list / npm view / npm install;
cargo reads Cargo.toml directly for installed crates
and falls back to the crates.io HTTP API for the version list (no cargo
command lists them all), with cargo search / cargo add
for the rest; Go parses go.mod for direct requires,
scrapes pkg.go.dev for search, and uses go list -m -versions /
go get for everything else. <space>pi picks a
manifest → installed packages → versions (the installed one highlighted,
Tab toggles prereleases) → install. <space>ps picks
a manifest → searches the registry → package → version → add. Network + restore
calls run on background threads so the editor stays responsive; an epoch guard
drops results from cancelled flows.
<space>p<space>pi · psNuGetnpmcargogocrates.io / pkg.go.devbackground
22 / LAZYGIT
Lazygit integration
:lazygit / :lg / <space>gg suspends the editor,
hands the full terminal to lazygit, and on exit reclaims the terminal AND
refreshes the git gutter for every open buffer so stages / commits / checkouts show up
immediately. Not a PTY-embedded pane — lazygit gets the whole screen (its UI hard-codes
panel widths and the bottom :terminal pane caps at 20 rows). Same yazi-style
takeover model: pop kitty keyboard protocol, drop mouse capture, leave alt screen, run,
reclaim. Exit detection is free — when the blocking call returns, lazygit is done.
:lazygit:lg<space>gggutter refreshfull-screen takeover
23 / TASKS
Integrated task runner
:task / <space>mm discovers workspace tasks from five
sources, unioned per workspace: npm scripts (npm / pnpm / yarn auto-picked
from the lockfile), Justfile recipes (skips _private +
[private]), cargo aliases + builtin verbs
(build / check / test / clippy /
run / fmt / doc), Makefile top-level
targets, and dotnet verbs (build / run /
test / restore / clean / publish).
Picker rows tag the source for disambiguation; selecting a task spawns it in a fresh
bottom-terminal tab labelled with the task name. :tasklast /
<space>ml re-runs the most recent invocation.
:tasknpm / pnpm / yarnjustcargomakedotnet<space>mm
24 / CMDLINE
Cmdline, history and tab
Tab / Shift-Tab cycle candidates in the : cmdline —
command names before the first space, filesystem entries after :e /
:w (directories get a trailing /, dotfiles hidden unless the
basename starts with .), open-buffer basenames after :b. Up /
Down walks the cmdline + search histories (cap 100 entries each, deduped against the
immediate previous, persisted to the per-cwd session file); the first Up
snapshots whatever you'd already typed so walking off the bottom brings the draft back.
Tab cycle: history/ historydraft restore
25 / ANDROID
Android emulators & JDWP debug attach
<space>A opens a which-key submenu for the Android SDK side of a
project. <space>Al lists installed AVDs (avdmanager list
avd) and connected devices (adb devices); <space>Ar
launches an AVD; <space>Ac scaffolds a new one from the available
system images. Listings + launches run on a background thread so the editor doesn't
block on the SDK tooling. For debug, <space>Ad attaches the DAP
layer to a running Android process over JDWP — adb forward wires the
JDWP port, the jdtls-hosted java-debug adapter accepts the attach over TCP, and
DapManager::start_attach_session skips the usual spawn / launch
handshake and goes straight to initialize + attach. Same stack + locals + breakpoints
UI as the four built-in DAP languages.
<space>A<space>Al · Ar · Ac<space>AdavdmanageradbJDWP attachjdtls / java-debug
26 / TERMINAL
Terminal pane, scrollback + drag-copy
:terminal / <space>tt opens a PTY-backed shell tab
in a bottom pane. Multi-tab (the task runner labels its tabs), tabs sized to the
pane body, SIGWINCH propagates on resize. Scrollback view: the
scroll wheel, Shift+PageUp / Shift+PageDown (or
Shift+↑ / ↓ for one line) walk back through history even at an idle
shell prompt where the program isn't capturing the mouse — the user stays anchored
to the same content as new output streams in (tmux-style), and typing snaps the
view back to live. Drag selects + auto-copies: plain left-drag
inside the pane body highlights cells, release lands the text on the system
clipboard. Works across the scrollback view too. <space>tp
toggles the pane without killing the PTY so a long-running pnpm dev /
cargo watch tucks out of the way while editing.
:terminal<space>tt<space>tptabs · SIGWINCHscroll wheel · Shift+PageUpdrag selectsauto-copy
All five edit code. They take very different shapes. binvim is the only one that ships as a single TUI
binary — the rest are full GUI applications with bundled or pluggable LSP / debug stacks. Numbers
below are typical
defaults; pick what fits your job.
Prefer to install by hand? The complete per-tool reference is below — every binary binvim spawns
on demand, all optional. When a tool isn't on $PATH (or in a relevant
node_modules/.bin/) the editor just skips that capability.
Is binvim a Neovim replacement?
binvim is a from-scratch modal TUI editor written in Rust — not a Neovim distribution, fork, or
config. It implements Vim grammar (operators, text objects, registers, macros) and ships
tree-sitter, multi-server LSP, four DAP debuggers, and a dozen formatters in one binary. It is for
developers who want to stop maintaining a vim-based editor config. If you want a 200-plugin
platform, Neovim is the better tool for that job.
Does binvim need a plugin manager or a large config?
No. There is no plugin manager. Tree-sitter grammars, 24 language servers wired by file extension,
four DAP debuggers, a dozen formatters, and opt-in GitHub Copilot are all compiled into a single
~42 MB binary. Configuration is an optional ~40-line TOML file at
~/.config/binvim/config.toml; with no config, binvim falls back to baked-in defaults.
What languages does binvim support?
Tree-sitter grammars are compiled in for Rust, TypeScript/TSX/JSX, JavaScript, Go, Python, C/C++,
Java, Kotlin, Ruby, PHP, Lua, TOML, Svelte, Vue, Zig, Nix, Elixir, Dockerfile, SQL, HTML, CSS,
Markdown, C#, Razor (.cshtml/.razor), YAML, XML, and Bash. 24 language
servers are wired by file extension, including rust-analyzer, tsserver,
gopls, pyright, clangd, jdtls, and
csharp-ls.
Can binvim debug code?
Yes. binvim has four built-in DAP debuggers: .NET via netcoredbg, Go via
delve, Python via debugpy, and Rust/C/C++ via lldb-dap. It
supports VS-style F5/F9/F10/F11 keys,
conditional and hit-count breakpoints, and live prelaunch build streaming into the debug Console.
How is binvim different from Helix?
binvim uses classic Vim grammar — verb-then-object operators, text objects, marks, registers,
dot-repeat, and macros — rather than Helix's selection-first model. It also ships opt-in GitHub
Copilot, a right-side AI assistant pane (:claude / :codex /
:opencode), four DAP debuggers, and integrated test and task runners across five
toolchains, all in one binary.
What platforms does binvim run on and how do I install it?
binvim runs on macOS, Linux, and Windows. Install with Homebrew
(brew install bgunnarsson/binvim/binvim), a curl | sh script, PowerShell
(iwr … | iex), Scoop, cargo (cargo install --locked binvim), or a Nix
flake.