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 el prefijo de llamada desde Vimscript, 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.inspecty la lista de mensajes. - Llamar a Vimscript desde Lua y a Lua desde Vimscript.
- Anticipar las conversiones de tipos que ocurren al cruzar el puente.
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 el valor de la expresión, y desde hace varias versiones existe la abreviatura :=expr, el equivalente más cercano a una consola interactiva.
:lua vim.opt.number = true
:lua =vim.bo.filetype
:=vim.fn.getcwd()
:lua for _, b in ipairs(vim.api.nvim_list_bufs()) do print(b) end
Para varias líneas dentro de un archivo de Vimscript existe el documento incrustado, que delimita un bloque completo:
lua << EOF
local 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 también, porque desde Neovim 0.9 el comando distingue por 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, no para módulos.
:luafile %
:source %
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.
nvim -l ./scripts/generar.lua --salida build/
nvim --headless -c "lua require('mi.tarea').run()" -c "qa!"
La cuarta la conoces desde el nivel 1 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 ya estudiaste.
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. vim.print hace lo mismo pero además lo imprime, respetando las tablas y devolviendo sus argumentos intactos, lo que permite intercalarlo en medio de una expresión sin alterar el flujo.
print(vim.bo) --> table: 0x7f9c... inservible
vim.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 y puede ser reemplazada por un plugin de notificaciones.
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.
El puente hacia Vimscript
Neovim no reescribió treinta años de Vimscript: lo mantiene y expone tres caminos hacia él. vim.cmd ejecuta comandos Ex, en su forma de cadena o en la forma estructurada, más segura porque no hay que escapar nada. vim.fn da acceso a cualquier función de Vimscript, incluidas las de plugins escritos en ese lenguaje. Y nvim_exec2 evalúa bloques enteros capturando su salida.
vim.cmd("colorscheme catppuccin") -- forma de cadena
vim.cmd.colorscheme("catppuccin") -- forma estructurada
vim.cmd({ cmd = "write", bang = true }) -- control total
local ancho = vim.fn.strdisplaywidth("añadir") -- funcion de Vimscript
local hay = vim.fn.executable("rg") == 1 -- devuelve 0 o 1, no booleano
local salida = vim.api.nvim_exec2("verbose set shiftwidth?", { output = true })
En sentido contrario, el prefijo v:lua permite llamar funciones Lua desde cualquier contexto de Vimscript: expresiones, opciones que aceptan una función y mapeos de tipo expresión.
set foldtext=v:lua.require'mi.folds'.texto()
set statusline=%!v:lua.require'mi.linea'.render()
inoremap <expr> <Tab> v:lua.require'mi.tab'.siguiente()
flowchart LR L[Codigo Lua] -->|vim.cmd| V[Comandos Ex] L -->|vim.fn| F[Funciones de Vimscript] L -->|vim.api| A[API de Neovim] V2[Vimscript] -->|v dos puntos lua| L V2 -->|luaeval| L A --> N[Estado del editor] F --> N V --> N style L fill:#89b4fa,color:#11111b style N fill:#a6e3a1,color:#11111b
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 cadena o de rutas; y reserva vim.cmd para comandos de usuario y órdenes que no tienen equivalente 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. Conviene tenerla presente porque genera errores silenciosos.
El vacío ambiguo
Una tabla {} vacía cruza como lista vacía. Si necesitas un diccionario vacío, usa vim.empty_dict().
Tres formas de nada
El nil de Lua borra claves y no viaja; lo que llega de Vim como v:null se representa con la centinela vim.NIL, distinta de nil y de false.
Booleanos que no lo son
Muchas funciones 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 conversión decide por la forma de la tabla. Una tabla con claves consecutivas desde 1 se convierte en lista; una con claves de cadena, en diccionario; una tabla mixta no tiene traducción y provoca un error. Por último, las cadenas de Lua son secuencias de bytes sin codificación, igual que en Vim, así que para contar caracteres visibles necesitas vim.fn.strchars o las funciones vim.str_utfindex y vim.str_byteindex.
vim.fn.setqflist({ { filename = "a.lua", lnum = 1, text = "aviso" } }) -- lista
vim.fn.json_encode(vim.empty_dict()) --> {} y no []
if vim.fn.filereadable(ruta) == 1 then end -- comparacion explicita
print(#"añadir", vim.fn.strchars("añadir")) --> 8 6 bytes frente a caracteres
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 siguiente 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.
- Imprime con
:=el valor de tres opciones de buffer y explica en qué se diferencia de usarprint. - Escribe un script que se ejecute con
nvim -l, lea un archivo con la API y escriba un resumen por la salida estándar. - Convierte tres llamadas de
vim.cmdde tu configuración a su equivalente envim.apiovim.fny justifica cada elección. - Demuestra con
json_encodela diferencia entre una tabla vacía yvim.empty_dict(), y describe un caso real donde importe. - Define una función Lua en un módulo y llámala desde una opción de Vimscript mediante
v:lua, comprobando que recibe los argumentos esperados.