wandres.dev
APRENDIZ · Config desde cero

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.

⏱ 22 min

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.

🎯 Al terminar esta lección sabrás
  • 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.

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.

ℹ️
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 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
⚠️
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:

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

catppuccin/nvim

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.

🎨
catppuccin/nvimcatppuccin/nvim

El tema más popular del ecosistema. Paleta suave e integraciones con casi todo. Esta guía usa su variante Mocha.

core

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.

nvim-treesitter/nvim-treesittersolo vim.pack

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.

⚠️
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 2.3.

🌳
nvim-treesitternvim-treesitter/nvim-treesitter

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

core

which-key

folke/which-key.nvim

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.

ℹ️
Diferir con el gestor nativo, si de verdad quieres

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.

⌨️
which-key.nvimfolke/which-key.nvim

Descubre y recuerda atajos leyendo los desc de tus keymaps. Imprescindible cuando la config crece.

core
⚠️
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.

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

⚠️
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

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

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.