Setup básica con Nvim + lazy.nvim

Por Arrecio


Neovim, o también nvim, es un editor de texto considerado una refactorización de Vim, a su vez basado en vi.

Por otro lado lazy.nvim es un gestor de paquetes de nvim que no debe confundirse con LazyVim que es un setup de Nvim.

Nvim es un editor altamente personalizable mediante scripts. A esas personalizaciones se las denomina setup y existen setups calificadas starter a partir de las cuales los usuarios generan sus propias personalizaciones a base de incorporar plugins que no son más que un conjunto ordenado de scripts destinados a modificar el aspecto o el comportamiento de Nvim modificando algunas o añadiendo otras.

Algunas de estas setups de inicio más famosas son Kickstart, NvChad, LunarVim o el propio LazyVim ya nombrado, sin olvidar AstroNvim.

En mi caso utilizo mi propia configuración, eso sí haciendo uso del gestor de plugins lazy.nvim cuyo nombre viene por la capacidad de cargar de manera tardía los plugins. El siguiente boceto representa una configuración típica de Nvim, yo uso una con una sutil diferencia y es que alojo el código del bootstrap en un fichero script aparte:

Representación de los directorios de configuración y datos de un setup típico con nvim.lazy

Setup con Lua

Una de las características de Nvim respecto a Vim es la integración del lenguaje de script Lua sin perder el seporte con VimScript tabién conocido como VimL (de VimLanguage).

Nvim es un software que goza de una magnífica documentación integrada accesible mediante el comando :h o :help, y también en la web oficial. El lugar donde comenzar es lua-guide o en el caso de la ayuda integrada :h lua-guide. Realmente todo lo que puedas aprender de esta entrada del blog puedes aprenderlo de la ayuda de Nvim.

Unos tip para moverte por la ayuda integrada. En la misma existen enlaces para visitar otras entradas de la ayuda, si quieres volver para atrás puedes hacer Control + Click derecho del ratón o también Control + T.

Arrancado el ejecutable de Nvim se producen una serie de pasos de inicialización. Entre ellos está la carga de la configuración de usuario mediante la ejecución de un archivo init.vim o init.lua. Yo voy a trabajar con init.lua. En el enlance de los pasos de inicialización hablamos del paso 8.

Los lugares donde Nvim buscará estos archivos son en el raíz del directorio de configuración y en el caso de init.lua. Para conocer el directorio de configuración se puede ejecutar el comando :echo stdpath('config'), en mi caso este directorio es ~/.config/nvim/.

stdpath es una función de VimScript que devuelve el standard-path de un determinado lugar del sistema de archivos en el que Nvim guardia o busca archivos. Igual que se ejecuta con 'config' puede hacerse con otras cadenas que representen ciertos directorios base. Si 'config' refiere al directorio en el que se guarda (y carga) la configuración 'data' lo hace al lugar donde se almacenan los datos de usuario aunque hay más. h: stdpath para más info. Si observamos la figura presentada párrafos atrás config sería el árbol de la izquierda y data no el de la derecha directamente pero sí el inmediatamente superior ~/.local/share/nvim.

Entre los pasos de inicialización está el de ejecutar cualquier script que se aloje en <path>/plugin/ donde puede ser cualquier directorio que forme parte del runtimepath el cual inicialmente contendrá únicamente el <config_dir> pero que puede manipularse desde pasos anteriores a este en la inicialización. En mi caso no tengo nada en <config_dir>/plugin/ sin embargo los plugins manejados por lazy sí que contendrán scripts en sus respectivos directorios /plugin.

El runtimepath (rtp)

El runtimepath (h: rtp para la ayuda) es como he dicho una lista de lugares en los que se buscarán los directorios plugin, pero también son los lugares donde la función require buscará las "librerías" de Lua. Tales librerías son meramente otros ficheros Lua. require ejecutará el código de ese archivo y devolverá el valor que se devuelva en la ejecución del mismo o nil si se alcanza el final del archivo sin retornar nada explícitamente.

El runtimepath es una variable almacenada en vim.opt y rtp es su alias. No obstante como nota curiosa si haces :lua print(vim.opt.runtimepath) y :lua print(vim.opt.rtp) se obtiene distinto resultado mostrando que cada objeto está alojado en una dirección distinta y por lo tanto son inicialmente distintos, aunque puedan compartir contenido.

Lo que lazy.nvim hace para extender las características de Nvim es descargar mediante git los plugins en el directorio que Nvim utiliza para almacenar datos y extender el runtime path a esos lugares donde bien seguro encontraremos scripts en un directorio plugin para que en su momento sean ejecutados.

Lo que todo el mundo debe conocer de Nvim

