Mi propia configuración NeoVim (I)
Por Arrecio
La verdad es que no utilizo habitualmente NeoVim ya que mi editor de referencia es Visual Studio Code. Ni siquiera VSCodium. Nos guste o no, Microsoft hace muy buenos productos y Code es una buen ejemplo de ello.
Que no use NeoVim o simplemente nvim de manera habitual no quiere decir que no me considere usuario de este editor de texto. En las conexiones remotas por terminal a mis máquinas Linux sí que lo utilizo, aunque con las limitaciones que puede tener un usuario poco experimentado como yo. Llevo algún tiempo queriendo hacerme una configuración personalizada desde cero ya que lo que hago habitualmente es importar las de otras personas y me incomoda un poco sentir que no aprovecho para nada sus posibilidades de personalización.
NeoVim es un vim con esteroides. Un rework de este editor que a su vez es una evolución del extinto vi. Fanáticos de esta familia de editores en modo texto llevan años evitando que sea fagocitado por la gran competencia que les ha ido saliendo en entorno gráfico. Además de Code, otros editores como Sublime Text o Atom (que sucumbió a Code en 2022), surgieron como alternativas a vim quien todavía sigue renovándose y goza de bastante buena salud. Eso sin contar otros editores que más que esto son un IDE como por ejemplo el gran catálogo ofrecido por JetBrains.
Lo que transforma un editor de texto tipo NeoVim o Code en una plataforma para desarrollar son sus plugins y de alguna manera de eso va este artículo y lo que supongo que le seguirán, de inflar NeoVim hasta convertirlo en algo que pueda usar cómodamente para escribir código, o por ejemplo este blog.
Por suerte, la presencia de los emuladores de terminal modernos y especialmente las llamadas Nerd Fonts están extendiendo la vida de los editores en modo texto como los de la familia vim bastante más allá de lo que se pensaba, ganando incluso adeptos.
Este artículo no va encaminado a introducir vim, o nvim, a un nuevo usuario. Para eso hay muchas mejores referencias. Además si algo caracteriza a todos los productos de esta familia es su ayuda integrada. Nada más abrir nvim puedes acceder a ella escribiendo :h o :help. En general tanto vim como NeoVim son dos grandes productos de software libre, con una genial documentación y tremendo soporte de su comunidad. Se tratan de editores muy queridos y muy bien cuidados, si bien su pronunciada curva de aprendizaje siempre le ha hecho considerarse como un editor orientado a frikis.
Si eres uno de estos fanáticos probablemente no estarás leyendo esto pero si quieres convertirte en uno atento a esta web VimGolf donde se organizan competencias en las que demostrar que tal dominas los comandos de vim.
Dicho lo anterior ni siquiera voy a hablar de los modos de vim, ni mucho menos de los comandos principales. Para una breve introducción visita, por ejemplo, Cómo usar Vim: Tutorial para principiantes que es perfectamente válido para nvim.
Lo que aporta NeoVim sobre vim es especialmente la utilización de Lua como lenguaje de configuración y extensión de funcionalidades como alternativa a vimscript, que sigue disponible y además puede ejecutarse desde lua con la función vim.cmd. Al ser un fork de vim no se garantiza que las nuevas funciones que se vayan añadiendo a vim o a vimscript por parte del equipo original se vean implementadas en NeoVim, por lo que si vas a ser usuario de esta familia de vim la primera decisión será elegir este estos dos. Yo he elegido NeoVim porque su comunidad es más grande, y no por ningún otro motivo. A partir de ahora me referiré siempre a este editor con nvim por ser el nombre del ejecutable que lo lanza.
Antes de seguir decir que evidentemente existen buenas alternativas a configurarte NeoVim desde cero, siendo algunas de ellas NvChad, AstroNvim, LunarVim (algo abandonada) o LazyVim. Todas ellas se tienen por distribuciones de NeoVim. En mi caso la que más he usado es NvChad y de hecho si usara NeoVim probablemente sería la que usaría antes que mi configuración personal pero el objetivo es aprender un poco acerca del funcionamiento de este editor.
Fichero de configuración (carga de lazy.nvim)
Como cualquier buen software que se precie, la configuración de todo el entorno se puede realizar en ficheros de texto plano localizados en un directorio en particular. En el caso de Linux se almacena en ~/.config/nvim/ y en Windows en ~/AppData/Local/nvim/. Además existe un directorio auxiliar que en el caso de Windows se localiza en ~/AppData/Local/nvim-data/ mientras que en Linux desconozco si puede variar según distribuciones, en Arch se localiza en ~/.local/share/nvim/. Yo estoy en Windows y voy a utilizar sus rutas en los ejemplos.
Como dije, podemos configurar nvim mediante Lua o vimscript. Yo voy a elegir Lua. El archivo de entrada de la configuración es en este caso ~/AppData/Local/nvim/init.lua.
Sabiendo esto lo primero que habrá que hacer es hacer que entre en funcionamiento un gestor de plugins. Porque con mucho que queramos que nuestro nvim tenga un aspecto personal esto no significa que tengamos que gestionar los paquetes a la antigua usanza lidiando con todo lo que ello significa. Yo he elegido lazy.vim más que nada porque es el único que he utilizado hasta el momento.
Vamos a hacer que nvim lo utilice siguiendo las instrucciones de su documentación oficial. Lo primero será modificar el ~/AppData/Local/nvim/init.lua:
require("config.lazy")
La función require de Lua lo que hace en este caso es cargar un módulo Lua de nvim. Como argumento recibe un path relativo al runtimepath que es una lista de lugares en los que buscar los módulos. nvim trata la última parte como un archivo si es que existe tal archivo con extensión .lua al final del camino. Como segunda opción, y si ese final es un directorio que contiene un archivo init.lua, es ese archivo el que carga. runtimepath siempre contiene a ~/AppData/Local/nvim/lua/. De no encontrar nada probablemente se emitirá un error.
En nuestro caso vamos a crear~/AppData/Local/nvim/lua/config/lazy.lua que será el cargado y ejecutado por require("config.lazy") al que voy a dotar del siguiente contenido que indican en la web de lazy.nvim:
-- 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)
-- Make sure to setup `mapleader` and `maplocalleader` before
-- loading lazy.nvim so that mappings are correct.
-- This is also a good place to setup other settings (vim.opt)
vim.g.mapleader = " "
vim.g.maplocalleader = "\\"
-- 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 },
})
Al tratarse del primer script vamos a analizar lo que sucede si ejecutamos ahora nvim. En este caso si tenemos conexión a Internet se clonará mediante git el repositorio de lazy.nvim en ~/AppData/Local/nvim-data/lazy/lazy.nvim/, esto es lo que ocurre en la primera parte del script.
A continuación el comando vim.opt.rtp:prepend(lazypath) hace que el contenido de la carpeta lua del repositorio descargado pase a estar disponible dentro del realtimepath y por tanto podremos usar require con ella.
Los siguientes dos comandos modifican dos variables de vim.g y este hecho no es relevante al caso.
Con require("lazy") se intenta cargar un lazy.lua o lazy/init.lua en todo el realtimepath encontrando en este caso ~/AppData/Local/nvim-data/lazy/lazy.nvim/lua/lazy/init.lua que retorna un objeto que es el módulo lazy.nvim que se configura mediante su función setup. Esta función, así como otras muchas del entorno lazy.nvim, puede recibir parámetros tipo cadena o tipo tabla. Si hubiera recibido un parámetro tipo cadena hubiera importado los plugins como si el nombre de un módulo se tratase, pero simplemente eso. Con la tabla podemos configurar más cosas aunque ahora mismo no voy a entrar en ello y más existiendo este enlace que introduce el asunto.
No obstante si ejecutamos ahora nvim recibiremos un error en relación a que se nos dice que No specs found for module "plugins" el cual podría desaparecer si comentamos la línea { import = "plugins" },. Esto lo podemos eliminar o bien creando un archivo plugins.lua o un plugins/init.lua siguiendo los razonamientos ya vistos. lazy.nvim espera que la carga de dicho script devuelva una lista de descriptores de plugins a cargar. Si creamos un archivo que simplemente contenga:
return {}
Entonces evitaremos el error. Haciendo un :Lazy en nvim accederemos al plugin manager donde observaremos que ya hay instalado un módulo que es el propio lazy.vim lo que permite al gestor de paquetes auto-actualizarse.
Además del archivo que acabamos de ver,
lazy.vimtratará de incorporar cualquier otro archivo.luaque se encentre en el directorio importado, en este casoplugins.
Cargando nuestro primer plugin
Siguiendo el hilo, tocar añadir nuestro primer plugin y este será nvin-tree. Para indicarle a laz.nvim que añada este plugin debemos incluirlo en la lista devuelta desde plugins/init.lua. Los elementos de esa lista puede ser una simple cadena indicando la denominación del plugin, o un diccionario cuyo primer elemento sea precisamente esa cadena con la denominación del plugin. El resto de campos servirá como configuración inicial o lo que lazy llama plugin spec.
Para cargar el plugin debemos hacer referencia a su fuente siendo la más habitual la url a su repositorio en github sin incluir https://github.com/, en el caso de nvin-tree la url completa es https://github.com/nvim-tree/nvim-tree.lua pues puedo referirlo con nvim-tree/nvim-tree.lua.
Para ver más formas de referenciar los plugins está esta entrada de la documentación en la que se denomina a esto Spec Source.
Por tanto con hacer que plugins/init.lua tenga este aspecto ya se cargaría el plugin nvim-tree en la siguiente ejecución de nvim:
return {
"nvim-tree/nvim-tree.lua"
}
Sin embargo añadirlo así no aportará ninguna funcionalidad a no ser que realicemos algunas operaciones a posteriori que podemos realizar en la misma carga si en lugar de añadir meramente la denominación del plugin añadimos el spec. Esto consiste en sustituir la referencia al plugin por una tabla en la que su primer elemento será esa referencia. Cambio mi plugins/init.lua a:
return {
{
"nvim-tree/nvim-tree.lua",
version = "*",
lazy = false,
dependencies = {
"nvim-tree/nvim-web-devicons",
},
config = function()
require("nvim-tree").setup {}
end,
}
}
Esa tabla a la que llamamos plugin spec contiene distintos elementos que se dividen en campos que modifican la manera en que se carga el plugin, o en la que se configura el plugin, u otros documentados en la misma página de los enlaces indicados.
Reiniciado nvim podemos hacer un :Lazy para comprobar que tanto nvim-tree como su dependencia nvim-web-devicons han sido incorporados al plugin manager. Y con un :NvimTreeToggle podremos comprobar que se abre el árbol del directorio de trabajo.
Aprovecho para hacer un comentario. El spec anterior lo he copiado de NvChad, sin embargo en la documentación de lazy.nvim recomiendan no utilizar config en favor de opt por lo que al final lo voy a dejar así:
return {
{
"nvim-tree/nvim-tree.lua",
version = "*",
lazy = false,
dependencies = {
"nvim-tree/nvim-web-devicons",
},
opts = {},
}
}
El motivo no es otro que se sobreescribe la función config del plugin, la cual es llamada automáticamente si se establece implícitamente un valor a opts aunque este sea una tabla vacía o una función que no hace nada ya que opts también admite una función.
¿Y qué ha pasado aquí?
Por resumir, lo que ha pasado es que en ~/AppData/Local/nvim-data/ se ha creado una carpeta llamada lazy en la que a su vez se crean carpetas para cada plugin. Ahora mismo debo tener al menos lazy.nvim, nvim-tree.lua y nvim-web-devicons.
En lazy.nvim tenemos todo el código del manejador de paquetes y si quieres aprender un poco más en el proceso de carga de plugins puedes echar un vistazo al archivo lua/lazy/core/loader.lua.
Si queremos explorar un poco como actúa cada plugin hay que tener en cuenta que estos básicamente son scripts que modifican el comportamiento de nvim mediante llamadas a su API o a la de otros plugins de los que en este caso dependen.
Vamos a trastear un poco para encontrar el punto de entrada del plugin. Como intuyo que en el caso de nvim-tree es ~/AppData/Local/nvim-data/nvim-tree.lua/lua/nvim-tree.lua lo voy a cambiar de nombre. Hecho esto al arrancar nvim ¡bingo!, obtengo el siguiente error:
Failed to run `config` for nvim-tree.lua
.../Local/nvim-data/lazy/lazy.nvim/lua/lazy/core/loader.lua:387: module 'nvim-tr
ee' not found:
no field package.preload['nvim-tree']
cache_loader: module 'nvim-tree' not found
cache_loader_lib: module 'nvim-tree' not found
no file '.\nvim-tree.lua'
no file 'C:\tools\neovim\nvim-win64\bin\lua\nvim-tree.lua'
no file 'C:\tools\neovim\nvim-win64\bin\lua\nvim-tree\init.lua'
no file '.\nvim-tree.dll'
no file 'C:\tools\neovim\nvim-win64\bin\nvim-tree.dll'
no file 'C:\tools\neovim\nvim-win64\bin\loadall.dll'
# stacktrace:
- lua/config/lazy.lua:25
- init.lua:2
En la línea 387 del loader.lua vemos que se realiza un require que debe devolver una tabla que se entiende el propio plugin con una función setup para configurar el plugin. El require se realiza sobre lo que devuelve la función get_main que por el error que hemos obtenido se ve que devuelve nvim-tree lo cual ahora mismo no tengo claro de donde se obtiene una vez que el plugin aparentemente se llama nvim-tree.lua (con el .lua formando parte del nombre). Si despejo la incógnita espero recordar el volver aquí y añadir una nota aclaratoria.
Pero por trastear un poco más, observo que en nvim-tree existe un directorio llamado plugin en el que existe otro archivo llamado nvim-tree.lua. Lo modifico para que me reporte un error en caso de que sea cargado y así es, ahora cuando inicio nvim me reporta:
Failed to source `C:/Users/arrec/AppData/Local/nvim-data/lazy/nvim-tree.lua/plugin/nvim-tree.lua`
vim/_editor.lua:445: C:\Users\arrec\AppData\Local\nvim\init.lua..nvim_exec2() called at C:\Users\arrec\AppData\Local\nvim\init.lua:0[1]..C:/Users/arrec/AppData/Local/nvim-data/lazy/nvim-tree.lua/plugi
n/nvim-tree.lua: Vim(source):E5112: Error while creating lua chunk: .../Local/nvim-data/lazy/nvim-tree.lua/plugin/nvim-tree.lua:3: '=' expected near '<eof>'
# stacktrace:
- vim\_editor.lua:445 _in_ **cmd**
- AppData\Local\nvim/lua/config/lazy.lua:25
- AppData\Local\nvim\init.lua:2
El error lo da en la línea 445 de vim\_editor.lua que no forma parte de lazy.nvim sino del toolkit de NeoVim para el uso Lua que en mi caso se encuentra en C:\tools\neovim\nvim-win64\share\nvim\runtime\lua\, aunque se termina lanzando desde la llamada a su setup (el de lazy).
Ahora mismo no tengo gran curiosidad en explorar el funcionamiento interno de NeoVim pero ahí lo dejo para quien quiera conocer en profundidad los entresijos de la carga de los plugins.
Mapeando comandos mediante accesos rápidos
Escribir :NvimTreeToggle cada vez que queramos mostrar el árbol de directorios no parece muy productivo pero podemos asignar combinaciones de teclas para que ejecuten ciertos comandos. Lo habitual es que la configuración de todas las combinaciones de teclas que configuremos se concentren en un mismo archivo, sin perjuicio de que algunos plugins hagan lo propio lo cual puede terminar deviniendo en conflictos. A esto se le llama mapeo de teclas o keymap y para ello se usa la función vim.keymap.set.
El archivo donde voy a colocar todos mis "mapeos" será en config/keymaps.lua por lo que mi init.lua deberá llamarlo:
require("config.lazy")
require("config.keymaps")
El contenido de mi config/keymaps.lua es:
local map = vim.keymap.set
-- nvimtree
map("n", "<C-n>", "<cmd>NvimTreeToggle<CR>", { desc = "nvimtree toggle window" })
El primer parámetro de vim.keymap.set (o el aquí alias suyo map) es el modo de vim en el que el acceso rápido estará disponible. En este caso es en Normal mode. El aquí segundo parámetro es la combinación de teclas, en este caso Control + n, el tercero indica que lo que se ejecutará será una pulsación de teclas (admite funciones Lua también). <cmd> indica dos dos puntos (:), a continuación escribe NvimToggle y presiona Enter. El último parámetro es una tabla con opciones, en este caso se establece la descripción del mapeo.
¿Y ahora qué?
Pues de momento tan sólo tengo un editor de texto al estilo vim en el que puedo mostrar un árbol de directorios con Control+n, y poco más. A partir de aquí lo que haré será añadir plugins a medida que vaya echando de menos algunas funcionalidades ya sea de VSCode o de algunas distribuciones que he usado de NeoVim. Igual con algo de suerte termina saliendo algo que pueda terminar usando habitualmente.