Setting up Cursor for a Rust codebase that respects rust-analyzer
Published 2026-04-22 by Owner
The first time I opened a Rust project in Cursor, two things broke immediately. Completions started fighting between rust-analyzer’s suggestions and Cursor’s Tab autocomplete, and cargo fmt on save started running twice — once from rust-analyzer’s format-on-save, once from Cursor’s. The error squigglies came from somewhere unclear because both servers were typechecking.
Fixing this took an afternoon of reading settings docs. Here’s the version I landed on, in case you want to skip that afternoon.
The conflict
Cursor inherits VS Code’s settings model but adds three layers on top: cursor.cpp.disabledLanguages, cursor.composer, and the auto-applied AI completion engine. By default, Cursor’s Tab completions are enabled for every language including Rust. rust-analyzer also offers completions through the LSP. Both providers feed VS Code’s completion list, which means Tab can trigger either one depending on which arrived first.
For Rust specifically, this matters more than for TypeScript or Python. rust-analyzer knows about lifetimes, trait bounds, and async cancellation. The model’s completions are guesses based on token patterns. When a guess overrides rust-analyzer’s Result<T, E> completion with a plausible-looking but wrong return type, you’ve added a debugging session for yourself.
The settings
Open .vscode/settings.json (or workspace settings in Cursor) and add:
{
"rust-analyzer.completion.autoimport.enable": true,
"rust-analyzer.completion.callable.snippets": "fill_arguments",
"rust-analyzer.inlayHints.enable": true,
"rust-analyzer.checkOnSave.command": "clippy",
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer",
"editor.formatOnSave": true,
"editor.semanticHighlighting.enabled": true
},
"cursor.cpp.disabledLanguages": [],
"editor.inlineSuggest.enabled": true,
"editor.suggest.preview": true
}
Two things matter here. First, the [rust] block scopes the formatter to rust files only — Cursor’s Prettier-based default doesn’t know about rustfmt.toml. Second, leaving cursor.cpp.disabledLanguages empty (instead of adding "rust" like several blog posts suggest) keeps Cursor’s AI active. You want it active. You just want rust-analyzer to win on conflicts.
The .cursorrules file
Create .cursorrules at the repo root:
This is a Rust project using cargo. Follow these conventions:
- Prefer `?` operator over unwrap() in non-test code
- Use `thiserror` for library error types, `anyhow` for application errors
- Async runtime is tokio; do not introduce async-std
- Avoid `unsafe` unless I explicitly ask for it; if you need it, explain why
- Format with rustfmt defaults; do not change rustfmt.toml without asking
- Use `tracing` crate for logging, not `log` or `println!`
- Tests go in `#[cfg(test)] mod tests` blocks at the bottom of the same file unless they're integration tests
- For new public APIs, write doc comments with `///` and include at least one `# Examples` block
The error-handling line matters most. Without it, the model defaults to whichever pattern was popular in its training data, which leans toward unwrap() in examples. That’s fine for tutorials and wrong for production.
Cargo.toml awareness
Cursor’s codebase indexing reads Cargo.toml but doesn’t reason about feature flags by default. If your crate has feature gates (#[cfg(feature = "tokio")]), the model often suggests code that’s only valid under one feature configuration without acknowledging the gate.
Workaround: when you ask for changes that touch feature-gated code, include the active features in your prompt — “I’m working with the tokio and tls features enabled” — so the suggestions match your build.
clippy as a feedback loop
The checkOnSave.command: "clippy" line is doing more work than it looks. Cursor’s chat panel can read diagnostics from the active editor. When clippy flags an issue, you can ask Cursor to fix it and the model will see the lint name and message in context. This works much better than copy-pasting the warning.
For complex lints (needless_lifetimes, let_unit_value), the fix is usually correct. For lints that require understanding ownership semantics across multiple functions, review carefully — the model still confuses borrow lifetimes in non-obvious ways.
What I’d skip
A few configurations I tried and reverted:
Disabling Cursor for Rust entirely. I tested this for a week. The autocomplete on simple stuff (writing tests, scaffolding match arms) is genuinely useful enough that disabling it made me slower than not using Cursor at all.
Custom keybindings to switch completion providers. Tried it. The cognitive cost of remembering which provider was active was higher than just letting both run and rejecting wrong suggestions.
Cargo workspace-aware prompts. Cursor’s context window will load the active crate, but multi-crate workspaces still trip it up. For workspace-scoped refactors, I switch to running aider or asking via chat with explicit file paths.
What this gets you
After about two days of working with this setup on a real Rust project (not a toy):
- rust-analyzer wins conflicts on type-aware completions
- Cursor’s AI handles scaffolding, tests, and docs without overriding the LSP
cargo fmtandclippyrun once on save, not twice- The model’s suggestions follow your error-handling and async conventions
It’s not magic. You’ll still review every suggestion, and rust-analyzer will still be the source of truth for whether something compiles. But the two stop fighting, and that’s most of what I wanted.