Y no hablo de los modos. Este no es un tutorial para usar Nvim (o Vim) pero ya puestos dejó un enlace a un pequeño tutorial sobre los modos en esta págica de freeCodeCamp.

Aquí tan solo se pretende introducir conceptos muy básicos de Nvim pero que pueden resultar confusos inicialmente al tratarse de la familia Vim de un conjunto de editores muy particular.

Comandos y "Ex Comandos"

Dentro del mundo de Vim los comandos se ejecutan mediante la pulsación de teclas una tras otra. Los comandos por defectos pueden consultarse en la ayuda con :h index. Algo importante a saber es que los comandos disponibles en cada momento depende del modo en el que se encuentre Nvim. No obstante existe otra forma de pedirle cosas a Nvim desde el editor mediante los llamados Ex-Commands.

Hemos visto dos de estos comandos como son :help (o :h) y :lua. El primero era para mostrar la ayuda el segundo es para ejecutar código Lua. Estos comandos inician con los dos puntos (:) que es la tecla a pulsar para entrar en modo comando.

Existe una lara lista de comandos integrados pudiendo consultarlos en la ayuda con :h Ex-command. Además ya veremos que se pueden añadir más comandos mediante el uso de la API de Nvim. Uno de estos comandos es :set utilizado para modificar las opciones (ayuda con :h options).

Respecto a los Ex-commands podemos ejecutarlos desde un script Lua mediante vim.cmd(...) pasando como parámetro la cadena con el comando pero sin incorporar los dos puntos, como siempre para ver la ayuda hacemos :h vim.cmd y aprenderemos otras vías alternativas para hacer esas mismas llamadas a los comandos.

Aunque pueda parecer abrumador Nvim es especialmente versatil pudiendo llegar al mismo resultado usando distintas vías. AL final el usuario utilizará la que más le guste o le parezca más cómoda.

En cuanto a los comandos se pueden crear comandos adicionales, llamados comandos de usuario, para ello ver h: lua-guide-commands-create.

Opciones y variables

Cuelgan del objeto vim otra serie de objetos importantes. Uno es vim.o donde estan las opciones a modificar con :set. La lista de opciones a modificar es cerrada por lo que si intentas incorporar opciones nuevas recibirás un error. Su manipulación tiene el mismo efecto que el comando set.

Luego tenemos vim.opt que almacena la misma información pero con un comportamiento diferente. Mientras en vim.o se almacena el resultado exáctamente de la misma manera en la que se pasaría con el comando :set, en vim.opt se una un método más Lua-style. Haz un :h vim.opt en Nvim y el primer ejemplo te ilustrará perfectamente la diferencia.

Otro espacio importante es vim.g donde se almacenan variables globales. En este caso ya no hablamos de listas cerradas. Son variables accesibles desde cualquier lugar. Luego existen otros espacios como vim.w que son variables accesibles desde cada ventana de Nvim, o vim.b desde cada buffer, o vim.t desde cada tab.

Respecto a esto último hay que tener en cuenta que el comportamiento de Nvim puede diferir de donde esté el foco en un determinado momento, además del modo en el que esté.

Para conocer más sobre ventanas, buffers y tabs consultar :h windows pero básicamente un buffer es la representación en memoria de un archivo de texto, la ventana es el lugar en el que se dibuja el buffer y un tab es un contenedor para ventanas que pueden estar dispuestas vertical u horizontalmente.

API

Existen varios espacios de nombres donde podemos encontrar funciones que utilizar en nuestros scripts. Uno de ellos es vim.fn donde encontramos las funciones del API de VimScript. Vim sigue desarrollándose y Nvim junto a él manteniendo siempre la API del Vim original accesible desde vim.fn. Existe un método alternativo usando la función vim.call(...).

Si recordamos el comando para mostrar el <config_dir> este era :echo stdpath('config'). Lo que hace esa línea es ejecutar el comando echo de VimScript que lo que hace es imprimir el resultado de la expresión a continuación la cual es una llamada a una función de VimScript llamada stdpath que devuelve un directorio del sistema, en este caso el de configuración. Mismo resultado podíamos haber obtenido con :lua print(vim.fn.stdpath('config')). No es difícil ver que el comando :lua se utiliza para ejecutar código Lua.

Visto el comando anterior es un buen momento para introducir vim.print que es una función que realiza un dump del objeto Lua con el que podemos visualizar todo su contenido y no únicamente la dirección que ocupa la tabla.

En vim.api se alojan funciones propias de Nvim, existiendo otras que penden directamente del objeto vim y que en su conjunto son conocidas como vim.builtin.

