vim.opt y las opciones: global, buffer y ventana
Las opciones de Neovim no son un espacio plano de nombres y valores: cada una tiene un ámbito declarado en el núcleo, y ese ámbito decide si tu asignación afecta a todo el editor, a un buffer o solo a la ventana activa.
Existe un rito de paso en la vida de toda configuración de Neovim: alguien pone vim.opt.tabstop = 2 en su init.lua, comprueba que funciona, abre un segundo archivo y descubre que la indentación es otra. La conclusión habitual es que Neovim es caprichoso. La conclusión correcta es que el ámbito de una opción no lo decide quien la escribe, lo decide la opción misma, y que el sistema estaba comportándose exactamente como está documentado. Entender la taxonomía de ámbitos y la diferencia entre las distintas puertas de acceso convierte esa clase de misterio en una consulta de treinta segundos.
- Clasificar cualquier opción por su ámbito real: global, de buffer, de ventana o global-local.
- Explicar por qué
vim.optdevuelve un objeto y qué comprobación clásica se rompe por ello. - Distinguir con precisión
vim.opt,vim.opt_local,vim.opt_global,vim.o,vim.boyvim.wo. - Decidir dónde escribir cada opción para que se aplique a los archivos correctos.
El ambito es una propiedad de la opcion
Neovim clasifica cada opción en uno de tres ámbitos, más un cuarto caso híbrido. Las globales viven una sola vez en todo el editor: ignorecase, hidden, updatetime, mouse. Las de buffer tienen un valor por cada buffer cargado: tabstop, expandtab, filetype, commentstring. Las de ventana tienen un valor por cada ventana abierta: number, wrap, cursorline, foldmethod.
El cuarto caso, las global-local, es el que genera más confusión: guardan un valor global que sirve de valor por defecto y admiten además un valor local que lo sustituye cuando está definido. undolevels y statusline son ejemplos habituales. Cuando el valor local está sin definir, la consulta cae al global; cuando lo defines, el global deja de importar para ese contexto.
Esta clasificación no es folclore ni convención documental: está codificada en el núcleo y puedes consultarla en tiempo de ejecución, cosa que haremos en la lección quinta. Por ahora basta con retener la consecuencia práctica.
flowchart TB
O[Escribes una opcion] --> Q{Cual es su ambito}
Q -->|global| G[Un unico valor para todo el editor]
Q -->|buffer| B[Un valor por cada buffer cargado]
Q -->|ventana| W[Un valor por cada ventana abierta]
Q -->|global local| H[Valor local si existe y si no el global]
B --> R[Escribela en ftplugin o en un autocomando FileType]
W --> R
G --> I[Escribela una vez en init.lua]
style H fill:#f9e2af,color:#11111b
style R fill:#89b4fa,color:#11111bvim.opt es un objeto, no un valor
Aquí está la diferencia que casi nadie explica bien. vim.o.number devuelve un booleano. vim.opt.number devuelve un objeto de opción: una tabla con metatabla que sabe convertir entre la representación interna, que es siempre una cadena o un número, y una representación cómoda en Lua.
Esa conversión es todo el valor de vim.opt. Las opciones de lista, como wildignore o path, se guardan internamente como cadenas separadas por comas. Las de mapa, como listchars o fillchars, como cadenas de pares clave-valor. Las de banderas, como formatoptions o shortmess, como cadenas de caracteres sueltos. Manipular eso con concatenación de cadenas funciona hasta que dos plugins quieren añadir su parte y el tercero se come una coma.
-- Sin vim.opt: cirugia de cadenas, fragil y no componible
vim.o.listchars = "tab:> ,trail:-,nbsp:+"
vim.o.shortmess = vim.o.shortmess .. "c"
-- Con vim.opt: estructura de datos real y operaciones componibles
vim.opt.listchars = { tab = "> ", trail = "-", nbsp = "+" }
vim.opt.shortmess:append("c")
vim.opt.wildignore:append({ "*.o", "*.pyc", "node_modules/**" })
vim.opt.path:remove("/usr/include")
vim.opt.formatoptions:remove({ "c", "r", "o" })
El precio de esa comodidad es el error clásico del nivel. Como vim.opt.number es una tabla, siempre es verdadera en una condición, tenga la opción el valor que tenga.
if vim.opt.number then end -- SIEMPRE entra: es una tabla
if vim.opt.number:get() then end -- correcto
if vim.o.number then end -- correcto y mas directo
La regla operativa que elimina el problema de raíz: usa vim.opt cuando asignes o modifiques, porque te da tablas y métodos; usa vim.o, vim.bo o vim.wo cuando consultes, porque te dan el valor escalar sin intermediarios. Si necesitas leer una opción de lista como tabla, entonces sí, vim.opt.x:get().
Local, global y el hueco entre ambos
Las tres variantes de vim.opt reproducen exactamente la semántica de los tres comandos ex correspondientes, y esa equivalencia es la forma más rápida de recordarlas.
vim.opt.x = v se comporta como el comando de asignación normal: para una opción local, establece a la vez el valor local del contexto actual y el valor global que heredarán los contextos nuevos. vim.opt_local.x = v se comporta como la asignación local: toca solo el contexto actual. vim.opt_global.x = v toca solo el valor global, dejando intacto el local del contexto actual.
Las familias escalares se reparten igual: vim.o corresponde a la asignación normal, vim.go a la global, y vim.bo y vim.wo a la local de buffer y de ventana respectivamente. Todas admiten forma indexada para actuar sobre un contexto que no es el actual.
vim.bo[bufnr].shiftwidth = 4 -- ese buffer, no el actual
vim.wo[winid].wrap = false -- esa ventana, no la actual
-- El acceso completo esta en la API, con ambito explicito
vim.api.nvim_set_option_value("shiftwidth", 4, { buf = bufnr })
local v = vim.api.nvim_get_option_value("wrap", { win = winid })
Ahí abajo, nvim_set_option_value es lo que sostiene a todas las demás. Su tabla de opciones acepta scope con valor local o global, más buf o win para nombrar el contexto. Cuando una situación se resiste a las abreviaturas, bajar a esta función quita toda ambigüedad porque obliga a declarar exactamente qué se está tocando.
Queda el hueco: una opción de buffer escrita en init.lua solo alcanza al buffer que existía en ese instante. El valor global que se establece de paso sirve como plantilla para los buffers nuevos, pero cualquier plugin de tipo de archivo que se ejecute después lo sobrescribirá para su tipo, y con razón, porque sabe más que tú sobre las convenciones de ese lenguaje.
El lugar correcto para cada opcion
De lo anterior se deduce una asignación de responsabilidades que resuelve el misterio del segundo archivo.
Las opciones globales van en init.lua o en el módulo de opciones que este cargue. Se establecen una vez y no vuelven a tocarse.
Las opciones de buffer y de ventana que dependen del tipo de archivo no van ahí. Van en un archivo bajo after/ftplugin/, que Neovim carga automáticamente cada vez que un buffer adquiere ese tipo, o en un autocomando de tipo de archivo si prefieres tenerlo todo en Lua. El directorio after importa: garantiza que tu valor se aplica después del que trae la distribución del propio Neovim.
-- after/ftplugin/python.lua -- se ejecuta por cada buffer de Python
vim.opt_local.shiftwidth = 4
vim.opt_local.expandtab = true
vim.opt_local.colorcolumn = "88"
-- Equivalente con autocomando, util si prefieres un unico archivo
vim.api.nvim_create_autocmd("FileType", {
pattern = { "markdown", "gitcommit" },
group = vim.api.nvim_create_augroup("opciones_prosa", { clear = true }),
callback = function()
vim.opt_local.wrap = true
vim.opt_local.spell = true
vim.opt_local.linebreak = true
end,
})
Nota el uso deliberado de vim.opt_local dentro del autocomando. Usar vim.opt ahí sería un error de diseño sutil pero real: además de configurar el buffer de Markdown, cambiaría el valor global y contaminaría todos los buffers que se abrieran después.
Hay un tercer lugar que conviene conocer aunque se use poco: los autocomandos de creación de ventana. Las opciones de ventana escritas en un plugin de tipo de archivo se aplican a la ventana que muestra el buffer en ese momento, no a las que lo muestren más tarde. Si divides la pantalla y abres el mismo archivo en la ventana nueva, el plugin de tipo no se vuelve a ejecutar porque el buffer ya tenía su tipo asignado. Para opciones de ventana que dependan del contenido, el evento adecuado es el de entrada en ventana o el de entrada en buffer, no el de tipo de archivo.
-- El numero de linea depende del buffer mostrado, pero es opcion de ventana
vim.api.nvim_create_autocmd({ "BufWinEnter", "WinEnter" }, {
group = vim.api.nvim_create_augroup("numeros_por_ventana", { clear = true }),
callback = function()
local especial = vim.bo.buftype ~= ""
vim.opt_local.number = not especial
vim.opt_local.relativenumber = not especial
end,
})
Ese patrón resuelve el desajuste más común entre ámbitos: un dato que se decide mirando el buffer pero que se escribe en la ventana. Reconocerlo es exactamente saber leer la columna de ámbito del catálogo de opciones, que es lo que haremos de forma sistemática en la lección quinta.
El modelo mental que resuelve de golpe todas las confusiones de esta lección es dejar de pensar en las opciones como casillas de memoria y empezar a pensarlas como consultas resueltas contra un contexto. Cuando Neovim necesita saber el ancho de tabulación no lee una variable global: pregunta cuál es el buffer activo y resuelve el valor para ese buffer. Cuando necesita saber si debe ajustar líneas largas, pregunta cuál es la ventana activa. El valor global no es el valor de la opción, es el valor por defecto que se copia al crear un contexto nuevo, y por eso escribirlo tarde no arregla nada de lo ya creado y por eso escribirlo pronto no protege de nada de lo que venga después. De aquí se deduce todo lo demás sin memorizar ninguna tabla. Se deduce que el ámbito no depende de dónde escribas la línea sino de qué opción sea, porque el contexto de resolución es una propiedad del núcleo. Se deduce que las opciones global-local son simplemente consultas con dos escalones de resolución en lugar de uno. Se deduce que el directorio de tipos de archivo existe porque es el único momento del ciclo de vida en que un contexto de buffer ya existe y todavía nadie ha empezado a editarlo. Y se deduce, finalmente, la disciplina que separa una configuración robusta de una acumulación de parches: antes de escribir cualquier opción, pregúntate contra qué contexto se resolverá y si ese contexto existirá cuando tu línea se ejecute. Esa única pregunta previene más fallos que cualquier lista de recetas.
- Elige cinco opciones de tu
init.luay clasifica cada una por ámbito antes de comprobarlo. Después verifica con la documentación y anota cuántas acertaste. - Busca en tu configuración alguna opción de buffer o de ventana escrita en el archivo raíz. Muévela a
after/ftplugin/o a un autocomando y comprueba que sigue aplicándose. - Escribe una condición con
vim.optsobre una opción booleana y observa que se cumple siempre. Corrígela de las dos formas posibles. - Modifica una opción de lista con
appendyremoveen lugar de reasignarla entera. Comprueba con:verbose setquién más la había tocado antes que tú. - Establece una opción global-local con
vim.opt_local, abre una ventana nueva y explica qué valor ves y por qué.