Saltar al contenido

vim.opt y los ámbitos: global, buffer, ventana y global-local

Cada opción de Neovim declara su ámbito en el núcleo, y ese ámbito decide si tu asignación afecta a todo el editor, a un buffer o a una ventana. El mapa completo de las puertas de acceso —vim.opt y sus variantes, vim.o, vim.bo, vim.wo, vim.go y nvim_set_option_value—, el objeto Option con sus métodos, y el lugar correcto de cada opción en tu configuración.

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.
  • Reproducir con las envolturas de Lua la semántica exacta de :set, :setlocal y :setglobal.
  • Explicar por qué vim.opt devuelve un objeto Option y qué comprobación clásica se rompe por ello.
  • Usar el índice doble vim.wo[win][buf] y saber cuándo bajar a nvim_set_option_value.
  • Decidir dónde escribir cada opción para que se aplique a los archivos correctos.

El ámbito es una propiedad de la opción

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. makeprg, 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 con nvim_get_option_info2, cosa que haremos de forma sistemática en la lección 8.5. Por ahora basta con retener la consecuencia práctica.

Seis puertas, tres semánticas

Todas las envolturas de Lua reproducen la semántica de uno de los tres comandos ex, y esa equivalencia es la forma más rápida de recordarlas:

Comando ex Familia objeto Familia escalar Qué toca
:set vim.opt vim.o el valor efectivo: local del contexto actual y global a la vez
:setlocal vim.opt_local vim.bo · vim.wo[w][0] solo el valor local
:setglobal vim.opt_global vim.go solo el valor global

La fila de :set esconde el dato que resuelve el misterio del segundo archivo: para una opción de buffer o de ventana, la asignación normal establece a la vez el valor local del contexto actual y el valor global que heredarán los contextos nuevos. Por eso vim.opt.tabstop = 2 en el init.lua parece funcionar —fijó el global, que sirve de plantilla— hasta que un ftplugin posterior escribe su propio valor local para ese tipo de archivo, y con razón, porque sabe más que tú sobre las convenciones de ese lenguaje.

Para las opciones global-local la misma fila cambia de significado: la asignación normal toca solo el valor global. Es exactamente lo que documenta :help global-local con makeprg: si una ventana tiene :setlocal makeprg=perlmake, un :set makeprg=gmake posterior cambia el global y esa ventana ni se entera, porque su local definido sigue ganando. Para volver al global hay que borrar el local: :setlocal makeprg= en las de cadena, :setlocal undolevels< en las numéricas y booleanas.

Las familias escalares tienen sus matices propios, y aquí conviene precisión porque casi todas las guías los cuentan mal:

  • vim.o se comporta como :set: lee y escribe el valor efectivo como cadena, número o booleano, resolviendo contra el buffer y la ventana actuales.
  • vim.bo se comporta como :setlocal sobre opciones de buffer, y admite índice: vim.bo[bufnr].shiftwidth = 4 alcanza un buffer que no es el actual.
  • vim.wo se comporta como :set para las opciones de ventana normales —fija el valor de esa ventana y su copia «global de ventana», la que heredará el siguiente buffer que muestres ahí—. Solo actúa como :setlocal con las global-local o cuando usas el índice doble: vim.wo[winid][0].spell = false equivale a :setlocal nospell en esa ventana, atado al buffer que muestra ahora. De momento solo se admite 0 como segundo índice (el buffer actual de esa ventana), y por eso en este curso lo verás siempre como vim.wo[0][0].
  • vim.go se comporta como :setglobal: útil sobre todo con las global-local, para cambiar el valor por defecto sin tocar los locales ya definidos.
LUA
vim.bo[bufnr].shiftwidth = 4        -- ese buffer, como :setlocalvim.wo[winid].wrap = false          -- esa ventana, como :set (deja plantilla)vim.wo[0][0].number = false         -- esta ventana Y este buffer, como :setlocal-- El acceso completo esta en la API, con ambito explicitovim.api.nvim_set_option_value("shiftwidth", 4, { buf = bufnr })local v = vim.api.nvim_get_option_value("wrap", { win = winid })vim.api.nvim_set_option_value("makeprg", "gmake", { scope = "global" })

Ahí abajo, nvim_set_option_value y nvim_get_option_value son la capa de la API 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 pareja quita toda ambigüedad porque obliga a declarar exactamente qué se está tocando.

vim.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 Option: 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 funciona hasta que dos plugins quieren añadir su parte y el tercero se come una coma.