En vim.uv se alojan las funciones asíncronas, recibe este nombre ya que las mismas se realizan a través de la librería libuv. En versiones anteriores se utilizaba vim.loop (de event-loop), ahora son claramente un alias el uno del otro algo que puede comprobarse haciendo :lua print(vim.uv == vim.loop).

Atajos de teclado

Otro lugar importe del contexto de vim es vim.keymap que es donde se manipulan los comandos tenidos estos como las combinaciones de teclas destinadas a realizar acciones dentro de nvim que se añadirán o sobreescribirán las que se cargan por defecto. Aviso que dominar todas las combinaciones de teclas, así como Ex-commands, de Nvim es un auténtico desafío.

Para establecer un comportamiento a una tecla o combinación de teclas, no necesariamente simultáneas sino también pulsadas en serio, se utiliza el método set, y para eliminarlas el método del de ese objeto vim.keymap.

La ayuda de Nvim es tán ilustrativa que me ahorrará poner ejemplos.

Eventos (Autocommands)

La ayuda de Nvim describe Autocommands como un comando, usualmente función, a ejecutar automáticamente en respuesta a un determinado evento. Una entrada en la ayuda más específica para Lua es lua-guide-autocommands.

FileType

Normalmente querremos que el editor se adapte al tipo de archivos que estamos editando. Seguramente querremos un comportamiento distinto cuando editamos un script de python que cuando editamos por ejemplo un markdown como el que escribo ahora, precisamente en Nvim. Para hacer esto es tan sencillo como ejecutar un código similar al siguiente en algún lugar de la configuración:

vim.api.nvim_create_autocmd("FileType", {

  pattern = "lua",

  callback = function(event)

    vim.keymap.set('n', 'K', vim.lsp.buf.hover, { buf = event.buf })

  end

})

En este caso cuando Nvim lance el evento FileType indicando que ha cargado un archivo Lua se ejecutará una función que añadirá al keymap, en modo normal, el uso de la tecla k mayúscula para ejecutar la función vim.lsp.buf.hover, cuya funcionalidad voy a ignorar. Para realizar esta misma operación existe una alternativa que comentaré más tarde.

Empezando una setup de Nvim

Con una imagen general de los conceptos anteriores ya podemos adentrarnos es el mundo Como ya se ha dicho todo comienza en el archivo <config_dir>/init.lua. En una instalación limpia de Nvim el runtimepath únicamente contiene ese <config_dir>.

Aunque existe la opción de incorporar todo el código de configuración en eser achivo init.lua lo normal es utilizar un árbol de archivos con distintas funcionalidades.

Editando nuestro init.lua debemos tener en cuenta que accederemos a esos otros archivos mediante require, función con la que se buscarán archivos tanto en el runtimepath como en el directorio lua/ de cada uno de esos elementos del runtimepath.

Inicializando lazy.nvim

El gestor de plugins es lo primero que seguramente querramos configurar. En este caso es lazy.nvim. En mi caso cree inicialmente un <config_dir>/init.lua con este contenido:

require ("lazy_init")

creando un archivo <config_dir>/lua/lazy_init.lua siguiendo lo que dicen en su repositorio, por tanto con el siguiente contenido:

-- Bootstrap lazy.nvim
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
  local lazyrepo = "https://github.com/folke/lazy.nvim.git"
  local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
  if vim.v.shell_error ~= 0 then
    vim.api.nvim_echo({
      { "Failed to clone lazy.nvim:\n", "ErrorMsg" },
      { out, "WarningMsg" },
      { "\nPress any key to exit..." },
    }, true, {})
    vim.fn.getchar()
    os.exit(1)
  end
end
vim.opt.rtp:prepend(lazypath)

-- Setup lazy.nvim
require("lazy").setup({
  spec = {
    -- import your plugins
    { import = "plugins" },
  },
  -- Configure any other settings here. See the documentation for more details.
  -- colorscheme that will be used when installing plugins.
  install = { colorscheme = { "habamax" } },
  -- automatically check for plugin updates
  checker = { enabled = true },
})

El código de init lo que hace es llamar al de lazy_init que a su vez se asegura de descargar el repositorio de lazy.nvim en <data_dir>/lazy/lazy.nvim pudiendo conocer el destivo con :echo stdpath('data').

Instalado el repositorio se añade el directorio en el que se almacena al runtimepath, algo que se hace al inicio del mismo (con prepend) para que tome prioridad.

Hecho lo anterior ya podemos acceder con require("lazy") llamada con la cual vamos a saber algo más sobre la función require en Nvim una vez que el archivo que termina cargando está alojado en <data_dir>/lazy/lazy.nvim/lua/lazy/init.lua.

