A Vim-grammar TUI editor written in Rust. Tree-sitter highlighting, multi-server LSP fan-out with rename / code-actions / signature-help / find-references / document & workspace symbols, fuzzy pickers, persistent undo, code folding, surround operations, smart-indent, system-clipboard yank, horizontal scrolling, and a Catppuccin Mocha palette — all in one binary, no plugins.
- Modal editing — normal / insert / visual (charwise, linewise, blockwise) with operators, text objects, marks, registers, dot-repeat, undo/redo, and macros.
- Surround operations —
ds<char>strips the surrounding pair,cs<old><new>swaps it, visualS<char>wraps the selection. Pair chars:(/)/b,[/],{/}/B,<,",',`. - Numeric
Ctrl-A/Ctrl-X— increment / decrement the next number on the current line. Recognises decimal,0x…,0b…,0o…, with optional leading-. Preserves leading zeros. - Smart indent on Enter — copies the current line's leading whitespace and adds one indent unit after
{ [ ( :=>->. Pressing Enter inside an auto-paired{|}splits the pair into three lines with the cursor on the indented middle. - HTML tag auto-completion — typing
>after<divwrites<div>|</div>. Self-closing tags, void elements, generics (Array<T>), comments and declarations all skipped. Active in.html,.cshtml,.razor,.jsx,.tsx,.vue,.svelte,.astro,.xml,.md. - Bracket and HTML-tag matching — when the cursor is on (or just past) a bracket or anywhere inside an HTML tag, the matching partner highlights with a Surface2 background + bold. Works through arbitrary nesting.
- Code folding — indent-based folds, toggled with
za/zo/zc, withzR/zMto open/close all. Folded blocks render as⏷ N lines. - Persistent undo — undo history is serialised per file under
~/.cache/binvim/undo/<hash>.jsonon save and reloaded on the next session, keyed by content hash so external edits invalidate stale history. - System-clipboard yank —
y,yy,Y,:y, visual yank, and the implicit yank ond/c/xmirror to the OS clipboard viaarboardwhenever they target the unnamed register. Named registers ("ay) stay local. - Yank flash — yanked range flashes a Catppuccin Peach background for 200ms so you see what's been picked up.
- Horizontal scrolling — long lines scroll automatically as the cursor moves past the edge; trackpad / mouse-wheel horizontal events scroll without moving the cursor; Vim-style
zh/zl(1 col) andzH/zL(half-width) work too. - Whitespace markers — every space, tab, non-breaking space, and end-of-line surface as a muted glyph (
·,→,⎵,¬). Configurable. - Format on save — biome for JS / TS / JSX / TSX / JSON / JSONC.
.editorconfigdirectives applied on save (final newline, trailing whitespace). - Auto-reload on disk change — when an open file changes externally and the buffer isn't dirty, binvim notices via mtime poll and reloads with a status note.
- Recents in the file picker — most-recently-opened files surface at the top of the file picker on an empty query, persisted at
~/.cache/binvim/recents.
Rust, TypeScript / TSX, JavaScript, JSON, Go, HTML, CSS, Markdown, C#. Pattern-priority resolution so (method_declaration name: (identifier) @function) deterministically beats the catch-all (identifier) @variable.
Per-language servers with initializationOptions, project-root detection, and a debounced didChange (50ms burst window) so rapid typing doesn't flood the server.
| Capability | Binding | Notes |
|---|---|---|
| Completion | auto + Ctrl-N/Ctrl-P |
Multi-server fan-out — completion items from primary plus auxiliary servers (e.g. Tailwind alongside tsserver) are merged in the popup. |
| Hover | K |
|
| Goto-definition | gd |
|
| Find references | gr |
Results open in a fuzzy picker; Enter jumps. |
| Document symbols | <space>o |
File outline. Hierarchy preserved with › separators. |
| Workspace symbols | <space>S |
Live server-side filter as you type. |
| Signature help | auto on ( / , |
Parameter being typed gets a Catppuccin Yellow highlight inside the popup. |
| Code actions | <leader>a |
Picks render with kind tag. Supports both WorkspaceEdit and command-shaped actions; round-trips workspace/applyEdit from the server. |
| Rename | <leader>r |
Prompt pre-fills the current word; submission applies the WorkspaceEdit across every affected file. |
| Diagnostics | inline + sign column | Undercurl on the offending range, severity glyph in the gutter. |
Multi-server fan-out — primary servers (rust-analyzer, tsserver, gopls, biome, OmniSharp, csharp-ls, …) plus auxiliaries layered on top. Tailwind class-name completion attaches alongside CSS / HTML / JSX / TSX / JS / TS / Astro / Vue / Svelte / Razor whenever Tailwind is detected (v3 tailwind.config.* or v4 CSS-first via a tailwindcss dependency in package.json). csharp-ls is layered onto Razor files so @code{} blocks get C# completion even without a dedicated Razor server.
Fuzzy file picker, buffer switcher, and live grep — all opened from leader (space).
Colours overridable via ~/.config/binvim/config.toml.
cargo build --releaseThe binary lands at target/release/binvim.
binvim [path]If path is omitted, binvim opens on the start page. Press : for a command (:e <path>, :q) or <space> to open the file picker.
| Keys | Action |
|---|---|
<space> |
File picker |
<space>b |
Buffer picker |
<space>g |
Live grep |
<space>e |
Yazi file manager |
<space>o |
Document symbols |
<space>S |
Workspace symbols |
<space>a |
Code actions |
<space>r |
Rename |
Beyond the standard :w, :q, :e <path>, :bd, :s/pat/repl/g, etc.:
| Command | Description |
|---|---|
:health |
Open a scratch buffer summarising version, CPU / RAM share, buffers, attached LSPs (with binary path + running flag), and Tailwind config detection. :checkhealth works too. |
:fmt / :format |
Run the configured formatter on the active buffer. |
:noh |
Clear the search highlight. |
binvim spawns these on demand. Each is optional — when a binary isn't on $PATH (or in a relevant node_modules/.bin/) the editor just skips that capability.
| Tool | Purpose | Install |
|---|---|---|
rust-analyzer |
Rust LSP | rustup component add rust-analyzer |
typescript-language-server |
JS / TS / JSX / TSX LSP | npm i -g typescript-language-server typescript |
gopls |
Go LSP | go install golang.org/x/tools/gopls@latest |
vscode-css-language-server |
CSS / SCSS / Less LSP | npm i -g vscode-langservers-extracted |
vscode-html-language-server |
HTML LSP | npm i -g vscode-langservers-extracted |
tailwindcss-language-server |
Tailwind class-name completion | npm i -g @tailwindcss/language-server (the unscoped npm package is an empty stub — use the scoped one) |
astro-ls |
Astro LSP | npm i -g @astrojs/language-server |
csharp-ls |
C# LSP (Roslyn-based, preferred) | dotnet tool install --global csharp-ls |
OmniSharp |
Razor / .cshtml IntelliSense (full) |
binvim probes ~/.local/bin/omnisharp/OmniSharp plus $PATH. Drop the official tarball there. |
rzls |
Razor language server | community tool — install if you have it; binvim auto-detects |
biome (project-local) |
JSON LSP + JS / TS / JSON formatter | npm i -D @biomejs/biome in the project |
rg |
Live grep backend | brew install ripgrep |
yazi |
<space>e file manager |
brew install yazi |
binvim auto-discovers project-local binaries by walking up to the closest node_modules/.bin/, so a devDependency in your project takes precedence over a global install.
Optional config file at ~/.config/binvim/config.toml:
schema_version = 1
[colors]
keyword = "#cba6f7"
"keyword.return" = "Magenta"
string = "#a6e3a1"
[start_page]
lines = [
" hello, world ",
" press : to start ",
]
[whitespace]
show = true # space=`·`, tab=`→ `, nbsp=`⎵`, eol=`¬`. On by default.[colors] — values may be hex (#rrggbb) or a named crossterm colour. Capture names follow tree-sitter conventions (keyword, string, function, type, …); a dotted suffix matches more specifically before falling back to the head (keyword.return overrides keyword).
[start_page] — lines overrides the baked-in ASCII logo shown when binvim is launched with no path. Each entry renders on its own row, horizontally centered; the block as a whole is vertically centered. Omit it (or leave it empty) to keep the default logo.
[whitespace] — show = true (the default) renders every space as ·, every tab as → plus space-fill to the tab width, every non-breaking space (U+00A0) as ⎵, and the end-of-line as ¬. All in the muted overlay colour. Set show = false to disable.
A missing or malformed config is ignored — the baked-in Catppuccin Mocha palette is used.
Source-available, not open source. Copyright (c) 2026 B. Gunnarsson — see LICENSE for the full text. In short: you may read the source, run it locally, modify your own copy, and submit pull requests upstream. You may not redistribute, publicly fork, or run it as a hosted service. For anything outside that scope, contact the licensor on Twitter/X at @bgunnarssonis.
src/
app.rs event loop, buffer/state management, ex-command dispatch
buffer.rs rope-backed text buffer
command.rs ex-command (`:`) parser
config.rs config loader and colour resolution
cursor.rs cursor + visual selection model
editorconfig.rs .editorconfig parser + on-save transforms
format.rs formatter dispatch (biome integration)
lang.rs tree-sitter language detection and highlight cache
lsp.rs LSP client (diagnostics, hover, completion, goto, multi-server fan-out)
mode.rs modes and operators
motion.rs motions
parser.rs keystroke → action parser
picker.rs fuzzy pickers
render.rs terminal rendering
text_object.rs text objects (`iw`, `i"`, `ap`, …)
undo.rs undo/redo history (in-memory + on-disk persistence)