wandres.dev
NINJA · IDE completo

Rust: rustaceanvim al máximo

El mejor setup de Rust en Neovim 0.12: rustaceanvim (nunca rust_analyzer suelto), runnables, testables, debuggables, expand macro, crates.nvim para Cargo.toml y depuración integrada.

⏱ 15 min

Rust en Neovim no se configura con el LSP genérico: se usa rustaceanvim, que envuelve rust-analyzer y le añade todo lo que hace especial a Rust — ejecutar tests inline, expandir macros, explicar un error del compilador, depurar con un comando.

🎯 Al terminar esta lección sabrás
  • Por qué rustaceanvim y no una ficha lsp/rust_analyzer.lua tuya.
  • Runnables, testables, debuggables y expand macro.
  • Ajustar rust-analyzer con opciones que existen de verdad.
  • crates.nvim para las dependencias de Cargo.toml.

rustaceanvim, no rust_analyzer

En la lección 2.5 viste el camino normal para cualquier servidor: una ficha en lsp/, un vim.lsp.enable y listo. Rust es la excepción deliberada del track, porque rust-analyzer ofrece un montón de peticiones fuera del protocolo estándar (runnables, expansión de macros, explicación de errores, árbol sintáctico) que un cliente LSP genérico no sabe pedir. rustaceanvim las implementa todas.

🛑
No declares rust_analyzer por tu cuenta

Con rustaceanvim instalado, no escribas una ficha lsp/rust_analyzer.lua ni llames a vim.lsp.enable("rust_analyzer"), y tampoco actives esa entrada del catálogo de nvim-lspconfig: el propio plugin configura y arranca el cliente, y montar el servidor a mano en paralelo provoca conflictos. Sus autores también desaconsejan expresamente instalar rust-analyzer con Mason: si la versión del binario no cuadra con la de tu toolchain aparecen fallos sutiles. Instálalo como componente de rustup y olvídate. (Y sí, el viejo rust-tools.nvim está archivado; rustaceanvim es su sucesor.)

rustaceanvim pide Neovim 0.12 o superior, no lleva setup() y hace su propia carga perezosa por tipo de archivo, así que no debes envolverlo en la del gestor de plugins. Se configura poniendo una tabla en vim.g.rustaceanvim antes de que el plugin se inicialice.

mrcjkb/rustaceanvim
return {
  "mrcjkb/rustaceanvim",
  version = "^9",
  lazy = false,
  init = function()
    require("config.rust")  -- define vim.g.rustaceanvim antes de arrancar
  end,
}
require("config.rust")  -- primero la config global, luego el plugin

vim.pack.add({
  {
    src = "https://github.com/mrcjkb/rustaceanvim",
    version = vim.version.range("^9"),
  },
})

El plugin implementa lazy-loading de verdad: con lazy.nvim va lazy = false y no le pongas ft ni event. Fija un rango de versión para no comerte un cambio incompatible.

Las opciones del servidor viven en su propio módulo:

vim.g.rustaceanvim = {
  server = {
    default_settings = {
      ["rust-analyzer"] = {
        cargo = { features = "all" },   -- equivale a --all-features
        check = { command = "clippy" }, -- clippy en lugar de cargo check
        procMacro = { enable = true },
      },
    },
  },
}
⚠️
cargo.allFeatures ya no existe

Si copias una configuración antigua verás cargo = { allFeatures = true }. Esa opción desapareció del esquema de rust-analyzer: hoy se escribe cargo = { features = "all" }, y su gemela para desactivar las de serie es cargo = { noDefaultFeatures = true }. Tampoco hace falta poner checkOnSave = true: ya viene activado por defecto. Lo que sí cambia el comportamiento es check.command, que es lo que convierte el chequeo al guardar en un pase de clippy.

Las teclas, en el ftplugin

rustaceanvim expone sus funciones por el comando :RustLsp. Como solo tienen sentido en archivos Rust, el sitio correcto para los atajos es un ftplugin, que Neovim ejecuta al abrir cada buffer del tipo:

local buf = vim.api.nvim_get_current_buf()
local function map(k, fn, desc)
  vim.keymap.set("n", k, fn, { buffer = buf, desc = desc })
end

map("<leader>rr", function() vim.cmd.RustLsp("runnables") end,            "Rust: runnables")
map("<leader>rt", function() vim.cmd.RustLsp("testables") end,            "Rust: testables")
map("<leader>rd", function() vim.cmd.RustLsp("debuggables") end,          "Rust: debuggables")
map("<leader>rm", function() vim.cmd.RustLsp("expandMacro") end,          "Rust: expandir macro")
map("<leader>re", function() vim.cmd.RustLsp("explainError") end,         "Rust: explicar error")
map("<leader>rD", function() vim.cmd.RustLsp("renderDiagnostic") end,     "Rust: diagnóstico completo")
map("<leader>rc", function() vim.cmd.RustLsp("openCargo") end,            "Rust: abrir Cargo.toml")
map("<leader>ro", function() vim.cmd.RustLsp("openDocs") end,             "Rust: docs.rs del símbolo")
map("K",          function() vim.cmd.RustLsp({ "hover", "actions" }) end, "Rust: hover con acciones")
ℹ️
Por qué todo cuelga de leader

