Tu init.lua desde cero
La estructura modular y por qué require encuentra tus ficheros, la regla para elegir entre vim.o y vim.opt de una vez por todas, las opciones que de verdad cambian algo, el presupuesto de teclas antes del primer plugin, los autocomandos con grupo, la configuración por proyecto con exrc, y cómo depurar la config cuando falle.
Llevas dos niveles usando el editor sin haber escrito una línea de configuración, y eso ha sido a propósito: ahora sabes qué te falta. Esta lección construye tu init.lua pieza a pieza, sin distribuciones prefabricadas, y con una regla que gobierna todo lo demás: entiendes cada línea o no la pones. Una configuración copiada funciona hasta el día en que falla, y ese día no tienes dónde mirar. Además vas a saldar las cinco deudas que dejaron los niveles anteriores: el 'inccommand', el cfilter, las vistas de los pliegues, los puntos de deshacer y la decisión sobre el portapapeles.
- Estructurar la configuración en módulos y explicar por qué
requirelos encuentra. - Aplicar la regla
vim.o/vim.optsin volver a dudar. - Elegir las opciones sabiendo qué compra cada una, incluida la del portapapeles.
- Repartir atajos dentro de un presupuesto de teclas, antes de instalar nada.
- Escribir autocomandos con grupo, y saber por qué el grupo no es opcional.
- Depurar la configuración cuando falle, sin adivinar.
Dónde vive, y por qué
En la lección 0.2 viste dos cosas que ahora se juntan. La configuración va en stdpath('config') —~/.config/nvim/— y el editor busca todo lo que no lleva compilado dentro en el runtimepath, del que esa carpeta es la primera entrada.
De ahí sale la regla que hace funcionar lo que viene: cualquier fichero dentro de un lua/ del runtimepath se importa con require, usando puntos en vez de barras y sin extensión.
~/.config/nvim/├── init.lua # el punto de entrada└── lua/ └── config/ ├── options.lua # → require("config.options") ├── keymaps.lua # → require("config.keymaps") └── autocmds.lua # → require("config.autocmds")No hay magia ni convención especial: lua/config/options.lua es require("config.options") porque ~/.config/nvim está en el runtimepath y Lua busca dentro de su lua/.
-- 1) El leader ANTES de cargar nada másvim.g.mapleader = " "vim.g.maplocalleader = "\\"-- 2) Opciones, atajos y automatismosrequire("config.options")require("config.keymaps")require("config.autocmds")-- 3) Los plugins entran aquí, en la lección 2.2El leader va primero, y no es una superstición
vim.g.mapleader tiene que estar definido antes de que se cargue cualquier plugin. Un plugin que declara <leader>ff lo resuelve en el momento en que se registra: si en ese instante el leader todavía es \, tu atajo queda como \ff para siempre y no entiendes por qué.
Por eso está en la primera línea y no en options.lua.
Los tres require se quedan
Según avance el curso irás añadiendo cosas a este init.lua. Estas tres líneas no se borran nunca. Si en algún momento copias un bloque de una lección posterior y config.autocmds desaparece, tus automatismos dejan de existir sin dar ningún error — que es la peor clase de fallo.
Lua mínimo
No hace falta dominar Lua —eso es el Nivel 7 entero—. Con estas seis cosas te mueves en el 95% de los casos:
vim.g.mapleader = " " -- variables globales de Vimvim.o.number = true -- opciones del editorvim.opt.shortmess:append("I") -- opciones que son listasvim.bo.shiftwidth = 2 -- opción local a ESTE buffervim.wo.wrap = false -- opción local a ESTA ventanavim.keymap.set("n", "<leader>w", "<cmd>w<cr>", { desc = "Guardar" })vim.api.nvim_create_autocmd("BufWritePre", { callback = function() end })vim.o o vim.opt: la regla
Esto confunde a todo el mundo porque durante años se recomendó lo contrario de lo que toca hoy. La regla es corta y en este curso se aplica sin excepciones:
vim.opor defecto.vim.optsolo cuando la opción es una lista y quieres modificarla sin reescribirla entera.
vim.o.number = true y vim.opt.number = true hacen lo mismo, y el primero es más directo. La diferencia aparece en opciones como 'shortmess', 'formatoptions' o 'wildignore', que son listas:
vim.opt.shortmess:append("I") -- añade a lo que ya habíavim.opt.formatoptions:remove("o") -- quita una letravim.o.shortmess = "filnxtToOFI" -- reescribe la lista entera: frágilCon vim.opt tienes :append, :remove y :prepend sobre el valor actual. Con vim.o tendrías que conocer y repetir el valor completo, y ese valor cambia entre versiones de Neovim. Ahí está la razón real de la regla: vim.opt te deja expresar un cambio relativo al defecto, y vim.o te obliga a congelar un absoluto.
Las opciones
Crea lua/config/options.lua. Estas no son «las que todo el mundo pone»: son las que cambian algo que vas a notar, y varias saldan promesas de los niveles anteriores.
local o = vim.o-- Números híbridos: absoluto en la línea actual, relativos alrededoro.number = trueo.relativenumber = true-- Indentacióno.tabstop = 2 -- ancho visual de un tabuladoro.shiftwidth = 2 -- lo que mueve un nivel de indentacióno.expandtab = true -- tabuladores a espacioso.smartindent = true-- Búsqueda (lecciones 0.3 y 1.3)o.ignorecase = true -- ignora mayúsculas...o.smartcase = true -- ...salvo que escribas algunao.inccommand = "split" -- previsualiza :s en una ventana aparte-- Interfazo.termguicolors = true -- 24 bits de coloro.signcolumn = "yes" -- columna de signos SIEMPRE visibleo.cursorline = trueo.scrolloff = 8 -- margen al llegar a un bordeo.wrap = falseo.laststatus = 3 -- una sola barra de estado para todas las ventanaso.confirm = true -- al salir con cambios, pregunta en vez de fallar-- Dónde se abren las ventanas nuevas (lección 1.1)o.splitright = trueo.splitbelow = true-- Ficheros y persistencia (lección 1.8)o.swapfile = falseo.undofile = true-- Plegado (lección 1.7): activo pero abierto de par en paro.foldlevelstart = 99o.foldtext = ""-- Respuestao.updatetime = 250 -- diagnósticos y avisos más ágileso.timeoutlen = 400 -- ventana para completar una secuencia de teclas-- Lo que Neovim trae dentro y hay que pedirvim.cmd.packadd("cfilter") -- :Cfilter para la quickfix (lección 1.6)Cuatro merecen explicación, porque parecen cosméticas y no lo son:
signcolumn = ‘yes’
Sin esto, la columna aparece cuando llega el primer diagnóstico y todo el texto se desplaza un carácter. Con yes siempre está y nada salta nunca.
relativenumber
Convierte el conteo en lectura: ves «7» al lado de la línea a la que quieres ir y escribes 7j o d7j. Sin él, nadie usa los conteos.
inccommand = ‘split’
Escribes un :%s y ves todas las líneas que va a cambiar en una ventana, antes de confirmar. Es el ensayo de la lección 1.3.
undofile
El árbol de deshacer sobrevive a cerrar el fichero. Es la red que hace razonable todo lo del Nivel 1.
El portapapeles: la decisión que te dejé pendiente en la lección 1.2
Verás en casi todas las configuraciones esta línea:
o.clipboard = "unnamedplus" -- NO está en el bloque de arriba, y es a propósitoFusiona el registro sin nombre con el del sistema: yy copia al portapapeles y p pega de él, sin escribir "+. Es cómodo y tiene un coste real: todo lo que borres también va al portapapeles. Un dd para eliminar una línea basura te machaca lo que tuvieras copiado del navegador, y x sobre una letra, también.
Este curso no la pone, y la razón es que ya sabes escribir "+y cuando cruzas la frontera del editor —lo aprendiste en la lección 1.2— y eso mantiene los dos mundos separados. Si la prefieres, ponla: es una decisión legítima. Lo que no quiero es que la heredes de un bloque copiado sin saber qué implica, porque después vienen tres meses de «se me borra el portapapeles» sin relacionarlo con esta línea.
El presupuesto de teclas
Antes de mapear nada. Esto es lo que evita el 90% de los «se me rompió la config» de dentro de tres meses.
Tu teclado ya está lleno. Casi todas las teclas de modo Normal hacen algo, y los plugins que instales querrán más. Antes de asignar una, la pregunta es si está libre — y :h index te lo dice, igual que :verbose nmap <tecla> te dice quién la tiene ahora.
| Zona | Estado |
|---|---|
<leader> + lo que sea |
tuyo. Es exactamente para esto |
s S Y |
redundantes (cl, cc, y$). Se pueden ceder, y en el Nivel 3 se ceden |
Ctrl-Space y Tab |
del completado. Mapearlas rompe el autocompletado en Insertar |
] y [ + letra |
la convención de «siguiente/anterior». Ya ocupadas por el núcleo desde 0.11 |
g + letra |
medio ocupado y medio libre. Mira antes |
hjkl wbe fFtT dcy u |
intocables. Son el idioma |
Tu configuración no es un fichero de ajustes: es un programa
Merece la pena detenerse aquí, porque el cambio de categoría explica casi todo lo que va a pasarte de ahora en adelante.
Un panel de preferencias es una lista de valores: marcas casillas, el programa las lee, y el peor resultado posible es que el editor se vea distinto de lo que querías. Un init.lua es código que se ejecuta al arrancar, y eso trae las cuatro cosas que trae el código. Puede tener errores de sintaxis, y entonces el editor arranca a medias. Tiene un orden, y por eso el leader va en la primera línea. Tiene dependencias: si borras un require, desaparece silenciosamente todo lo que había detrás. Y puede consumir tiempo, que es por qué existe el Nivel 25 entero sobre rendimiento del arranque.
Eso suena a lista de inconvenientes y es justo lo contrario, porque a cambio recibes todo lo que la ingeniería ha construido alrededor del código en cincuenta años. Tu configuración se puede versionar, y entonces «esto funcionaba la semana pasada» deja de ser una sensación y pasa a ser un git diff. Se puede compartir y clonar, y una máquina nueva se convierte en dos comandos. Se puede depurar, con las mismas herramientas con las que depuras cualquier otra cosa. Se puede condicionar —if vim.fn.has("nvim-0.12") == 1— para que la misma configuración funcione en el servidor viejo y en tu portátil. Y se puede factorizar, que es lo que estamos haciendo al partirla en módulos.
Es exactamente el mismo salto que dio la infraestructura cuando dejó de ser servidores configurados a mano y pasó a ser ficheros en un repositorio, y por las mismas razones: no porque escribir código sea más agradable que marcar casillas —no lo es—, sino porque un artefacto de texto versionado se puede razonar, revisar y reproducir, y un estado acumulado a base de clics no.
De ahí sale la regla de esta lección, que parece pedantería y es autodefensa: entiendes cada línea o no la pones. Una línea copiada que no entiendes es una dependencia que no sabes que tienes, en un programa que se ejecuta cada vez que abres el editor. Y de ahí sale también la otra: { desc = "..." } en cada mapeo. No es documentación decorativa; es lo que hará que dentro de un año puedas leer tu propia configuración sin arqueología.
Tus primeros keymaps
local map = vim.keymap.set-- Salir a Normal sin estirarse al Escmap("i", "jk", "<Esc>", { desc = "Salir a Normal" })-- Limpiar el resaltado de la última búsquedamap("n", "<Esc>", "<cmd>nohlsearch<cr>", { desc = "Quitar resaltado" })-- Guardar y cerrarmap("n", "<leader>w", "<cmd>write<cr>", { desc = "Guardar fichero" })map("n", "<leader>q", "<cmd>quit<cr>", { desc = "Cerrar ventana" })-- Moverse entre ventanasmap("n", "<C-h>", "<C-w>h", { desc = "Ventana izquierda" })map("n", "<C-j>", "<C-w>j", { desc = "Ventana abajo" })map("n", "<C-k>", "<C-w>k", { desc = "Ventana arriba" })map("n", "<C-l>", "<C-w>l", { desc = "Ventana derecha" })-- Mover el bloque seleccionado, reindentandomap("x", "J", ":m '>+1<cr>gv=gv", { desc = "Bajar selección" })map("x", "K", ":m '<-2<cr>gv=gv", { desc = "Subir selección" })-- Mantener el cursor centrado en los saltos largosmap("n", "<C-d>", "<C-d>zz", { desc = "Media pantalla abajo, centrado" })map("n", "<C-u>", "<C-u>zz", { desc = "Media pantalla arriba, centrado" })map("n", "n", "nzzzv", { desc = "Siguiente coincidencia, centrada" })map("n", "N", "Nzzzv", { desc = "Anterior coincidencia, centrada" })-- Pegar encima de una selección sin perder lo copiado (lección 1.2)map("x", "<leader>p", [["_dP]], { desc = "Pegar sin sobrescribir el registro" })-- Puntos de deshacer más finos al escribir (lección 1.8)map("i", "<Space>", "<Space><C-g>u", { desc = "Punto de deshacer" })map("i", "<CR>", "<C-g>u<CR>", { desc = "Punto de deshacer" })Tres merecen atención:
n → nzzzv. Cada coincidencia queda centrada (zz) y con los pliegues abiertos (zv). Es exactamente el arreglo del que hablaba la lección 1.7: sin el zv, buscar dentro de un fichero plegado te deja a ciegas.
<leader>p → "_dP, y solo en modo Visual. Manda lo sustituido al agujero negro y conserva tu copia, para poder pegar lo mismo en cinco sitios seguidos. Fíjate en que el modo es x y no v: v incluiría el modo Selección, donde no lo quieres.
Los dos de <C-g>u son los de la lección 1.8, con el orden distinto que ya sabes que no es un descuido.
Estos cuatro <C-hjkl> son negociables
Funcionan perfectamente hoy. Pero son cuatro teclas de las buenas, y hay plugins que las quieren: los de navegación entre paneles de un multiplexor, y algunos de completado.
No es un problema mientras seas consciente. Si algún día instalas algo que las pida, sustituyes estas cuatro líneas por las suyas —no conviven, una reemplaza a la otra— y :verbose nmap <C-h> te dirá quién la tiene.
Automatismos
Los autocomandos ejecutan algo cuando pasa un evento. Cuatro que se ganan el sitio:
local aug = vim.api.nvim_create_augrouplocal au = vim.api.nvim_create_autocmd-- Resaltar brevemente lo que copias: confirma qué has cogidoau("TextYankPost", { group = aug("resaltar_yank", { clear = true }), callback = function() vim.hl.on_yank({ timeout = 200 }) end,})-- Volver a donde estabas la última vez que abriste el ficheroau("BufReadPost", { group = aug("recordar_posicion", { clear = true }), callback = function() local marca = vim.api.nvim_buf_get_mark(0, '"') if marca[1] > 0 and marca[1] <= vim.api.nvim_buf_line_count(0) then pcall(vim.api.nvim_win_set_cursor, 0, marca) end end,})-- Conservar los pliegues manuales entre sesiones (lección 1.7)local vistas = aug("vistas", { clear = true })au("BufWinLeave", { group = vistas, pattern = "?*", command = "silent! mkview" })au("BufWinEnter", { group = vistas, pattern = "?*", command = "silent! loadview" })El group con clear = true no es opcional
Sin él, cada vez que recargues la configuración se registra otro autocomando igual, y acabas con el mismo callback ejecutándose cinco veces por evento. Es la causa número uno de configuraciones que se vuelven lentas sin motivo aparente, y es invisible: nada falla, solo va peor.
Con el grupo, clear = true borra los anteriores del mismo grupo antes de registrar el nuevo. Ejecuta :autocmd resaltar_yank para ver cuántos tienes; si hay más de uno, ahí está el fallo.
Los grupos los estudiarás a fondo en el Nivel 9.
Lo que NO conviene automatizar todavía
Un clásico de las configuraciones copiadas es borrar los espacios finales al guardar. Suena limpio y produce diffs de Git llenos de líneas que tú no tocaste, en proyectos donde nadie te lo ha pedido.
El formateo al guardar se monta en el Nivel 3, respetando la configuración del proyecto y con :undojoin para que un solo u deshaga tu edición y el formateo juntos. Hasta entonces, mejor no tocar ficheros ajenos por tu cuenta.
Configuración por proyecto
Una pieza que casi nadie conoce y que resuelve un problema real: opciones que solo valen para este repositorio —una indentación distinta, un makeprg concreto—.
vim.bo.shiftwidth = 4vim.o.makeprg = "cargo test"Se activa con vim.o.exrc = true en tu configuración. A partir de ahí, Neovim busca .nvim.lua, .nvimrc o .exrc en el directorio actual y en sus padres, y como ejecutar código de un repositorio ajeno es exactamente lo que suena, la primera vez te pregunta si confías en el fichero. Lo apruebas con :trust, queda registrado por su contenido, y si el fichero cambia vuelve a preguntar.
Cuando algo falle
Y va a fallar. Estos cinco comandos son el orden en que se busca:
| Comando | Responde |
|---|---|
nvim --clean |
¿es mi configuración o es el editor? |
:messages |
¿qué error salió al arrancar y se fue de la pantalla? |
:verbose set opcion? |
¿quién puso esta opción, y en qué fichero y línea? |
:verbose nmap <tecla> |
¿quién se ha quedado con esta tecla? |
:checkhealth vim.deprecated |
¿estoy usando API que ya no toca? |
Los dos :verbose son los que más se agradecen y casi nadie conoce: te dicen el fichero y la línea exactos donde se puso lo que estás mirando. Con una configuración propia son útiles; con plugins instalados son la diferencia entre depurar y adivinar.
Y para recargar sin cerrar: :restart, de la lección 0.2. Ten en cuenta que no restaura los buffers, así que si estabas a media faena, :mksession! Session.vim | restart source Session.vim.
Tu config mínima, viva
- Crea la estructura y los tres ficheros. Escribe el
init.luacon el leader y sus tresrequire, todavía sin plugins. - Reinicia con
:restarty comprueba con:messagesque no hay ningún error. - Comprueba que las opciones están puestas:
:set relativenumber? inccommand? undofile?. - Busca algo con
/y limpia el resaltado conEsc. Después escribe un:%ssin pulsarEntery observa la ventana de previsualización que te acaba de darinccommand. - Copia una línea y observa el resaltado del yank.
- Cierra el fichero, ábrelo de nuevo, y comprueba dos cosas: que vuelves a la misma línea y que
usigue deshaciendo lo de antes. - Ejecuta
:Cfiltersin argumentos. Si el comando existe, elpackaddfuncionó. - Usa
:verbose set scrolloff?y comprueba que te señala tu propiooptions.lua, con su número de línea. - Cambia a propósito una línea por algo inválido, reinicia, y observa cómo Neovim arranca a medias y te lo dice. Arréglalo. Ese susto, hoy y controlado, vale por tres del mes que viene.
- Y la comprobación de fondo: recorre tu
options.lualínea a línea y, para cada una, di en voz alta qué compra. La que no sepas explicar, bórrala o búscala con:h 'opcion'.