Gestor de plugins: lazy.nvim y vim.pack
Instala y configura los dos gestores desde cero: vim.pack, integrado en Neovim 0.12, y lazy.nvim. Aprende cuándo usar cada uno, cómo hacerlos convivir y por qué en 0.12 necesitas muchos menos plugins.
Un plugin es solo código Lua que Neovim carga desde el runtimepath. Un gestor lo clona, lo actualiza y fija su versión. Desde 0.12 el editor trae el suyo —vim.pack— y lazy.nvim sigue siendo el rey de la carga perezosa. No compiten: la pregunta útil no es cuál eliges, sino qué plugin va en cada sitio. Empezamos instalando los dos por separado, y solo después los juntamos.
- Montar
vim.packdesde cero y entender la estructura de directorios. - Montar lazy.nvim desde cero, con su bootstrap y su carga de specs.
- Decidir con criterio en qué lado cae cada plugin.
- Hacerlos convivir en una configuración híbrida sin que se pisen.
El falso dilema
Casi todo lo que leerás plantea esto como una elección excluyente: “¿lazy.nvim o vim.pack?”. Es una pregunta mal hecha. Los dos son gestores de Git con una capa de Lua encima, los dos dejan los plugins en directorios del runtimepath, y pueden convivir en la misma configuración.
La diferencia real está en la capa de arriba. vim.pack responde a “instala esto y mantenlo en esta versión”. lazy.nvim responde además a “…y no lo cargues hasta que pase tal cosa”, que es otro problema distinto. La mayor parte de una configuración no necesita esa segunda respuesta.
flowchart TD I[init.lua] --> P[vim.pack] I --> L[lazy.nvim] P --> A[tema, treesitter, librerias] L --> B[eventos, ft, cmd, teclas] A --> R[runtimepath] B --> R style P fill:#a6e3a1,color:#11111b style L fill:#cba6f7,color:#11111b style R fill:#89b4fa,color:#11111b
La estructura de directorios
Antes de instalar nada, esto es lo que vas a construir. Vale para las dos vías:
~/.config/nvim/
├── init.lua <- el único archivo que Neovim lee solo
├── nvim-pack-lock.json <- lo escribe vim.pack (va a Git)
├── lua/
│ ├── config/ <- tu configuración, cargada con require()
│ │ ├── options.lua
│ │ ├── keymaps.lua
│ │ ├── autocmds.lua
│ │ └── lazy.lua <- solo si usas lazy.nvim
│ └── plugins/ <- solo si usas lazy.nvim: una spec por archivo
│ ├── colorscheme.lua
│ └── which-key.lua
└── lsp/ <- servidores LSP (lección 2.1)
Todo lo que hay bajo lua/ es alcanzable con require: lua/config/options.lua se carga con require("config.options"). La carpeta lua/plugins/ no tiene nada de especial para Neovim; solo significa algo si le dices a lazy.nvim que la importe, y eso lo haremos explícito más abajo.
Instalar vim.pack
No hay nada que instalar: es una función del editor. No hay bootstrap, no hay clonado inicial, no hay if not fs_stat(...).
-- 1) El leader SIEMPRE antes que cualquier plugin
vim.g.mapleader = " "
vim.g.maplocalleader = " "
require("config.options")
require("config.keymaps")
-- 2) Los plugins
vim.pack.add({
{ src = "https://github.com/catppuccin/nvim", name = "catppuccin" },
{ src = "https://github.com/nvim-mini/mini.nvim" },
})
-- 3) Su configuración: el código ya se puede usar en la línea siguiente
require("catppuccin").setup({ flavour = "mocha" })
vim.cmd.colorscheme("catppuccin")
require("mini.surround").setup({
-- Prefijo gs: nativamente "dormir N segundos", o sea libre.
-- Deja s y S para flash.nvim (leccion 2.3).
mappings = {
add = "gsa", delete = "gsd", replace = "gsr",
find = "gsf", find_left = "gsF", highlight = "gsh",
},
})
Guarda, reinicia con :restart y ya está: los plugins que no estaban se clonan durante el add().
Una spec tiene tres claves:
| Clave | Qué es |
|---|---|
src |
La URL. Cualquier cosa que acepte git clone |
name |
El nombre del directorio. Por defecto, el del repositorio |
version |
Rama, etiqueta o hash. Sin ella, la rama por defecto |
Para version también vale vim.version.range("1.0"), que coge la mayor etiqueta semver del rango — pero solo funciona si el repositorio publica etiquetas del estilo v1.2.0; v1 o 1.2 no sirven.
vim.pack clona en site/pack/core/opt, dentro del directorio de datos de Neovim, y asume que todo lo que hay ahí lo gestiona él. Necesita git en el PATH. Si algún día lo abandonas no pierdes nada: lo que queda en disco son repositorios de Git normales.
La documentación de vim.pack lo etiqueta como experimental, con la coletilla de que es lo bastante estable para el día a día. Traducción: úsalo, pero cuenta con que algún detalle de la API pueda afinarse. No es motivo para no empezar; sí lo es para leer las notas de cada versión antes de actualizar Neovim.
Instalar lazy.nvim
lazy.nvim sí hay que instalarlo, y él no puede instalarse a sí mismo. Por eso toda config con lazy empieza con el mismo bloque: comprobar si está en disco y clonarlo si no.
-- 1) Bootstrap: clonar lazy.nvim la primera vez
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
vim.fn.system({
"git", "clone", "--filter=blob:none", "--branch=stable",
"https://github.com/folke/lazy.nvim.git", lazypath,
})
end
-- 2) Ponerlo al principio del runtimepath para poder hacer require
vim.opt.rtp:prepend(lazypath)
-- 3) Arrancarlo, diciéndole DÓNDE están tus specs
require("lazy").setup({
spec = { { import = "plugins" } }, -- importa todo lua/plugins/*.lua
checker = { enabled = true, notify = false },
ui = { border = "rounded" },
})
Y en el init.lua, una sola línea:
vim.g.mapleader = " "
vim.g.maplocalleader = " "
require("config.options")
require("config.keymaps")
require("config.lazy") -- a partir de aquí manda lazy
Este es el paso que casi todos los tutoriales dan por sabido, y sin él la carpeta lua/plugins/ es un directorio cualquiera que nadie lee. Esa línea es la que convierte cada archivo de lua/plugins/ en una spec. Si creas lua/plugins/tema.lua y no ves ningún cambio, lo que falta es esto.
Una spec es un archivo que devuelve una tabla. El primer elemento es el repositorio, y el resto son opciones:
return {
"folke/which-key.nvim",
event = "VeryLazy", -- cuándo cargarlo
opts = { preset = "modern" }, -- lazy llama a setup(opts) por ti
}
Las cuatro claves de carga perezosa —las que justifican todo lazy.nvim— son:
| Clave | Carga el plugin cuando… |
|---|---|
event |
Ocurre un evento (BufReadPost, InsertEnter, VeryLazy) |
ft |
Abres cierto tipo de archivo |
cmd |
Ejecutas cierto comando |
keys |
Pulsas cierta tecla |
Con :Lazy abres el panel para instalar, actualizar y revisar. :Lazy profile te dice qué plugin te está costando el arranque.
Dónde cae cada plugin
Ya tienes los dos montados. El criterio no es cuál te gusta más, sino si el plugin necesita no estar cargado.
| Señal | Lado |
|---|---|
| Lo necesitas en el primer frame (el tema) | vim.pack |
| El plugin no admite carga perezosa | vim.pack |
| Es una librería que otros plugins requieren | vim.pack |
| Se configura en dos líneas y no tiene coste medible | vim.pack |
| Solo aparece con un comando o una tecla (depurador, diffs) | lazy.nvim |
| Solo aplica a ciertos tipos de archivo | lazy.nvim |
| Tiene dependencias con un orden estricto | lazy.nvim |
| Tiene un paso de compilación no trivial | lazy.nvim |
Quieres sobrescribir sus opts desde otro archivo |
lazy.nvim |
En una config real la columna izquierda es casi siempre más larga. Y en 0.12 lo es todavía más, porque las piezas caras de antes —el LSP, el completado, el resaltado— ya no son plugins.
Cada plugin diferido es una regla que puede fallar: un event que no dispara, un ft que no cubre el tipo de archivo real, un plugin que carga tarde y se pierde el autocomando al que quería engancharse. Si no sabes decir en qué momento exacto debe entrar un plugin, cárgalo siempre y mide. Diferir algo que tarda tres milisegundos no compensa una tarde depurando por qué a veces funciona y a veces no.
Tus tres primeros plugins
De aquí en adelante, cada instalación muestra las vías que tienen sentido para ese plugin. Cuando solo aparece una, el motivo está escrito.
El tema
Un tema no se difiere nunca: si llega tarde, ves el editor gris durante medio segundo. En lazy hay que decirlo con lazy = false y priority = 1000; en vim.pack es el comportamiento normal. Va aquí en las dos vías porque es el ejemplo más claro de plugin que NO gana nada con lazy, y conviene verlo lado a lado.
return {
"catppuccin/nvim",
name = "catppuccin",
priority = 1000, -- antes que el resto
lazy = false, -- nunca diferido
opts = { flavour = "mocha" },
config = function(_, opts)
require("catppuccin").setup(opts)
vim.cmd.colorscheme("catppuccin")
end,
}vim.pack.add({
{ src = "https://github.com/catppuccin/nvim", name = "catppuccin" },
})
require("catppuccin").setup({ flavour = "mocha" })
vim.cmd.colorscheme("catppuccin")Sin la clave name el directorio se llamaría nvim, porque vim.pack usa el nombre del repositorio.
El tema más popular del ecosistema. Paleta suave e integraciones con casi todo. Esta guía usa su variante Mocha.
Treesitter
El primero que no es cosmético. Instala los parsers que Neovim necesita para entender la estructura de tu código: el editor trae la librería de tree-sitter y las funciones que la usan, pero solo seis parsers de fábrica (C, Lua, Markdown, Vimscript, Vimdoc y el de las propias queries). Para Rust, Python o TypeScript no viene ninguno. Es la misma relación que con el LSP: Neovim pone el cliente, tú instalas los servidores. La lección 2.3 lo desarrolla.
Solo vim.pack: la rama main NO admite carga perezosa, así que lazy.nvim no aporta absolutamente nada aquí. En lazy habría que escribir lazy = false para desactivar justo lo único que lazy hace. Si ya usas lazy para otras cosas, la spec sería la misma con branch = main, lazy = false y build = :TSUpdate.
vim.pack.add({
{ src = "https://github.com/nvim-treesitter/nvim-treesitter", version = "main" },
})
require("nvim-treesitter").install({
"lua", "vim", "vimdoc", "c", "rust", "markdown",
})install() es asíncrono y no hace nada si el parser ya está. En el primer arranque los parsers tardan un momento en aparecer.
Con los parsers en disco, el que enciende las funciones es Neovim. Un solo autocomando, sin listas que mantener a mano:
-- Sin esto, cada archivo se abre ENTERO PLEGADO y parece que has roto
-- el editor. Si te pasa alguna vez, zR lo abre todo. Lección 2.3.
vim.o.foldlevelstart = 99
vim.api.nvim_create_autocmd("FileType", {
callback = function(args)
-- ¿Hay parser para este tipo de archivo? Si no, no hacemos nada.
local lang = vim.treesitter.language.get_lang(vim.bo[args.buf].filetype)
if not lang or not vim.treesitter.language.add(lang) then return end
vim.treesitter.start(args.buf, lang) -- resaltado
vim.wo[0][0].foldexpr = "v:lua.vim.treesitter.foldexpr()" -- plegado
vim.wo[0][0].foldmethod = "expr"
end,
})
Fíjate en que no hay ninguna lista de lenguajes: el autocomando pregunta si existe parser y actúa en consecuencia. Instala un parser nuevo y funciona solo, sin tocar esto. El plegado que acabas de encender se maneja con za para abrir y cerrar el de debajo del cursor, y zR para abrirlos todos; lo verás bien en la lección 2.3.
La rama main es una reescritura incompatible. El módulo nvim-treesitter.configs ya no existe, y con él desaparecieron ensure_installed, highlight, indent e incremental_selection. La rama master sigue publicada pero está congelada y solo sirve para Neovim 0.11. Si un tutorial te enseña require("nvim-treesitter.configs").setup(...), está escrito para la rama muerta. Lo exprimimos en la lección 2.3.
Instalador de parsers y proveedor de queries. No implementa funciones: se las da al editor para que las encienda.
which-key
El caso contrario a Treesitter, y por eso va aquí: no hace falta hasta que pulsas leader, así que VeryLazy lo saca por completo del arranque. Este es un plugin donde lazy.nvim gana de verdad.
return {
"folke/which-key.nvim",
event = "VeryLazy", -- tras el arranque, sin frenarlo
opts = { preset = "modern" },
}vim.pack.add({ { src = "https://github.com/folke/which-key.nvim" } })
require("which-key").setup({ preset = "modern" })En la vía nativa no existe VeryLazy, pero se puede imitar con un autocomando (ver abajo).
Reinicia, pulsa <leader> (espacio) y espera medio segundo: aparece el menú con los desc que escribiste en la lección anterior.
En la vía nativa no hay VeryLazy, pero nada te impide escribir el retardo:
vim.api.nvim_create_autocmd("BufReadPre", {
once = true,
callback = function()
vim.pack.add({ { src = "https://github.com/folke/which-key.nvim" } })
require("which-key").setup({ preset = "modern" })
end,
})Funciona, y para uno o dos plugins es razonable. Para veinte, eso es reimplementar lazy.nvim a mano — y ahí es donde lazy deja de ser opcional.
Descubre y recuerda atajos leyendo los desc de tus keymaps. Imprescindible cuando la config crece.
Antes de que la config crezca, fija tus reglas y no las rompas. En esta guía son estas. <C-Space> y <Tab> pertenecen al autocompletado —el nativo de 0.12 y también el de blink.cmp o nvim-cmp—, así que no los mapees a nada más. s y S van para el plugin de saltos, y mini.surround se muda al prefijo gs, que nativamente no sirve para nada; lo que no puedes es aceptar los valores por defecto de los dos, porque ambos quieren s y uno desaparece. Los saltos entre elementos de una lista van en ] y [; tus acciones, detrás de <leader>. Un conflicto de teclas no da error: simplemente algo deja de responder y crees que el plugin está roto.
La configuración híbrida
Con los dos gestores entendidos por separado, juntarlos es directo. El nativo lleva el grueso; lazy entra solo para lo que gana con carga perezosa.
-- 1) Leader antes que nada
vim.g.mapleader = " "
vim.g.maplocalleader = " "
require("config.options")
require("config.keymaps")
require("config.autocmds")
-- 2) Lo que siempre está: gestor nativo
vim.pack.add({
{ src = "https://github.com/catppuccin/nvim", name = "catppuccin" },
{ src = "https://github.com/nvim-treesitter/nvim-treesitter", version = "main" },
{ src = "https://github.com/nvim-mini/mini.nvim" },
}, { load = true })
require("catppuccin").setup({ flavour = "mocha" })
vim.cmd.colorscheme("catppuccin")
require("nvim-treesitter").install({ "lua", "vim", "vimdoc", "c", "rust", "markdown" })
require("config.treesitter")
require("mini.surround").setup({
-- Prefijo gs: nativamente "dormir N segundos", o sea libre.
-- Deja s y S para flash.nvim (leccion 2.3).
mappings = {
add = "gsa", delete = "gsd", replace = "gsr",
find = "gsf", find_left = "gsF", highlight = "gsh",
},
})
require("mini.pairs").setup({})
-- 3) Lo que gana con carga perezosa: lazy.nvim
require("config.lazy")
Al lua/config/lazy.lua de antes solo hay que añadirle dos opciones:
require("lazy").setup({
spec = { { import = "plugins" } },
performance = {
reset_packpath = false, -- vim.pack necesita site en el packpath
rtp = { reset = false }, -- y sus plugins ya están en el runtimepath
},
checker = { enabled = true, notify = false },
ui = { border = "rounded" },
})
En lua/plugins/ dejas solo lo que de verdad se difiere: which-key con event, el depurador con cmd, los plugins por tipo de archivo con ft.
Que los dos gestores se lleven bien depende de tres detalles nada obvios, y los tres se explican por lo mismo: lazy.nvim optimiza el arranque tomando el control del runtimepath.
vim.pack.add()antes derequire("config.lazy"). Si un plugin gestionado por lazy necesita algo del lado nativo, tiene que estar ya en el runtimepath.load = trueexplícito en eladd. Dentro deinit.lua,vim.pack.addpor defecto solo añade el directorio al runtimepath y deja los archivosplugin/para la carga automática del arranque… que lazy desactiva poniendoloadpluginsafalse, porque los carga él. Conload = truese cargan en el momento deladdy el problema desaparece.- Desactiva los dos reseteos de rendimiento de lazy.
reset_packpathdeja elpackpathreducido a$VIMRUNTIME, y entoncesvim.packno encuentra sus propios plugins;rtp.resetreescribe el runtimepath con una lista fija, tirando los directorios quevim.packacababa de añadir. Pagas unos milisegundos de arranque por tener las dos vías vivas.
Qué pierdes y qué ganas
| Pierdes | Ganas |
|---|---|
| Dos lockfiles que versionar y dos comandos de actualización | El grueso de tu config no depende de ningún plugin externo |
:Lazy profile solo mide lo que gestiona lazy (para el resto, nvim --startuptime) |
Menos capas entre tú y el runtimepath: los fallos se leen mejor |
| Los reseteos de runtimepath de lazy, que ahorran unos milisegundos | El gestor se actualiza con el editor, no por su cuenta |
| Ejemplos: casi toda la documentación de internet asume lazy para todo | Tus specs de lazy dejan de ser 40 líneas con event = "VeryLazy" copiado sin pensar |
Referencia: el día a día de vim.pack
Esto no hace falta para empezar. Vuelve aquí cuando lo necesites.
El lockfile
vim.pack mantiene un lockfile en ~/.config/nvim/nvim-pack-lock.json, junto a tu init.lua, con la revisión exacta de cada plugin.
Versiónalo con la config
Trátalo como una parte más de tu configuración: al repositorio de Git, igual que el init.lua.
Manda sobre la spec
Si el lockfile existe, la primera llamada a vim.pack instala a la revisión del lockfile, no a lo que resolvería version. Otra máquina, mismos commits.
No se edita a mano
Lo escribe el editor. Se repara solo si se corrompe, e incluso si lo borras entero se regenera con lo que haya en disco.
Actualizar
vim.pack.update() -- todos
vim.pack.update({ "mini.nvim" }) -- solo uno
vim.pack.update(nil, { offline = true }) -- mirar sin descargar
Abre un buffer de confirmación en una pestaña aparte con el changelog de cada plugin. :write aplica, :quit descarta. Dentro, ]] y [[ saltan entre plugins, K muestra el detalle de un cambio y gO lista la estructura. Después, :restart.
Cada actualización real queda en nvim-pack.log, en el directorio de logs de Neovim.
Volver atrás
git checkout HEAD -- nvim-pack-lock.json
-- tras :restart
vim.pack.update(nil, { offline = true, target = "lockfile" })
Ese target = "lockfile" es el equivalente exacto de :Lazy restore.
Borrar
Quita primero la spec del init.lua —si no, se reinstala en el siguiente arranque—, reinicia, y entonces:
vim.pack.del({ "which-key.nvim" })
Hooks de compilación
En lazy es una clave de la spec (build = ":TSUpdate"). Con vim.pack es un autocomando:
vim.api.nvim_create_autocmd("PackChanged", {
callback = function(ev)
-- kind es "install", "update" o "delete"
if ev.data.spec.name == "un-plugin-con-make" and ev.data.kind ~= "delete" then
vim.system({ "make" }, { cwd = ev.data.path })
end
end,
})
Diez líneas tuyas contra una clave de una tabla: exactamente el tipo de decisión que esta lección quiere que tomes con los ojos abiertos.
El flujo de trabajo
Durante años la conversación fue “qué gestor usas”, y era fontanería disfrazada de identidad. Lo que 0.12 cambia no es quién clona los repos: es cuántos repos necesitas clonar. Con el gestor, el LSP y el completado dentro del editor, una configuración seria de 2026 puede vivir con un puñado de plugins en vez de cuarenta. Y fíjate en lo que hemos visto con Treesitter, porque es el patrón que se va a repetir: los plugins que sobreviven a este cambio no son los que hacen cosas, son los que traen datos que el editor no puede traer solo —parsers, queries, servidores, binarios—. Cuando dudes si un plugin sigue teniendo sentido en 0.12, pregúntate qué te da exactamente: si es una función, mira antes si el editor ya la trae; si son datos, casi seguro que lo necesitas.
- Monta la estructura de directorios y arranca solo con
vim.pack: el tema ymini.surround. Comprueba convim.pack.get()que aparecen con su revisión. - Añade
nvim-pack-lock.jsona tu repositorio de config y haz commit. - Monta lazy.nvim por separado, con
which-keycomo única spec enlua/plugins/. Borra a propósito la líneaspec = { { import = "plugins" } }y comprueba que which-key deja de cargarse: así ves qué hace esa línea. - Junta los dos con las tres reglas de la convivencia. Reinicia y confirma que siguen funcionando el tema y which-key.
- Instala el parser de un lenguaje que uses a diario y abre un archivo de ese tipo. Después ejecuta
:TSUninstallsobre ese parser, reabre el archivo y observa qué se pierde exactamente: eso es lo que aporta el plugin. - Coge tres plugins que quieras instalar y decide, por escrito, en qué lado cae cada uno y por qué.
Con esto dominas el ciclo instalar → fijar versión → cargar. En el Nivel 2 lo usaremos para construir el IDE.