Skip to content

Repository files navigation

██████╗ ██╗███╗   ██╗██╗   ██╗██╗███╗   ███╗
██╔══██╗██║████╗  ██║██║   ██║██║████╗ ████║
██████╔╝██║██╔██╗ ██║██║   ██║██║██╔████╔██║
██╔══██╗██║██║╚██╗██║╚██╗ ██╔╝██║██║╚██╔╝██║
██████╔╝██║██║ ╚████║ ╚████╔╝ ██║██║ ╚═╝ ██║
╚═════╝ ╚═╝╚═╝  ╚═══╝  ╚═══╝  ╚═╝╚═╝     ╚═╝

binvim — the first vim IDE

binvim is the first vim IDE — a vim-native integrated development environment in a single binary, rather than a plain modal editor (Vim, Helix), an editor you assemble into an IDE from plugins (Neovim + distributions like AstroNvim / LazyVim), or a GUI IDE with bolted-on vim emulation (VS Code, Visual Studio, Rider). → What is a vim IDE?

A Vim-grammar TUI editor written in Rust. Tree-sitter highlighting (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 / .csproj / .manifest family, Bash, .editorconfig, .gitignore), multi-server LSP fan-out (rename, code-actions, inlay hints, semantic tokens layered over tree-sitter, document highlight, signature help, snippet expansion, find-references, document & workspace symbols, :messages capture for window/showMessage + window/logMessage + server stderr), opt-in GitHub Copilot via copilot-language-server with inline ghost completions, built-in debuggers via DAP for .NET (netcoredbg), Go (delve), Python (debugpy), and Rust / C / C++ (lldb-dap) with project / bin / script pickers, .NET launchSettings profiles, breakpoints, stack frames, locals with lazy expansion, VS / Rider F-keys, per-language formatters dispatched by extension (biome, csharpier, gofmt/goimports, ruff, clang-format, shfmt, stylua, prettier, taplo, rufo, php-cs-fixer, google-java-format, zig fmt, nixfmt, mix format, ktfmt, sql-formatter, plus .editorconfig reflow on every save), window splits with per-buffer layouts and pick-on-split (<C-w>v → picker → instant side-by-side), real multi-cursor with Sublime-style Ctrl-N selections, fuzzy pickers with file-type icons and match-character highlighting, <leader>/ per-language comment toggle, sessions with persistent per-buffer jumplists, tab bar, persistent undo, code folding, surround operations, smart-indent, OS-clipboard paste, horizontal scrolling, a :health dashboard that surfaces per-LSP init state + pending-request breakdown + cache counts, and a Catppuccin Mocha palette — all in one binary, no plugins.

How binvim compares

binvim is the first vim IDE. Here's how it stacks up against the editors and IDEs people usually weigh:

Features