LUA
-- Sin vim.opt: cirugia de cadenas, fragil y no componiblevim.o.listchars = "tab:> ,trail:-,nbsp:+"vim.o.shortmess = vim.o.shortmess .. "c"-- Con vim.opt: estructura de datos real y operaciones componiblesvim.opt.listchars = { tab = "> ", trail = "-", nbsp = "+" }vim.opt.shortmess:append("c")vim.opt.wildignore:append({ "*.o", "*.pyc", "node_modules/**" })vim.opt.wildignore:prepend("*.tmp")vim.opt.path:remove("/usr/include")vim.opt.formatoptions:remove({ "c", "r", "o" })-- Y para leer como estructura, el metodo :get()vim.print(vim.opt.listchars:get())   -- { tab = "> ", trail = "-", nbsp = "+" }

append, prepend y remove reproducen :set+=, :set^= y :set-=; el objeto admite incluso los operadores +, ^ y - como azúcar de lo mismo. 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.

Escribe con vim.opt, lee con vim.o

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

El lugar correcto para cada opción

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 —el que montaste en la lección 2.1—. 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 FileType si prefieres tenerlo todo en un único archivo. El directorio after importa: garantiza que tu valor se aplica después del que trae la distribución del propio Neovim.

LUA
-- after/ftplugin/python.lua  -- se ejecuta por cada buffer de Pythonvim.opt_local.shiftwidth = 4vim.opt_local.expandtab = truevim.opt_local.colorcolumn = "88"-- Equivalente con autocomando, util si prefieres un unico archivovim.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. De la maquinaria de autocomandos —grupos, patrones, orden de disparo— se ocupa entero el Nivel 9.

Hay un tercer caso que conviene conocer aunque se use poco: las opciones de ventana escritas en un ftplugin 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, la ventana nueva hereda las opciones locales de la original —al dividir se copian—, pero si en una ventana cualquiera abres después ese mismo archivo, el ftplugin no se vuelve a ejecutar porque el buffer ya tenía su tipo asignado. Para opciones de ventana que dependan del contenido mostrado, el evento adecuado es de ventana, no de tipo de archivo:

LUA
-- El numero de linea depende del buffer mostrado, pero es opcion de ventanavim.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.

Cuatro opciones que parecen de buffer y no lo son (o al revés)

filetype y syntax son de buffer pero no se copian a los buffers nuevos: las asignan los autocomandos de detección. scroll y winfixheight son de ventana pero no viajan al dividir. Y foldmethod es de ventana aunque casi todo el mundo la escriba como si fuera de buffer: dos ventanas sobre el mismo archivo pueden plegar de forma distinta. Cuando una opción se comporte «raro», consulta su ámbito antes de sospechar del editor: :lua= vim.api.nvim_get_option_info2('foldmethod', {}).scope.

Una opción no es una variable, es una consulta con contexto

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.

Responde buf, win o global desde los metadatos del núcleo. Si responde buf o win, su sitio no es el init.lua sino after/ftplugin/ o un autocomando.

El índice doble es el equivalente exacto de :setlocal nospell: ni el global de la opción ni la copia de plantilla de la ventana cambian. Con vim.wo.spell = false a secas, el siguiente buffer que muestres en esta ventana también llegaría sin corrector.

El prefijo :verbose añade el archivo y la línea donde se estableció el valor vigente. Si señala un ftplugin del runtime de Neovim, tu corrección va en after/ftplugin/, no en init.lua.

append opera sobre la lista real; la reasignación completa habría borrado las entradas ajenas. Compruébalo antes y después con :lua= vim.opt.wildignore:get().

Audita el ámbito de tu configuración

  1. Elige cinco opciones de tu init.lua y clasifica cada una por ámbito antes de comprobarlo. Verifica con nvim_get_option_info2 y anota cuántas acertaste.
  2. Busca en tu configuración alguna opción de buffer o de ventana escrita en el archivo raíz. Muévela a after/ftplugin/ con vim.opt_local y comprueba que sigue aplicándose… y que ya no contamina otros tipos de archivo.
  3. Escribe una condición con vim.opt sobre una opción booleana y observa que se cumple siempre. Corrígela de las dos formas posibles: :get() y vim.o.
  4. Reproduce el experimento de :help global-local: pon un makeprg local en una ventana, cambia el global desde otra y comprueba con :setlocal makeprg? y :setglobal makeprg? qué valor ve cada una. Después borra el local con :setlocal makeprg= y repite.
  5. Ejecuta :lua vim.wo[0][0].number = false, abre otro buffer en esa misma ventana y explica qué valor de number ves y por qué. Repite con vim.wo.number = false y compara.