Todo lo que necesitan saber para configurar neovim usando lua

Última actualización: 2022-04-21

Después de mucho tiempo en desarrollo neovim 0.5 por fin fue liberado como una versión estable. Entre las nuevas mejoras tenemos un mejor soporte para lua y la promesa de una api estable para crear nuestra configuración usando este lenguaje. Aprovechando esto hoy voy compartir con ustedes todo lo que aprendí mientras migraba mi configuración de vimscript a lua.

Vamos a hablar de lo que podemos hacer en lua y su interacción con vimscript. Aunque presentaré muchos ejemplos, no les diré qué configuración deben activar o con qué valor. No hablaré de cosas específicas de algún lenguaje, y tampoco abordaré el tema de "convertir neovim en un IDE". Sólo espero darles una buena base para que puedan migrar su propia configuración.

Asumiré que su sistema operativo es linux (o algo parecido) y que su configuración está en la ruta ~/.config/nvim/init.vim. Todo lo que mencionaré debería funcionar en cada sistema donde pueden instalar neovim, sólo tengan en cuenta que la ruta de init.vim puede ser diferente en su caso.

Comencemos.

Primeros pasos

Lo primero que deben saber es que podemos incorporar código escrito en lua directamente en nuestro init.vim. Podemos comenzar la migración de manera gradual desde init.vim, y reemplazar init.vim por init.lua sólo cuando estemos listos.

Empezemos con el "hola mundo" para probar que todo funciona de manera correcta. Pueden colocar esto en su init.vim:

lua <<EOF
print('hola desde lua')
EOF

Si ejecutan nuevamente init.vim o reinician neovim el mensaje hola desde lua debería aparecer justo debajo de su barra de estado. Aquí estamos utilizando algo llamado lua-heredoc. Todo lo que está encerrado en <<EOF ... EOF es considerado un "script" que será evaluado por el comando lua. Resulta útil cuando queremos ejecutar código con múltiples líneas pero no es estrictamente necesario si sólo necesitamos una. También podemos hacer esto.

lua print('esto también funciona')

Pero si vamos a llamar código lua desde vimscript lo que yo recomendaría sería "llamar" un script verdadero. En lua podemos hacer eso con la función require. Para que esto funcione necesitamos crear una carpeta llamada lua y colocarla en un directorio que se encuentre en el runtimepath de neovim.

Lo más conveniente sería usar el mismo directorio desde se encuentra init.vim, entonces lo que haremos será crear ~/.config/nvim/lua, y dentro de esa carpeta colocaremos el script que llamaremos basic.lua. Por ahora sólo imprimiremos un mensaje.

print('hola desde ~/config/nvim/lua/basic.lua')

Ahora desde init.vim podemos invocarlo de esta manera.

lua require('basic')

Aquí neovim buscará en todos los directorios del runtimepath una carpeta llamada lua y luego dentro de esa carpeta buscará basic.lua. El último script que encuentre que cumpla estas condiciones será ejecutado.

Una particularidad de lua que van a encontrar es que podemos usar el . como un separador de ruta. Por ejemplo, imaginemos que tenemos el archivo ~/.config/nvim/lua/usermod/settings.lua. Si queremos llamar al archivo settings.lua podemos hacerlo de la siguiente manera.

require('usermod.settings')

Es una convención común que van a encontrar si revisan el código de otras personas. Sólo recuerden que el punto es un separador de ruta.

Con este conocimiento ya están preparados para empezar su configuración en lua.

Opciones del editor

Cada opción en neovim está disponible para nosotros en la variable global llamada vim... bueno, es más que una variable, es un módulo. Con vim tenemos acceso a opciones, a la api de neovim e incluso un conjunto de funciones auxiliares (una librería estándar). Por ahora lo que nos interesa es algo que llaman "meta-accessors", es lo que usaremos para acceder a las opciones del editor.

Ámbitos

Al igual que en vimscript, en lua tenemos diferentes ámbitos para cada opción. Tenemos opciones que son globales, opciones que actúan sólo en una ventana, otras que aplican sólo para los archivos abiertos, etc. Cada uno tiene su propio espacio dentro del módulo vim.

Sirve para leer o modificar opciones generales del editor.

vim.o.background = 'light'

Lee o modifica valores específicos para una ventana.

vim.wo.colorcolumn = '80'

Lee o modifica valores específicos para un archivo (buffer).

vim.bo.filetype = 'lua'

Lee o modifica valores globales. Aquí más que todo encontrarán valores usados por plugins. La única opción que conozco que no está necesariamente ligada a un plugin es la tecla líder.

