Skip to content
xynorashPublic

About

xynovim: a personal Neovim 0.12 configuration for Rust on LazyVim: instant rust-analyzer and clippy diagnostics, nvim-dap debugger, test UI with coverage, git safety net. Inspired by JetBrains tools. Developed on Linux; Windows passes the portable parts in CI, macOS is not reliably green.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

xynovim

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.

ci neovim 0.12 license tests

Website: https://xynorash.github.io/xynovim/ (with guides to the workflows below)

xynovim editing the netwatch demo crate: Island Dark theme, inlay hints, sticky scope from treesitter-context, statusline with git branch, server state and clippy state

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.

Quick start

git clone https://github.com/xynorash/xynovim ~/.config/nvim
~/.config/nvim/scripts/bootstrap.sh

Then 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-legacy branch. 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).

What you get

  • 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 sqlx strings, 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).

Guides

Short guides on the website, one per workflow, each with the keys, the reasoning and the limits:

Plugins

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.

Tour

Two-tier diagnostics

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.

a clippy lint shown inline: "length comparison to zero [bacon-ls]"

rust-analyzer, while typing the full clippy message with its suggestion
a type error shown the moment it is typed diagnostic float with the clippy help text

diagnostics recording

Video: 02-diagnostics.mp4

Treesitter, visibly

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
:Inspect floating window listing treesitter captures, LSP semantic tokens and extmarks :InspectTree split with the syntax tree following the cursor

vaf selects a whole function through the treesitter text objects, and the sticky scope in action:

text object sticky scope
vaf selecting parse_ports with its doc comment in visual mode treesitter-context pinning the enclosing for, match and arm headers

treesitter recording

Video: 04-treesitter.mp4

Refactoring and rust-analyzer commands (<leader>r)

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.

which-key popup listing the <leader>r group

visual selection and a "No code actions available" notification

Video: 03-refactor.mp4

Tests and coverage

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
neotest summary: 5 tests, 3 passed, a tick per test coverage bars next to the covered lines of parse_ports

Video: 05-tests.mp4

Debugging

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.

dap-ui stopped at a breakpoint: locals, stack, console and inline values

Video: 06-debug.mp4

One feature set for both tiers: :RustContext

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)
compile_error reported by both tiers, the statusline shows a clippy error feature picker listing all features, default only, no-default, csr, json, ssr
choose the target resolved
target picker: host default, wasm32, x86_64 linux the csr side greyed out as inactive, statusline back to clippy ok

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

Git, and a safety net

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
lazygit inside nvim with delta rendering the diff local history picker for src/lib.rs

diffview file panel and side-by-side diff

Video: 08-git.mp4

Island Dark

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
blink.cmp menu next to a documentation window snacks file picker

Video: 09-theme.mp4

The AI agent, sandboxed

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.

Install

Platforms

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: nvim 0.12, git, curl, tar, unzip, gcc (tree-sitter parsers), the tree-sitter CLI (nvim-treesitter builds parsers with it; Mason installs it as the tree-sitter-cli package, :checkhealth xynovim warns when it is missing), gzip, node and npm (Mason's markdownlint-cli2, markdown-toc, yaml-language-server).
  • Required for the Rust workflow: cargo, rustup (with the stable toolchain, its clippy and llvm-tools components) and rust-analyzer on PATH (rustup component add rust-analyzer).
  • Installed by Mason (scripts/bootstrap.sh step 3 runs scripts/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. The bacon package is a separate Mason package (also pulled in with bacon-ls); its binary is not used: bacon-ls runs cargo/clippy directly. bootstrap.sh stops without cargo, rustup or rust-analyzer; BOOTSTRAP_SKIP_PREREQS=1 goes 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.sh

On 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.ps1

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

Keys you will use

<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

Commands

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

Macros and dependencies

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.

the unknown crate uuid in a path, with the error code actions: Add crate uuid crates.io search results
feature picker of uuid with v4 toggled Cargo.toml with the new dependency Import uuid::Uuid? dialog

add crate recording

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.

derive macro not found derive name completion with docs use serde::{Deserialize, Serialize} added
serde keys with docs rename_all values clap arg keys with docs

attributes recording

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

ranked Error candidates the remembered choice offered in another file signature help on a macro_rules call
macro expansion of derive(Parser) generated items of the expansion

imports and macros recording

Video: 13-imports.mp4

New Rust projects

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 ~/projects if 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 as test, core, std, alloc, proc_macro); hyphens stay (cargo accepts them). Then the command does cd into the project, opens src/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>-app or <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 through xyno_rust.rust_context.root (the current buffer, else the cwd), asks the kind and the name and creates crates/<name> (or the directory of the manifest's dir/* glob) with the same inheritance rewrite. It refuses outside a workspace that has a members = [...] list, and a name that collides with an existing member (my-core and my_core count 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 in options.lua).
  • Dashboard sizes: the list has 14 entries now. The tall layout (blank line between the entries) needs 2n + 11 rows (39), consecutive lines with the header need n + 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.

the dashboard with the three Rust entries and New egui Project workspace name inventory parent directory
kind of the first member: binary or library member name prefilled inventory-core the creation notification with the layout
the tree with crates/inventory-core, Cargo.toml and .gitignore root Cargo.toml: workspace, resolver, workspace.package member Cargo.toml with the inherited fields
binary name hello-bin parent directory for the binary the creation notification of the binary
the binary opened in src/main.rs

New Rust projects recording

Video: 15-rust-new.mp4

New egui project

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 ~/projects if 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_app is shown) and cargo's rules apply (a letter or _ first, at most 64 characters, no Rust keyword, nothing cargo reserves such as test or core, not a dependency name such as egui or log); 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_template becomes the crate name (Cargo.toml package.name, src/main.rs, assets/sw.js, the Cargo.lock entry, ...) except in links to the template; eframe template becomes a title derived from the name (<title>, the window title, the heading); the authors line is rewritten (Name <e-mail>, Name, or removed); README.md is replaced by a short generated one (run and web commands taken from the template's README), fill_template.sh / .ps1 are deleted, the licenses stay, and git init -b main is run (no commit). Then the project becomes the working directory, src/main.rs opens and the next steps are shown: cargo run --release, <leader>rX lists the cargo targets, trunk serve for the web.
  • Offline: the ZIP is cached in stdpath("cache")/xynovim/ with its ETag; the next run sends If-None-Match and 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-toolchain pin (channel 1.95.0 today) is not copied, because it makes rustup download and use exactly that toolchain (without rust-analyzer); your own Rust is used. Only the template's rust-version in Cargo.toml (the minimum supported Rust, 1.95 today) 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 unreadable rustc is 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 (starts cargo build in a terminal afterwards). All three are read at startup: set them in options.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.

the dashboard with the New egui Project entry project name weather-panel author name prefilled, crate name: weather_panel
author e-mail prefilled parent directory the creation notification and src/main.rs
Cargo.toml with name and authors index.html with the title src/main.rs using weather_panel::TemplateApp

New egui Project recording

Video: 14-egui-new.mp4

Tests: run them yourself

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 report

the test runner finishing four spec files: 129 passed, 0 failed

Video: 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>r command, :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, then scripts/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.

The demo project and the recordings

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

More documentation

Website

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.

Acknowledgements

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.

Layout

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.

About

xynovim: a personal Neovim 0.12 configuration for Rust on LazyVim: instant rust-analyzer and clippy diagnostics, nvim-dap debugger, test UI with coverage, git safety net. Inspired by JetBrains tools. Developed on Linux; Windows passes the portable parts in CI, macOS is not reliably green.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages