¿Cómo convertir tu configuración de init.vim a init.lua?

Requisitos

  1. Curiosidad. Es la condición más importante para leer este post. Si la tenés, con algo de esfuerzo sumado a algo de tiempo podrás aprender Vim, Neovim, etc.
  2. Conocimientos básicos o intermedios de Vim y haber leído el post anterior.
  3. Conocimientos básicos de vim-plug.

Repaso de conceptos importantes

En el post anterior vimos cómo un usuario de Vim puede comenzar a usar Neovim. En el presente artículo haremos la transición de Vim script a Lua. Ok, booting the post 😄 ...

De Vim script a Lua

Recordemos que Vim usa Vim script (aka VimL) como lenguaje de scripting nativo. Es un lenguaje específico que permite personalizar y extender su funcionalidad. Existe en dos formas: el Vim script tradicional (también llamado "legacy script") y Vim9script (introducido en Vim 9.0), que ofrece mejor rendimiento y una sintaxis más moderna. Sin embargo, Vim9script ni siquiera se usa de manera predeterminada en Vim 9.

Lua, en cambio, es un lenguaje de programación ligero, de alto nivel y multiparadigma, diseñado principalmente para ser incorporado en aplicaciones. Se destaca por su simplicidad, eficiencia y portabilidad. Neovim incorporó Lua como lenguaje de scripting de primera clase, lo que ha ganado popularidad debido a su mejor rendimiento y facilidad de uso comparado con Vim script tradicional.

Neovim usa una implementación de Lua llamada LuaJIT, que compila código Lua a código máquina nativo durante la ejecución. Es un detalle técnico relevante, pero no la razón principal para preferir Lua sobre Vim script en la configuración del editor — en ese terreno el cuello de botella suele estar en el startup time y en el diseño de los plugins, no en la velocidad del lenguaje.

Diferencia de idiosincrasia:

  • Vim script: scripting lineal, global y directo. Bueno para pequeños ajustes, pero difícil de escalar.
  • Lua (Neovim): lenguaje estructurado, orientado a modularizar, reusar y escalar. Favorece el uso de variables locales, tablas y funciones que mejoran el mantenimiento de la configuración.

Por qué importa esto en la práctica:

  • La API de Neovim en Lua es estructurada y predecible (tablas, namespaces como vim.api.*, vim.opt), donde Vim script ofrecía variables globales y comandos planos.
  • El ecosistema de plugins modernos asume Lua como lenguaje nativo — los gestores actuales (lazy.nvim, packer.nvim), los runtime helpers, los plugins de LSP y UI conviven mejor con configs en Lua.
  • La modularización con require permite dividir la configuración en archivos lógicos con scoping limpio, algo que Vim script soporta de forma rudimentaria.

LuaJIT existe y es rápido, pero esa rapidez rara vez es lo que el sysadmin nota al usar Neovim — lo que nota es que su configuración escala mejor y se integra con el ecosistema sin fricciones.

3 conceptos esenciales — lo mínimo que siempre debés recordar

Concepto Por qué es esencial
Tablas (table) Las tablas son la única estructura de datos compuesta en Lua. Opciones (vim.o, vim.opt), autocmds, comandos, mappings y configuraciones de plugins.
Variables locales (local) Permiten organizar el código, evitar errores y mejorar el rendimiento. Imprescindible para no contaminar el entorno global.
API de Neovim (vim.api.*) Es la manera oficial y flexible de interactuar con Neovim: crear comandos, autocmds, mappings y manipular el editor desde Lua.

Si entendés estos 3 conceptos, podés crear y mantener cualquier configuración básica y muchas avanzadas.

¿Qué es una tabla y qué no en la configuración de Neovim (Lua)?

Tablas 🆗:

  • vim.g
  • vim.opt / vim.o
  • vim.api
  • Configuraciones de plugins
  • Listas/patrones

No son tablas ⚠:

  • Funciones
  • Comandos ejecutados
  • Strings, números, booleanos

El podio de las tablas globales más importantes en Neovim

Estas son las tres tablas globales más importantes del entorno Lua en Neovim:

🏅 vim

