Skip to content

bgunnarsson/binvim

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.
  • Surround operationsds<char> strips the surrounding pair, cs<old><new> swaps it, visual S<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+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. None of them span line boundaries.
  • 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.
  • Code folding — indent-based folds, toggled with za/zo/zc, with zR/zM to 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 -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.
  • 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.

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. scoop update binvim picks up new releases automatically — the manifest's autoupdate block rewrites the URL + hash from each new tag.

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 — 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 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).

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.

Buffer / tab navigation

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

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/[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.

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; 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.

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]
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.

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

License

Contributing

Stars

Watchers

Forks

Contributors