Buen momento para indicar donde buscará require('lazy') la carga del módulo. Empezará buscando un archivo con el nombre lazy.lua en el mismo directorio, probará también con lua/lazy.lua y también con lua/lazy/init.lua. Esto lo hará de manera ordenada con todos los directorios del runtimepath. En este caso se procuró incorporar al runtimepath, en el primer lugar de la lista, el directorio donde se han alojado las fuentes de lazy.nvim.

Animo al lector a abrir tal archivo y analizar el contenido para conocer más sobre el estándar de construcción de módulos, usualmente partiendo de una tabla local M = {} a la que se van incorporando funciones. La última línea del archivo será retornar precisamente esa tabla.

Incorporando plugins

Con la configuración anterior lazy.nvim se encargará de ejecutar plugins a través de configuraciones dadas en cualquier archivo Lua <config_dir>/lua/plugins/. Esos archivos deben retornar un objeto o una lista de objetos que cumplan el plugin spec de lazy.nvim. En mi caso utilizo un archivo para cada plugin en los cuales se retorna un único plugin spec por script. Otras soluciones utilizan un único archivo que devuelve una larga lista o separan la lista en varias listas por archivos temáticos, siendo cuestión de gustos el como construir tu setup.

En la documentación de los plugins de nvim más modernos normalmente se encuentra siempre una tabla que respeta este plugin spec y que se utiliza como configuración inicial facilitando la instalación ya que únicamente deberemos copiar y pegar.

En mi caso quiero utilizar los archivos del directorio plugins únicamente para incorporar el plugin a Nvim a través de lazy.nvim. La configuración la deseo en otro lugar, concretamente yo lo hago en <config_dir>/lua/configs/ donde almaceno un fichero por cada plugin que deseo configurar algo que hago estableciendo el valor config del plugin spec como sigue:

return {
    ...,
    config = function()
        require "configs.<pluginname>"
    end
}

En el caso de que no quiera realizar configuración alguna lo que hago es darle esta forma:

return {
    ...,
    opts = {}
}

Y que tal y como se indica en la documentación de lazy.nvim esto provocará la llamada automática a la función config del plugin, si es que esta existe y normalmente siempre existe y establece una configuración por defecto.

No existen reglas fijas para establecer un plugin spec pero yo decidí fijarme estas y no configuro opts directamente en el plugin spec, lo hago en el archivo de configuración donde llamo manualmente a setup.

Por otro lado el contenido y forma de opts será dependiente del plugin y normalmente esto estará documentado en su repositorio. En el caso de que no sea así existe una especie de convención por la que se implementa la función setup en un archivo lua/<pluginname>/config.lua del código fuente del plugin.

La mayoría de los creadores de plugins sigue esa especie de convención. El código se almacena en archivos en lua/<pluginname>/, empezando por un init.lua que es el que resulta llamado por require("<pluginname>") y desde ahí se llaman a diferentes archivos con los que se termina construyendo el módulo, entre ellos suele estar config.lua aunque por mi experiencia algunos desarrolladores son más cuidadosos que otros con la estructuración y la limpieza.

Aunque evidentemente cada plugin puede requerir configuración muy distinta en la mayoría de los casos basta con establecer una tabla de opciones y llamar a la función setup del plugin pasando tales opciones para inicializarlo. Además en el archivo de configuración suelo aprovechar para establecer las keymaps con las que acceder, en su caso, a las funcionalidades del plugin. Un ejemplo de configuración para el plugin nvim-tree:

--nvim ~/.config/nvim/lua/config/tree.lua
---@type nvim_tree.config
local config = {
    sort = {
      sorter = "case_sensitive",
    },
    view = {
      width = 30,
    },
    renderer = {
      group_empty = true,
    },
    filters = {
      dotfiles = true,
    },
  }

-- setup
require("nvim-tree").setup(config)

-- keymaps 
vim.keymap.set("n", "<leader>e", "<cmd>NvimTreeFocus<CR>", { desc = "nvim tree focus window"})
vim.keymap.set("n", "<C-n>", "<cmd>NvimTreeToggle<CR>", { desc = "nvim tree toggle window"})

Y en cuanto al archivo con el que se carga el plugin el mismo lo muestro a continuación, siendo una de las excepciones a la regla de un archivo un plugin. Aunque el plugin spec de lazy.nvim contiene un campo llamado dependencies para listar otros plugins para que se cargen con el primero yo prefiero ambos de manera separada. Con nvim-tree uso nvim-web-icons:

