Saltar al contenido

Lua dentro de Neovim: ejecutar, inspeccionar y el puente con Vimscript

Las formas de ejecutar Lua en el editor, en un archivo y en modo intérprete; la inspección de valores con vim.print y vim.inspect; y el puente bidireccional con Vimscript mediante vim.cmd, vim.fn y v:lua, con las conversiones de tipos que se pierden al cruzar.

Sabes Lua y sabes cómo se cargan los módulos. Falta la frontera: dónde termina el lenguaje y empieza el editor. Neovim ejecuta Lua en varios contextos distintos, imprime valores de tres maneras diferentes y mantiene un puente bidireccional con treinta años de Vimscript que no piensa desaparecer. Esta lección cierra el nivel enseñándote a moverte por esa frontera sin perder información por el camino.

  • Ejecutar Lua desde el editor, desde un archivo y desde la línea de comandos.
  • Inspeccionar valores con vim.print, vim.inspect y la lista de mensajes.
  • Llamar a Vimscript desde Lua con vim.cmd y vim.fn, y a Lua desde Vimscript con v:lua.
  • Anticipar las conversiones de tipos que ocurren al cruzar el puente, y sus tres trampas.

Cuatro maneras de ejecutar Lua

La primera es el comando :lua, que evalúa un fragmento en el contexto del editor vivo. Su variante más útil lleva un signo igual: :lua =expr imprime la expresión pasada por vim.inspect, y la abreviatura :=expr hace exactamente lo mismo con tres teclas menos. Es el equivalente más cercano a una consola interactiva, y la herramienta con la que has verificado media docena de afirmaciones de este nivel.

VIM
:lua vim.o.number = true:lua =vim.bo.filetype:=vim.fn.getcwd():lua for _, b in ipairs(vim.api.nvim_list_bufs()) do print(b) end

Cada comando :lua compila su propio chunk —su propia closure—, así que las local de un comando no existen en el siguiente: por eso los drills del nivel usan globales cuando algo debe sobrevivir entre comandos. Dos variantes completan la familia. Con un rango, :{range}lua ejecuta líneas del buffer como Lua: seleccionas un bloque en Visual, escribes :lua, y acabas de evaluar tu propio archivo a trozos, sin guardarlo. Y para incrustar varias líneas dentro de un archivo de Vimscript existe el documento aquí:

VIM
lua << EOFlocal mapa = { n = "normal", i = "insercion" }vim.print(mapa)EOF

La segunda forma es ejecutar un archivo entero. :luafile ruta.lua lo hace explícitamente, y :source ruta.lua llega al mismo sitio porque distingue por la extensión. Ninguna de las dos pasa por package.loaded, así que el archivo se reejecuta cada vez: son la herramienta natural para iterar sobre un script suelto —:luafile % mientras lo editas—, no para módulos, que ya viste en la 7.4 que viajan por require y su caché.

La tercera es el modo intérprete: nvim -l script.lua arranca Neovim sin interfaz, ejecuta el script y sale. Tienes toda la API, el bucle de eventos y vim.fn disponibles, pero sin dibujar una sola celda de pantalla. Es la manera canónica de escribir herramientas y tests que necesiten el editor como biblioteca, y volverá en el Nivel 13 cuando lances procesos externos.

Shell
nvim -l ./scripts/generar.lua --salida build/nvim --headless -c "lua require('mi.tarea').run()" -c "qa!"

La cuarta la conoces desde el Nivel 2 y es la que de verdad importa: los archivos que Neovim carga solo. El init.lua, todo lo que él requiera, y los directorios plugin/, ftplugin/ y after/ del runtimepath, que se ejecutan automáticamente en el orden que estudiaste en la lección 2.1.

Ver lo que está pasando

Depurar configuración es sobre todo mirar valores, y aquí hay una jerarquía clara. print convierte con tostring, así que una tabla se muestra como table: 0x55f..., un dato inútil. vim.inspect serializa cualquier valor a una cadena legible y recursiva —tablas anidadas incluidas— y la devuelve, sin imprimir nada. vim.print junta las dos mitades: imprime sus argumentos ya serializados y los devuelve intactos, lo que permite intercalarlo en medio de una expresión sin alterar el flujo.

LUA
print(vim.bo)                       --> table: 0x7f9c...   inserviblevim.print(vim.bo.filetype, vim.bo.shiftwidth)vim.print(vim.api.nvim_get_mode())  --> { blocking = false, mode = "n" }local texto = vim.inspect(config, { depth = 2, newline = " ", indent = "" })vim.notify(texto, vim.log.levels.DEBUG)

Los mensajes que emiten estas funciones se acumulan y se consultan con :messages. Cuando algo falla durante el arranque, ese es el primer sitio donde mirar, junto con :checkhealth. Y para avisos dirigidos al usuario, vim.notify es la vía correcta, porque respeta el nivel de severidad y puede ser sustituida por un plugin de notificaciones sin que tu código cambie.

El truco de intercalar vim.print

Como vim.print devuelve sus argumentos, puedes envolver cualquier subexpresión sin tocar la lógica: local x = vim.print(calcular(a)) sigue asignando lo mismo y, de paso, te enseña el valor. Es lo más parecido a un punto de traza sin depurador, y quitarlo es borrar diez caracteres.

El puente hacia Vimscript

Neovim no reescribió treinta años de Vimscript: lo mantiene y expone caminos en ambas direcciones. Conviene ver el mapa entero antes de los detalles:

LUA
vim.cmd("colorscheme habamax")                  -- forma de cadenavim.cmd.colorscheme("habamax")                  -- forma estructuradavim.cmd({ cmd = "write", bang = true })         -- control totallocal ancho = vim.fn.strdisplaywidth("añadir")  -- funcion de Vimscriptlocal hay   = vim.fn.executable("rg") == 1      -- devuelve 0 o 1, no booleanolocal salida = vim.api.nvim_exec2("verbose set shiftwidth?", { output = true })

Tres detalles de vim.fn que ahorran búsquedas. Para funciones autoload de plugins, la sintaxis con corchetes: vim.fn["fugitive#Head"](). Los valores convierten directamente —un Float de Vim llega como número de Lua, v:null llega como vim.NIL—. Y como viste en la 7.3, vim.fn fabrica cada función bajo demanda con __index: no hay una lista que puedas recorrer, hay un gancho que resuelve nombres.

En sentido contrario, el prefijo v:lua permite llamar funciones Lua desde cualquier expresión de Vimscript, incluidas las opciones que evalúan expresiones y los mapeos <expr>:

VIM
set statusline=%!v:lua.require'mi.linea'.render()inoremap <expr> <Tab> v:lua.require'mi.tab'.siguiente()

Dos reglas estrictas que el mensaje de error no explica bien. La forma con require solo admite comilla simple y sin paréntesis: v:lua.require'mi.mod'.f() funciona; v:lua.require("mi.mod").f() y v:lua.require"mi.mod".f() no. Y v:lua solo existe en posición de llamada: no puedes guardarlo en una variable ni pasarlo como Funcref — let F = v:lua.mifn es un error, porque un Funcref de Vimscript no puede representar una función de Lua.

La pregunta natural es cuál usar. La regla práctica: prefiere vim.api cuando exista, porque es la superficie estable, tipada y pensada para ser llamada desde código; usa vim.fn para lo que solo existe en Vimscript, como las funciones de cadenas, rutas y listas históricas; y reserva vim.cmd para comandos de usuario y órdenes que no tienen equivalente razonable en la API.

Lo que se pierde al cruzar

Cada llamada a vim.fn o a la API convierte valores entre el mundo de Lua y el de Vim, y esa conversión no es biyectiva: dos mundos con sistemas de tipos distintos no pueden mapearse sin pérdida. Las tres trampas concretas:

El vacío ambiguo

Una tabla {} vacía cruza como lista vacía, por convenio. Si necesitas un diccionario vacío, vim.empty_dict() crea una tabla marcada con metatabla que cruza como {} y no como [].

Tres formas de nada

El nil de Lua borra claves y no puede viajar dentro de una tabla, como viste en la 7.1. Lo que llega de Vim como v:null se representa con la centinela vim.NIL, que es verdadera, distinta de nil y de false.

Booleanos que no lo son

Muchas funciones históricas de Vimscript devuelven 0 o 1, y en Lua ambos son verdaderos. Compara siempre de forma explícita contra 1.