Es la tabla principal y núcleo del entorno Lua en Neovim. Contiene todo lo necesario para interactuar con el editor:

  • Subtablas como vim.api, vim.cmd, vim.opt, vim.g, vim.fn, etc.
  • Siempre disponible sin necesidad de importar.

🥈 _G

Es la tabla global del lenguaje Lua. Desde aquí podés definir funciones o variables globales accesibles desde cualquier parte de tu configuración:

  • _G.mi_funcion = function() ... end
  • También contiene vim como una de sus claves.

🥉 package (de Lua)

Maneja los módulos cargados por require. Es crucial para modularizar tu configuración:

  • package.loaded permite ver y recargar módulos.

Nota

Tablas como vim.api, vim.opt, vim.g son importantes, pero no son globales de nivel superior: viven dentro de vim. Lo mismo aplica para math, string, table, etc., que viven en _G.

→ Estas 3 (vim, _G, package) forman la base del entorno ejecutable de cualquier configuración Neovim moderna en Lua.

Subtablas más importantes

Tabla Contiene Ejemplo equivalente en Vim script
vim.o Opciones globales set tabstop=4
vim.opt Acceso a opciones con métodos (:append, :remove, :prepend); requerido para listas set nrformats+=alpha
vim.cmd Comandos que se ejecutan en modo ex colorscheme PaperColor
vim.g Variables globales let g:loaded_perl_provider = 0
vim.keymap.set Asignación de atajos de teclado a una función o combinación de teclas nnoremap ñ :wq<CR>
vim.api.nvim_create_user_command Creación de comandos personalizados command Nbuild execute 'w | Silent nikola build'

Diccionario cruzado Vim script / Lua

Concepto Vim script Lua
Variable global let g:mi_var = 10 vim.g.mi_var = 10
Variable local let l:var = 10 (function-local), s:, b:, w: por contexto local var = 10
Lista let lista = [1,2,3] lista = {1, 2, 3}
Diccionario let dict = {'a':1} dict = {a = 1}
Función function! Hola() ... endfunction function hola() ... end
Función anónima {a, b -> a + b} (lambda, desde Vim 8.0) function() ... end
Autocomando autocmd BufWrite ... vim.api.nvim_create_autocmd
Mapping nnoremap vim.keymap.set
Comando command ... vim.api.nvim_create_user_command

Ejemplo de migración Vim script → Lua

Bueno, con la teoría suficiente, ✋✋ a la obra 😉. Supongamos que tenemos esta configuración del archivo init.vim:

set tabstop=4
set nrformats+=alpha

nnoremap ñ :wq<CR>

command -nargs=+ Silent execute 'silent !<args>' | redraw!
command Nbuild execute 'w | Silent nikola build'

filetype plugin on
runtime plugins.vim
colorscheme PaperColor

Es un archivo pequeño, pero que sirve de punto de partida para moverse a una configuración en Lua.

Crear el entorno para Neovim

El sistema modular de Neovim

Como vimos en el post anterior, mi recomendación es comenzar con un archivo init.vim. Una vez que ya estamos usando Neovim podemos realizar la migración a init.lua. Sin embargo, tenemos que crear los directorios para que Neovim guarde su configuración. Lo único que dejaremos por ahora en Vim script es la configuración de plugins con vim-plug. Por lo tanto ejecutamos:

mkdir -p ~/.config/nvim/lua/utils
mkdir -p ~/.local/share/nvim/site/autoload
# Suponiendo que usabas un archivo para gestionar plugins en Vim
mv ~/.vim/autoload/plug.vim ~/.local/share/nvim/site/autoload
  • El directorio ~/.config/nvim es obligatorio.
  • El directorio ~/.config/nvim/lua no es obligatorio, pero es muy útil para modularizar la configuración de Neovim.
  • Los nombres dados de los archivos y directorios dentro de ~/.config/nvim/lua son totalmente arbitrarios.

En Neovim tendremos una estructura más modular. Si bien podemos tener todo en init.lua, vamos a aprovecharnos de esta configuración que es mucho más flexible.

Archivo ~/.config/nvim/init.lua

