Variables y estado: vim.g, vim.b, vim.w, vim.t, vim.v y vim.env
Los diccionarios de variables de Neovim parecen tablas de Lua y no lo son: son proxies sobre estructuras del núcleo, cada lectura devuelve una copia y mutar una tabla dentro de vim.g no tiene ningún efecto. Los cinco ámbitos con su tiempo de vida, el entorno del proceso con vim.env, y cuándo el estado de tu config pertenece a un módulo de Lua y no al editor.
Una configuración madura de Neovim acaba teniendo estado propio: si el formateo automático está activo, cuál es la raíz del proyecto actual, qué perfil de tema se aplicó, si ya se inicializó tal integración. La pregunta de dónde poner ese estado parece trivial y no lo es, porque Neovim ofrece cinco sitios distintos y ninguno es un simple almacén. Cada uno tiene un tiempo de vida propio, una política de visibilidad propia y un coste de traducción propio. Elegir bien es lo que hace que el estado se limpie solo cuando debe limpiarse; elegir mal produce esa clase de fallo que solo aparece tras dos horas de sesión y que nadie consigue reproducir.
- Describir los diccionarios
vim.g,vim.b,vim.w,vim.tyvim.vy su tiempo de vida. - Explicar por qué son proxies con copia y qué operaciones dejan de funcionar por ello.
- Leer contexto de
vim.v—count1,event,shell_error— sabiendo qué es de solo lectura. - Distinguir
vim.envdel entorno que heredan los procesos lanzados desde el editor. - Decidir con criterio si un dato pertenece a un diccionario del editor o a un módulo de Lua.
Cinco diccionarios y un entorno
vim.g expone las variables globales del editor, las que en Vimscript se escriben con el prefijo g:. Es el bus de interoperación por excelencia: ahí viven mapleader, los indicadores que desactivan plugins incorporados como loaded_netrwPlugin y prácticamente toda la configuración de los plugins escritos en Vimscript.
vim.b guarda variables por buffer, vim.w por ventana y vim.t por pestaña. Los tres admiten forma indexada, de modo que vim.b[bufnr].clave alcanza un buffer que no es el actual, y vim.w[winid].clave una ventana concreta por su identificador.
vim.v expone las variables predefinidas v: del editor. No es un sitio donde guardar sino donde leer contexto: vim.v.count y vim.v.count1 traen el contador que precedió a un mapeo, vim.v.shell_error el código de salida del último comando externo, vim.v.event los datos del autocomando en curso, vim.v.register el registro elegido. Casi todas son de solo lectura —asignarlas falla con un error de variable de solo lectura—, con un puñado de excepciones documentadas y deliberadas como v:errmsg, v:warningmsg, v:hlsearch o v:searchforward, que existen precisamente para que un script pueda escribirlas.
vim.env es distinto de los anteriores: no es un diccionario del editor sino el entorno del proceso. Escribir en él modifica el entorno real de Neovim, y por tanto el que heredarán los procesos que el editor lance después: terminales integrados, servidores de lenguaje, y todo lo que arranques con vim.system en el Nivel 13.
vim.g.mapleader = " "vim.g.loaded_netrwPlugin = 1 -- desactiva un plugin incorporadovim.b.disable_autoformat = true -- solo este buffervim.b[otro_buf].raiz_proyecto = "/srv/app"vim.env.PATH = vim.fn.stdpath("data") .. "/mason/bin:" .. vim.env.PATHvim.env afecta a todo lo que lances después
Modificar vim.env.PATH es la forma correcta de que los servidores de lenguaje y los terminales integrados encuentren binarios instalados por un gestor de herramientas como Mason (lección 3.1). También significa que un error ahí se propaga a todos los procesos hijos y produce fallos que parecen del servidor y son tuyos. Y no confundas dirección: vim.env empuja hacia los hijos, pero cambiarlo no altera el shell desde el que arrancaste Neovim, que es otro proceso.
Los proxies copian, y eso lo cambia todo
Ninguno de esos nombres es una tabla de Lua. Son tablas vacías con metatabla: leer una clave dispara una llamada al núcleo que serializa el valor interno y construye una tabla nueva; escribir dispara la conversión inversa. Es la misma frontera de tipos que cruzaste con vim.fn en la lección 8.1, ahora en dirección contraria. La consecuencia desconcierta a todo el mundo la primera vez.
vim.g.config = { tema = "oscuro", nivel = 3 }vim.g.config.tema = "claro" -- NO tiene efectoprint(vim.g.config.tema) -- sigue siendo "oscuro"-- El patron correcto: leer, modificar la copia, reescribir enterolocal c = vim.g.configc.tema = "claro"vim.g.config = cNo hay mutación en el sitio porque no hay sitio compartido. De la misma frontera se deducen tres restricciones más.
Primera: solo cruzan los valores que el sistema de tipos del editor sabe representar. Números, cadenas, booleanos, listas y diccionarios pasan sin problema. Funciones de Lua, metatablas, valores de usuario y referencias circulares no. Intentar guardar una función en vim.g no produce un fallo elegante en el punto de uso, sino un error en el punto de asignación, que al menos es honesto.
Segunda: la ambigüedad de la tabla vacía y de los nulos que ya conoces de vim.fn aplica aquí igual —{} cruza como lista, vim.empty_dict() como diccionario, y un v:null que venga de vuelta llega como vim.NIL, no como nil.
Tercera, de rendimiento: leer una tabla grande de vim.g dentro de un bucle la reconstruye entera en cada vuelta. Rara vez importa, pero si lo haces en una función que corre en cada pulsación, el coste deja de ser teórico. Lee una vez fuera del bucle.
Borrar, en cambio, es simétrico y simple: asignar nil elimina la variable, como haría :unlet.
El tiempo de vida como criterio de diseño
La pregunta de dónde guardar algo se responde casi siempre mirando cuándo debe dejar de existir. Ese es el criterio dominante, muy por encima de la comodidad sintáctica.
Un dato que describe un buffer concreto —si el formateo al guardar está desactivado ahí, cuál es la raíz del proyecto de ese archivo, qué cliente de lenguaje lo atiende— pertenece a vim.b. La razón no es estética: cuando el buffer se elimina, la variable desaparece con él. Has obtenido invalidación de caché gratuita y sin escribir una línea de limpieza.
Con vim.w hay un matiz que sorprende: al dividir una ventana, las opciones locales de ventana se copian a la nueva —lo viste en la lección 8.2—, pero no des por hecho que las variables w: viajen con la misma garantía. Trata siempre vim.w como una caché reconstruible y nunca como la única copia de algo que no puedas recalcular.
vim.v, ya dicho, es para leer. Su utilidad está en escribir mapeos y autocomandos que se comportan según el contexto:
-- Un atajo que respeta el contador tecleado por el usuario.-- Ojo con la tecla: s y S son de flash.nvim en esta guia (leccion 3.3).vim.keymap.set("n", "<leader>zj", function() vim.cmd("normal! " .. vim.v.count1 .. "j")end, { desc = "Bajar respetando el contador" })Una nota sobre vim.g que conviene interiorizar pronto: es un espacio de nombres plano y compartido con todos los plugins instalados. No hay ámbitos, no hay prefijos obligatorios y no hay aviso de colisión. Si guardas ahí una clave con un nombre genérico, estás apostando a que ningún plugin presente ni futuro elija el mismo nombre. Cuando no tengas más remedio que usarlo, prefija siempre con algo inequívocamente tuyo.
Dónde vive el estado de tu configuración
Llegamos a la recomendación que ordena todo lo anterior: el estado propio de tu configuración pertenece a un módulo de Lua, no a vim.g.
Un módulo es una tabla devuelta por un archivo bajo lua/, que require guarda en caché tras la primera carga —la mecánica exacta la viste en la lección 7.4—. Todo lo que ese archivo declare como local es privado; lo que exponga en la tabla es público. No hay serialización, no hay conversión de tipos, no hay colisión de nombres con otros plugins, y puedes guardar funciones, metatablas y closures sin restricción alguna.
-- lua/mi/estado.lualocal M = {}local cache = {} -- privado de verdadfunction M.raiz(bufnr) bufnr = bufnr or vim.api.nvim_get_current_buf() if cache[bufnr] then return cache[bufnr] end cache[bufnr] = vim.fs.root(bufnr, { ".git", "Makefile" }) return cache[bufnr]endfunction M.olvidar(bufnr) cache[bufnr] = nil endreturn MEse módulo tiene un problema que vim.b no tendría: la caché no se limpia sola. La solución no es abandonar el módulo, sino conectarlo al ciclo de vida con un autocomando que llame a la función de olvido cuando el buffer desaparece —los autocomandos son el tema del Nivel 9, pero el patrón cabe en cinco líneas:
vim.api.nvim_create_autocmd({ "BufDelete", "BufWipeout" }, { group = vim.api.nvim_create_augroup("invalidar_raiz", { clear = true }), callback = function(args) require("mi.estado").olvidar(args.buf) end,})Y ese es justamente el punto: el módulo te da control explícito sobre la política de invalidación, mientras que los diccionarios del editor te dan una política implícita y fija. Elige según cuál de las dos necesites. Reserva vim.g para lo que tiene que ser visible desde fuera de Lua: la tecla líder, los indicadores que leen los plugins de Vimscript, las opciones que un plugin documenta como variables globales. Es un bus de interoperación, no un armario.
Queda un cuarto tiempo de vida que no aparece en ningún diccionario: el de los datos que deben sobrevivir al cierre del editor. Ni vim.g ni un módulo sirven, porque ambos mueren con el proceso. Lo que corresponde ahí es el disco, y vim.fn.stdpath te da la ruta normalizada para cada propósito: "config" para lo que escribes tú, "data" para lo que instalan las herramientas, "state" para estado de sesión, "cache" para lo desechable. Escribir un archivo propio en stdpath("state") y leerlo al arrancar es un patrón perfectamente respetable; guardarlo junto a tu configuración versionada no lo es, porque mezcla lo que escribes tú con lo que escribe el programa.
El módulo cacheado y el desarrollo
require guarda la tabla devuelta en package.loaded, de modo que volver a pedir el módulo no vuelve a ejecutar el archivo y el estado sobrevive. Eso es deseable en funcionamiento normal y molesto mientras desarrollas, porque tus cambios no se ven hasta reiniciar. Durante el desarrollo, package.loaded["mi.estado"] = nil antes del require fuerza la relectura; en producción, no lo hagas nunca, porque perderías precisamente el estado que querías conservar.
El estado sin política de muerte es una fuga
Toda variable que se crea plantea, aunque nadie la formule, la pregunta de cuándo dejará de ser cierta. Las configuraciones que envejecen mal son casi siempre las que guardaron estado sin responderla: un indicador global que se puso a cierto en el primer buffer de un tipo y que sigue a cierto tres horas después en un contexto donde ya no significa nada, una caché de raíz de proyecto que se calculó en un monorepo y contamina el siguiente archivo, un contador global que dos autocomandos incrementan sin saber el uno del otro. Y la observación que ordena todo el diseño es esta: los diccionarios de Neovim no son cinco almacenes intercambiables sino cinco políticas de invalidación ya escritas. vim.b no es un lugar donde guardar cosas de un buffer, es la afirmación de que ese dato deja de ser verdad cuando el buffer deja de existir. vim.w afirma lo mismo respecto de una ventana, y vim.t respecto de una pestaña. Elegir el diccionario es, literalmente, escribir la condición de caducidad del dato sin escribir código de limpieza, que es la única forma de código de limpieza que no se olvida nunca de ejecutarse. Por eso la pregunta correcta antes de guardar algo no es dónde cabe ni qué resulta más cómodo de escribir, sino cuándo deja de ser cierto. Si la respuesta coincide con la vida de un buffer, de una ventana o de una pestaña, el editor ya tiene el sitio hecho. Si no coincide con ninguna, entonces necesitas un módulo de Lua y necesitas escribir tú la política, explícitamente, porque nadie va a hacerlo por ti.
Sigue valiendo 1. La segunda línea mutó una copia huérfana. Reescribe con el patrón leer-modificar-reasignar y repite la consulta.
Tras recrear el buffer la variable ya no existe: esa es la política de invalidación de vim.b funcionando sola. El equivalente con vim.g te habría dejado el indicador colgado para siempre.
El terminal integrado es un proceso hijo: nace con el entorno que tenga Neovim en ese momento. Todo lo que arranques con vim.system (Nivel 13) hereda igual.
Sin contador tecleado vale 1, que es lo que lo hace cómodo: vim.v.count valdría 0. Es la diferencia entre multiplicar por lo tecleado y multiplicar por cero.
Da a cada dato su tiempo de vida
- Inventaria el estado que guarda tu configuración hoy. Para cada dato escribe en una frase cuándo deja de ser cierto, y solo entonces decide dónde debería vivir.
- Guarda una tabla en
vim.g, muta un campo directamente y comprueba que no cambia nada. Reescribe la operación con el patrón de lectura, modificación y reescritura. - Sustituye un indicador global que solo tenga sentido por buffer por su equivalente en
vim.b. Elimina el código de limpieza que ya no hace falta. - Escribe el módulo de caché por buffer de esta lección y conéctalo al autocomando de invalidación. Ábrete tres proyectos distintos y comprueba con
:lua= require('mi.estado').raiz()que cada buffer resuelve su raíz, y que la tabla no crece tras cerrar buffers. - Añade un directorio al
PATHconvim.env, abre un terminal integrado y verifica que el proceso hijo lo heredó. Comprueba después, desde otra terminal del sistema, que tu shell original no cambió. - Intenta guardar una función en
vim.gy lee con calma el error. Guárdala después en un módulo y explica por qué ahí sí se puede.