-- espacio como tecla líder
vim.g.mapleader = ' '

Una cosa que deben tener en cuenta es que algunos nombres de variables en vimscript no son válidos en lua. Aún podemos acceder a ellos pero debemos usar una sintaxis diferente. Por ejemplo vim-zoom tiene una variable llamada zoom#statustext y en vimscript podemos modificarla usando let, así:

let g:zoom#statustext = 'Z'

En lua tendríamos que hacer esto:

vim.g['zoom#statustext'] = 'Z'

Esta sintaxis también nos sirve para acceder a propiedades que tienen el nombre de una palabra reservada. Por ejemplo for, do y end son palabras reservadas; entonces si tenemos alguna variable con esas propiedades podemos usar esta sintaxis para evitar un error.

Lee o modifica variables de entorno.

vim.env.FZF_DEFAULT_OPTS = '--layout=reverse'

Tengo entendido que los cambios a estas variables sólo tendrán efecto en la sesión activa del editor.

Pero entonces ¿cómo sabemos qué "ámbito" usar cuando vamos a crear nuestra configuración? No se preocupen por eso, pueden pensar en vim.o y compañía como una especie de acceso rápido a una variable, es mejor usarlo para leer valores. Para realizar modificaciones tenemos otra propiedad.

vim.opt

Con vim.opt podremos modificar opciones generales, de ventana y de archivo.

-- opción de buffer
vim.opt.autoindent = true

-- opción de ventana
vim.opt.cursorline = true

-- opción general
vim.opt.autowrite = true

En este sentido vim.opt actúa como el comando :set en vimscript, nos da una manera consistente de declarar nuestros valores.

Puedo decirles que asignar vim.opt a una variable llamada set funciona a la perfección. Por ejemplo, imaginemos que tenemos este fragmento en vimscript.

" comportamiento de la tecla tab
set tabstop=2
set shiftwidth=2
set softtabstop=2
set expandtab

Podemos migrar sin esfuerzo a lua de esta manera.

local set = vim.opt

-- comportamiento de la tecla tab
set.tabstop = 2
set.shiftwidth = 2
set.softtabstop = 2
set.expandtab = true

Cuando declaran variables en lua no olviden la palabra clave local. En lua las variables son globales por defecto (esto incluye las funciones).

¿Qué pasa con las variables globales o las variables de entorno? Para eso deberían seguir usando vim.g y vim.env respectivamente.

Lo interesante de vim.opt es que cada propiedad es como un objeto especial, son lo que llaman "meta-tabla". Quiere decir que son objetos que implementan sus propias funciones para operaciones comunes.

En el primer ejemplo teníamos esto: vim.opt.autoindent = true, tal vez estén pensando que también pueden inspeccionar su valor de manera normal, así:

print(vim.opt.autoindent)

No obtendrán el valor que esperan, print les dirá que vim.opt.autoindent es una tabla. Si quieren acceder a su valor deben usar el método :get().

print(vim.opt.autoindent:get())

Si realmente quieren saber qué hay dentro de vim.opt.autoindent pueden usar vim.inspect.

print(vim.inspect(vim.opt.autoindent))

O incluso mejor, si tienen la version 0.7 pueden user el comando :lua = para inspeccionar su valor desde el modo de comandos.

:lua = vim.opt.autoindent

Eso les mostrará el estado interno de la propiedad.

Tipos de datos

Incluso cuando asignamos un valor a una propiedad de vim.opt hay algo de magia en el fondo. Ahora tienen que saber cómo vim.opt maneja los valores en comparación con vimscript.

Puede que no parezca muy especial pero aún creo que vale la pena mencionarlos.

En vimscript para activar o desactivar alguna opción hacemos esto.

set cursorline
set nocursorline

Este es el equivalente en lua.

vim.opt.cursorline = true
vim.opt.cursorline = false

Para algunas opciones se espera una lista separada por comas. En este caso podríamos proveer la cadena texto nosotros mismos.

vim.opt.wildignore = '*/cache/*,*/tmp/*'

Ó podríamos usar una tabla.

vim.opt.wildignore = {'*/cache/*', '*/tmp/*'}