Hay una asimetría más, que es la de listas y diccionarios: como Lua usa una sola estructura para los dos —la moneda que pagaste en la 7.2—, la conversión decide por la forma de la tabla. Claves consecutivas desde 1: lista. Claves de cadena: diccionario. Una tabla mixta no tiene traducción y provoca un error al cruzar. Los números se reparten igual de silenciosamente: los enteros cruzan como Number y los decimales como Float. Y las cadenas de Lua son secuencias de bytes, así que una cadena con un byte NUL cruza como Blob, y para contar caracteres visibles en vez de bytes necesitas vim.fn.strchars o las conversoras vim.str_utfindex y vim.str_byteindex.

LUA
vim.fn.setqflist({ { filename = "a.lua", lnum = 1, text = "aviso" } })  -- listavim.fn.json_encode(vim.empty_dict())          --> {}   y no []if vim.fn.filereadable(ruta) == 1 then end    -- comparacion explicitaprint(#"añadir", vim.fn.strchars("añadir"))   --> 7  6   bytes frente a caracteres

Esa primera línea, por cierto, es la quickfix de la lección 1.6 poblada desde Lua: una lista de diccionarios de Lua que cruza el puente y se convierte en la lista de entradas que ya sabes navegar con ]q. El puente no es teoría: es cómo cada pieza que conoces se vuelve programable.

Una frontera, no dos editores

Cierra el nivel con esta idea, porque ordena todo lo demás. Neovim no es un editor con Lua pegado al lado: es un núcleo en C con un estado —buffers, ventanas, opciones, registros— y varias fachadas que hablan con él. Vimscript es la fachada histórica; la API con sus funciones nvim_ es la fachada estable, la misma que usan los clientes remotos por RPC; y Lua es la fachada que ejecutas dentro del proceso, con acceso directo a las otras dos. Nada de lo que escribes en Lua es privilegiado por ser Lua: cuando llamas a vim.fn.expand estás cruzando al intérprete de Vimscript y volviendo, y cuando llamas a vim.api.nvim_buf_set_lines estás invocando exactamente la misma función que invocaría un plugin escrito en Python al otro lado de un socket. Por eso las conversiones de tipos existen y por eso duelen: hay una serialización real entre modelos de datos distintos, y la ambigüedad entre lista y diccionario o entre nil y v:null es el precio de que un lenguaje con una sola estructura compuesta hable con otro que tiene dos. Interiorizar esa arquitectura te da dos superpoderes. El primero, saber siempre a qué capa dirigirte y en qué orden preferirlas. El segundo, y más valioso, entender que el editor entero es programable desde fuera: cualquier cosa que hagas a mano tiene un equivalente en la API, y a partir del Nivel 8 vas a dejar de configurar opciones para empezar a escribir programas que manejan el editor. Lua era solo el idioma; lo que viene ahora es el vocabulario.

Drills

:= serializa con vim.inspect antes de imprimir: las tablas salen legibles, no como table: 0x.... Es la herramienta de inspección de menor fricción del editor entero.

Escribe tres líneas de Lua en cualquier buffer, selecciónalas y ejecuta :lua sobre el rango. Verás :'<,'>lua en la línea de comandos: acabas de evaluar un trozo de archivo sin :luafile y sin guardar.

[] {}. La misma tabla vacía de Lua, dos JSON distintos según la marca. El día que un servidor rechace tu petición porque enviaste [] donde esperaba {}, este drill valdrá una tarde.

1 true. El primero es un número —verdadero incluso cuando fuera 0—; el segundo es la comparación que de verdad querías. Grábate el idioma == 1.

Cruza el puente en las dos direcciones

  1. Imprime con := el valor de tres opciones de buffer y compara con lo que enseña print para las mismas expresiones; explica la diferencia.
  2. Escribe un script que se ejecute con nvim -l, lea un archivo con io.open o la API y escriba un resumen (líneas y bytes) por la salida estándar; ejecútalo desde la terminal.
  3. Convierte dos llamadas de vim.cmd de tu configuración a su equivalente en vim.api o vim.fn, y justifica en un comentario cada elección de capa.
  4. Demuestra con json_encode la diferencia entre {} y vim.empty_dict(), y provoca después el error de la tabla mixta intentando pasar { 1, 2, clave = "x" } a una función de vim.fn.
  5. Define una función en un módulo tuyo y úsala desde un mapeo <expr> mediante v:lua.require'...'; comprueba primero que la variante con paréntesis falla, y luego que la correcta funciona.
  6. Ejecuta :lua print(#"añadir", vim.fn.strchars("añadir")) y explica los dos números en términos de bytes y caracteres.