██████╗ ██╗███╗ ██╗██╗ ██╗██╗███╗ ███╗
██╔══██╗██║████╗ ██║██║ ██║██║████╗ ████║
██████╔╝██║██╔██╗ ██║██║ ██║██║██╔████╔██║
██╔══██╗██║██║╚██╗██║╚██╗ ██╔╝██║██║╚██╔╝██║
██████╔╝██║██║ ╚████║ ╚████╔╝ ██║██║ ╚═╝ ██║
╚═════╝ ╚═╝╚═╝ ╚═══╝ ╚═══╝ ╚═╝╚═╝ ╚═╝
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.
binvim is the first vim IDE. Here's how it stacks up against the editors and IDEs people usually weigh:
- What is a vim IDE?
- binvim vs Neovim
- binvim vs Helix
- binvim vs AstroNvim / LazyVim
- binvim vs VS Code
- binvim vs Visual Studio
- binvim vs Rider
- 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-clickin Normal mode adds a secondary cursor at the click position; cursors render as Lavender blocks. Enter Insert viai(anchored) ora(shifts each cursor by +1); typing and Backspace mirror at every site simultaneously (bottom-up edit order keeps indices stable).Esccollapses; a plain (non-Ctrl) click also collapses. - Multi-selection (
Ctrl-N) — Sublime-style. In Visual-char mode,Ctrl-Nfinds the next literal-text occurrence of the current selection and adds it as a parallel selection. Repeat to keep adding.d/c/yapply across every selection;clands 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/yapply column-wise per row,>/<fall back to indent / outdent over the line range. - 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. - Insert-mode word / line-start delete shortcuts.
Alt/Option+Backspacedeletes the previous word (peels trailing whitespace, then one homogeneous run of word chars or punctuation — same as macOS Option-Delete).Cmd/Super+Backspacedeletes from the cursor back to column 0.Ctrl+Backspacealiases to the word-delete for terminal users without a usable Option key. None of them span line boundaries. - 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. - 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 -Dcounter next to the branch name.]h/[hjump to next / previous hunk;<leader>hppreviews the hunk under the cursor in a hover popup (three lines of surrounding context).<leader>hsstages the hunk viagit apply --cached,<leader>huunstages,<leader>hrdiscards (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 —
:Gblametoggles per-line virtual text at end-of-line: author • relative age (3d,2w,4mo) • short SHA, in muted italic. Parsed fromgit 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>.jsonon save and reloaded on the next session, keyed by content hash so external edits invalidate stale history. - System-clipboard yank + paste —
y,yy,Y,:y, visual yank, and the implicit yank ond/c/xmirror to the OS clipboard viaarboardwhenever they target the unnamed register.p/P(Normal and Visual mode) read from the OS clipboard first — anything youCmd-C'd in another app wins over the in-memory yank. Named registers ("ay) stay local. - Visual-mode paste —
p/Pover 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) andzH/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>for:fmtruns the right tool per extension. biome for JS / TS / JSX / TSX / JSON / JSONC; csharpier for.cs;gofmt/goimportsfor.go;ruff format(orblackas fallback) for.py;clang-formatfor.c/.h/.cpp/.cc/.hpp/.cxx/.hxx;shfmtfor.sh/.bash/.zsh;styluafor.lua; Prettier (project-localnode_modules/.bin/prettierfirst, 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 formatfor.toml;rufofor.rb;php-cs-fixerfor.php(temp-file dance — no stdin mode);google-java-formatfor.java;zig fmtfor.zig;nixfmt(oralejandra) for.nix;mix formatfor.ex/.exs;ktfmtfor.kt/.kts(temp-file dance);sql-formatterfor.sql;.editorconfigindent reflow for.cshtml/.razor(csharpier rejects those, so we fall through)..editorconfigdirectives (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 — open buffers + per-buffer cursor + viewport persist to
~/.cache/binvim/sessions/<cwd-hash>.jsonon 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 withbinvim foo.rs— only buffer restoration is gated on a bare invocation. - Tab completion inside
:—Tab/Shift-Tabcycle 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 nextTabre-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.
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, Tailwindclass="…[16px]…"bracket attributes, …). - CSS — replacement query so selectors and properties don't collide:
.class-nameis@constructor(Yellow),#id-nameis@label(Sapphire),property:is@property(Lavender),--custom-propis@variable, at-rules (@media/@keyframes/…) are@keyword(Mauve). .editorconfig— comments,[*.cs]section headers in Pink,key = valuepairs with the key in Lavender,=in Sky, value in Green..gitignorefamily —#comments,!-negation prefix in Mauve, patterns in Lavender.
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.
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.
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::detecton 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-Djump a half-page;Home/Endjump 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.
Colours overridable via ~/.config/binvim/config.toml.
brew install bgunnarsson/binvim/binvimThe 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.
curl -fsSL https://binvim.dev/install.sh | shPulls 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.
iwr https://binvim.dev/install.ps1 -UseBasicParsing | iexPulls 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.
scoop bucket add binvim https://github.com/bgunnarsson/binvim
scoop install binvimUses the manifest at scoop/binvim.json in this repo. scoop update binvim picks up new releases automatically — the manifest's autoupdate block rewrites the URL + hash from each new tag.
cargo install --locked binvimBuilds 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 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 installerFor 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.
cargo build --releaseThe binary lands at target/release/binvim. Requires a stable Rust toolchain.
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-installIt 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.
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.
| 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 — yazi by default; opens the built-in sidebar tree instead when [file_explorer] tree = 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>/ |
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), 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. Four backends are wired up:
- .NET / NuGet — requires the
dotnetSDK onPATH(dotnet package searchneeds SDK 8.0.4xx+; the full per-version list is reliable on the .NET 10 SDK). - npm — requires
npmonPATH(npm view/npm search/npm install, honouring a project-local.npmrcfor private registries). - Cargo / crates.io — requires
cargoonPATHfor search & add; the version list is fetched from the crates.io API (so it needscurland network access). - Go modules — requires
goonPATHfor the version list & add; search scrapes pkg.go.dev (so it needscurland network access).
pip is not yet supported — PyPI disabled package search, so it needs a different approach. The Cargo and Go backends shell out to curl for the one step their toolchain can't do (crates.io has no cargo command for listing all versions; the Go toolchain has no search), 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.
| Keys | Action |
|---|---|
H / L |
Previous / next buffer (same as :bp/:bn) |
gt / gT |
Same as H / L (Vim aliases) |
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 |
| 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.
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/[gr] |
Buffer-local substitute. g global, r interprets pat as a regex ($1/$2 capture refs honoured in the replacement). Default is literal text. |
:S/pat/repl/[gr] |
Project-wide substitute. ripgrep enumerates the files containing pat, opens each, applies the substitution buffer-wide, saves. r flips ripgrep into regex mode too. |
: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. |
: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. |
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; built-in sidebar tree via [file_explorer] tree = true doesn't need it |
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.
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]
tree = false # `<space>e` opens yazi by default; set true for the built-in sidebar tree.
[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.[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.
[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.
[file_explorer] — tree = false (the default) keeps <leader>e as the yazi shell-out. Setting tree = true switches it to a built-in left-side sidebar 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 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. No file operations (create / delete / rename) yet.
[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.
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.tomlThe baked-in default is Catppuccin Mocha; themes/catppuccin-mocha/theme.toml mirrors it explicitly with annotated comments as a copy-paste starting point.
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 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)