Si revisan el contenido de vim.o.wildignore notarán que es la cadena de texto */cache/*,*/tmp/*, eso significa que funcionó. Y si quieren estar muy seguros de que funcionó, revisen con este comando dentro de neovim.

:set wildignore?

Obtandrán el mismo resultado.

La magia no termina ahí. En ocasiones no necesitamos sobreescribir los valores de la lista, a veces queremos agregar un elemento o tal vez necesitamos eliminarlo. Para facilitar estas tareas vim.opt tiene soporte para las siguientes operaciones:

Añadir elemento al final de la lista

Tomemos la opción errorformat como ejemplo. Si queremos añadir algo usando vimscript utilizamos este comando.

set errorformat+=%f\|%l\ col\ %c\|%m

En lua podemos lograr el mismo efecto de dos maneras:

Usando el operador +.

vim.opt.errorformat = vim.opt.errorformat + '%f|%l col %c|%m'

O la función append.

vim.opt.errorformat:append('%f|%l col %c|%m')

Añadir al inicio

En vimscript:

set errorformat^=%f\|%l\ col\ %c\|%m

En lua:

vim.opt.errorformat = vim.opt.errorformat ^ '%f|%l col %c|%m'

-- o su equivalente

vim.opt.errorformat:prepend('%f|%l col %c|%m')

Eliminar un elemento

En vimscript:

set errorformat-=%f\|%l\ col\ %c\|%m

En lua:

vim.opt.errorformat = vim.opt.errorformat - '%f|%l col %c|%m'

-- o su equivalente

vim.opt.errorformat:remove('%f|%l col %c|%m')

Algunas opciones tienen un formato donde se debe especificar una propiedad y el valor para esa propiedad. Como ejemplo tenemos listchars.

set listchars=tab:▸\ ,eol:↲,trail:·

En lua podemos usar tablas para representar esta opción.

vim.opt.listchars = {eol = '', tab = '', trail = '·'}

Nota: para que la opción listchars tenga efecto deben activar la opción list. Ver :help listchars

Ya que esto también es una tabla podemos hacer las mismas operaciones que mencioné en la sección anterior.

Invocando funciones de vim

Vimscript como cualquier otro lenguaje de programación tiene sus propias funciones nativas (muchas funciones) y gracias al módulo vim podemos acceder a ellas usando vim.fn. Al igual que vim.opt, vim.fn es una meta-tabla, pero en este caso nos permite tener una sintaxis conveniente para llamar funciones de vim. Podemos usar vim.fn para invocar funciones nativas, funciones creadas por nosotros mismos e incluso funciones de plugins que no están en escritos en lua.

Podríamos por ejemplo validar la versión de neovim de esta manera:

if vim.fn.has('nvim-0.7') == 1 then
  print('tenemos neovim 0.7')
end

¿Por qué estoy comparando el resultado de has con un 1? Vimscript no siempre ha tenido booleanos, estos fueron agregados a partir de la versión 7.4.1154. Entonces funciones como has devuelven 0 o 1 y en lua cualquiera de esos valores pasa la evaluación de un if.

Hay casos donde el nombre de la función no es válido en lua. Ya saben que podemos usar corchetes así:

vim.fn['fzf#vim#files']('~/projects', false)

Pero también podemos usar la función vim.call.

vim.call('fzf#vim#files', '~/projects', false)

En la práctica vim.fn.unafuncion() y vim.call('unafuncion') tienen exactamente el mismo efecto.

Ahora déjenme mostrarle algo genial. La integración lua-vimscript es tan buena que podríamos utilizar un "plugin manager" sin necesidad de adaptaciones especiales.

vim-plug en lua

Sé que hay un montón de gente que utiliza vim-plug y tal vez se estén preguntando si tienen que migrar a un plugin manager que esté escrito en lua. No tienen que hacerlo, vim.fn y su acompañante vim.call son suficientes para usarlo desde lua.

local Plug = vim.fn['plug#']

vim.call('plug#begin', '~/.config/nvim/plugged')

-- Los plugins van aquí
-- ....

vim.call('plug#end')

Esas tres líneas de código es todo lo que necesitan. Pueden probarlo, esto funciona.

local Plug = vim.fn['plug#']

vim.call('plug#begin', '~/.config/nvim/plugged')

Plug 'wellle/targets.vim'
Plug 'tpope/vim-surround'
Plug 'tpope/vim-repeat'

vim.call('plug#end')

Todo eso es válido en lua. Si una función sólo recibe un argumento y ese argumento es una cadena de texto o una tabla, pueden omitir los paréntesis.

Si necesitan usar el segundo argumento de Plug deben usar los paréntesis y el segundo argumento debe ser una tabla. Vamos a comparar. Si en vimscript tenemos esto:

Plug 'scrooloose/nerdtree', {'on': 'NERDTreeToggle'}

En lua debemos representarlo así:

Plug('scrooloose/nerdtree', {on = 'NERDTreeToggle'})

Desafortunadamente vim-plug tiene opciones llamadas for y do que como ya mencioné son palabras reservadas, para estos casos debemos envolver el nombre de la propiedad con corchetes y comillas.

Plug('junegunn/goyo.vim', {['for'] = 'markdown'})

Una última cosa, la opción do se usa para ejecutar una acción cuando se instala o actualiza un plugin. Esta opción acepta una cadena de texto o una función. Si queremos usar una función no estamos obligados a pasar una "función de vim", podemos usar una función de lua sin ningún problema.

Plug('VonHeikemen/rubber-themes.vim', {
  ['do'] = function()
    vim.opt.termguicolors = true
    vim.cmd('colorscheme rubber')
  end
})

Ahora ya saben, no tienen que preocuparse si su plugin manager no está escrito en lua. Siempre y cuando exponga alguna función podremos usarlo en lua.

Vimscript aún es nuestro amigo

Algunos de ustedes habrán notado que en el último ejemplo usé vim.cmd para configurar el tema del editor. Esto es porque aún hay cosas que no podemos hacer en lua. Con vim.cmd podemos ejecutar expresiones escritas en vimscript. Esto nos permite invocar comandos que no tienen un equivalente en lua.

vim.cmd también es capaz de ejecutar múltiples líneas de vimscript. Significa que podemos hacer múltiples cosas en una sola llamada a vim.cmd.

vim.cmd [[
  syntax enable
  colorscheme rubber
]]

Cualquier fragmento de su init.vim que no puedan "traducir" a lua pueden colocarlo una cadena de texto y pasarlo a vim.cmd.

Ya que podemos ejecutar cualquier comando de vim tengo que mencionar que eso incluye source, con esto podemos invocar scripts escritos en vimscript. Por ejemplo, digamos que estamos migrando nuestra configuración pero aún no estamos listos para migrar nuestros atajos de teclado. Podemos crear un archivo keymaps.vim con nuestros atajos y ejecutarlo desde lua.

vim.cmd 'source ~/.config/nvim/keymaps.vim'

Atajos de teclado

No, no necesitamos vimscript. Podemos crearlos usando lua.

Para estos casos tenemos la función vim.keymap.set (introducida en la versión v0.7). Esta función acepta 4 argumentos.

Digamos que queremos trasladar este atajo a lua.

nnoremap <Leader>w :write<CR>

Tendríamos que hacer esto.

vim.keymap.set('n', '<Leader>w', ':write<CR>')

Por defecto los atajos serán "no recursivos", lo que significa que no debemos preocuparnos por ocasionar ciclos infinitos. Podríamos crear versiones alternativas de algún atajo. Por ejemplo, si queremos centrar la pantalla después de realizar una búsqueda con *.

vim.keymap.set('n', '*', '*zz', {desc = 'Buscar y centrar pantalla'})

Podemos usar * en el atajo y la acción, esto no creará ningún conflicto.

Hay ocasiones donde necesitamos un atajo recursivo, generalmente cuando la acción que queremos ejecutar fue creada por un plugin. En esta situación podemos proveer la opción remap = true en el último argumento.

vim.keymap.set('n', '<leader>e', '%', {remap = true, {desc = 'Ir al par correspondiente'}})

Un beneficio extra de esta función es que nos permite usar funciones de lua como nuestra "acción".

vim.keymap.set('n', 'Q', function()
  print('Hola')
end, {desc = 'Saludar'})

Hablemos un poco de la opción desc. Esta nos permite agregar una descripción a nuestros atajos. Podemos leer estas descripciones con el comando :map <atajo>.

Entonces, ejecutar :map * nos mostrará:

n  *           * *zz
                 Buscar y centrar pantalla

Querrán utilizar estas descripciones cuando su atajo ejecute una función de lua. Por qué? Si revisamos Q con el comando :map Q obtenemos:

n  Q           * <Lua function 50>
                 Saludar

No podemos leer el código de la función, entonces el único indicio que tenemos de su funcionalidad es la descripción. Tambien vale la pena mencionar que plugins pueden extraer esta descripción y mostrarlas de una manera más amigable.

Comandos de usuario

A partir de la version v0.7 neovim nos permite crear nuestros propios "ex-commands" usando lua, con la función vim.api.nvim_create_user_command.

nvim_create_user_command espera tres argumentos:

Digamos que tenemos este comando en vimscript.

command! -bang ProjectFiles call fzf#vim#files('~/projects', <bang>0)

En lua tenemos la posibilidad de usar vimscript in una cadena de texto.

vim.api.nvim_create_user_command(
  'ProjectFiles',
  "call fzf#vim#files('~/projects', <bang>0)",
  {bang = true}
)

O podemos usar una función de lua.

vim.api.nvim_create_user_command(
  'ProjectFiles',
  function(input)
    vim.call('fzf#vim#files', '~/projects', input.bang)
  end,
  {bang = true, desc = 'Buscar en directorio projects'}
)

Sí, podemos agregar descripciones a nuestros comandos. Pero en esta ocasión sólo estarán disponibles si el comando ejecuta una función de lua. Para inspeccionar un comando podemos ejecutar :command <nombre>. Entonces, el comando :command ProjectFiles nos mostrará:

    Name              Args Address Complete    Definition
!   ProjectFiles      0                        Buscar en directorio projects

Si nuestro comando ejecuta una expresión de vimscript nos mostrará ese código en la columna Definition.

Autocomandos

Los autocomandos nos permiten ejecutar funciones y comandos cuando neovim emite un evento. Pueden encontrar la lista de eventos en la documentación: :help events.

Digamos que tenemos un autocomando que modifica ligeramente un tema del editor, específicamente el tema rubber.

augroup highlight_cmds
  autocmd!
  autocmd ColorScheme rubber highlight String guifg=#FFEB95
augroup END

Este bloque debe ejecutarse antes de invocar el comando colorscheme.

Este es el equivalente en lua.

local augroup = vim.api.nvim_create_augroup('highlight_cmds', {clear = true})

vim.api.nvim_create_autocmd('ColorScheme', {
  pattern = 'rubber',
  group = augroup,
  command = 'highlight String guifg=#FFEB95'
})

Noten que aquí estamos usando una opción llamada command, esta nos permite utilizar únicamente expresiones escritas en vimscript. También podemos usar una función de lua pero no con la propiedad command, tenemos que usar callback.

local augroup = vim.api.nvim_create_augroup('highlight_cmds', {clear = true})

vim.api.nvim_create_autocmd('ColorScheme', {
  pattern = 'rubber',
  group = augroup,
  desc = 'Cambiar color de cadenas de texto'
  callback = function()
    vim.api.nvim_set_hl(0, 'String', {fg = '#FFEB95'})
  end
})

Los autocomandos también pueden tener una descripción. Pero al igual que los comandos de usuario sólo estarán disponibles si están vinculadas a una función de lua. Si queremos revisar los autocomandos de un evento podemos ejecutar el comando :autocmd <evento> <patrón>. El comando :autocmd ColorScheme rubber debería mostrarnos:

ColorScheme
    rubber    Cambiar color de cadenas de texto

Para conocer más detalles de los autocomandos pueden leer la documentación, :help nvim_create_autocmd() y :help autocmd.

Plugin manager

Tal vez quieran usar un plugin manager que este escrito en lua sólo porque sí. Por lo que he visto estas son sus opciones:

Es un manejador de plugins rápido y sencillo. No es broma, tiene menos de 300 líneas de código y fue creado para descargar, actualizar y eliminar plugins. Es todo. Si eso es todo lo que necesitan no busquen más, este es el manejador que quieren.

Si quieren algo con más funcionalidades packer es la alternativa. Aparte de lo básico este manejador ofrece opciones para cargar plugins sólo cuando son necesarios, tiene soporte para declarar y manejar plugins con dependencias, tiene soporte para luarocks (este es como un repositorio de paquetes de lua) y también puede manejar "plugins locales". Hace otras cosas pero creo que ya entienden el punto, es bastante completo.

No está escrito en lua pero quise agregarlo porque ofrece una interfaz para lua. También quise listarlo porque ofrece más funcionalidades que paq pero menos que packer, así que si buscan un punto intermedio este puede ser una buena opción.

Conclusión

Aprendimos cómo usar lua desde vimscript. Sabemos cómo usar vimscript desde lua. Ahora tenemos todas las herramientas para activar, desactivar y modificar cualquier tipo de opción o variable disponible en neovim. Conocemos los métodos para crear nuestros atajos de teclado. Aprendimos sobre comandos y autocomandos. Sabemos cómo usar un manejador de plugins desde lua ya sea que esté escrito en lua o no. Ya estamos listos.

Para los que quieran ver un ejemplo de la vida real, aquí les dejo un enlace a mi configuración en github:

Fuentes


¿Tienen alguna pregunta? Pueden dejar un comentario en cualquiera de estas plataformas:

O puedes contactarme en twitter @VonHeikemen_

Gracias por su tiempo. Si este artículo les pareció útil y quieren apoyar mis esfuerzos para crear más contenido, pueden dejar una propina en buy me a coffee ☕.

Buy Me A Coffee