-- Cargamos los plugins primero — algunas configuraciones
-- (como `colorscheme`) dependen de tenerlos disponibles en el runtimepath
vim.cmd('source ~/.config/nvim/plugins.vim')

-- Cargamos nuestros módulos (el require ejecuta el archivo;
-- el efecto viene del side-effect, no del valor capturado)
require('utils.commands')
require('utils.extracommands')
require('keymaps')
require('opciones')

Archivo ~/.config/nvim/lua/keymaps.lua

-- Alias
local map = vim.keymap.set
-- maps
map('n', 'ñ', ':wq<CR>')

Nota sobre vim.keymap.set: usa remap = false por default, así que map('n', 'ñ', ':wq<CR>') ya es equivalente a nnoremap (no a nmap). El "nore" que en VimL diferenciaba nnoremap de nmap está implícito en el default de la API Lua.

Archivo ~/.config/nvim/lua/opciones.lua

-- Alias
local vo = vim.o
local vop = vim.opt

-- Opciones
vo.tabstop = 4
vop.nrformats:append('alpha')

Archivo ~/.config/nvim/lua/utils/commands.lua

-- Comando auxiliar para correr cosas en shell silenciado
vim.api.nvim_create_user_command('Silent', function(opts)
  vim.cmd('silent !' .. table.concat(opts.fargs, ' '))
  vim.cmd('redraw!')
end, { nargs = '+' })

-- Build del sitio Nikola
vim.api.nvim_create_user_command('Nbuild', function()
  vim.cmd('w')
  vim.cmd('Silent nikola build')
end, {})

Archivo ~/.config/nvim/lua/utils/extracommands.lua

-- Alias
local vcmd = vim.cmd

-- Extra commands
vcmd('filetype plugin on')
-- Esta configuración es innecesaria porque ya está en init.lua
-- vcmd('runtime plugins.vim')
vcmd('colorscheme PaperColor')

Archivo ~/.config/nvim/plugins.vim

Este es el único archivo que dejamos en Vim script (con código Lua embebido):

"Plugins (Plug)
call plug#begin('~/.vim/plugged')

Plug 'itchyny/lightline.vim'
Plug 'nyngwang/NeoTerm.lua'
Plug 'NLKNguyen/papercolor-theme'

call plug#end()

lua << EOF                                                       
require("neo-term").setup {
  exclude_filetypes = { "oil" }
}                          
EOF

nnoremap <F4> :NeoTermToggle<CR>
tnoremap <F4> <C-\><C-n>:NeoTermToggle<CR>
tnoremap <Esc> <C-\><C-n>:NeoTermEnterNormal<CR>

Y listo...

Explicación

¿Por qué definimos alias en los archivos de módulos?

Los alias son básicamente variables. Los definimos porque este es un ejemplo de tipo "esqueleto" de configuración. Cuando comiences a agregar más opciones, atajos de teclado y comandos personalizados, no querrás tipear tanto 😉.

Nota sobre redefinición de comandos

vim.api.nvim_create_user_command sobrescribe por default un comando con el mismo nombre (la opción force tiene valor true por defecto, según la doc oficial de la API). Es decir: podés llamar dos veces a nvim_create_user_command('Nbuild', ...) y la segunda definición reemplaza a la primera sin error. No hace falta borrar antes con nvim_del_user_command. Esto es distinto del :command de VimL, que sí requiere command! para sobrescribir — la API Lua no hereda esa restricción.

El ejemplo de Nbuild deja ver diferencias más grandes entre VimL y Lua. Las analizamos en cinco dimensiones:

1. Brevedad vs mantenibilidad
  • VimL:

vim command -nargs=+ Silent execute 'silent !<args>' | redraw! command Nbuild execute 'w | Silent nikola build'

Es breve, directo y funciona. Pero:

  • Es poco expresivo
  • No permite manejar errores fácilmente
  • No escala bien con lógica compleja

  • Lua:

lua vim.api.nvim_create_user_command('Silent', function(opts) vim.cmd('silent !' .. table.concat(opts.fargs, ' ')) vim.cmd('redraw!') end, { nargs = '+' })

