Saltar al contenido

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.pack desde 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.

La estructura de directorios

Antes de instalar nada, esto es lo que vas a construir. Vale para las dos vías:

Texto
~/.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 3.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(...).

init.luaLUA
-- 1) El leader SIEMPRE antes que cualquier pluginvim.g.mapleader = " "vim.g.maplocalleader = "\\"require("config.options")require("config.keymaps")require("config.autocmds")-- 2) Los pluginsvim.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 siguienterequire("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 3.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 cuatro claves, y solo la primera es obligatoria:

Clave Qué es
src La URL. Cualquier cosa que acepte git clone. La única obligatoria
name El nombre del directorio. Por defecto, el del repositorio
version Rama, etiqueta o hash. Sin ella, la rama por defecto
data Datos tuyos, que te llegarán en los eventos PackChanged

Tanto es así que puedes pasar una cadena suelta en vez de una tabla y se toma como src. 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.

Que no se te apile todo en el init.lua

El ejemplo de arriba mete la instalación y la configuración en el mismo sitio, y con tres plugins se lee bien. Con quince, no. La lección anterior dedicó una sección a por qué la configuración va en módulos, y eso no cambia porque el gestor sea otro: vim.pack no impone ninguna estructura, así que la pones tú.

lua/config/pack.luaLUA
-- 1) Instalar todo de una vez: una sola pasada de gitvim.pack.add({  { src = "https://github.com/catppuccin/nvim", name = "catppuccin" },  { src = "https://github.com/nvim-mini/mini.nvim" },  { src = "https://github.com/folke/which-key.nvim" },}, { load = true })-- 2) Configurar cada uno en su fichero, en un orden que decides túfor _, modulo in ipairs({  "tema",        -- lua/plugins/tema.lua  "mini",        -- lua/plugins/mini.lua  "which-key",   -- lua/plugins/which-key.lua}) do  require("plugins." .. modulo)end
lua/plugins/tema.luaLUA
require("catppuccin").setup({ flavour = "mocha" })vim.cmd.colorscheme("catppuccin")

Y en el init.lua, una línea: require("config.pack").

Con esto las dos vías producen la misma estructura de directorios —un fichero por plugin bajo lua/plugins/— y la diferencia queda donde de verdad está: en cómo se declara la carga. En lazy, cada fichero devuelve una tabla que el gestor interpreta; aquí, cada fichero ejecuta su configuración cuando lo llamas.

Y la lista explícita del bucle no es un defecto: es la diferencia

Podrías recorrer el directorio y cargar todo lo que haya, y ahorrarte mantener la lista. No lo hagas: el orden importa —el tema antes que lo que lo use, una librería antes que quien la requiere— y un vim.fs.dir() te lo daría en orden alfabético, que es decir «al azar».

Esa lista es exactamente lo que lazy.nvim resuelve por ti con dependencies y su grafo de carga. Aquí lo llevas tú, en cinco líneas que puedes leer. Es el resumen honesto de todo el capítulo: no es que una vía sea mejor, es que una te da un mecanismo y la otra te da una decisión.

Dónde deja los plugins

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.

Marcado como experimental, usable a diario

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.