Fíjate en lo que no hemos hecho: ni <C-Space>, que es del autocompletado nativo de 0.12, ni <Tab>, que gobierna su menú, ni s ni S, que en esta guia son de flash.nvim (y por eso mini.surround vive en el prefijo gs). Las acciones van en <leader>, los saltos en ] y [. La única tecla nativa que pisamos es K, y solo dentro de buffers Rust: el hover de rustaceanvim es un superconjunto del de vim.lsp.buf.hover, así que el cambio no te quita nada.

Runnables, testables y debuggables

▶️

Runnables

<leader>rr lista lo ejecutable en el contexto: main, un test concreto, un ejemplo. Elige y corre, sin salir al terminal. Repite lo último con el bang: :RustLsp! runnables.

🧪

Testables

<leader>rt es lo mismo restringido a tests. Con la opción tools.test_executor en background los ejecuta en segundo plano y te muestra los fallos como diagnósticos en el propio archivo.

🐞

Debuggables

<leader>rd lanza el mismo target bajo el debugger. rustaceanvim carga solo las configuraciones de nvim-dap cuando el cliente se adjunta, y detecta codelldb si lo tienes instalado.

🔬

Expand macro

<leader>rm expande recursivamente la macro bajo el cursor y te enseña el código generado. Oro puro para entender un derive o una macro procedural.

Dos comandos más que casi nadie conoce y que valen la lección entera: <leader>re (explainError) busca el diagnóstico con código de error y te muestra la explicación del índice oficial de errores de Rust, y <leader>rD (renderDiagnostic) te enseña el diagnóstico tal y como lo imprimiría cargo, con el contexto de varios archivos junto. Para pelearte con el borrow checker o con genéricos, esa vista es la que de verdad ayuda.

El parser de Rust no es opcional

⚠️
La configuración de Treesitter que circula por ahí no compila

nvim-treesitter.configs, ensure_installed, highlight e indent desaparecieron con la rama master. En la rama main, que es la única viva, el plugin solo instala parsers; el resaltado lo enciende Neovim con vim.treesitter.start().

require("nvim-treesitter").install({ "rust", "toml" })

-- Sin foldlevelstart = 99, cada archivo se abre ENTERO PLEGADO.
-- Ponlo una vez en tus opciones (leccion 2.3).
vim.o.foldlevelstart = 99

vim.api.nvim_create_autocmd("FileType", {
  pattern = { "rust" },
  callback = function()
    vim.treesitter.start()
    vim.wo[0][0].foldexpr = "v:lua.vim.treesitter.foldexpr()"
    vim.wo[0][0].foldmethod = "expr"
  end,
})

Aquí el parser no es solo cosmético: el comando :Rustc unpretty, que te enseña la representación intermedia de la función bajo el cursor (necesita además una toolchain nightly), requiere el parser de Rust instalado.

crates.nvim: dependencias en Cargo.toml

saecki/crates.nvim
return {
  "saecki/crates.nvim",
  tag = "stable",
  event = { "BufRead Cargo.toml" },
  config = function()
    require("crates").setup()
  end,
}
vim.pack.add({
  { src = "https://github.com/saecki/crates.nvim" },
})

require("crates").setup()

Con el tag stable te quedas en la última release; sin él vas al filo. Los ajustes finos están en la wiki del proyecto.

Con él, Cargo.toml deja de ser un archivo ciego: ves qué versión de cada crate es la última publicada y gestionas las dependencias sin salir del buffer.

Formateo y depuración

rustfmt viene con la toolchain, y ya declaraste rust = { "rustfmt" } en conform (lección 2.4), así que se formatea al guardar respetando tu rustfmt.toml.

Para depurar necesitas nvim-dap (lección 2.4) y un adaptador: codelldb da mejor experiencia que lldb en Rust y rustaceanvim lo detecta solo si está instalado. Ojo al matiz: <leader>rd solo carga las configuraciones que genera rust-analyzer, mientras que require("dap").continue() carga todas las de Rust, incluidas las que tengas en un .vscode/launch.json.

El bucle de Rust en Neovim

Escribe, guarda (clippy te revisa al instante), <leader>rt para correr el test bajo el cursor, y si falla <leader>rD para leer el error como lo imprime cargo o <leader>re para que te lo expliquen; si aun así no lo ves, <leader>rd y breakpoints. Todo sin tocar el ratón ni cambiar de ventana. Pero hay una lección más general escondida aquí: rustaceanvim existe porque rust-analyzer habla más de lo que el protocolo estándar sabe escuchar. Cuando un servidor te ofrece extensiones propias, la pregunta útil no es “¿qué plugin lo hace bonito?”, sino “¿qué peticiones extra está exponiendo este servidor y quién las traduce a comandos?”. Sabiendo eso puedes leer el README de cualquier plugin de lenguaje y decidir en dos minutos si te aporta algo o solo envuelve lo que tu cliente ya hace.

⚔️ Rust en su salsa
  1. Instala rustaceanvim con un rango de versión y comprueba que no tienes ninguna ficha ni vim.lsp.enable para rust-analyzer.
  2. Pon check.command en clippy y verifica con :checkhealth rustaceanvim que todo está en su sitio.
  3. Abre un proyecto Cargo y, sobre un test, pulsa <leader>rt para correrlo.
  4. Rompe un tipo a propósito y compara <leader>re con <leader>rD sobre el mismo error.
  5. Sobre un derive, usa <leader>rm para ver qué código genera.
  6. Instala el parser de Rust y confirma que vaf selecciona una función entera.