Es más verboso, sí, pero:

  • La función puede ser más compleja si se necesita
  • Es programable y reutilizable
  • Puede integrarse con condicionales, bucles, o lógica externa
2. Control de errores y depuración
  • En VimL: si un comando falla, tenés poco margen para capturar y manejar el error.
  • En Lua: podés usar pcall, vim.notify, vim.fn.systemlist, y construir mensajes personalizados o fallback si algo sale mal.
local ok, _ = pcall(function()
  vim.cmd('silent !nikola build')
end)
if not ok then
  vim.notify('Error al ejecutar nikola build', vim.log.levels.ERROR)
end
3. Modularización
  • En Lua podés agrupar comandos en módulos, con require(...).
  • En VimL tenés que recurrir a runtime, source, y no hay namespaces claros.
require('utils.commands')        -- registra los user commands al cargarse
require('utils.extracommands')   -- aplica colorscheme y filetype detection
require('keymaps')               -- registra los atajos de teclado
require('opciones')              -- opciones generales de configuración

Esto facilita dividir la configuración en archivos lógicos, más fácil de leer, mantener y compartir.

4. Compatibilidad futura y ecosistema Neovim
  • Neovim ha declarado que Lua es su lenguaje de configuración nativo moderno.
  • La mayoría de los plugins modernos están escritos en Lua.
  • VimL sigue siendo soportado, pero está siendo superado funcionalmente.
5. Legibilidad a largo plazo
  • Aunque VimL sea más corto, en un entorno de colaboración o con configuraciones grandes, Lua:

  • Es más explícito.

  • Permite documentar.
  • La API de Neovim en Lua permite un uso más explícito y predecible de tipos de datos.
  • Es más fácil de extender con lógica externa.

Conclusión

Si sabés migrar opciones, mappings, comandos y cargar plugins → podés convertir el 80% o más de tu init.vim (equivalente al viejo .vimrc) a init.lua.

Migrar de Vim script a Lua no es una mejora cosmética: cambia las posibilidades de la configuración. Lo que en Vim script son comandos breves, en Lua son funciones programables — más verbosas, sí, pero abiertas a control de errores con pcall, modularización con require y un ecosistema de plugins que ya asume Lua como lenguaje nativo.

La estructura mostrada acá es una entre muchas. El esqueleto init.lua + módulos en lua/ se sostiene porque escala: cada archivo crece sin contaminar al resto, y la configuración deja de ser una pared de líneas para volverse un conjunto de piezas que podés rearmar cuando lo necesites.

Lo que no migramos en este post es la configuración de plugins, que dejamos en plugins.vim. Reemplazar vim-plug por un gestor en Lua como lazy.nvim es el siguiente paso natural — y tema de este otro post.

Al cierre del post dejé un resumen de 10 conceptos clave para tener a mano cuando empieces a extender tu config.

Apéndice: Los 10 conceptos fundamentales de Lua (resumen)

Resumen de los 10 conceptos clave de Lua en Neovim. Los 3 marcados como (esencial) son los que vimos al principio; el resto extiende esa base:

Concepto Descripción breve
Tablas (table) (esencial) Estructuras clave-valor o listas. Base de toda la configuración.
Variables locales (local) (esencial) Para mantener el código limpio y evitar contaminación global.
Funciones anónimas Permiten definir acciones sin nombre, usadas en mappings, autocmds y comandos.
API de Neovim (vim.api.*) (esencial) Puente entre Lua y Neovim: crear comandos, mappings, autocmds.
vim.opt vs vim.o vim.o da acceso directo al valor; vim.opt lo envuelve en un objeto type-aware con métodos (necesario para listas).
require Para cargar módulos externos y dividir la configuración.
Paréntesis en llamadas Obligatorio usar () al llamar funciones.
Referencias Asignar una tabla a otra variable sin copiarla (ejemplo: local vg = vim.g).
_G (global) Tabla especial que contiene las variables y funciones globales accesibles en toda la configuración.
package.loaded Permite ver o manipular qué módulos están cargados, útil para recargar configuraciones dinámicamente.

Fuentes y más Recursos

Comentarios

Comments powered by Disqus