lua/config/lazy.luaLUA
-- 1) Bootstrap: clonar lazy.nvim la primera vezlocal 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 requirevim.opt.rtp:prepend(lazypath)-- 3) Arrancarlo, diciéndole DÓNDE están tus specsrequire("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:

init.luaLUA
vim.g.mapleader = " "vim.g.maplocalleader = "\\"require("config.options")require("config.keymaps")require("config.autocmds")require("config.lazy")   -- a partir de aquí manda lazy

Los tres require siguen ahí, y esto no es un recordatorio decorativo

Compara este bloque con el de la lección 2.1: las tres líneas de config están intactas. A partir de ahora vas a copiar init.lua de muchos sitios —de esta guía y de internet—, y el fallo más común al hacerlo es que en el bloque nuevo falte uno de los tres.

No da ningún error. Simplemente dejas de tener autocomandos, y tres semanas después no entiendes por qué el resaltado del yank «se ha estropeado». Cada vez que pegues un init.lua entero, cuenta los require.

Sin spec = { { import = 'plugins' } } no pasa nada

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:

lua/plugins/which-key.luaLUA
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.

La carga perezosa no es gratis

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

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 3.3 lo desarrolla.

Con los parsers en disco, el que enciende las funciones es Neovim. Un solo autocomando, sin listas que mantener a mano:

lua/config/treesitter.luaLUA
-- 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 1.7.vim.o.foldlevelstart = 99vim.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 es el que dominaste en la lección 1.7: za para abrir y cerrar, zR para abrirlo todo — solo que ahora los pliegues los deduce el árbol de sintaxis.

Casi todo lo escrito sobre nvim-treesitter está muerto

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

Instalador de parsers y proveedor de queries. No implementa funciones: se las da al editor para que las encienda.

which-key

Reinicia, pulsa <leader> (espacio) y espera medio segundo: aparece el menú con los desc que escribiste en la lección anterior.

Diferir con el gestor nativo, si de verdad quieres

En la vía nativa no hay VeryLazy, pero nada te impide escribir el retardo:

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

El presupuesto de teclas empieza hoy

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.

init.luaLUA
-- 1) Leader antes que nadavim.g.mapleader = " "vim.g.maplocalleader = " "require("config.options")require("config.keymaps")require("config.autocmds")-- 2) Lo que siempre está: gestor nativovim.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 3.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.nvimrequire("config.lazy")

Al lua/config/lazy.lua de antes solo hay que añadirle dos opciones:

lua/config/lazy.lua (añadido para la convivencia)LUA
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.

Las tres reglas de la convivencia

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.

  1. vim.pack.add() antes de require("config.lazy"). Si un plugin gestionado por lazy necesita algo del lado nativo, tiene que estar ya en el runtimepath.
  2. load = true explícito en el add. Dentro de init.lua, vim.pack.add por defecto solo añade el directorio al runtimepath y deja los archivos plugin/ para la carga automática del arranque… que lazy desactiva poniendo loadplugins a false, porque los carga él. Con load = true se cargan en el momento del add y el problema desaparece.
  3. Desactiva los dos reseteos de rendimiento de lazy. reset_packpath deja el packpath reducido a $VIMRUNTIME, y entonces vim.pack no encuentra sus propios plugins; rtp.reset reescribe el runtimepath con una lista fija, tirando los directorios que vim.pack acababa 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

LUA
vim.pack.update()                          -- todosvim.pack.update({ "mini.nvim" })           -- solo unovim.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

Shell
git checkout HEAD -- nvim-pack-lock.json
LUA
-- tras :restartvim.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:

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

lua/config/pack-hooks.luaLUA
vim.api.nvim_create_autocmd("PackChanged", {  group = vim.api.nvim_create_augroup("pack_hooks", { clear = true }),  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,})

Y hay que cargarlo, o no existe:

lua/config/pack.luaLUA
require("config.pack-hooks")   -- ANTES del add: el evento tiene que estar registradovim.pack.add({ … }, { load = true })

El orden importa y es el fallo típico: si registras el autocomando después del add, la instalación ya ha ocurrido y el evento se disparó cuando no había nadie escuchando. Funciona a partir de la segunda vez, que es la peor forma posible de fallar.

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

vim.pack.update() descarga y abre la confirmación:write confirma · :quit descarta:restart arranca con el código nuevovim.pack.del() borra del disco
:Lazy panel de gestión:Lazy sync instala, actualiza y limpia:Lazy restore vuelve al lockfile:Lazy profile mide el arranque

El mejor gestor es el que te lleva a tener menos plugins

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.

Tu primer setup con plugins

  1. Monta la estructura de directorios y arranca solo con vim.pack: el tema y mini.surround. Comprueba con vim.pack.get() que aparecen con su revisión.
  2. Añade nvim-pack-lock.json a tu repositorio de config y haz commit.
  3. Monta lazy.nvim por separado, con which-key como única spec en lua/plugins/. Borra a propósito la línea spec = { { import = "plugins" } } y comprueba que which-key deja de cargarse: así ves qué hace esa línea.
  4. Junta los dos con las tres reglas de la convivencia. Reinicia y confirma que siguen funcionando el tema y which-key.
  5. Instala el parser de un lenguaje que uses a diario y abre un archivo de ese tipo. Después ejecuta :TSUninstall sobre ese parser, reabre el archivo y observa qué se pierde exactamente: eso es lo que aporta el plugin.
  6. 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.