--nvim ~/.config/nvim/lua/plugins/tree.lua
return {
    {
        "nvim-tree/nvim-tree.lua",
        -- cmd = {"NvimTreeToggle", "NvimTreeFocus" },
        config = function()
            require "configs.tree"
        end
    },
    {
        "nvim-tree/nvim-web-devicons"
    }
}

En el ejemplo tengo comentada la línea con las opciones cmd. Examinando el plugin spec de lazy.nvim esto se utiliza para establecer la ejecución de qué comandos provocará la carga tardía (Lazy) del plugin. En este caso prefieron que inicie automáticamente al inicio de Nvim, de lo contrario no cargaría la configuración y los keymap no se establecerían.

En lazy.nvim, demás de carga taría por la ejecución de un Ex-Command, también puede establecerse por eventos, pulsaciones de teclas o dependiendo te tipo de fichero que se cargue en el buffer.

Más allá de los plugins

Teniendo claro el gestor de plugins a usar y los plugins que queremos usar y configurar podemos modificar o añadir más funcionalidades que estos no aportan habiendo puesto ya un ejemplo de un Autocommand para cuando se dispara el evento FileType. En ese ejemplo además se ilustraba el uso de vim.keymap.set (:h vim.keymap.set) función que podemos utilizar ya desde nuestro init.lua o cualquier fichero auxiliar que carguemos a través del uso de require.

Cualquier setup de Nvim normalmente querrá realizar una o más de las siguientes acciones:

Mi init.lua es relativamente sencillo cargando un script distinto para cada uno de los grandes grupos de acciones a realizar con el setup:

require("lazy_init")
require("set")
require("keymap")
require("usercommands")
require("autocommands")

En el caso de lazy_init contiene el grueso del setup que es la carga de los plugins, acción bastante transparente ya que se encarga de todo lazy.nvim.

Aunque se indique ese require en primer lugar hay que tener en cuenta que la carga real de los plugins se realizará más tarde, a partir del evento UIEnter, por lo que cualquier plugin puede terminar sobreescribiendo las acciones que configuremos a continuación en los otros archivos. Esto suele ser el foco de algunos comportamientos inesperados.

Para mantener la setup lo más limpia los scripts se limitarán a realizar las acciones que su propio nombre indica y set se limitará a modificar vim.o (o vim.opt, o ambos, según gustos), keymap será básicamente una lista de vim.keymap.set, usercommands una lista de vim.api.nvim_create_user_command y autocommands una lista de vim.api.nvim_create_autocmd.

Para una lista más exahustiva de la API para comandos de usuario ver h: api-command y para Autocommands ver :h api-autocmd y en general para toda la API :h api.

A partir de aquí ya sólo queda profundizar más por tu cuenta para intentar adaptar Nvim a tus necesidades.

Solo una cosa más

Existe otro directorio opcional dentro del <config_dir> a tener en cuenta, este es el directorio <config_dir>/after en el que se pueden alojar scripts que se ejecutarán una vez suceda alguna acción contemplada. Por ejemplo dentro de after podemos crear el directorio ftplugin donde almacenar scripts con nombre de alguno de los typos/lenguajes soportados, o sus alias. Estos scripts son conocidos como FileTypePlugins y se ejecutan cada vez que en un buffer se abre un archivo de ese tipo.

Hay que tener una cosa más con estos plugins de contexto y es que a no ser que sea lo pretendido las opciones que debemos configurar no deben ser las globales sino las locales al buffer abierto o la ventana en la que se muestra, para esto están vim.bo (buffer options) y vim.wo (window options) que son la versión local de vim.o para los buffers y las ventanas respectivamente. vim.opt_local puede usarse igualmente y referirá a las opciones del elemento en el que estemos ya sea ventana o buffer.

En lo que respecta a las keymap debe añadirse una opción para que indique que debe operar únicamente en el buffer activo. Podemos obtener el mismo resultado que en el ejemplo del autocommand del FileType creando un script <config_dir>/after/ftplugin/lua.lua con el siguiente contenido:

vim.keymap.set('n', 'K', vim.lsp.buf.hover, { buf = 0 })

Puede que en algún otro ejemplo de otro lugar veas buffer = true en lugar de buf = 0 para indicar el buffer actual, ese es un método en desuso. En el código fuente de keymap puedes ver que el contenido de buffer se traslada a buf y cuando este valor es true se traslada 0.

Existen más posibilidades con el directorio after aunque no parece sencillo encontrar documentación al respecto, si haces un :h after no parece que realmente te lleve al lugar adecuado de la ayuda. Donde debemos acudir de nuevo es a h: runtimepath y examinar el apartado para after. Hasta ahora no he encontrado utilidad más que a ftplugin y aún me debato entre si usar esta opción o la del Autocommand para FileType.