xynovim guide

Instant Rust diagnostics in Neovim with rust-analyzer and clippy

A Rust file in Neovim is only as responsive as its diagnostics. This guide describes the two-tier setup in xynovim, a personal Neovim configuration: rust-analyzer answers while you type, and clippy follows about half a second later, on every change and before you save. It explains why there are two tiers, what each one reports, how to read the result, and what is deliberately not covered. The timings are my own measurements on a warm session, not a promise for your machine.

a clippy lint shown inline in Neovim: length comparison to zero, with the source bacon-ls
Recorded from the real configuration on the demo crate.

Why two tiers

rust-analyzer reports some errors from memory, without running cargo. That tier is fast: type errors such as a mismatched type (E0308) appeared in about 0.01 to 0.15 seconds in my measurements. It is also narrow. Unresolved names, methods, types and imports (E0425, E0599, E0432) are only reported by rust-analyzer with diagnostics.experimental.enable, and I measured that setting on a copy of a real 196-file axum and sqlx workspace: off gave no rust-analyzer diagnostics, on gave 82 false errors. So it stays off, and those errors come from the second tier.

The second tier runs clippy. It is slower, about 0.56 seconds median when warm, but it sees everything cargo sees, plus lints. Together the two give an instant answer for type errors and a deeper answer shortly after.

a type error shown in Neovim the moment it is typed, reported by rust-analyzer
The first tier: a type error from rust-analyzer, shown while typing.

How the clippy tier works

The depth tier is bacon-ls with its native cargo backend (version 0.31.0 or newer, installed by Mason). It mirrors your workspace into a hardlinked shadow under target/bacon-ls-live/ and re-runs clippy --workspace --all-features --no-deps --keep-going after a 500 ms debounce on every buffer change. Because it works on the shadow, the result reflects what is in the buffer, not only what is on disk.

Two settings matter and both look backwards. checkOnSave is off: with an auto-save of about one second it became a second cargo pipeline racing the live one, and the runs cancelled each other. updateOnInsert is on: without it bacon-ls advertises no document sync and its diagnostics never refresh. The reasoning and the measurements are in docs/DETAILS.md.

Reading the diagnostics

  • The virtual text shows the first line of the message plus the source in brackets, for example [bacon-ls]. Clippy sends about fourteen lines per lint, so the full text is one key away: <leader>cd opens the diagnostic float.
  • A problem that both tiers report on the same line (same rustc error code, or the same first line) is shown once, preferring rust-analyzer's text.
  • The statusline has a rust-analyzer part (RA ready, RA loading, RA stopped) and a clippy part: clippy running, clippy stale while you have typed since the last run, clippy ok when there are no errors, clippy 2E (the error count) when there are some, and clippy failed when cargo cannot run at all, for instance with a broken manifest.
the diagnostic float with the full clippy message and its suggestion
The full clippy message and its suggestion, opened with the diagnostic float key.

One feature set for both tiers

A crate with mutually exclusive features is where two checkers disagree. :RustContext (also <leader>rF) lets you choose the Cargo features and the build target per project, and applies the choice to rust-analyzer and bacon-ls together. The choice is stored per workspace and survives restarts. In the demo crate, ssr and csr are mutually exclusive: with all features on, the conflict is reported (once, as rust-analyzer's message, with clippy's identical report merged into it), and choosing only ssr makes it go away.

a compile_error from rust-analyzer shown inline, with clippy 1E in the statusline
The feature conflict is shown once, from rust-analyzer, with clippy 1E in the statusline, until a feature set is chosen with :RustContext.

Keeping the servers alive

A language server that dies stays dead in a stock setup, and the editor looks fine while showing stale errors. Here the statusline shows RA stopped or clippy stopped, and the server is restarted automatically after half a second, at most three times a minute, then it gives up with an error message. :RustRestart restarts both by hand. Saving Cargo.toml or Cargo.lock from Neovim restarts bacon-ls 500 ms after the last save, so a stale cannot find crate error does not linger, and the statusline shows deps resolving until rust-analyzer has reloaded. When an open .rs, .toml or .lock file is reloaded from disk after a change made outside Neovim (a git checkout, a script), or the git HEAD of the workspace moves, bacon-ls is restarted on the next focus event so it does not keep checking old content.

In a Cargo workspace, bacon-ls is rooted at the workspace, not at the member crate you happen to open, so there is one client per workspace. It does not start for a loose .rs file without a crate, nor for sources under ~/.cargo/registry or ~/.rustup.

What it does not cover

  • Clippy runs without --all-targets, so test modules, tests/, examples and benches get rust-analyzer's type errors but no live unresolved-name errors. cargo clippy --all-targets or <leader>tr reports them.
  • The two tiers can briefly show the same type error twice.
  • rust-project.json projects were not tested.

Try it

Install the configuration, open demo/netwatch or any Cargo project, type a wrong type into a function and watch the statusline. :checkhealth xynovim lists what is missing.

Install
git clone https://github.com/xynorash/xynovim ~/.config/nvim   # or ln -s <checkout> ~/.config/nvim
~/.config/nvim/scripts/bootstrap.sh

xynovim is a personal project: the home page has the tour, the install details and the platform status.

In the repository