Personalizar LazyVim sin romperlo
Las reglas de fusión que gobiernan todo override: opts como tabla o como función, keys que se extienden, desactivan o reemplazan, enabled = false, tus opciones y autocmds contra los de la distro, y la recuperación de las teclas que LazyVim repartió por ti.
Aquí está la habilidad clave del Arquitecto: tomar una base ajena y hacerla tuya sin romperla. LazyVim está diseñada para que la modifiques con elegancia — override, no parche — y ese diseño descansa en un puñado de reglas de fusión que, una vez aprendidas, convierten cualquier personalización en un archivo pequeño y declarativo. Esta lección te da las reglas completas, las siete operaciones que cubren el día a día, y la menos glamurosa y más urgente de todas: recuperar las teclas que la distro repartió por ti.
- Enunciar las reglas de fusión de specs: qué se extiende, qué se fusiona y qué se reemplaza.
- Sobreescribir las
optsde un plugin, sabiendo cuándo tabla y cuándo función. - Gestionar
keys: añadir, desactivar una sola, sobreescribir o reemplazarlas todas. - Entender el caso especial de Treesitter, donde casi todo el mundo copia API muerta.
- Añadir plugins propios y desactivar los de la distro con
enabled = false. - Imponer tus opciones, keymaps y autocmds sobre los de LazyVim, sabiendo por qué ganan.
- Resolver el choque de
s,Sy<C-Space>que LazyVim te deja servido, y cambiar el tema.
La regla de oro y las reglas de fusión
La regla de oro primero: nunca edites los archivos internos de LazyVim (los de ~/.local/share/nvim/lazy/LazyVim/). En su lugar, crea un archivo en lua/plugins/ con una spec del mismo plugin: como tu import va el último en la spec (lección 5.2), lazy.nvim fusiona tu spec parcial con la de la distro, y tus cambios sobreviven a las actualizaciones porque nunca tocaste sus tripas.
Ahora bien, «fusionar» no significa lo mismo para cada campo de la spec, y conocer la tabla exacta es lo que separa un override que funciona de uno que hace algo parecido a lo que querías. Según la documentación de LazyVim:
| Campo de la spec | Qué hace tu valor con el de la distro |
|---|---|
cmd |
Se extiende: tus comandos se suman a los suyos |
event |
Se extiende: tus eventos se suman |
ft |
Se extiende: tus tipos de archivo se suman |
keys |
Se extiende: tus mapeos se suman (con reglas finas, abajo) |
opts |
Se fusiona en profundidad con las opciones de la distro |
dependencies |
Se extiende: tus dependencias se suman |
| cualquier otro campo | Se reemplaza: tu valor pisa el suyo |
Y una válvula de escape uniforme: para cmd, event, ft, keys y opts puedes pasar una función en vez de un valor; la función recibe los valores por defecto y puede modificarlos o devolver otros nuevos. Esa dualidad valor/función es el tema central de la lección, así que empecemos por donde más se usa.
1) Sobreescribir las opciones de un plugin: tabla o función
La tabla es el caso común. Devuelve una spec con el mismo plugin y tus opts; se hace un deep merge con las de LazyVim, clave a clave y en profundidad:
return { { "gitsigns.nvim", opts = { signs = { add = { text = "┃" }, change = { text = "┃" } }, }, },}La fusión profunda respeta todo lo que no menciones: la distro configura veinte opciones de gitsigns y tú cambias dos; las otras dieciocho siguen intactas. Por eso tu archivo puede — y debe — ser mínimo: solo las diferencias.
La función es para cuando la fusión no basta, y merece que entiendas exactamente cuándo es ese momento. El deep merge funciona clave a clave, pero hay dos operaciones que no puede expresar: insertar en una lista (fusionar dos listas las machaca, no las concatena) y borrar una clave que la distro puso (con una tabla solo puedes cambiar valores, no eliminarlos). Para esas, opts como función recibe las opciones ya fusionadas de las capas anteriores y las edita con lógica:
return { { "nvim-lualine/lualine.nvim", opts = function(_, opts) -- insertar en una lista que la distro ya pobló: table.insert(opts.sections.lualine_x, " " .. os.date("%H:%M")) -- borrar algo que la distro puso: opts.sections.lualine_y = nil -- si modificas opts in situ no hace falta return; -- si devuelves una tabla, esa REEMPLAZA todo return opts end, },}El error silencioso de la función
Dentro de opts = function(_, opts) hay dos contratos posibles y mezclarlos es el fallo más común: o modificas opts in situ y no devuelves nada, o devuelves la tabla final completa. Si construyes una tabla nueva con solo tus dos opciones y la devuelves, acabas de descartar toda la configuración de la distro para ese plugin — sin error, sin aviso, solo un plugin que de repente se comporta como recién instalado. Cuando un override «apaga» cosas que no tocaste, revisa esto primero.
La regla de decisión, en una línea: tabla para cambiar valores, función para tocar listas, borrar claves o decidir con lógica. Empieza siempre por la tabla; pásate a la función cuando la necesites, no antes.
2) Las keys: el campo con reglas propias
keys se extiende como los demás campos de lista, pero además tiene tres operaciones finas documentadas que lo cubren todo:
return { { "folke/flash.nvim", keys = { -- 1) DESACTIVAR un mapeo concreto de la distro: mismo lhs, valor false. -- Ojo: hay que declarar el MISMO modo que el original. { "S", mode = { "n", "x", "o" }, false }, -- 2) SOBREESCRIBIR: mismo lhs con un rhs nuevo { "s", mode = { "n", "x", "o" }, function() require("flash").jump() end, desc = "Flash: saltar" }, -- 3) AÑADIR: un lhs que la distro no usaba { "<leader>sj", function() require("flash").jump() end, desc = "Flash" }, }, },}Y la cuarta operación, la drástica: keys como función descarta todos los mapeos que la distro definía para ese plugin y usa únicamente lo que devuelvas (devolver {} lo deja sin ninguno). Es la herramienta para cuando quieres quedarte dos teclas de un plugin que reparte seis, y la usaremos más abajo con flash.
El modo importa al desactivar
La documentación de LazyVim lo avisa expresamente: para desactivar un mapeo con false tienes que declarar exactamente el mismo modo que el original — { "s", mode = { "n", "x", "o" }, false } para un mapeo definido en esos tres modos. Si omites el modo, solo afecta a Normal, y el mapeo seguirá vivo en Visual y operador-pendiente, que es la clase de fallo a medias que cuesta una tarde diagnosticar.
3) El caso especial: Treesitter
La API que copias de internet no existe
La rama main de nvim-treesitter es una reescritura incompatible. El módulo nvim-treesitter.configs desapareció, y con él highlight, indent e incremental_selection. El plugin hoy hace una sola cosa: instalar parsers y aportar queries, con require("nvim-treesitter").install { "lua", "rust" }. El resaltado lo enciende vim.treesitter.start() desde un autocomando FileType, y el plegado sale de vim.wo[0][0].foldexpr = 'v:lua.vim.treesitter.foldexpr()' con foldmethod = 'expr'. Si un tutorial te enseña require("nvim-treesitter.configs").setup(...), está escrito para la rama congelada. LazyVim migró a la rama main en su versión 15, y la CLI de tree-sitter pasó a ser necesaria para compilar parsers — :LazyHealth te avisa si falta.
Dicho eso, dentro de LazyVim sí escribes ensure_installed, y aquí es donde se confunde casi todo el mundo:
return { { "nvim-treesitter/nvim-treesitter", opts = { ensure_installed = { "swift", "zig" } }, -- se AÑADEN a las de LazyVim },}ensure_installed no es una opción del plugin: es una opción de la spec de LazyVim, que la lee, la extiende como lista y luego llama por ti a la API real de la rama main. Por eso funciona aquí y por eso no hace absolutamente nada en la config a mano que montaste en el Nivel 2 — allí llamas tú directamente a require("nvim-treesitter").install. Es un ejemplo perfecto de algo que verás más veces: la distro inventa pequeñas opciones de conveniencia por encima de los plugins, y saber cuáles son suyas y cuáles del plugin es parte de leerla bien.
Lo que NO debes tocar de esa spec
No cambies el branch ni intentes reconfigurar cuándo carga: LazyVim ya gobierna esa spec en la rama main y sabe lo que hace. Limítate a añadir lenguajes por ensure_installed. Y si algún día sacas Treesitter de la distro, recuerda que fuera de aquí la rama main no admite carga perezosa: va con lazy = false, branch = "main" y build = ":TSUpdate".
4) Añadir tus propios plugins
Igual que en tu config a mano: la spec en lua/plugins/, y se suma a la base de LazyVim. Puedes organizarte con un archivo por plugin o agrupar por funcionalidad; lazy.nvim carga todos los archivos de la carpeta.
Antes de añadir, dos comprobaciones de Arquitecto: ¿la distro ya trae algo que hace esto (mira :Lazy y which-key)? ¿Y existe un extra oficial que lo integra mejor que tu spec a pelo (mira :LazyExtras)? La mitad de los plugins que la gente añade a LazyVim duplican una función que ya estaba a dos teclas.
5) Desactivar un plugin de LazyVim
Ponle enabled = false en una spec tuya:
return { { "folke/flash.nvim", enabled = false }, -- fuera flash entero { "akinsho/bufferline.nvim", enabled = false }, -- no quiero pestañas}El plugin deja de instalarse y de cargar, y con él caen sus keymaps y su configuración. Es reversible borrando la línea. Un archivo disable.lua con tus bajas es, junto a los overrides, la mitad de la historia de tu relación con la distro — y se lee de un vistazo.
6) Opciones, keymaps y autocmds que ganan
Como viste en el ciclo de arranque (lección 5.2), LazyVim carga sus propios options, keymaps y autocmds antes que los tuyos, así que tus archivos de lua/config/ no configuran desde cero: corrigen encima, y la última palabra es tuya.
-- LazyVim ya trae relativenumber, undofile, ignorecase+smartcase...-- aquí van solo tus desviaciones:vim.opt.scrolloff = 10vim.g.snacks_animate = false -- apaga todas las animaciones de la distrolocal map = vim.keymap.setmap("n", "<leader>ww", "<cmd>w<cr>", { desc = "Guardar rápido" })-- ¿quitar un keymap suelto que pone LazyVim en este archivo? bórralo:-- vim.keymap.del("n", "<leader>bd")Para los autocmds hay un mecanismo específico que merece conocerse: LazyVim agrupa todos los suyos en augroups con el prefijo lazyvim_, precisamente para que puedas eliminarlos por nombre:
-- no quiero spell automático en markdown y texto:vim.api.nvim_del_augroup_by_name("lazyvim_wrap_spell")Abre los archivos de ~/.local/share/nvim/lazy/LazyVim/lua/lazyvim/config/ para ver la lista completa de grupos: cada uno es un comportamiento con interruptor. Y ojo a un matiz: dentro de los keymaps por defecto verás que la distro usa un helper propio para mapear sin pisar; ese helper es interno — en tus archivos usa vim.keymap.set normal, como siempre.
7) El reparto de teclas: el conflicto que viene de fábrica
Esta es la personalización que de verdad importa, porque no falla con un error sino con silencio. Empieza por el diagnóstico, siempre:
:map s:map <C-Space>:map <Tab>En una LazyVim recién instalada, :map s te responde que s y S son de flash.nvim, y :map <C-Space> te dice que esa tecla también (desde la versión 15, flash asumió la selección incremental que antes hacía Treesitter). La buena noticia es que en lo primero LazyVim coincide con esta guía; la mala, que lo segundo hay que arreglarlo.
Lo que LazyVim hace bien: s para flash, gs para surround
Es exactamente el reparto de la lección 3.3, así que aquí no tienes que tocar nada. LazyVim cede s y S a flash porque un salto es una moción y merece dos teclas, y en su extra coding.mini-surround mueve surround al prefijo gs (gsa, gsd, gsr), que queda libre para este uso. Se pierden s y S de Vim, que son cl y cc — coste casi nulo.
El choque aparece cuando tú añades mini.surround con sus valores por defecto encima, por ejemplo trayendo tu archivo del Nivel 2 sin adaptarlo. Entonces hay dos plugins reclamando s y gana el que cargue último. Si eso te pasa, la solución es la misma que ya usas: fijar el prefijo gs explícitamente.
return { { "mini.surround", opts = { mappings = { add = "gsa", -- añadir delimitadores delete = "gsd", -- borrarlos replace = "gsr", -- cambiarlos find = "gsf", find_left = "gsF", highlight = "gsh", }, }, },}O más simple: activa el extra con :LazyExtras y no escribas nada, porque hace justo esto.
Lo que sí hay que quitarle: <C-Space>
Esta no se negocia. En este track <C-Space> está reservado para el autocompletado —el nativo de 0.12 se enciende con vim.o.autocomplete y se afina con vim.opt.completeopt— y <Tab> para moverse por su menú. LazyVim se la da a flash, y el síntoma es que tu completado deja de responder sin dar ningún error.
La vía quirúrgica usa la operación 1 de la sección de keys — desactivar un mapeo concreto con false, declarando sus modos exactos:
return { "folke/flash.nvim", keys = { { "<c-space>", mode = { "n", "x", "o" }, false }, },}Y si prefieres quedarte solo con los mapeos de flash que tú elijas y descartar el resto de los suyos (los de r y R en operador-pendiente incluidos), pasa keys como función: lazy descarta las teclas heredadas y usa únicamente lo que devuelvas.
return { "folke/flash.nvim", -- keys como función DESCARTA las de LazyVim y deja solo estas dos: keys = function() return { { "s", mode = { "n", "x", "o" }, function() require("flash").jump() end, desc = "Flash: saltar" }, { "S", mode = { "n", "x", "o" }, function() require("flash").treesitter() end, desc = "Flash: nodo" }, } end,}La comprobación que cierra el asunto
Reinicia y vuelve a ejecutar :map s y :map <C-Space>. :map s debe mostrar un solo dueño, flash, y :map <C-Space> debe salir limpio en modo Normal. Si :map s te lista dos entradas, todavía tienes la bomba puesta: solo que hoy ha cargado en el orden que te favorece. Y para saber quién definió cualquier mapeo sospechoso, :verbose map <tecla> te da el archivo y la línea.
8) Cambiar el tema
LazyVim usa Tokyo Night por defecto. El tema es un caso curioso e instructivo: no se cambia configurando el plugin del tema, sino configurando el plugin LazyVim, que entre sus opts acepta colorscheme — porque es la distro quien decide cuándo aplicarlo durante el arranque:
Fíjate en la maniobra: dos specs en el mismo archivo, una instala el tema y la otra hace override de las opts de "LazyVim/LazyVim" — la distro tratada como un plugin más, que es lo que es. Es el mismo mecanismo de la operación 1 aplicado a la propia base.
Toda abstracción tiene fugas, y esta lección es el mapa de las suyas
Mira lo que has hecho en esta lección con perspectiva, porque el patrón vale para cualquier framework que uses en tu vida. LazyVim te ofrece una abstracción preciosa: «declara tus diferencias en un archivo y el sistema las fusiona». Mientras la abstracción aguanta, no necesitas saber nada de lo que hay debajo — escribes opts = { ... } y funciona. Pero el recorrido completo de la lección es la historia de sus fugas: la fusión no sabe insertar en listas, y tuviste que bajar a opts como función. La fusión no sabe borrar claves, y tuviste que aprender el contrato del return. Desactivar una tecla exige conocer los modos exactos del mapeo original. ensure_installed parece una opción del plugin y es de la distro. El tema no se configura en el tema. Cada una de esas esquinas es un sitio donde la abstracción se rinde y te pide que sepas cómo funciona por dentro.
Eso no es un defecto de LazyVim: es una ley. Todas las abstracciones no triviales tienen fugas — la fusión declarativa, el ORM que un día te exige SQL, el recolector de basura que un día te exige entender la memoria, la capa de red que un día te exige entender TCP. La consecuencia práctica es la que importa, y es una regla de inversión: el tiempo que una abstracción te ahorra lo cobra, con intereses, el día que gotea — salvo que ya entiendas lo que abstrae. Para ti la fusión de specs no es magia, porque montaste specs a mano en el Nivel 2; por eso cada fuga de esta lección te ha costado un párrafo y no una tarde. Esa es la relación correcta con cualquier abstracción: úsala para no pensar en lo de debajo cada día, no para no saberlo nunca. Y cuando evalúes una herramienta nueva, no preguntes solo qué te ahorra: pregunta qué te va a exigir saber el día que se le note la costura.
Hazla tuya
- Ejecuta
:map sy:map <C-Space>y anota quién los tiene ahora mismo. - Recupera
<C-Space>por la vía quirúrgica o conkeyscomo función. Reinicia y confirma con:mapque tu autocompletado responde otra vez. - Haz un override con tabla: cambia los signos de gitsigns. Comprueba con
:Lazyque el plugin sigue con el resto de su config intacta. - Haz un override con función: inserta la hora en una sección de lualine. Luego provoca el error silencioso a propósito — devuelve una tabla solo con tu cambio — y observa qué pierde el statusline. Deshazlo.
- Desactiva un plugin que no uses con
enabled = falsey verifica en:Lazyque ya no carga. - Elimina un autocmd de la distro por su nombre de grupo (
lazyvim_wrap_spelles buen candidato) y comprueba el efecto abriendo un markdown. - Añade
swiftaensure_installedde Treesitter y comprueba que se suma a los de LazyVim en vez de sustituirlos. - Cambia el tema a Catppuccin con el override doble, y explica en voz alta por qué son dos specs y no una.