A personal Neovim configuration for Rust, built on LazyVim and inspired by JetBrains' tools. Instant diagnostics, a debugger and test UI, a git safety net, and a test suite you can run yourself.
Website: https://xynorash.github.io/xynovim/ (with guides to the workflows below)
xynovim is a Neovim 0.12 configuration for Rust development. It adds instant diagnostics (rust-analyzer while you type, clippy through bacon-ls half a second later), a debugger (nvim-dap with codelldb), a test UI with cargo-nextest and coverage in the gutter, Cargo workspace, feature and crate helpers, and a git safety net (Local History, lazygit, diffview) to a LazyVim base.
git clone https://github.com/xynorash/xynovim ~/.config/nvim
~/.config/nvim/scripts/bootstrap.shThen start nvim in a Cargo project (or in demo/netwatch) and run :checkhealth xynovim. This replaces your current Neovim configuration, so move ~/.config/nvim aside first if you have one. Prerequisites and the Windows and macOS status are under Install.
Built on LazyVim, developed on Linux (Arch) and also run in CI on Windows and macOS (see Platforms for exactly what that shows), with the Island Dark colour scheme generated from my own colour-scheme export. Everything on this page was recorded from the real config running on the small demo crate in demo/netwatch: the screenshots and videos are regenerated by one script (docs/media/record.sh), nothing is mocked up.
The previous from-scratch Windows config lives on the
windows-legacybranch. Every notable change is in CHANGELOG.md.
The website, https://xynorash.github.io/xynovim/, is published from the dedicated gh-pages branch by the pages workflow (see Website below).
- Two diagnostic tiers: rust-analyzer answers while you type, clippy follows about half a second later, on every keystroke and before any save.
- A debugger and a test runner that look like the ones in an IDE: breakpoints with Rust-aware variable views, a test summary tree, coverage in the gutter.
- A safety net: every save is snapshotted as a git ref (a "Local History"), and the AI agent runs inside a sandbox.
- Treesitter everywhere: sticky scope, text objects, SQL injected into
sqlxstrings, a live syntax-tree view. - Tested: 906 passing checks across 32 spec files, one per feature area, runnable with one command after a clone (below).
Short guides on the website, one per workflow, each with the keys, the reasoning and the limits:
- Instant Rust diagnostics in Neovim with rust-analyzer and clippy: How xynovim gets Rust errors on screen while you type: rust-analyzer for instant type errors, clippy through bacon-ls half a second later, before any save.
- Debugging Rust in Neovim with nvim-dap and codelldb: Set breakpoints, read Rust values and stop at panics in Neovim: the nvim-dap and codelldb setup of xynovim, with the debuginfo setting that makes locals work.
- Running Rust tests with coverage in Neovim: Run Rust tests from Neovim with neotest and cargo-nextest, read a summary tree, and see line coverage from cargo-llvm-cov in the gutter, on demand.
- Cargo workspaces, features and dependencies in Neovim: Create Rust binaries, libraries and workspaces, add crates with their features and fix missing derive features from Neovim: :CrateAdd, :CrateFeatures and more.
- A git safety net for Neovim: Local History, lazygit and diffview: Every save in Neovim written as a git ref you can restore, plus lazygit and diffview: how xynovim recovers deleted files and edits that undo cannot bring back.
- The Island Dark colour scheme for Neovim: Island Dark is the dark colour scheme of xynovim: a generated palette mapped to Treesitter captures, LSP tokens and the plugins in use, tuned for contrast.
These are the original parts of my config that I extracted into standalone plugins, so anyone can use them without the rest of xynovim. All nine are public, tagged v0.1.0 with a release (xyno-sandbox is at v0.1.1), and install with lazy.nvim. The plugins are MIT licensed; xynovim itself stays Apache-2.0. Linux is the platform I test on; each README states what is and is not verified on Windows and macOS. The plugins page has the install order, one complete lazy.nvim spec for the Rust toolkit and the questions I expect.
Standalone:
- xyno-history.nvim: Local History, every throttled save snapshotted into private git refs.
- xyno-sandbox.nvim: runs a command-line coding agent in bubblewrap and refuses to start it without one (Linux only).
- xyno-clipboard.nvim: an OSC 52 clipboard provider for tmux, SSH and herdr (Linux only).
- xyno-guard.nvim: keeps auto-save away from dependency sources and files changed on disk.
Rust toolkit (xyno-rust.nvim is the base):
- xyno-rust.nvim: Cargo context, targets, routes, navigation, server restart handling and a statusline indicator.
- xyno-new.nvim: new Rust binary, library, workspace and egui projects; needs xyno-rust.nvim.
- xyno-imports.nvim: auto-import, ranked choice and organize imports; needs xyno-rust.nvim.
- xyno-crates.nvim: crates.io search, crate add, features and crate import; needs xyno-rust.nvim and xyno-imports.nvim.
- xyno-macros.nvim: macro attribute completion, signature help and
:MacroExpand; needs xyno-rust.nvim and xyno-imports.nvim.
xynovim still carries its own in-tree copies of this code; the plugins are the same code under another namespace.
rust-analyzer's own diagnostics appear as you type (type errors in about 0.01 to 0.15 s). bacon-ls runs clippy against a hardlinked shadow of your workspace on every buffer change (about 0.56 s median), so lints show up before you save. Each message shows its first line plus the source; the full text is one <leader>cd away.
| rust-analyzer, while typing | the full clippy message with its suggestion |
|---|---|
![]() |
![]() |
Video: 02-diagnostics.mp4
Treesitter drives the highlighting, the sticky scope at the top of the window (the for / match / arm headers you have scrolled past stay pinned: see the hero shot above), the text objects and the sqlx SQL injection. Two built-in views make it concrete:
:Inspect: captures and semantic tokens under the cursor |
:InspectTree: the live syntax tree |
|---|---|
![]() |
![]() |
vaf selects a whole function through the treesitter text objects, and the sticky scope in action:
| text object | sticky scope |
|---|---|
![]() |
![]() |
Video: 04-treesitter.mp4
Every rust-analyzer command rustaceanvim offers has a key under <leader>r, discoverable through which-key: extract variable / function / constant / module, inline, structural search and replace, semantic join lines, move item, expand macro, view HIR / MIR, explain error code, workspace symbol including dependencies, go to super method, and more.
Video: 03-refactor.mp4
neotest with the rustaceanvim adapter and cargo-nextest: a summary tree with per-test results (<leader>ts; run a file with <leader>tt, the nearest test with <leader>tr), and line coverage from cargo-llvm-cov shown in the gutter (<leader>tc, toggle with <leader>tg).
| the summary tree | coverage in the gutter |
|---|---|
![]() |
![]() |
Video: 05-tests.mp4
nvim-dap with codelldb, a dap-ui layout sized for Rust's long type names, and lldb's Rust formatters loaded automatically (a String shows as text and a struct as {host:"127.0.0.1", ...}, not as raw memory). <leader>db sets a breakpoint, <leader>dR lists every debuggable target of the workspace, and a panic stops at the panicking frame.
Video: 06-debug.mp4
Cargo features and the build target are chosen per project and applied to both rust-analyzer and bacon-ls, so the two tiers never disagree. In the demo ssr and csr are mutually exclusive: with all features on, both tiers report the conflict; choosing only ssr makes it go away in both.
| the conflict, in both tiers | choose features (<Tab> multi-selects) |
|---|---|
![]() |
![]() |
| choose the target | resolved |
|---|---|
![]() |
![]() |
The choice is stored per workspace and survives restarts. :RustTargets lists every bin / example / test / bench with run, test, build, profile and flamegraph actions; :RustTools covers cargo-deny, audit, machete, bloat and build timings. Video: 07-context.mp4
lazygit (with delta), diffview for diffs, merge conflicts and file history, mergiraf as a syntax-aware merge driver, and Local History: every save (manual or auto-save, throttled) also writes the whole worktree, untracked files included, as a commit under refs/lochist/ using a private index. Deleted files and git reset --hard become recoverable without touching your real index, stash or branches. :LocalHistory browses and diffs, :LocalHistoryRestore restores, :LocalHistoryPrune cleans up.
| lazygit | local history |
|---|---|
![]() |
![]() |
Video: 08-git.mp4
The colour scheme is generated from my own colour-scheme export, which is not distributed (the generated palette is lua/island/palette.lua), by tools/icls2nvim.py, which converts a scheme file you supply yourself: colours, italics, underlines and the org.rust.* entries that colour structs, enums, macros and lifetimes. Contrast was checked against WCAG targets and lifted where the export was too dim. Floats and the completion menu stay solid on a transparent editor (<leader>uO toggles the editor background).
| completion with documentation | picker |
|---|---|
![]() |
![]() |
Video: 09-theme.mp4
99 with the Claude Code provider is wired in (<leader>9), and its claude --dangerously-skip-permissions runs inside bubblewrap, which exists only on Linux, so the agent only runs where a sandbox exists (Linux with bwrap; WSL2 with bwrap is the route on Windows, not tested here). Without one (Windows, macOS, Linux without bwrap) 99 refuses to start a request and says why. Two explicit booleans override that, and either one warns on every request: vim.g.xynovim_99_sandbox = false turns the sandbox off on purpose (even where bwrap works), vim.g.xynovim_99_allow_unsandboxed = true runs the agent anyway only when there is no backend (it never disables a working sandbox); 0, "false" or 1 count as nothing. The <leader>9P provider picker only accepts the guarded Claude Code provider under the same rule (the other CLIs would run with no sandbox or checkpoint). Where bwrap works: the filesystem is read-only except the project and Claude's own state, credentials and agent sockets are hidden, .git/hooks, .git/config and Claude's settings are locked read-only, the environment is scrubbed of tokens, and every request is preceded by a Local History snapshot with auto-save paused. It is a seatbelt, not a jail: the network stays open (details in docs/DETAILS.md). There are no screenshots: the sandbox has no UI, and a real request needs an account.
The shell scripts (scripts/bootstrap.sh, scripts/test.sh) are bash conveniences for Linux and macOS, with PowerShell counterparts for Windows (scripts/bootstrap.ps1, scripts/test.ps1). The config itself is Lua and is meant to run wherever Neovim 0.12 runs with the prerequisites below. What is shown, and no more, comes from the platforms workflow (windows-latest, macos-latest, Neovim v0.12.5) and the ci workflow (Ubuntu):
| Platform | Status | Evidence (GitHub Actions) |
|---|---|---|
| Linux | Developed on (Arch); fully tested. | Ubuntu, ci run 39695896272: bootstrap, startup and the whole suite: 1749 passed, 0 failed, 76 skipped. |
| Windows | Works in CI: the plugin restore from the lockfile (scripts/bootstrap.ps1), a silent headless start, and the specs that are not Linux-only. 10 of the 38 spec files of that run are skipped whole there and not demonstrated. |
platforms run 39673715062: 1077 passed, 0 failed, 87 skipped. Green in 5 of the 6 runs from 39660597368 on (39660597368, 39663956677, 39673715062, 39679589563, 39679743409); the one red run (39668277143) was a path assertion in a spec, fixed afterwards. |
| macOS | Not reliably green. Neovim starts and most specs pass, but the job did not pass in 4 of the 5 runs that finished from 39660597368 on (39679589563 was cancelled). | Green once: run 39663956677 (1431 passed, 0 failed, 73 skipped). Red in 39660597368 (2 timing specs failed), 39668277143 (a live :MacroExpand spec timed out), 39673715062 (a treesitter parser would not compile during the plugin restore, so nothing after it ran) and 39679743409 (an auto-import timing spec failed). |
Linux-only by design, so nothing of it works on Windows or macOS: the 99 agent sandbox (bubblewrap; without a backend 99 refuses to start, see The AI agent, sandboxed), the remote clipboard, the tmux-based and bash-stub test tooling (scripts/test.sh, --coverage; use scripts/test.lua or scripts/test.ps1 elsewhere) and the mold linker setup (a Linux speed-up; mold and clang are optional anyway).
Not demonstrated on Windows or macOS (specs skipped there or failing there; the full list is in docs/KNOWN_ISSUES.md): Local History (the headline safety net has no Windows evidence at all), startup options, new Rust and egui project creation, the bacon-ls attach on Windows, live rust-analyzer flows on Windows, and debugging through codelldb (it does not install through Mason on either). Treat Windows and macOS as "the config starts and the portable parts pass", not as "every feature works".
Prerequisites (scripts/bootstrap.sh checks the ones it needs itself (see the script) and prints what is missing; :checkhealth xynovim checks the rest later, with what each one is for):
- Required to bring the config up:
nvim0.12,git,curl,tar,unzip,gcc(tree-sitter parsers), thetree-sitterCLI (nvim-treesitter builds parsers with it; Mason installs it as the tree-sitter-cli package,:checkhealth xynovimwarns when it is missing),gzip,nodeandnpm(Mason'smarkdownlint-cli2,markdown-toc,yaml-language-server). - Required for the Rust workflow:
cargo,rustup(with thestabletoolchain, itsclippyandllvm-toolscomponents) andrust-analyzeron PATH (rustup component add rust-analyzer). - Installed by Mason (
scripts/bootstrap.shstep 3 runsscripts/provision.lua, which also downloads the treesitter parsers; otherwise they come with the first interactive start):bacon-ls>= 0.31.0,codelldb,taplo,wgsl-analyzer,marksman,lua-language-server,json-lsp,yaml-language-server,tree-sitter-cli,markdownlint-cli2,markdown-toc,stylua,shfmt. Thebaconpackage is a separate Mason package (also pulled in withbacon-ls); its binary is not used: bacon-ls runs cargo/clippy directly.bootstrap.shstops withoutcargo,rustuporrust-analyzer;BOOTSTRAP_SKIP_PREREQS=1goes on anyway (a machine without Rust). - Optional, each one only costs its feature:
mold+clang(linker, see below),cargo-nextest,cargo-deny,cargo-expand,lazygit,delta,bwrap(the 99 sandbox, Linux only; without it 99 refuses to run, see above).
git clone https://github.com/xynorash/xynovim ~/.config/nvim # or ln -s <checkout> ~/.config/nvim
~/.config/nvim/scripts/bootstrap.shOn Windows (PowerShell; the shape CI runs, with the checkout linked as %LOCALAPPDATA%\nvim):
winget install Neovim.Neovim Git.Git OpenJS.NodeJS.LTS Rustlang.Rustup # or scoop; a C compiler is needed too, e.g. zig.zig
rustup component add rust-analyzer clippy
git clone https://github.com/xynorash/xynovim $env:USERPROFILE\xynovim
$env:USERPROFILE\xynovim\scripts\bootstrap.ps1bootstrap.ps1 links the checkout as the config directory with a junction (no administrator rights), checks the prerequisites and prints what is missing with the winget/scoop command, then runs the same three steps as bootstrap.sh. The config lives in %LOCALAPPDATA%\nvim (or $env:XDG_CONFIG_HOME\nvim), the data in %LOCALAPPDATA%\nvim-data.
Why a script and not just nvim --headless '+Lazy! restore' +qa: on an empty plugin directory lazy.nvim installs in several rounds (a plugin's own specs, such as LazyVim's extras, are only read once it is cloned) and rewrites lazy-lock.json after every round with the plugins it knows so far. The pins of the ones found later are dropped, so those land on their newest commit (5 of 79 did in the first fresh-install tests, before 18 theme-only plugins were dropped (61 plugins are locked now), and the lockfile changed; kulala was a sixth until submodules = false fixed its pin, see step 10). The script runs Lazy! install, puts the original lockfile back, runs Lazy! restore and fails if lazy-lock.json differs afterwards; CI runs it and then git diff --exit-code lazy-lock.json. A plain Lazy! restore on an install that already holds every plugin ended with an identical lockfile in the same test.
<leader> is Space; which-key shows the menu of every prefix.
| Keys | What |
|---|---|
<leader>r… |
rust-analyzer: rv extract variable, rf function, rc constant, ri inline, rs structural search / replace, rj join lines, rm expand macro, rh / rH HIR / MIR, re explain error code, rl usages in file (read / write), ru go to super method, rw / rW workspace symbol (+ dependencies), rF features / target, rX targets, rZ cargo tools, rN HTTP routes, rR reload workspace; dependencies and macros: rA add crate, rG enable the missing feature, rI import name (ranked), rO organize imports, rx expand macro (lens), rq proc-macro status; <A-j> / <A-k> move item |
<leader>d… |
debugger: db breakpoint, dc continue, dr / dR debuggables (file / workspace), du dap-ui |
<leader>t… |
tests: tt file, tr nearest, tT all, ts summary, to output, tw watch, tD doc-tests, tc run coverage, tg toggle coverage signs |
<leader>g… |
git: gg lazygit, gv diffview, gV branch vs origin, gF file history, gH local history |
<leader>c… |
cd diagnostic float, cj split / join (treesj), cI / cO incoming / outgoing call trees, cn doc comment stub |
<leader>9… |
the 99 agent: 9v vibe, 9s search, 9o last interaction, 9x stop |
<leader>H, <C-e>, <M-1..4> |
harpoon: add file, menu, jump |
<leader>uO, uB, uu |
toggle editor transparency, colour-blind diagnostics, undotree |
dashboard b, i, w, e |
new Rust binary, library, workspace, egui project |
:RustContext (features and target for both tiers), :RustTargets, :RustTools, :RustNew, :RustNewBin, :RustNewLib, :RustNewWorkspace, :RustNewMember (new binary, library or workspace, also on the dashboard), :EguiNew (new egui project from the current eframe_template, also on the dashboard), :RustRoutes (axum / actix / rocket routes), :RustRestart, :CrateAdd, :CrateFeatures, :ImportName, :ImportForget, :ImportOrganize, :MacroAttrs, :MacroExpand, :MacroSignature, :MacroFormatFix, :ProcMacroStatus, :ActJob (run the GitHub Actions job under the cursor with act), :LocalHistory, :LocalHistoryRestore, :LocalHistoryPrune, :ColorblindDiagnostics, :checkhealth xynovim. Plugin management is stock LazyVim: :Lazy sync updates (the update checker is off), :LazyExtras toggles extras, :Mason manages language servers.
Adding a crate used to leave its macros and features half used: no completion inside #[serde(, no use for a crate that is not a dependency yet, a missing derive feature that nothing pointed at, a stale cannot find crate error after cargo add. The assessment (measured on a crate with serde, clap, thiserror, anyhow and tokio; probes in docs/assessment/) and the seven gaps it found are in docs/PLAN_MACROS_DEPS.md, together with what of the plan is done. Everything runs in nvim on top of rust-analyzer, cargo and treesitter; no extra language server.
| Keys / command | What it does |
|---|---|
<leader>rA, :CrateAdd [query] |
search crates.io (version, downloads, features with their descriptions), pick features and target (package of the file, --dev, --build; workspace.dependencies entries are used), then cargo add |
code action "Add crate x" (hint on the first segment of a path that is no dependency) |
the same add, then the import that triggered it |
<leader>rG, code action "Enable feature derive of serde" |
the fix for cannot find derive macro / attribute / macro / function X: the feature is found in the crate's own source and Cargo.toml is edited |
:CrateFeatures [crate] |
feature explorer: features on / off, what each unlocks, the default set, an "unused?" hint, toggling in Cargo.toml |
<leader>rI, :ImportName [name] |
import a name: the candidates ranked, best first, with the reasons; the choice is remembered per project (:ImportForget [name] clears it). Also lists crates that are not dependencies yet (below) |
<leader>rO, :ImportOrganize |
merge, normalize and remove unused imports, one undo step |
completion inside #[serde(, #[arg(, #[command(, #[error(, #[tokio::main(, #[derive( |
keys, enumerated values (rename_all = " gives camelCase, ...), derive names; :MacroAttrs shows what the source sees at the cursor |
<C-k> (insert), :MacroSignature |
matcher arms of the macro_rules! call under the cursor, format-string and sqlx::query! bind hints, :MacroFormatFix adds the missing format arguments |
<leader>rx, :MacroExpand |
the macro under the cursor in a read-only split with treesitter highlighting: <CR> steps into a nested call, <BS> back, gi lists the generated items, q closes; cargo expand is the fallback when installed |
<leader>rq, :ProcMacroStatus |
which proc-macro crates loaded, macros that were not expanded and where; r rebuilds, i / <CR> writes a slow crate's macros to procMacro.ignored in the workspace's rust-analyzer.toml, R refreshes |
Dependency resolution (phase 0). Saving Cargo.toml or Cargo.lock from nvim restarts bacon-ls 500 ms after the last save (it used to keep a stale cannot find crate error for a minute or more) and the statusline shows deps resolving until rust-analyzer finishes reloading (6 s after the write when it never went busy, 120 s at most), so a red error in that window is understood as transient. A cargo add in a terminal gets the same treatment on the next focus event.
Auto-import. vim.g.xynovim_auto_import (set before the first Rust buffer): "ask" (default) asks (a yes/no dialog that stays until answered; never in a headless nvim) when exactly one Import X exists for an unresolved name after InsertLeave, a save or a paste; "single" applies it as one undo step with a one-line note; "off" never. Several candidates are never guessed at: :ImportName ranks them (used in this file, then in the module, then in the crate, the derive or attribute context, the return type, dependency order, the prelude) and remembers your choice in stdpath("state")/xynovim/imports.json.
Import from crates that are not dependencies yet. When rust-analyzer offers no import for a name, :ImportName also lists crates.io candidates, marked "(adds the crate)". A lowercase name gets the crate of exactly that name. Other names (Uuid, Utc) get crates whose name equals or contains it, top 3 by downloads, labelled "(guess)", because crates.io is searched by crate name, not by exported items. Choosing one runs cargo add (newest release, default features, into the file's package; dev-dependencies for tests, benches and examples), then waits up to 60 s for rust-analyzer to load the crate and applies the import it offers from that crate. A crate that is already a dependency is never suggested, nothing is added or imported automatically, and a chosen guess is not remembered. Offline there are no candidates. The same add path serves the "Add crate" action. In Lua: require("xyno_imports.engine").register_source(fn, {async = true}), with fn(name, ctx, cb). Accepting a derive from #[derive( completion imports it through the same engine: the remembered or only candidate, otherwise the one from the derive's own crate.
Attribute schemas. The completion is data driven: one JSON file per crate in data/macro_attrs/ of the xyno-macros.nvim plugin (serde, clap, thiserror, tokio, strum, derive_more, sqlx, utoipa, schemars, bon), used only when the package depends on the crate within the file's version range and the attribute is in scope (a derive that provides it is on the item). A project adds or overrides schemas with .xynovim/macro_attrs/<anything>.json in the workspace root (same format, see data/macro_attrs/README.md of the plugin, whose own project directory .xyno-macros/attrs xynovim replaces by this one; an attribute of the same name replaces the shipped one, a broken file is skipped and listed by :MacroAttrs). A dependency attribute without a schema offers the keys the workspace and the crate's own sources already write inside it, most frequent first.
Limits. The unknown-crate hint is deliberately conservative (a miss is better than a false hint): a file with a glob import from a dependency (use predicates::prelude::*, which brings modules such as predicate) gets no hint, nor does one over 20000 lines (require('xyno_crates.crate_add').config.max_lines; the import engine's syntax scan has the same cap). #[serde( keys are offered while serde's derive feature is off, because the schema is chosen by dependency and derive, not by features. A "(guess)" can miss: if the crate has no item of that name, the import step gives up after 60 s with a message, the crate stays added. Plain Import X and Import X as Y count as one candidate, whichever rust-analyzer lists first. Network access is optional: crates.io answers are cached and everything degrades to the local registry and a message. Tested in the headless suite with real cargo and rust-analyzer (no field-test round yet); the pickers and key presses were not driven in a real terminal (see docs/DETAILS.md).
In action. Three recordings on the demo crate (regenerate with docs/media/record.sh 11-add-crate 12-attributes 13-imports).
Adding a crate that is no dependency yet. uuid::Uuid::new_v4() without uuid in Cargo.toml: the code action offers "Add crate uuid", the picker lists the crates.io results and the features of the chosen release (here v4 toggled on), the target package is asked, then cargo add runs and the error goes away without waiting. Typing Uuid afterwards asks to import it.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Video: 11-add-crate.mp4
Attributes and derives. serde without its derive feature gives cannot find derive macro Serialize; <leader>rG enables the feature in Cargo.toml. #[derive(Des completes derive names with docs and adds the use on accept; #[serde( and #[arg( offer keys with their docs, and rename_all = " offers its values.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Video: 12-attributes.mp4
Ambiguous imports and macros. Error exists in std, std::io, std::fmt and clap: <leader>rI ranks them with the reasons; the pick is remembered, so the next file offers it as the single import. <leader>rx shows the expansion of a derive, gi lists what it generated, and <C-k> shows the matcher arms of a macro_rules! call (the arm that fits the arguments typed so far is highlighted).
![]() |
![]() |
![]() |
![]() |
![]() |
Video: 13-imports.mp4
Three entries on the dashboard and four commands (the older :RustNew picker for a crate in the current directory stays):
| Dashboard | Command | What it creates |
|---|---|---|
New Rust Binary (b) |
:RustNewBin [name] |
cargo new --bin --vcs git <name>: Cargo.toml (the edition your cargo uses today), src/main.rs, .gitignore, a git repository |
New Rust Library (i) |
:RustNewLib [name] |
cargo new --lib --vcs git <name>: the same with src/lib.rs; a name with hyphens shows the crate identifier it will have (my-lib becomes my_lib in Rust code) |
New Rust Workspace (w) |
:RustNewWorkspace [name] |
a workspace with a first member (below) |
:RustNewMember [name] |
adds a binary or library member to the workspace of the current buffer |
b, i and w were free (taken: f n p g r c s x l q e; i stands for the library, l is Lazy). The entries sit before New egui Project.
- Prompts (a binary / library): project name, parent directory (
vim.g.xynovim_projects_dir, else~/projectsif it exists, else the current directory; a missing one is created only after a yes). An existing target is refused and cancelling any prompt ends the command silently. The same cargo name rules as the egui flow (not empty, at most 64 characters, starts with a letter or_, ASCII letters, digits,_and-, no Rust keyword, nothing cargo reserves such astest,core,std,alloc,proc_macro); hyphens stay (cargo accepts them). Then the command doescdinto the project, openssrc/main.rs(src/lib.rs) and says what to do next (cargo run). - Workspace: cargo has no one-step command for it, so it is built here. Prompts: workspace name, parent directory, the kind of the first member (
binary/library, asked first) and the member's name (prefilled<workspace>-appor<workspace>-core). Layout:
my-ws/
Cargo.toml [workspace] resolver = "3", members = ["crates/*"]
[workspace.package] version = "0.1.0", edition = "2024"
[workspace.dependencies] (empty)
.gitignore /target
crates/
my-ws-app/
Cargo.toml version.workspace = true, edition.workspace = true
src/main.rs (src/lib.rs for a library)
The edition is whatever the installed cargo writes into a new project (read once per session from a throwaway cargo new, never hard-coded), the resolver is 3 for edition 2024 and newer, else 2. The crates/* glob picks up later members without editing the manifest. git init -b main runs without a commit (skipped with a note when git is missing). Afterwards the command does cd to the workspace root, opens the member's source file and notifies the layout and the next steps (cargo run -p <member>, <leader>rX lists the cargo targets).
:RustNewMember: finds the workspace throughxyno_rust.rust_context.root(the current buffer, else the cwd), asks the kind and the name and createscrates/<name>(or the directory of the manifest'sdir/*glob) with the same inheritance rewrite. It refuses outside a workspace that has amembers = [...]list, and a name that collides with an existing member (my-coreandmy_corecount as the same). An explicit list gets the new path added with its layout kept; keys the workspace's[workspace.package]lacks stay in the member.- Failures: everything is async with a progress notification. When cargo fails its first stderr line is shown and whatever the run created (the project, the whole workspace directory, parent directories it made) is removed again; a restored manifest for
:RustNewMember. cargo missing is one clear message. - Options:
vim.g.xynovim_projects_dir(read at startup: set it inoptions.lua). - Dashboard sizes: the list has 14 entries now. The tall layout (blank line between the entries) needs
2n + 11rows (39), consecutive lines with the header needn + 12(26), and below that the header is hidden, so every entry and its key stay visible from 80x18 up (80x24, 100x30 and 190x50 are tested in a real terminal).
In action. Recorded with docs/media/record.sh 15-rust-new: the dashboard with all entries, then the workspace flow (key w, the workspace name inventory, the parent directory, the kind library, the member name prefilled as inventory-core), the creation notification with the layout, the tree (crates/inventory-core, Cargo.toml, .gitignore), the root Cargo.toml ([workspace], resolver, [workspace.package]) and the member Cargo.toml with version.workspace and edition.workspace; last a binary (:RustNewBin, name hello-bin) ending in src/main.rs. The parent directory is a temp directory (/tmp/xyno-rust-demo, removed by record.sh), never ~/projects.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Video: 15-rust-new.mp4
The dashboard has a New egui Project entry (e) and there is the command :EguiNew [name]. Both follow the steps of emilk/eframe_template against the current template: the ZIP of its main branch is downloaded every time (never a copy kept in this config) and personalised, so a new egui or eframe release of the template is picked up without touching the config.
- Prompts: project name, author name (prefilled from
git config user.name), author e-mail (user.email), parent directory (vim.g.xynovim_projects_dir, else~/projectsif it exists, else the current directory; a missing one is created only after a yes). Cancelling any prompt aborts without leaving anything behind. The name is both the package name and the crate identifier the template uses everywhere, so-becomes_(crate name: my_appis shown) and cargo's rules apply (a letter or_first, at most 64 characters, no Rust keyword, nothing cargo reserves such astestorcore, not a dependency name such aseguiorlog); a bad name is asked again with the reason, an existing target directory is refused. - What it personalises (the edits of the template's README and
fill_template.sh, applied to every text file, not to a fixed list):eframe_templatebecomes the crate name (Cargo.tomlpackage.name,src/main.rs,assets/sw.js, theCargo.lockentry, ...) except in links to the template;eframe templatebecomes a title derived from the name (<title>, the window title, the heading); theauthorsline is rewritten (Name <e-mail>,Name, or removed);README.mdis replaced by a short generated one (run and web commands taken from the template's README),fill_template.sh/.ps1are deleted, the licenses stay, andgit init -b mainis run (no commit). Then the project becomes the working directory,src/main.rsopens and the next steps are shown:cargo run --release,<leader>rXlists the cargo targets,trunk servefor the web. - Offline: the ZIP is cached in
stdpath("cache")/xynovim/with its ETag; the next run sendsIf-None-Matchand downloads again only when the template changed. Without network the cached copy is used with a warning, and without a cache the error says so. - Toolchain: the template's
rust-toolchainpin (channel1.95.0today) is not copied, because it makes rustup download and use exactly that toolchain (withoutrust-analyzer); your own Rust is used. Only the template'srust-versioninCargo.toml(the minimum supported Rust,1.95today) is kept. When the project is created,rustc --version(run in the new project, so a project-local override counts) is compared with it, and an older Rust gives one warning:this project needs Rust >= 1.95 (you have 1.9x): run rustup update stable. Nothing is installed or downloaded; a missing or unreadablerustcis only mentioned. - Options:
vim.g.xynovim_projects_dir,vim.g.xynovim_egui_template({ repo = "emilk/eframe_template", ref = "main", base_url = "https://github.com" }; another repo or ref, or a mirror),vim.g.xynovim_egui_build_after_new = true(startscargo buildin a terminal afterwards). All three are read at startup: set them inoptions.lua.
In action. Recorded with docs/media/record.sh 14-egui-new: the dashboard entry, the project name weather-panel (shown as crate name: weather_panel), the author name and e-mail prompts prefilled from git config (a demo identity in the recording), the parent directory (a temp directory), then the created project: Cargo.toml with the name and authors, the <title> in index.html, and src/main.rs using weather_panel::TemplateApp. To keep the recording fast and repeatable, record.sh downloads the template ZIP once from the real repository and the tape points base_url at that file (file://); the feature itself downloads the current template from GitHub.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Video: 14-egui-new.mp4
The config ships with an automated suite: 32 spec files, 906 checks passing, 35 skipped with a stated reason, 0 failing, in about six minutes. The passing and skipped counts depend on the machine (the figures are from my Linux machine (Arch), which has debug = true in ~/.cargo/config.toml, a ~/.cargo/bin and no pwsh; the Ubuntu CI run 39695896272 shows 1749 passed, 76 skipped, 0 failed). Each file runs in its own headless Neovim with the full config and a hard timeout, so a hanging test cannot stall the rest. After cloning and bootstrapping:
scripts/test.sh # everything (or: make test)
scripts/test.sh routes smoke # only spec files whose path contains one of the words
scripts/test.sh --list # the spec files, nothing run
scripts/test.sh --coverage # also a per-file line-coverage reportVideo: 10-test-suite.mp4
- Nothing of yours is touched: the specs run against a temp config / state / cache; only the installed plugins are reused. Missing tools (rust-analyzer, tmux, bubblewrap, lazygit, ...) make their specs skip with a message, never fail.
- One spec per feature area: startup and options, the Island theme, every
<leader>rcommand,:RustContext,:RustTargets, Local History, auto-save guards, the agent sandbox and the 99 guard, the two diagnostic tiers' configuration, server supervision, debugger and test integration, git and lint tooling, snippets and treesitter injections, the UI helpers, health / bootstrap / CI, and the claims this documentation makes, and the website. tests/COVERAGE.md maps every feature to the spec that tests it and lists what has no test and why. - Line coverage of the repo's own Lua was about 86 % before the macros and dependencies modules (
--coverage, not re-run since; the denominator is estimated, treat it as a guide). - CI: the ci workflow runs
scripts/bootstrap.sh(clone plugins on their locked commits, download parsers and Mason tools), a startup smoke test, thenscripts/test.sh, on a clean Ubuntu machine for every push. - How the harness works, how to write a spec and what the helpers offer: docs/TESTING.md.
demo/netwatch is a small, realistic Rust crate (a trait, a CLI, unit and integration tests, features) used as the target of every screenshot and video above and handy for trying the config. The recordings are plain vhs tapes in docs/media/tapes/; docs/media/record.sh copies the crate to a temp dir, warms the build and replays them in a real terminal, so they can be regenerated after any change:
docs/media/record.sh # everything
docs/media/record.sh 05-tests # one tape(needs vhs, ttyd, chromium, ffmpeg and the JetBrainsMono Nerd Font).
- docs/DETAILS.md: the engineering notes behind each setting, what was measured and what was not verified.
- docs/KNOWN_ISSUES.md: limitations that were found and deliberately left alone.
- docs/TESTING.md and tests/COVERAGE.md: the test suite.
- CHANGELOG.md: every notable change.
The official site (https://xynorash.github.io/xynovim/) is one static page in site/ with no external requests (system fonts, no CDN, no analytics) that presents the config with the recordings above. python3 scripts/build-site.py assembles it into _site/ (the site plus only the media files the page references from docs/media/, with the numbers filled in from this README, so they cannot drift). .github/workflows/pages.yml builds it on every push to main that touches the site, the media, this README or the build script, and publishes it as a single orphan commit to the branch gh-pages, which holds nothing but the built site; GitHub Pages serves that branch (Settings > Pages > Deploy from a branch > gh-pages / root). Preview locally under the same /xynovim/ prefix: see site/README.md.
Thank you to JetBrains for their amazing products. I am a full-in JetBrains user and consider JetBrains' products, the whole suite of IDEs and tools, the best I have ever used. RustRover, one IDE of that suite, is the specific source of inspiration for xynovim: I was so satisfied with those tools that I wanted to build my own dream configuration from that inspiration, and I still use JetBrains tools. The Island Dark colour scheme is derived from JetBrains' Islands Dark scheme. xynovim is my own independent personal project, not affiliated with or endorsed by JetBrains, not competing with them and not an alternative to their products.
lua/config/ options, keymaps, autocmds, statusline/UX helpers, rust_assist, lazy
lua/xynovim/ :checkhealth xynovim
snippets/ rust.json (registered with blink)
lua/plugins/ one spec per concern (bacon-ls, rustaceanvim, ninety-nine, auto-save, dap, gitsigns, telescope, island-theme, xyno-history, xyno-sandbox, xyno-clipboard, xyno-guard, xyno-rust, xyno-new, xyno-imports, xyno-crates, xyno-macros, …)
colors/ island-dark colourscheme entry point
lua/island/ Island Dark: init.lua (mapping), palette.lua (generated)
lua/lualine/ island-dark lualine theme
tools/ icls2nvim.py (converts a JetBrains .icls you supply into the palette)
scripts/ bootstrap.sh and bootstrap.ps1 (fresh-install two-step restore, prerequisite check), test.sh (spec runner), test.lua and test.ps1 (the same runner without bash), coverage.lua (coverage report), build-site.py (builds the website), make-share-images.sh (the share cards)
tests/ *_spec.lua, harness.lua, helpers.lua, runner.lua, coverage_hook.lua, fixtures/ (see Tests)
Makefile make test / make coverage
plugin/after/ transparency, unsafe highlight
after/ftplugin/ Rust buffer settings (plain `'`, comment spelling)
queries/ base rust injections (copy of nvim-treesitter's minus the sqlx macros)
after/queries/ treesitter query extensions (sqlx SQL injection)
.github/ CI: bootstrap, lockfile check, startup smoke test, spec suite; the platforms workflow runs bootstrap, startup and the suite on Windows and macOS; the pages workflow publishes the website to gh-pages
site/ the website's source (index.html, guides/, assets/, 404.html, ...), see site/README.md
LazyVim extras enabled (lazyvim.json): ai.claudecode, editor.neo-tree, editor.overseer, lang.rust, lang.toml, lang.yaml, lang.json, lang.markdown, lang.git, dap.core, test.core, ui.treesitter-context, editor.navic, coding.mini-surround, coding.neogen, editor.inc-rename, util.rest.

![a clippy lint shown inline: "length comparison to zero [bacon-ls]"](/xynorash/xynovim/raw/main/docs/media/02-diagnostics-clippy.png)


































