Editor

  • Modal editing — normal / insert / visual (charwise, linewise, blockwise) with operators, text objects, marks, registers, dot-repeat, undo/redo, and macros.
  • Multi-cursor (mirrored, real)Ctrl-click in Normal mode adds a secondary cursor at the click position; cursors render as Lavender blocks. Enter Insert via i (anchored) or a (shifts each cursor by +1); typing and Backspace mirror at every site simultaneously (bottom-up edit order keeps indices stable). Esc collapses; a plain (non-Ctrl) click also collapses.
  • Multi-selection (Ctrl-N) — Sublime-style. In Visual-char mode, Ctrl-N finds the next literal-text occurrence of the current selection and adds it as a parallel selection. Repeat to keep adding. d/c/y apply across every selection; c lands in Insert mode with a mirrored cursor at every former selection start.
  • Visual-block mode (Ctrl-V) — third visual kind alongside char and line. Rectangular selection across a line span; d/c/y apply column-wise per row, >/< fall back to indent / outdent over the line range.
  • Reselect (gv) — brings back the last Visual selection: same kind, cursor on the same end, moved along with any edits since. Inside Visual it swaps the current selection with the previous one.
  • Surround operationsds<char> strips the surrounding pair, cs<old><new> swaps it, visual S<char> wraps the selection, ys{motion}<char> wraps a motion or text object (ysiw)), yss<char> the line, and yS / ySS put the pair on lines of their own. Pair chars: (/)/b, [/], {/}/B, <, ", ', `.
  • Case operatorsgu / gU / g~ / g? lowercase, uppercase, toggle or ROT13 a motion or text object (gUiw, g~$); guu, gUU, g~~ and g?? take the line, and Visual u / U / ~ / g? the selection, block mode included. No register is touched.
  • Joining linesJ joins the next line with one space between, 3J joins three lines, and gJ joins without adding or removing any whitespace. Visual J / gJ join every line the selection covers.
  • Replace mode (R) — typing goes over the text and on past the line end; Backspace puts back what it replaced, and 3R types the text three times. Visual r<char> replaces every selected character, block mode included.
  • Putting (gp, ]p)gp / gP put like p / P and leave the cursor just after the text; ]p puts lines below re-indented to the cursor's line, and [p above.
  • Re-indent (=)==, =ip, =% and Visual = re-indent lines by the rules Enter uses: a level deeper after a line that opens a block, a level shallower for one that starts by closing it.
  • Re-flow (gq, gw)gqq, gqip and Visual gq fill paragraphs to .editorconfig's max_line_length (79 without one), keeping a comment's // / # marker on every line; gw leaves the cursor where it was.
  • Filter (!)!{motion}, !! and Visual ! open : with the lines' range (:5,7!); the command you type gets them on stdin and its output replaces them, while a failing command leaves them alone. :{range}!cmd works typed directly.
  • 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. In Visual they change the first number on each selected line, block selections included, and g Ctrl-A / g Ctrl-X count up line by line (1, 2, 3…).
  • 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.
  • Insert-mode word / line-start delete shortcuts. Alt/Option+Backspace deletes the previous word (peels trailing whitespace, then one homogeneous run of word chars or punctuation — same as macOS Option-Delete). Cmd/Super+Backspace deletes from the cursor back to column 0. Ctrl+Backspace aliases to the word-delete for terminal users without a usable Option key, and Vim's Ctrl-W does the same word-delete. Ctrl-U deletes back to the indent, and a second press from the indent takes the indent too. None of them delete across a line break mid-line; from column 0 they join the line onto the one above, like Backspace.
  • Registers"a"z name a register for any yank, delete or put, and "A"Z append to one. Vim's own fill themselves: "0 holds the last yank, "1"9 shift down as each delete of a line or more arrives, "- keeps the smaller ones, and the read-only ". / "% / "# / ": / "/ give the last insert, the current and alternate file, the last command line and the last search. "_ is the black hole, "+ / "* the system clipboard, and "= evaluates an arithmetic expression for p. :reg lists them all.
  • Macrosq{a-z} records until the next q, @{a-z} replays it, and @@ replays the last one again; Q replays the last macro too. @: runs the last : command line again, with a count (3@:) that stops at the first error, as in Vim. A macro's keys replay through the same path as typing, so [keymaps] mappings inside one still apply.
  • Insert-mode register pasteCtrl-R {reg} inserts a register at the cursor: az, 09, " (unnamed), + / * (system clipboard), - (the last small delete), or the read-only . (last insert), % / # (current / alternate file), : (last command line) and / (last search). The text goes in literally — no auto-pair, no re-indent — and multi-line registers make real lines. A " marks the cursor while the register name is pending; Esc cancels. Ctrl-A inserts the last inserted text (".). Ctrl-R = asks for an arithmetic expression and inserts its value; "= does the same for p.
  • Insert Ctrl-X completionCtrl-X Ctrl-N / Ctrl-P complete from the buffer's words (nearest first), Ctrl-X Ctrl-L whole lines, Ctrl-X Ctrl-F file paths — in the same popup as LSP completion, narrowing as you type.
  • DigraphsCtrl-K {a}{b} in Insert enters a special character: a:ä, thþ, d-ð, ->, Eu, a*α (Vim's RFC 1345 codes, either way round). :digraphs lists the set.
  • Ctrl-C — leaves Insert, Visual, a pending operator or count, and the : / / prompts, as Esc does; from Insert it keeps nothing for ., as in Vim.
  • Command-line keys — on the : and / prompts, Ctrl-R {reg} inserts a register, Ctrl-R Ctrl-W / Ctrl-R Ctrl-A the word / WORD under the cursor; Ctrl-W / Ctrl-U delete the word before the cursor / everything before it; Ctrl-B / Ctrl-E go to the start / end; Ctrl-P / Ctrl-N walk the history like <Up> / <Down>.
  • Command-line windowq: / q/ / q? open a split below listing the ex / search history, oldest first, with an empty line at the bottom. Edit it in Normal mode; <CR> runs the line under the cursor, Ctrl-C or :q closes it.
  • Visual-block insert — in Ctrl-V block mode, I / A insert before / after the block on every row at once (A pads rows that stop short), $A appends at each row's own end, and . repeats it on a block of the same size.
  • Visual uppercase keysY / D / X / C / R take whole lines from any Visual mode, as in Vim; in block mode D / C run to each row's end (so do $ blocks), and O jumps to the other corner. Ctrl-C leaves Visual like Esc.
  • Insert-mode indent / outdentCtrl-T adds one indent unit (per .editorconfig) to the current line and Ctrl-D removes one, keeping the cursor on the same character of text. Ctrl-T works on a blank line too.
  • Insert-mode literal insertCtrl-V inserts the next key as it is: Ctrl-V Tab is a real tab under space indentation, Ctrl-V ( skips auto-pair. Ctrl-V u00e9 inserts a character by code, as do U (8 hex digits), x (2), o (3 octal) and up to 3 decimal digits. Control characters are refused, since binvim can't display them yet.
  • Insert-mode copy from above / belowCtrl-Y inserts the character at the cursor's column from the line above, Ctrl-E the one from the line below; repeat to copy a run.
  • Insert-mode one-shot Normal commandCtrl-O runs one Normal-mode command and comes back to Insert: Ctrl-O dd, Ctrl-O 0, Ctrl-O :w. The mode chip reads (INSERT) until the command finishes. Pressed at the end of a line, it comes back to the end of the line, and so does Ctrl-O $.
  • HTML tag auto-completion — typing > after <div writes <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. % jumps to the partner — from the first bracket at or after the cursor on the line, or from an HTML tag to its pair — and works with operators (d%); N% goes to the line N percent of the way through the file.
  • Paragraph motions} / { move to the empty line after / before the paragraph; a count skips that many and a run of empty lines counts as one. From the start of a line, d} / y} / c} work on the paragraph's whole lines and leave the blank one, as in Vim. The ip / ap text objects take the paragraph (plus its trailing blank lines for ap) line-wise — dap, cip, yip, vip — and treat whitespace-only lines as blank.
  • Sentence motions) / ( move to the start of the next / current sentence: one ends at ., ! or ? (plus any closing brackets or quotes) followed by whitespace, and an empty line starts one of its own. Counts and operators work (d)), and both go in the jump list. The is / as text objects take a sentence, as with the space after it — das, cis, vis.
  • Tag objectsit / at take an element's content, or the element with its tags (dit, vat), in any buffer; a count reaches outer elements.
  • Argument objectsia / aa take a comma-separated argument inside (), [] or {} (aa with one comma next to it), ignoring commas in nested brackets and quoted strings.
  • Bracket motions]d / [d jump to the next / previous diagnostic and show its message; [( [{ ]) ]} go to the unmatched bracket around the cursor, after an operator too (d])).
  • Syntax objectsaf / if take the function around the cursor (or its body) and ac / ic the class, struct or impl, from the tree-sitter parse — Rust, TS/JS, Python, Go, C#, Lua, C and C++.
  • Search/ and ? take Vim's regex syntax (\(…\), \|, \+, \{n,m}, \< / \>, \s / \d / \w, […], \zs / \ze, \v / \V) with smart case — an upper-case letter makes it case-sensitive, \c / \C decide outright — and every match is highlighted in full. * / # search the word under the cursor as a whole word, g* / g# as bare text, and n / N take a count. An offset after the pattern moves the cursor off the match — /pat/e to its last char, /pat/e+1 / /pat/s-1 / /pat/b counted from its ends, /pat/+2 two lines down — and n / N keep it; // searches the last pattern again with a new offset. gn / gN select the next match, and after an operator act on it — cgn then . changes one match after another. Matches highlight as you type the pattern and the view follows the first one; Esc puts the cursor back.
  • Line and column motions<CR> / + and - go N lines down / up to the first non-blank, N_ to the first non-blank N−1 lines down (so d_ is dd), and N| to column N. All of them work with operators. Long lines scroll sideways rather than wrap, so g0 / g^ / gm / g$ go to the first char / first non-blank / middle / last char on screen, gM to the middle of the line, and gj / gk move like j / k.
  • Marksm{a-z} sets a mark, ' jumps to its line and ` to its exact spot. Vim's own marks work too: '' goes back to before the latest jump, '. to the last change, `^ to where Insert was last left, '[ / '] to the start and end of the last change or yank, and '< / '> to the last Visual selection. Marks move with the text as you edit, and keep their line and column through format-on-save and a reload from disk. :marks lists them, and :jumps lists the jump list. Uppercase marks (mA) remember their file too: 'A from any other file switches back to it at the mark, for the rest of the session.
  • Insert where you left off (gi) — goes back to where Insert was last left, following any edits since, and starts Insert there.
  • Change listg; goes back through the places you changed, newest first, and g, forward again; a count skips that many. Changes on one line count once, each buffer keeps its last 100, and the places move with the text. :changes lists them.
  • Code folding — indent-based folds, toggled with za/zo/zc, with zR/zM to open/close all. Folded blocks render as ⏷ N lines. zj / zk move between folds, zv opens what hides the cursor, zO / zC / zA work recursively, and zm / zr / zx set and re-apply the fold level.
  • z scrollingz<CR> / z. / z- put the cursor's line at the top / middle / bottom with the cursor on its first non-blank, and zs / ze scroll sideways to put the cursor at the left / right edge.
  • Info and open-under-cursor keysCtrl-G (file, length, position), g Ctrl-G (column / line / word / byte counts, or the selection's), ga (character code), gx (open the URL under the cursor), gf / <C-w>f (open the file under the cursor, path:line jumping to the line; <C-w>f in a split), gI (insert at column 0) and Ctrl-L (redraw).
  • Git gutter + hunks — leftmost gutter column shows working-tree changes against the git index: Green for added lines, Yellow for modified, Red for the line just below a deletion. Status line gains a +A ~M -D counter next to the branch name. ]h / [h jump to next / previous hunk; <leader>hp previews the hunk under the cursor in a hover popup (three lines of surrounding context). <leader>hs stages the hunk via git apply --cached, <leader>hu unstages, <leader>hr discards (refuses while the buffer is dirty so unsaved edits can't be lost). Refreshed on save, on buffer switch, on initial open, and after any stage / unstage / reset.
  • Inline git blame:Gblame toggles per-line virtual text at end-of-line: author • relative age (3d, 2w, 4mo) • short SHA, in muted italic. Parsed from git blame --porcelain. Per-buffer toggle. Suppressed on rows that already show an inline diagnostic.
  • Persistent undo — undo history is serialised per file under ~/.cache/binvim/undo/<hash>.json on save and reloaded on the next session, keyed by content hash so external edits invalidate stale history. Within a session undo is a tree: an edit after u keeps what you undid, g- / g+ walk every state in the order it was made, and only the current branch is saved. :earlier / :later go back and on by steps, time (10s, 5m, 1h) or file writes (1f), and :undolist lists the branches.
  • System-clipboard yank + pastey, yy, Y, :y, visual yank, and the implicit yank on d/c/x mirror to the OS clipboard via arboard whenever they target the unnamed register. p / P (Normal and Visual mode) read from the OS clipboard first — anything you Cmd-C'd in another app wins over the in-memory yank. Named registers ("ay) stay local.
  • Visual-mode pastep / P over a selection (word / multi-line / block) swaps the selection with the register's contents. Linewise content over a charwise selection drops its trailing newline so paste doesn't open a stray blank line.
  • 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) and zH / zL (half-width) work too.
  • Double-click to select word, drag to extend by words — a second left-click at the same buffer position within 350 ms expands to the inner word and enters Visual-char. Continue holding and drag to grow or shrink the selection a word at a time; the cursor snaps to whole-word boundaries and only jumps once a new word is crossed (dragging through whitespace keeps the previous boundary).
  • Whitespace markers — every space, tab, non-breaking space, and end-of-line surface as a muted glyph (·, , , ¬). Configurable.
  • Format on save / on-demand<leader>f or :fmt runs the right tool per extension. biome for JS / TS / JSX / TSX / JSON / JSONC; csharpier for .cs; gofmt / goimports for .go; ruff format (or black as fallback) for .py; clang-format for .c / .h / .cpp / .cc / .hpp / .cxx / .hxx; shfmt for .sh / .bash / .zsh; stylua for .lua; Prettier (project-local node_modules/.bin/prettier first, then global) for the file types biome doesn't currently format — .md / .mdx / .vue / .svelte / .html / .htm / .css / .scss / .less / .yaml / .yml / .graphql / .gql; taplo format for .toml; rufo for .rb; php-cs-fixer for .php (temp-file dance — no stdin mode); google-java-format for .java; zig fmt for .zig; nixfmt (or alejandra) for .nix; mix format for .ex / .exs; ktfmt for .kt / .kts (temp-file dance); sql-formatter for .sql; .editorconfig indent reflow for .cshtml / .razor (csharpier rejects those, so we fall through). .editorconfig directives (final newline, trailing whitespace) apply on every save regardless of extension.
  • 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.

Sessions & tabs

  • Sessions — open buffers + per-buffer cursor + viewport persist to ~/.cache/binvim/sessions/<cwd-hash>.json on clean shutdown and restore on launch when no file argument is passed. Buffers whose paths no longer exist are silently dropped. Restored sessions drop you on the start page with the tab row above it advertising what's loaded — H/L (or :bn/:bp, :b<n>, a tab click) brings you into a buffer. Ex (:) and search (/ / ?) history rides on the same file (capped at 100 entries each, dedup against the immediate previous); <Up> / <Down> inside either prompt walks it, and the first <Up> snapshots whatever you'd already typed so walking off the bottom brings the draft back. Histories load even when you launch with binvim foo.rs — only buffer restoration is gated on a bare invocation.
  • Tab completion inside :Tab / Shift-Tab cycle candidates in the cmdline. Three modes picked by the head: command names before the first space (every alias the parser knows, filtered by prefix), filesystem entries after :e / :edit / :w / :write (directories get a trailing /, dotfiles hidden unless the basename starts with .), open-buffer basenames after :b / :buffer. Any other key drops the cycle so the next Tab re-derives candidates against the latest text.
  • Tab bar — every open buffer renders as a tab at the top of the screen. Active tab in Surface1 + Lavender + bold, inactive tabs in Subtext0, dirty buffers carry a Peach +. Click a tab to switch; click its × to close (refuses dirty, same as :bd). / chevrons appear at the bar edges when tabs scroll off either side. The bar matches the editor background.

Tree-sitter highlighting

Rust, TypeScript / TSX / JSX, JavaScript, JSON, Go, Python, C / C++, Java, Ruby, PHP, Lua, TOML, Svelte, Zig, Nix, Elixir, Dockerfile / Containerfile, SQL, HTML, CSS, Markdown, C#, Razor (.cshtml / .razor), YAML, XML (including .csproj / .fsproj / .vbproj / .props / .targets / .config / .manifest / .nuspec / .resx / .xaml), Bash, .editorconfig, .gitignore family (.gitignore, .gitattributes, .dockerignore, .npmignore).

Pattern-priority resolution so (method_declaration name: (identifier) @function) deterministically beats the catch-all (identifier) @variable.

A few language-specific tweaks on top of the bundled queries:

  • JSX / TSX — overlay tags lowercase elements (<div>) as @tag (Pink) and PascalCase components (<Foo>, <Foo.Bar>) as @constructor (Yellow). {expr} braces inside JSX get treated as JSX-template syntax (@operator) instead of falling through to the object-literal punctuation tone.
  • Razor@inject / @using / @{…} / @if / @(…) / @*…*@ etc. paint as @keyword.directive; C# inside the blocks is highlighted by the C# query. A byte-level overlay handles HTML tag / attribute names + C# keywords inside broken-parse regions (BOM headers, Tailwind class="…[16px]…" bracket attributes, …).
  • CSS — replacement query so selectors and properties don't collide: .class-name is @constructor (Yellow), #id-name is @label (Sapphire), property: is @property (Lavender), --custom-prop is @variable, at-rules (@media/@keyframes/…) are @keyword (Mauve).
  • .editorconfig — comments, [*.cs] section headers in Pink, key = value pairs with the key in Lavender, = in Sky, value in Green.
  • .gitignore family# comments, !-negation prefix in Mauve, patterns in Lavender.

LSP

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 — items from primary + auxiliary servers (e.g. Tailwind alongside tsserver) merge in the popup. Each row shows a colour-coded kind chip and the server-supplied detail.
Snippet expansion on accept LSP items with insertTextFormat == 2 get their $N / ${N:default} / $0 placeholders parsed; cursor lands at $1, defaults mirror to later bare references.
Hover K Markdown parsed into structured lines — fenced code blocks tree-sitter-highlighted with the language tag's grammar.
Inlay hints inline textDocument/inlayHint annotations render between buffer chars in dim italic. Respects horizontal scroll.
Semantic tokens inline textDocument/semanticTokens/full layered over the tree-sitter pass. LSP modifiers drive visible colour shifts: let mut foo in red, async fn in lavender, std:: symbols in sapphire, static in teal, readonly in peach, deprecated in red. Toggle off via [lsp] semantic_tokens = false.
Document highlight auto on cursor settle textDocument/documentHighlight paints a Surface2 background on every occurrence of the symbol under the cursor (across the visible buffer, anchored to live cursor position). Toggle off via [lsp] document_highlight = false.
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 LSP-aware. 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.
Server messages :messages Captures window/showMessage and window/logMessage notifications plus server stderr into a bounded ring (500 entries). Error / warning showMessages also flash through the status line. Severity-coloured scrollable overlay, dismissed with Esc/q/:q.

Multi-server fan-out — primary servers (rust-analyzer, tsserver, gopls, biome, OmniSharp, csharp-ls, pyright, clangd, jdtls, intelephense, …) 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). Emmet abbreviation expansion (emmet-ls) attaches to the same markup-flavoured file set, surfacing ul>li*3>a[href]-style snippets in the completion popup; the snippet inserter prepends the current line's leading whitespace to every continuation line so closing tags line up with the opener.

Debugger (DAP)

Built-in debuggers via an adapter-agnostic DAP client. Four adapters ship today:

Language Adapter binary Selection
.NET netcoredbg *.csproj / *.sln / *.fsproj walks up to a .sln / .git root. Two-stage picker: project, then launchSettings.json profile.
Go dlv dap go.mod. Picker enumerates every directory with package main under the workspace root; the buffer's own dir auto-picks when it matches.
Python python3 -m debugpy.adapter pyproject.toml / setup.py / requirements.txt / Pipfile. Active .py buffer wins; otherwise picks from main.py / manage.py / app.py / __main__.py / run.py / server.py / cli.py.
Rust / C / C++ lldb-dap (or legacy lldb-vscode) Cargo.toml. Picker rows are each [[bin]] / src/main.rs / src/bin/*.rs across the workspace; prelaunch cargo build --bin <name>, launch target/debug/<name>.

Adding a fifth adapter is one row in dap/specs.rs's registry plus a build_launch_args fn and a dap_resolve_* resolver in app/dap_glue.rs.

Capability Binding Notes
Start <leader>ds / F5 Walks up from the active buffer to pick the adapter, then enumerates targets (.csproj / package main / .py script / [[bin]]). 0 → error, 1 → straight through, >1 → picker. Auto-restarts an active session (collapses the old dq → ds round-trip into one keystroke; waits up to 1.5 s for the previous debuggee to release its listening port).
Launch profile (after .NET project pick) Reads Properties/launchSettings.json. Profiles with commandName: "Project" (Kestrel hosting) are runnable. 0 → framework defaults; 1 → use directly; >1 → profile picker. The chosen profile's applicationUrl becomes ASPNETCORE_URLS; its environmentVariables flow into the launched process env.
Stop <leader>dq / Shift+F5 Sends disconnect terminateDebuggee:true; closes the bottom pane.
Continue <leader>dc / F5 (while paused)
Step over / into / out <leader>dn / di / dO / F10 / F11 / Shift+F11
Toggle breakpoint <leader>db / F9 Gutter marker (or when conditional). Survives across sessions; resent to the adapter on every toggle so conditions are never silently dropped.
Conditional breakpoint :dapb if <expr> Attach a condition to the cursor line's breakpoint (creates one if absent). Use :dapb if (no arg) to strip the condition while keeping the breakpoint. Aliases: :dapb cond, :dapb condition.
Hit-count breakpoint :dapb hit <expr> Attach a hitCondition. Most adapters accept bare integers (:dapb hit 5 = pause after the fifth hit); some also accept comparators (>= 10). :dapb hit (no arg) strips it.
Strip conditions :dapb plain Drop both condition and hitCondition from the cursor line's breakpoint, keeping it as an unconditional pause.
Clear breakpoints (file) <leader>dB Drops every breakpoint in the active buffer; resends to the adapter if a session's alive.
Toggle pane <leader>dp Bottom split. Frames + locals on the left, debug-console on the right. Auto-opens on session start, auto-closes on session end.
Focus pane <leader>df / click on the pane Enters Mode::DebugPane. Five tabs across the top: Console, Locals, Breakpoints, Frames, Watches. Sessions open on the Console tab so launch chatter is visible immediately. Tab / Shift-Tab (or /, or click the label) cycles between them. 1-5 jumps to a tab by number. j/k/g/G navigate within the active tab. Enter (or Space) on a Locals row expands the structured value. Ctrl-Y/Ctrl-E scrolls vertically; h/l (or Shift-←/Shift-→, or Shift+ScrollWheel) scrolls horizontally for rows wider than the pane; 0 snaps back to column 0. A « / » glyph marks hidden content on either edge. c/n/i/O step without leaving the pane. : enters the command line. Esc returns to Normal.
Doc / Workspace symbols <leader>do / dS LSP pickers, scoped under the debug menu so "navigate around code while debugging" actions cluster in one place.
Watch expressions :dapwatch <expr> / :dapunwatch <n> / :dapunwatch all / :dapwatches User-managed list, evaluated against the top frame on every stopped event. Rendered above frames in the debug pane (red value when the server returns an error for the expression — typo, name not in scope at the current frame). Survives across sessions; only the cached value clears between stops.

Variable expansion — structured locals render with / markers; expansion lazily fetches children per variables_reference and caches them across re-renders. All caches clear on stopped/continued (DAP doesn't promise vref stability between stops).

Diagnostic surfacing — adapter stderr (e.g. netcoredbg's dlopen() error: libdbgshim.dylib not found) streams into the pane's status_line and output buffer instead of vanishing into Stdio::null(). Unverified breakpoints, JIT-rebinding events, and setBreakpoints failures show up as console-category output so a never-hits is diagnosable instead of mysterious.

Pickers

Fuzzy file picker, live grep, recents, document / workspace symbols, code actions, references, and debug-project / debug-profile prompts — opened from leader (<space>).

  • File-type icons — path-based rows (Files, Recents, Buffers, Grep, References) get a Nerd Font icon per row derived from Lang::detect on the basename; unknown extensions fall back to a generic document glyph. Symbol / Code-action pickers stay icon-free (rows aren't files).
  • Match-character highlighting — fuzzy-matched chars render in Catppuccin Yellow + Bold so it's obvious which letters of your query produced the row's rank.
  • Navigation — mouse wheel moves the selection by ±3; PageUp/PageDown jump a page; Ctrl-U/Ctrl-D jump a half-page; Home/End jump to first/last; ^J/^K (and arrows) move by one.

Each picker is a centered floating popup with the directory part of paths dimmed and the basename bright.

Catppuccin Mocha defaults

Colours overridable via ~/.config/binvim/config.toml.

Vim compatibility

binvim speaks Vim's grammar: counts, registers, operators over motions and text objects, marks, macros and . behave the way Vim users expect. The Editor bullets describe each feature and the Ex commands table lists the : commands; this section is the map — what's there, where binvim differs from Vim on purpose, and what's left out.

What's supported

  • Insert modeCtrl-W, Ctrl-U, Ctrl-R {reg} (and Ctrl-R =), Ctrl-A, Ctrl-T / Ctrl-D, Ctrl-V {key}, Ctrl-E / Ctrl-Y, Ctrl-O, Ctrl-K {a}{b} digraphs, Ctrl-X Ctrl-N / Ctrl-P / Ctrl-L / Ctrl-F completion, and Ctrl-C to leave.
  • Motions%, { / }, ( / ), <CR> / + / - / _ / |, gj / gk / g0 / g^ / g$ / gm / gM, ]d / [d, and [{ / ]} / [( / ]).
  • Marks and jumps'', '., `^, '[ / '], '< / '>, file marks AZ across buffers, gv, gi, the change list (g; / g,, :changes), :marks, :jumps, and Ctrl-^ / :e# / :b#.
  • Operatorsgu / gU / g~ / g?, ys / yss / yS surround, gJ, R Replace mode, gp / gP / ]p / [p, = re-indent, gq / gw wrap, !{motion} / !! filter, and Visual Ctrl-A / Ctrl-X / g Ctrl-A / g Ctrl-X.
  • Text objectsip / ap, is / as, it / at, ia / aa, and tree-sitter af / if / ac / ic.
  • Search and substitute — Vim regex with smart case, offsets (/pat/e+1, /pat/s-1, /pat/+2) and //, gn / gN, highlight while typing, :s with \1, & and the c / i / I / n flags, and & / g& / :& / :&& / :~.
  • Ex commands — the full range grammar, :m / :t / :j / :> / :< / :d x / :y x / :pu, :le / :ri / :ce / :retab, :normal, :g / :v, :sort, :! / :r ! / :w !, :sp / :vs / :new / :vnew / :only / :close, :cd / :pwd, :bufdo / :windo / :cdo / :cfdo, :grep / :vimgrep / :make, :set, and ZZ / ZQ / :x / :wa / :qa / :wqa / :xa / :e!.
  • Registers and the command line — numbered "1"9, "-, the read-only ". / "% / ": / "/ / "#, "A"Z append, "= arithmetic, @:, Ctrl-R / Ctrl-W / Ctrl-U / Ctrl-B / Ctrl-E / Ctrl-P / Ctrl-N in the : and / prompts, and the q: / q/ / q? window.
  • Visual mode — block I / A / $A, O, line-wise D / X / Y / C / R, * / #, char and line I / A, and Ctrl-C.
  • Undo — an undo tree with g- / g+, :earlier / :later by steps, time or writes, and :undolist.
  • Folds, scrolling and infozj / zk / zv / zO / zC / zA / zr / zm / zx, z<CR> / z. / z- / zs / ze, Ctrl-G, g Ctrl-G, ga, gx, gf / <C-w>f, gI and Ctrl-L.

Where binvim differs on purpose

These are binvim's own keys, and they stay:

  • H / L step through buffers. Buffers are binvim's tabs (Buffer / tab navigation), so they get keys on the home row. Vim's jump to the top / bottom of the screen has no key here.
  • U redoes, next to u; Ctrl-R redoes too. Vim's line-undo U isn't implemented.
  • Q replays the last macro, as in Neovim. Vim's Q enters Ex mode, which binvim doesn't have.
  • <C-w>v / <C-w>s open the file picker in the new pane, since a split usually wants a second file. <C-w>V / <C-w>S and :sp / :vs split onto the same buffer, as Vim does.
  • Insert Ctrl-N / Ctrl-P open LSP completion, the better list when a server is attached. Vim's keyword completion is Ctrl-X Ctrl-N / Ctrl-X Ctrl-P.
  • Ctrl-J / Ctrl-K move the current line down / up. In Vim, Ctrl-J is a second j and Ctrl-K does nothing in Normal mode.
  • :update upgrades the toolchain (binvim-install), the partner of :install. :w writes, and :x writes only when there are changes.

Different from Vim, not yet decided

  • Visual S wraps the selection in a surround; in Vim it changes the lines, like R.
  • Visual K isn't bound; in Vim it looks up the selection with keywordprg. binvim's K is LSP hover, in Normal mode only.
  • Ctrl-] isn't bound, since there are no tags; gd goes to the definition through LSP.
  • Char- and line-wise Visual I / A follow Vim's documented rules. Vim's own behaviour in the undocumented corners (VjA from column 0, vkI going backwards) differs.

Left out

  • Abbreviations (:ab, :iab) — they'd need new configuration surface.
  • Manual folds (zf, zd, zE) — folds follow indentation.
  • Vimscript — the "= register evaluates arithmetic only, and :set knows the short list of options in the Ex commands table; any other gives an error.

Install

macOS — Homebrew

brew install bgunnarsson/binvim/binvim

The tap lives at github.com/bgunnarsson/homebrew-binvim. The formula compiles from source (depends_on "rust" => :build) — first install takes a minute or two while the tree-sitter grammars compile.

Linux — install script

curl -fsSL https://binvim.dev/install.sh | sh

Pulls the matching musl-static tarball (x86_64 or aarch64) from the latest GitHub Release, verifies its SHA-256, and drops the binary at ~/.local/bin/binvim. Override with BINVIM_VERSION=v0.1.0 or BINVIM_INSTALL_DIR=/opt/bin if needed.

Windows — PowerShell installer

iwr https://binvim.dev/install.ps1 -UseBasicParsing | iex

Pulls the x86_64-pc-windows-msvc zip from the latest GitHub Release and drops binvim.exe (+ binvim-install.exe) into %LOCALAPPDATA%\binvim\bin\. The script doesn't mutate PATH — it prints the one-liner to do that yourself, so the install stays reversible. Override with $env:BINVIM_VERSION = 'v0.1.0' or $env:BINVIM_INSTALL_DIR = 'C:\bin'.

:terminal on Windows requires Windows 10 1809+ (ConPTY). Older versions will fail to open a PTY.

Windows — Scoop

scoop bucket add binvim https://github.com/bgunnarsson/binvim
scoop install binvim

Uses the manifest at scoop/binvim.json in this repo — the repo doubles as the bucket. The release script points it at each new release's Windows zip, so scoop update binvim picks up a release as soon as it's out.

crates.io

cargo install --locked binvim

Builds from source against the version published on crates.io. --locked uses the Cargo.lock shipped with the crate so the dep set matches what was tested for the release. Both binvim and binvim-install land in ~/.cargo/bin/.

Nix flake

nix run github:bgunnarsson/binvim              # one-shot, in a temporary store path
nix profile install github:bgunnarsson/binvim  # install permanently to your profile
nix run github:bgunnarsson/binvim#binvim-install  # run the toolchain installer

For NixOS / home-manager system configs, add binvim as a flake input and use the default overlay:

{
  inputs.binvim.url = "github:bgunnarsson/binvim";
  outputs = { self, nixpkgs, binvim, ... }: {
    nixosConfigurations.<hostname> = nixpkgs.lib.nixosSystem {
      modules = [
        { nixpkgs.overlays = [ binvim.overlays.default ]; }
        ({ pkgs, ... }: { environment.systemPackages = [ pkgs.binvim ]; })
      ];
    };
  };
}

nix develop github:bgunnarsson/binvim drops you into a shell with the toolchain (cargo, rustfmt, clippy, pkg-config + the tree-sitter C build deps) for hacking on binvim itself.

From source

cargo build --release

The binary lands at target/release/binvim. Requires a stable Rust toolchain.

binvim-install — set up LSPs, formatters, and DAP adapters

A second binary, binvim-install, ships alongside binvim from every install path (Homebrew, the Linux tarball, cargo build --release). Run it once to bring up the toolchains binvim drives:

binvim-install

It opens a checkbox list of every language binvim supports. Pick the ones you care about, and it'll detect which package managers you have on $PATH (brew, apt-get, npm, cargo, rustup, go, pipx, pip, gem, dotnet, nix, composer), pick the right installer per tool, dedupe shared tools across languages (prettier, lldb-dap, vscode-langservers-extracted, …), show you the plan, and run the installs once you confirm. Anything that can't be auto-installed (netcoredbg, OmniSharp) prints the manual steps instead. The full per-tool reference table still lives below under External tools for users who'd rather install by hand.

The same flow is available inside the editor:install (or :installer) opens a full-screen overlay with the identical three-stage UX (bundles → optional Node.js versions → plan review). y on the plan stage suspends binvim lazygit-style and runs the installs against the host terminal, then drops back into the editor with a status-line summary. Both entry points share the catalog + runner in binvim::install, so adding a language only requires touching one place.

:update runs the same overlay but only upgrades tools you already have on $PATH to the catalog's pinned (or newest) versions — handy after a binvim release bumps its pins. Tools that aren't installed are left untouched and flagged "not installed — run :install to add it". Managers that own their own version (brew, apt, nix) upgrade via their native upgrade command; pinned managers (npm, cargo, go, gem, pipx, dotnet, composer) re-run their install at the pin.

The first checkbox in the :update list is binvim itself. It detects how the running binary was installed — Homebrew, cargo, the install script, Scoop, or Nix — from the executable's path and runs the matching upgrade (brew upgrade …, cargo install --locked --force binvim, re-running install.sh, scoop update binvim, nix profile upgrade binvim). A source/dev build is detected too and shown with manual instructions instead. The new binary takes effect on the next launch.

Run

binvim [path]

If path is omitted and a session exists for this cwd, the session restores (start page + tab row above it). Otherwise the start page renders alone. Press : for a command (:e <path>, :q) or <space> to open the file picker.

Leader bindings

Keys Action
<space> File picker
<space>? Recent files
<space>G Live grep
<space>gg Open lazygit — suspends the editor, hands the terminal to lazygit, refreshes every buffer's git gutter on exit (same as :lazygit / :lg)
<space>e File explorer — built-in sidebar tree by default; shells out to yazi instead when [file_explorer] yazi = true
<space>a Code actions
<space>r Rename (LSP-aware) — opens a modal preview overlay (per-edit checkboxes, before/after snippet per occurrence) before anything touches disk. j/k move, <Space> toggles, a/n flip all on/off, o jumps to the edit site (cancels), <Enter> applies only the enabled edits, <Esc> cancels
<space>R Replace all (literal-string in buffer)
<space>f Format active buffer
<space>i Set up the toolchain for this language — opens :install preselected to the current buffer's language bundle (LSP + formatter + DAP), so you review and confirm with y. When a file's LSP or formatter is missing on open, a popup (same style as the file picker) lists what's missing — Enter opens the installer, Esc skips (disable the popup with [install] prompt_on_open = false)
<space>/ Toggle line comment(s) — current line in Normal, every selected line in Visual. Per-language prefix (//, #, --); block-only languages (HTML / Markdown / CSS / XML / Razor) wrap with their pair
<space>bd Delete buffer (refuses dirty)
<space>bD Delete buffer (force)
<space>ba Delete all buffers (refuses dirty)
<space>bA Delete all buffers (force)
<space>bo Close other buffers
<space>bn Next buffer
<space>bp Previous buffer
<space>ds Start debug session
<space>dq Stop debug session
<space>db Toggle breakpoint
<space>dB Clear breakpoints in active file
<space>dc Continue
<space>dn Step over (next)
<space>di Step into
<space>dO Step out
<space>dp Toggle debug pane
<space>df Focus debug pane
<space>do Document symbols (LSP)
<space>dS Workspace symbols (LSP)
<space>tt Spawn a new terminal tab (<space>tt again to add another)
<space>tp Toggle terminal pane visibility — PTYs stay alive in the background while hidden
<space>tf Focus the terminal pane (drop into Mode::Terminal — typing flows to the shell again)
<space>tq Close the active terminal tab (pane hides when the last one goes)
<space>mm Task picker — discover + run a workspace task (same as :task / :tasks)
<space>ml Re-run the most recent task (same as :tasklast / :trun)
<space>ss Test picker (same as :test)
<space>sn Run the nearest test (same as :testnearest)
<space>sf Run every test in the active file (same as :testfile)
<space>sl Re-run the most recent test (same as :testlast)
<space>sq Cancel the running test adapter (same as :testcancel)
<space>sr Toggle the streaming results overlay (same as :testresults)
<space>jc / <space>jC Spawn a new Claude tab in the right-side pane — uppercase variant additionally pre-types @<active-buffer cwd-relative path> into the input once the tool is ready. Same shift-pair pattern for <space>jx / <space>jX (Codex) and <space>jo / <space>jO (opencode). Each invocation always opens a fresh instance; use <space>jf to focus an existing pane and <space>jp to toggle visibility (PTYs keep draining hidden). <space>jq closes the active side tab.
<space>pi Package manager — manage installed packages: pick a project manifest (.csproj / package.json / Cargo.toml / go.mod / requirements.txt), pick an installed package, then a version to change to. The installed version is highlighted; Tab toggles prereleases; type to narrow the version list.
<space>ps Package manager — search & add: pick a manifest, type to search the registry, pick a package, then a version to add.

The package manager detects the ecosystem from the active buffer's workspace. Five backends are wired up:

  • .NET / NuGet — requires the dotnet SDK on PATH (dotnet package search needs SDK 8.0.4xx+; the full per-version list is reliable on the .NET 10 SDK).
  • npm — requires npm on PATH (npm view / npm search / npm install, honouring a project-local .npmrc for private registries).
  • Cargo / crates.io — requires cargo on PATH for search & add; the version list is fetched from the crates.io API (so it needs curl and network access).
  • Go modules — requires go on PATH for the version list & add; search scrapes pkg.go.dev (so it needs curl and network access).
  • Python / PyPI — no Python tooling required at all; it reads and edits requirements.txt directly (so add just rewrites the pin — it never runs pip, sidestepping which-virtualenv ambiguity) and pulls the version list from PyPI's JSON API. Because PyPI retired package search (its search page is now bot-walled), "search" resolves an exact package name instead. Needs curl and network access.

The Cargo, Go, and Python backends shell out to curl for the steps their toolchain can't do (crates.io has no cargo command for listing all versions; the Go toolchain has no search; PyPI is HTTP-only), so curl must be on PATH for those.

Hold <space> (or <space>b / <space>d / <space>g / <space>h / <space>j / <space>m / <space>p / <space>s / <space>t) for ~250 ms and a which-key popup lists the available next keys.

Buffer / tab navigation

Keys Action
H / L Previous / next buffer (same as :bp/:bn)
gt / gT Same as H / L (Vim aliases)
Ctrl-^ Alternate buffer — the file active before this one (same as :e# / :b#); N Ctrl-^ goes to buffer N
Ctrl-O / Ctrl-I Jumplist back / forward — persists across sessions per-buffer
Click a tab Switch to it
Middle-click a tab Close it (refuses dirty, same as :bd)
Click × on a tab Close it (refuses dirty)
Click / Scroll the visible tab slice by one

Window splits

Keys Action
<C-w> v Split vertically + open the file picker for the new pane
<C-w> s Split horizontally + open the file picker for the new pane
<C-w> V Split vertically with the same buffer (Vim's :vsplit)
<C-w> S Split horizontally with the same buffer (Vim's :split)
<C-w> h/j/k/l Focus the neighbouring window on the left/down/up/right
<C-w> q / c Close the active window (refuses if it's the last one)
<C-w> o Close every window except the active one
<C-w> = Reset every split ratio back to 50/50
<C-w> [N] > / < Widen / narrow the active window by N columns (default 1)
<C-w> [N] + / - Grow / shrink the active window's height by N rows (default 1)
<C-w> T Promote the focused pane's buffer to its own tab (non-destructive — the split stays)

By default <C-w>v / <C-w>s create a split and open the file picker so the new pane lands on a different file straight away — typical case is "show me file A on the left and file B on the right." The uppercase <C-w>V / <C-w>S keep Vim's classic behaviour of opening the same buffer in both panes (useful for viewing two parts of one long file with independent cursors). :e other.txt swaps the focused pane's buffer without disturbing other panes. Moving focus into a pane that points at a different buffer swaps the live buffer state under you, so each window keeps its own cursor, viewport, syntax highlighting, fold state, git stripe, blame, and markdown concealed render.

Splits are scoped to the tab they were created in. H / L / :b N cycle between tabs; each tab carries its own layout, so splitting in one tab doesn't bleed into the others. A file picked into a split via <C-w>v lives in that tab's layout but stays out of the tabline until you promote it — <C-w>T from its focused pane adds it as a tab (the split stays intact), or :b <name> from anywhere does the same as a side effect of jumping to it.

Ex commands

Beyond the standard :w, :q, :e <path>, :bd, :s/pat/repl/g, etc.:

Command Description
:health Full-screen dashboard: version, CPU / RAM, buffers, attached LSPs (binary path + running flag + init state + per-kind pending-request breakdown), per-buffer LSP cache counts (doc-hi: N cached · sem-tok: M cached), Tailwind / formatter / editorconfig / git detection. :checkhealth works too. Stuck-init LSPs (binary running but not answering initialize — the classic rustup-wrapper failure mode) get a loud red NOT INITIALIZED chip + a hint pointing at :messages for stderr.
:workspaces / :ws Dump every running LSP client + its currently-attached workspace folders to the status line. In a monorepo / sibling-repo session you'll see entries like rust-analyzer: ~/code/api + ~/code/shared-lib · tsserver: ~/code/web — a single rust-analyzer process is holding both Cargo roots via workspace/didChangeWorkspaceFolders rather than two parallel processes.
:messages / :message Severity-coloured scrollable overlay of captured window/showMessage + window/logMessage notifications plus server stderr (bounded 500-entry ring). Esc / q / :q to dismiss; j/k/Ctrl-D/Ctrl-U/g/G to scroll. Error / Warning showMessages also flash through the status line as they arrive.
:terminal / :term [cmd] Open the embedded PTY shell as a bottom split pane (vt100 / xterm escape parsing, 10k-row scrollback, full SGR colour + attrs, mouse-tracking forwarding). Mode::Terminal forwards keystrokes to the PTY. Two escape hatches: Esc drops focus to Mode::Normal; <C-w> does the same but primes the window-leader so <C-w>k / <C-w>q / <C-w>> continue to work for the editor windows above. <leader>tf re-focuses the pane. To send a literal Esc to the embedded program (vi-mode shells, vim, less), press Ctrl-[ — same byte, doesn't trigger the leave-mode behaviour. Click inside the pane forwards to the PTY when the inner program asked for mouse tracking (DECSET 1000 / 1002 / 1003 / 1006 — htop, vim mouse=a, less mouse mode, …), otherwise pulls focus in. Selection / copy goes through the host terminal app's native Shift+drag → Cmd-C. PTY auto-resizes with the host. Default command is $SHELL (fallback /bin/sh); :terminal htop spawns that instead.
:fmt / :format Run the configured formatter on the active buffer. Same path as <leader>f.
:s/pat/repl/[flags] Buffer-local substitute, with the same pattern syntax as / — smart case included. The replacement takes & / \0 for the match, \1\9 for groups ($1 still works), \r for a line break. Flags: g every match on the line, i / I ignore case or don't, n count the matches and change nothing, c ask before each one (y / n / a / q / l, Ctrl-E / Ctrl-Y to scroll). An empty pattern reuses the last search.
:S/pat/repl/[flags] Project-wide substitute, with the same syntax and flags. ripgrep enumerates the files containing pat, opens each, applies the substitution buffer-wide, saves (with n, only counts).
:& / :&& / :~ The last :s again on the range: :& without its flags, :&& with them, :~ with the last search's pattern in place of its own. In Normal mode & runs it on the current line, g& on every line with its flags (:%s//~/&).
:{range} addresses ., $, N, +N / -N on any address, 'a, '< / '>, /pat/, ?pat?, \/, \? and %, joined by , or by ; (which counts the second from the first). A range on its own goes to its last line, and : on a Visual selection fills in '<,'>.
:m / :t / :co {addr} Move, or copy, the lines to below the address — 0 for the top.
:j[!] [count] Join the lines as J does, or as gJ with !.
:> / :< [count] Shift the lines one indent for each > / < typed (:>>> is three).
:d / :y [x] [count] Delete / yank the lines into register x; a count takes that many lines from the range's last.
:pu[t][!] [x] Put register x on lines of their own below the line, or above it with ! (:0pu for the top).
:le / :ri / :ce Align the lines left (:le 4 indents by 4), right or centred in a width (:ce 60; default max_line_length, else 80).
:retab[!] [N] Lay tabs and spaces out again for tabstop N — spaces or tabs as the file indents; ! takes runs of spaces too.
:norm[al][!] {keys} Type keys in Normal mode on each line of the range (:%norm A;), each line its own undo step; ! leaves [keymaps] out.
:g/pat/cmd / :g! / :v Run cmd — any : command, :normal included — on every line matching pat, or with :g! / :v every line that doesn't (:g/TODO/d, :g/^/m0). One undo step.
:sort[!] [flags] [/pat/] Sort the lines, the whole file by default: ! reversed, i ignoring case, n / x on the first decimal / hex number, u dropping repeats, /pat/ on what follows the match — or on the match itself with r.
:!cmd Run cmd in your shell (cmd /C on Windows) and show what it prints; % / # stand for the current / alternate file (:!wc -l %).
:r file / :r !cmd Read a file, or what a command prints, in below the line — :0r for the top, a bare :r for the current file.
:w !cmd Hand the lines (the whole file by default) to cmd on its stdin and show its output; the file isn't written.
:sp / :vs [file] Split the window — stacked or side by side — onto file or the same buffer; :new / :vnew split onto a new empty buffer. <C-w>v / <C-w>s still open the picker.
:on[ly] / :clo[se] Close every other window, or this one.
:cd [dir] / :pwd Change the working directory — home without dir, back with - — or show it. The pickers, grep and the file tree follow it; language servers keep their root.
:bufdo / :windo {cmd} Run cmd in every buffer or window; :cdo / :cfdo at every quickfix entry, or once per file. A range picks them by number (:2,4bufdo); the first error stops the run.
:grep / :vimgrep / :make :grep args runs rg --vimgrep args; :vimgrep /pat/[g][j] files searches with a Vim pattern; :make [args] runs the workspace's build in a task tab. Each fills the quickfix list and jumps to the first entry (! / j stay put).
:set opt / noopt / opt! / opt=n / opt? Change or show an option for the session: ic, scs, ws, hls, is, tw, rnu, list, and et / sw / ts over .editorconfig's. :set alone lists them.
:reg[isters] List every register with its contents — named, numbered, read-only and clipboard alike. :display works too.
:marks / :jumps / :changes List the buffer's marks, the jump list, or the change list.
:earlier / :later [N] Walk the undo tree by steps, by file writes (3f) or by time (10s, 5m, 1h). :undolist lists its branches.
:digraphs List the built-in RFC 1345 digraph set that Insert's Ctrl-K {a}{b} enters.
:debug / :dap Start a debug session. :dapstop, :dapc, :dapn, :dapi, :dapo, :dapb, :dapclear, :dappane cover the rest of the surface. :dapb accepts arg forms: :dapb if <expr> for a conditional breakpoint, :dapb hit <expr> for hit-count, :dapb plain to strip both. Conditional breakpoints render as in the gutter; the breakpoints pane lists each row's expression inline.
:noh Clear the search highlight.
:x / ZZ / ZQ :x and ZZ write only if the buffer is modified, then quit; ZQ is :q!. :q, :wq and :x refuse (E162) while another buffer has unsaved changes.
:wa / :qa / :wqa :wa writes every modified buffer (formatter and .editorconfig included) and leaves you where you were; :qa quits unless something is unsaved, :qa! quits regardless, :wqa / :xa write everything and quit.
:e! Reload the file from disk, discarding unsaved changes.
:copilot Report Copilot sign-in status in the status line. Subcommands: signin re-fires device-flow auth; reload re-checks status (auto-polled every 3s while pending); signout clears the local sign-in.
:test (+ aliases) Integrated test runner. :test opens a fuzzy picker of discovered tests for the active workspace's adapter (currently cargo test only — adapter is selected by walking up from the active buffer for Cargo.toml). :testnearest runs the test enclosing the cursor (walks upward for #[test] / #[tokio::test] / #[rstest] / #[async_std::test]). :testfile runs every test whose module path matches the active buffer's file. :testlast re-runs the most recent invocation. :testcancel kills the running adapter. Results stream live into a :health-style scrollable overlay (j/k/Ctrl-D/Ctrl-U/g/G to scroll, Esc / q / :q to dismiss), with pass / fail / ignored counts on completion. Failures populate the quickfix list — ]q / [q walks them; cargo test's panic-location output gives you accurate file:line entries for assertion failures.
:task / :tasks Integrated task runner. Discovers workspace tasks from npm scripts (package.json:scripts.* — npm / pnpm / yarn auto-picked from the lockfile), Justfile recipes (skips _private + [private]), cargo aliases (.cargo/config.toml:[alias]) + the 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 so duplicate names disambiguate themselves. Selecting a task spawns it in a fresh bottom-terminal tab labelled with the task name, so you can run dev + lint + build in parallel and tell them apart in the tab strip.
:tasklast / :trun Re-run the most recent task. Spawns a new tab rather than re-using the previous one so consecutive runs sit side-by-side for comparison.
:lazygit / :lg Suspend the editor, hand the full terminal to lazygit, and refresh every open buffer's git gutter on exit. Same effect as <leader>gg.

External tools

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
pyright-langserver Python LSP (basedpyright-langserver is tried as a fallback) npm i -g pyright (or npm i -g basedpyright)
clangd C / C++ LSP brew install llvm / apt install clangd
bash-language-server Bash / shell LSP npm i -g bash-language-server
yaml-language-server YAML LSP npm i -g yaml-language-server
lua-language-server Lua LSP brew install lua-language-server
vue-language-server Vue LSP npm i -g @vue/language-server
svelteserver Svelte LSP npm i -g svelte-language-server
marksman Markdown LSP brew install marksman (single Go binary)
taplo TOML LSP + formatter cargo install taplo-cli --features lsp
ruby-lsp Ruby LSP gem install ruby-lsp
intelephense PHP LSP npm i -g intelephense
jdtls Java LSP (Eclipse JDT-LS) brew install jdtls — binvim hashes the buffer's parent dir into ~/.cache/binvim/jdtls/<hash> as the workspace data dir so projects don't trample each other
zls Zig LSP brew install zls
nil (or nixd) Nix LSP nix profile install nixpkgs#nil (nixd via nix profile install nixpkgs#nixd)
elixir-ls Elixir LSP brew install elixir-ls (binvim probes language_server.sh as a fallback if the package only ships the shim)
kotlin-language-server Kotlin LSP brew install kotlin-language-server (JVM-backed; same friction profile as jdtls)
docker-langserver Dockerfile LSP npm i -g dockerfile-language-server-nodejs
sqls SQL LSP go install github.com/sqls-server/sqls@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)
emmet-ls Emmet abbreviation completion in HTML / CSS / JSX / TSX / Vue / Svelte / Astro / Razor buffers npm i -g emmet-ls
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.
biome (project-local) JSON LSP + JS / TS / JSON formatter npm i -D @biomejs/biome in the project
csharpier .cs formatter dotnet tool install --global csharpier
gofmt / goimports Go formatter (goimports preferred when on $PATH — it also organises imports) Ships with Go; go install golang.org/x/tools/cmd/goimports@latest for the imports variant
ruff (or black) Python formatter (ruff preferred, black as fallback) pipx install ruff / pipx install black
clang-format C / C++ formatter brew install llvm / apt install clang-format
shfmt Shell-script formatter brew install shfmt / go install mvdan.cc/sh/v3/cmd/shfmt@latest
stylua Lua formatter cargo install stylua / brew install stylua
prettier Formatter for the file types biome doesn't cover — Markdown / MDX, Vue, Svelte, HTML, CSS / SCSS / Less, YAML, GraphQL. Project-local preferred (walks up to node_modules/.bin/prettier), falls back to global npm i -g prettier (or -D per project; Svelte additionally needs prettier-plugin-svelte in node_modules)
rufo Ruby formatter gem install rufo
php-cs-fixer PHP formatter composer global require friendsofphp/php-cs-fixer
google-java-format Java formatter brew install google-java-format
zig fmt Zig formatter (ships with the toolchain) brew install zig
nixfmt (or alejandra) Nix formatter nix profile install nixpkgs#nixfmt-rfc-style (alejandra via nix profile install nixpkgs#alejandra)
mix format Elixir formatter (ships with the toolchain) brew install elixir
ktfmt Kotlin formatter brew install ktfmt
sql-formatter SQL formatter (multi-dialect) npm i -g sql-formatter
netcoredbg .NET debug adapter (DAP) Build from github.com/Samsung/netcoredbg. The binary and its libdbgshim.dylib / ManagedPart.dll / Microsoft.CodeAnalysis.*.dll siblings need to live in the same directory — symlink them next to the binary if you copy out of the build's install dir.
dlv Go debug adapter (DAP) go install github.com/go-delve/delve/cmd/dlv@latest
debugpy (Python module) Python debug adapter (DAP) pip install debugpy (or pipx inject into a venv). binvim runs it as python3 -m debugpy.adapter.
lldb-dap Rust / C / C++ debug adapter (DAP) Ships with LLVM 18+: brew install llvm (then add $(brew --prefix llvm)/bin to $PATH). Falls back to the legacy lldb-vscode if lldb-dap isn't present.
java-debug (jdtls plugin) Android Java / Kotlin debug adapter (DAP) Download com.microsoft.java.debug.plugin-*.jar from github.com/microsoft/java-debug into ~/.cache/binvim/java-debug/; jdtls loads it for <leader>ab attach debugging.
rg Live grep backend brew install ripgrep
yazi <space>e file manager — optional; only needed with [file_explorer] yazi = true, the built-in sidebar tree is the default brew install yazi
sdkmanager / avdmanager Android SDK command-line tools — emulator management (<leader>a), no Android Studio brew install --cask android-commandlinetools, then sdkmanager --licenses
adb Android platform-tools (device bridge) brew install --cask android-platform-tools / apt install android-tools-adb
emulator Android emulator runtime sdkmanager emulator — binvim locates it under $ANDROID_HOME/emulator

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.

Configuration

Optional config file at ~/.config/binvim/config.toml:

schema_version = 1

[colors]
# Editor surface
background        = "#1e1e2e"   # buffer body bg; unset = inherit terminal default
chrome_bg         = "#181825"   # tabs, popups, status segments, side panes

# Chrome neutrals
foreground        = "#cdd6f4"   # main chrome text
dim               = "#6c7086"   # muted (line numbers, hints, comments)
emphasis          = "#b4befe"   # active tab fg, multi-cursor block, picker title
surface           = "#45475a"   # active tab bg, picker selection
border            = "#585b70"   # popup borders, dividers

# Chrome accents
accent            = "#fab387"   # debug chip, breakpoint, dirty-tab dot
accent_secondary  = "#a6e3a1"   # terminal chip, active debug sub-tab, git added
chip_fg           = "#1e1e2e"   # fg on coloured chips

# Diagnostic / severity
error             = "#f38ba8"
warning           = "#f9e2af"
info              = "#89b4fa"
hint              = "#89dceb"

# Syntax captures (tree-sitter / LSP semantic tokens)
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.

[line_numbers]
relative = true   # cursor row shows absolute, others show distance. On by default.

[copilot]
enabled = false   # GitHub Copilot via copilot-language-server (npm). Off by default.

[file_explorer]
yazi = false      # `<space>e` opens the built-in sidebar tree by default; set true to shell out to yazi.

[lsp]
semantic_tokens = true     # `textDocument/semanticTokens/full` layered over tree-sitter.
document_highlight = true  # Surface2 bg on every occurrence of the symbol under the cursor.

[install]
prompt_on_open = true      # Hint (once/language/session) when a file's LSP or formatter is missing.

[update]
check = true               # Ask crates.io once a day whether a newer binvim is out.

[clipboard]
osc52 = "auto"             # Emit OSC 52 over SSH so a remote yank reaches your local clipboard.

[keymaps.normal]
H = "^"                    # A key → the keys it types instead. Unlisted keys keep their defaults.
L = "$"

[colors] — values may be hex (#rrggbb) or a named crossterm colour. The section drives both chrome and syntax colouring.

Chrome palette. The neutrals + accents above (background, chrome_bg, foreground, dim, emphasis, surface, border, accent, accent_secondary, chip_fg, error, warning, info, hint) paint every chrome surface in the editor: tab bar, status line, popups (whichkey / hover / signature / notification / floating cmdline / picker / completion), terminal pane, debug pane, gutter signs, severity glyphs, buffer overlays (search / yank / multi-cursor / match-pair / doc-highlight), :health, :messages, and the start page. Set only background and binvim auto-derives the four neutrals (chrome_bg, surface, border, foreground, dim) by luminance-aware mixing — a one-line theme yields a coherent chrome. Each accent has a baked-in Catppuccin Mocha default that you can override.

Namespaced overrides. Every chrome role also has a dotted-namespace key that overrides only one surface, falling back to its broad theme key (and through to the Catppuccin default). Use these when you want one specific tint without re-tinting everything else:

Family Keys
Notifications notification.{info,warning,success,error}
Git stripe git.{added,modified,deleted}
Diagnostics diagnostic.{error,warning,info,hint}
Gutter gutter.{breakpoint,pc_marker}
File tree file_tree.folder
Tab bar tab.{active_bg,active_fg,inactive_fg,dirty,close}
Terminal pane terminal.{chip_bg,chip_fg,active_tab_bg}
Debug pane debug.{chip_bg,active_tab_bg}
Mode chip mode.{normal,insert,visual,command,search,picker,prompt,terminal,debug}
Buffer overlays search.highlight_bg, yank.flash_bg, multi_cursor.bg, match_pair.bg, doc_highlight.bg

Syntax captures. 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.

[line_numbers]relative = true (the default) renders the gutter Vim-style: the cursor's row shows its absolute (1-indexed) line in a brighter Subtext1 tone, every other row shows the count of lines away from the cursor. Pairs naturally with count-prefixed motions like 5j / 12k / 3dd. Set relative = false to fall back to plain 1-indexed numbering on every row.

[clipboard]osc52 controls the terminal OSC 52 escape that binvim can emit alongside its usual arboard clipboard write. arboard only ever reaches the machine binvim runs on; OSC 52 asks the terminal to write the local clipboard, so a yy inside a binvim running over SSH lands on your own desktop.

The default is "auto" — emit it over SSH, skip it locally. Locally arboard has already done the job, and the sequence isn't free: it puts every yank, base64'd, into the terminal's output stream, which is where script, asciinema and tmux logging will keep it. Force it with osc52 = true (always) or osc52 = false (never).

Inside tmux or screen it needs one line of their config. Both swallow an application's OSC 52 by default. binvim sends the sequence raw and wrapped in the multiplexer's DCS passthrough, so enabling either route is enough — for tmux, set -g set-clipboard on or set -g allow-passthrough on in ~/.tmux.conf. Your terminal emulator also has to support OSC 52 (most do; Terminal.app does not).

[keymaps] — remap keys in Normal ([keymaps.normal]), Visual ([keymaps.visual]) and Insert ([keymaps.insert]) mode and on the : / / command line ([keymaps.command]), the way Vim's nnoremap / vnoremap / inoremap / cnoremap do. Each entry maps a key, or a sequence of keys, to the keys it should type instead, in Vim notation: H = "^", J = "10j", gh = "^", "<C-s>" = ":w<CR>", "<leader>w" = ":w<CR>". Keys you don't list keep their defaults. Keys that start a longer mapping wait for the next one. If they already mean something by themselves — x with xx mapped, or J with both J and Jk mapped — they wait at most [keymaps] timeout milliseconds (default 1000, Vim's timeoutlen) and then run; a prefix that's unfinished anyway, like g or <leader>, waits as long as it would with nothing mapped. When the next key rules the longer mapping out, the held keys run as typed — or as the shorter mapping, when they're mapped on their own. In Insert mode and on the command line a held key always means something — it's text — so with jk = "<Esc>" a lone j is still typed, once the wait runs out or as soon as the next key isn't k. A value can also be a table with a description, "<leader>x" = { keys = ":w<CR>", desc = "Save" }: <leader> mappings show in the which-key popup under their desc (or their keys), replacing a built-in row on the same key. <Space>, <CR>, <Esc>, <Tab>, <BS>, <Del>, the arrows, <Home> / <End>, <PageUp> / <PageDown>, <F1><F24>, <leader> and the <C-…> / <A-…> / <S-…> modifiers are understood; <lt> is a literal <, and mapping a key to "<Nop>" switches it off.

A mapping fires wherever a command or a motion starts — 3H, "aH and dH all use it — but never where the key is an argument: fH still finds an H, rH still replaces with one, and gH or <space>bH are left alone. That holds in Insert and on the command line too, where any key may otherwise start a mapping: the register name after Ctrl-R, the key after Ctrl-V, both keys of a Ctrl-K digraph and the source key after Ctrl-X are always read as themselves. Expansions aren't remapped, so j = "k" next to k = "j" swaps the two instead of looping. A count you type multiplies a count inside the mapping: with J = "10j", 3J moves 30 lines. Macros record the keys you pressed, so replaying one applies your mappings the same way. An entry that doesn't parse, or a table or setting under [keymaps] that binvim doesn't know ([keymaps.operator], a mistyped timout), is skipped, named in the status line at startup and listed under keymaps in :health — which also counts each mode's mappings; the rest of the config still loads. [lsp] — both toggles default true. semantic_tokens = false gates the textDocument/semanticTokens/full request and the highlight-cache overlay off entirely (no wire traffic, no render delta). document_highlight = false gates textDocument/documentHighlight similarly. Useful if your LSP's semantic-token output collides badly with the tree-sitter pass, or if the on-every-cursor-settle highlight echo is more distracting than useful for your workflow.

[install]prompt_on_open = true (the default) is the first-run toolchain nudge: when you open a file whose language is missing its primary LSP or formatter (probed on $PATH), a popup (the same overlay style as the file picker, so a competing notification like Copilot sign-in can't paint over it) lists what's missing — Enter opens :install preselected to that language's bundle so you review and confirm with y, Esc dismisses. It fires at most once per language per session, is skipped for large files (which don't attach a server anyway) and for languages binvim can't auto-install (Razor's OmniSharp), never opens over another overlay or mid-edit, and never nags about DAP adapters — only the LSP + formatter that make a language feel "set up." Set prompt_on_open = false to silence it; <leader>i still works on demand.

[update]check = true (the default) asks crates.io on launch whether a newer binvim has been published. The result is cached in ~/.cache/binvim/update-check.json for 24 hours, so the network call happens at most once a day no matter how often you launch; every other launch answers from the cache file. When a newer version exists it shows up in three places: a notification on startup, a line under the start-page logo (▲ update available — binvim x.y.z (you have a.b.c)), and the version row in :health. Nothing is uploaded — it's a plain GET for the crate's published version list, via curl (same as the <space>p package manager; no HTTP client is linked in). Failures are silent: offline, no curl, or a flaky network leaves the editor exactly as it was. Set check = false to skip it entirely.

[file_explorer]yazi = false (the default) points <leader>e at the built-in left-side sidebar tree pane; setting yazi = true switches it to the yazi shell-out. In the tree pane: 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 after external file changes, <space>e (or q / Esc) from inside the pane closes it. Three-state <leader>e toggle from the editor: closed → focused → unfocused-but-visible → closed, so clicking into a buffer drops focus without losing the pane. The file currently open in the focused window renders in the accent colour + bold so it stays identifiable even after the j/k cursor moves elsewhere; double-click in the pane opens a file. File operations: a creates a new entry (a trailing / makes it a folder, and missing parent folders are created along the way), r renames the cursor entry through a prompt pre-filled with its current name — a buffer that has the file open follows the rename, so saves keep landing in the right file — and d deletes the cursor entry after a y confirmation; any other key cancels.

[copilot]enabled = true opts into GitHub Copilot. binvim attaches copilot-language-server (npm package @github/copilot-language-server, install with npm i -g @github/copilot-language-server) as an auxiliary LSP for every buffer. Authentication happens through the language server itself: on first launch the server emits a device-flow prompt with a verification URL + user code, which binvim surfaces in the status line. Visit the URL, enter the code, and the token persists at ~/.config/github-copilot/hosts.json for the next session. The status auto-polls every 3 s while you complete the device flow so the editor flips to "signed in" within seconds of you clicking through. binvim itself doesn't carry an HTTP client or talk to GitHub directly — the language server handles all networking and auth. Once signed in, ghost completions appear inline as muted italic text after the cursor in Insert mode (~250 ms idle pause to trigger). Accept / dismiss split: <Tab> accepts the Copilot ghost (it wins over the LSP popup when both are visible — the popup auto-closes on accept), <Enter> accepts the LSP completion popup item, any other key dismisses the ghost. Default is enabled = false.

A missing or malformed config is ignored — the baked-in Catppuccin Mocha palette is used.

Theme presets

Ready-made [colors] blocks live in themes/ — one folder per theme, each containing a theme.toml. Every preset ships the full chrome palette (the 12 neutrals + accents), so switching theme flips every chrome surface — tab bar, popups, status line, panes — to that theme's own tones rather than leaking Catppuccin defaults.

Dark themes Light themes
catppuccin-mocha, dracula, tokyo-night, night-owl, one-dark, gruvbox, nord, github-dark, monokai, visual-studio catppuccin-latte, light-owl, solarized-light, ayu-light, github-light

There is no built-in theme loader — copy the file contents into your ~/.config/binvim/config.toml, e.g.:

cat themes/tokyo-night/theme.toml >> ~/.config/binvim/config.toml

The baked-in default is Catppuccin Mocha; themes/catppuccin-mocha/theme.toml mirrors it explicitly with annotated comments as a copy-paste starting point.

Licence

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.

Project layout

src/
  app.rs           slim entry — App struct + new/run + TerminalGuard
  app/
    state.rs       supporting types (Register, BufferStash, HoverState, …)
    pair.rs        bracket and HTML tag matching + auto-pair helpers
    view.rs        viewport, scrolling, folds, highlight cache, tab-bar geometry
    search.rs      search, jumps, per-line range queries for the renderer
    registers.rs   registers, macros, dot-repeat, OS-clipboard mirror
    buffers.rs     buffer switching, open/close, disk reload, recents, sessions
    save.rs        save flow, formatter, .editorconfig on-save, git branch
    edit.rs        primitive edits — insert / replace / surround / undo / number / multi-cursor mirror
    visual.rs      visual-mode helpers (incl. block + Ctrl-N multi-selection)
    dispatch.rs    apply_action — operator / motion / text-object glue
    input.rs       per-mode key handlers, mouse handler, `:`-command dispatch
    lsp_glue.rs    LSP event handling, request helpers, snippet expansion
    dap_glue.rs    DAP event handling, debug-pane focus mode, project / profile pickers
    picker_glue.rs picker open / handle / refilter, yazi shell-out
    file_tree.rs   built-in sidebar tree explorer — state + key handler + click flow
    health.rs      `:health` output
  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 (one arm per extension; stdin→stdout helper + temp-file dance for tools without stdin support)
  lang.rs          tree-sitter language detection and highlight cache
  lsp.rs           slim entry — re-exports public API
  lsp/
    types.rs       wire-side types + URI helpers
    specs.rs       per-extension server dispatch + workspace discovery
    client.rs      LspClient — spawn + send/recv frames
    io.rs          reader-thread loop + JSON-RPC dispatcher
    manager.rs     LspManager — fan-out + response routing
    parse.rs       response parsers
  dap.rs           slim entry — re-exports public API
  dap/
    types.rs       wire-side types — DapIncoming / DapEvent / breakpoint / frame / variable structs
    specs.rs       adapter registry (.NET / Go / Python / Rust), per-adapter target discovery, $PATH lookup
    client.rs      DapClient — spawn + stdin / stdout / stderr fan-out
    io.rs          reader-thread loop (Content-Length framing, same as LSP)
    manager.rs     DapManager — protocol state machine + drain
  mode.rs          modes and operators
  motion.rs        motions
  parser.rs        keystroke → action parser
  picker.rs        fuzzy pickers
  render.rs        terminal rendering (incl. tab bar)
  session.rs       per-workspace session persistence
  text_object.rs   text objects (`iw`, `i"`, `ap`, …)
  undo.rs          undo/redo history (in-memory + on-disk persistence)

About

The first vim IDE — a vim-native IDE in a single binary: multi-server LSP, tree-sitter, four DAP debuggers, opt-in Copilot, an AI pane, and test/task runners. No plugin manager.

Topics

Resources

Contributing

Stars

17 stars

Watchers

1 watching

Forks

Releases

Contributors

Languages