Anatomía de un plugin: la estructura que Neovim ya conoce
Qué es realmente un plugin: una entrada en el runtimepath con subdirectorios de significado convenido. Carga automática de plugin y ftplugin, carga bajo demanda de lua, el papel de after, el espacio de nombres de los módulos y la documentación como parte del artefacto.
Un plugin de Neovim no es un artefacto que se instale, ni un paquete que se registre, ni una categoría que el editor conozca. Es un directorio. Lo único que lo convierte en plugin es que su ruta figure en una lista llamada runtimepath y que dentro tenga subdirectorios cuyos nombres el editor ha acordado interpretar de cierta manera. No hay manifiesto, no hay proceso de activación, no hay ciclo de vida gestionado por un contenedor: hay una lista de rutas y un puñado de convenciones de nombres heredadas de Vim. Entender esa sencillez radical es lo que separa a quien copia plantillas de quien diseña plugins, porque a partir de ahí cada decisión —qué va en plugin/, qué en lua/, cuándo se ejecuta cada cosa— deja de ser magia del gestor y pasa a ser una consecuencia previsible de un mecanismo que cabe en un párrafo.
- Explicar el
runtimepathcomo el único mecanismo de descubrimiento y el orden en que se recorre. - Distinguir los directorios que Neovim ejecuta solo por existir de los que solo sirven bajo demanda.
- Diseñar el espacio de nombres de
lua/como contrato público y evitar colisiones. - Incorporar
doc/y loshelptagsal artefacto desde el primer commit.
El runtimepath es todo el mecanismo
Cuando el editor arranca construye una lista ordenada de directorios raíz. Tu configuración personal va primero, los directorios de sistema después, y al final las rutas equivalentes bajo after. Cualquier gestor de plugins —lazy.nvim, vim.pack, un git clone a mano dentro de un directorio de paquetes— hace exactamente una cosa: insertar rutas en esa lista. No existe un registro de plugins instalados; existe una lista de rutas y una serie de operaciones que se aplican a todas ellas por igual.
-- La lista real, ya expandida, en el orden en que se recorre
vim.print(vim.api.nvim_list_runtime_paths())
-- Buscar un fichero en todas las rutas a la vez
vim.print(vim.api.nvim_get_runtime_file("plugin/*.lua", true))
-- Anadir tu carpeta de trabajo al frente, sin gestor de por medio
vim.opt.rtp:prepend(vim.fn.expand("~/dev/mi-plugin.nvim"))
No existe ninguna operación llamada instalar en Neovim. Un gestor de plugins descarga un repositorio, escribe su ruta en la lista y, como mucho, genera el índice de ayuda. Todo lo que un gestor hace lo puedes hacer tú con git clone y una línea de prepend, y saberlo es lo que te permite depurar el día que el gestor se comporta de forma inesperada: la pregunta correcta nunca es qué hizo el gestor, sino qué rutas hay en la lista y en qué orden.
El orden importa por dos razones. La primera es que ciertos directorios se ejecutan de arriba abajo, de modo que quien va antes define primero y quien va después puede sobrescribir. La segunda es que las rutas bajo after se recorren al final precisamente para dar la última palabra al usuario: es el mecanismo canónico para corregir un plugin sin bifurcarlo. Un plugin bien educado nunca coloca nada en after salvo que su propósito explícito sea enmendar a otro.
flowchart TB a[Arranque de Neovim] --> b[Lista runtimepath ordenada] b --> c[Ejecuta todo plugin barra estrella punto lua] c --> d[Lee init de la configuracion] d --> e[Al abrir un buffer ejecuta ftplugin del tipo detectado] e --> f[Modulos de lua barra solo cuando alguien hace require] f --> g[Rutas after al final para tener la ultima palabra] style c fill:#f38ba8,color:#11111b style f fill:#a6e3a1,color:#11111b
Directorios con significado convenido
Solo unos pocos nombres tienen efecto, y conviene memorizar la diferencia entre los que el editor ejecuta por su cuenta y los que se limitan a estar disponibles. Confundirlos es el origen del noventa por ciento de los plugins lentos y de los errores de tipo funciona en mi configuración pero no en la tuya.
plugin/
Todos sus ficheros Lua o Vimscript se ejecutan al arrancar, sin condiciones. Es el único punto de entrada automático y por eso debe ser mínimo: registrar comandos y autocomandos, nada más.
ftplugin/
Se ejecuta al detectar un tipo de archivo, una vez por buffer. Ahí van opciones locales y atajos que solo tienen sentido para ese lenguaje, nunca configuración global.
lua/
No se ejecuta jamás por sí solo. Es el árbol de módulos que require recorre bajo demanda. Aquí vive el noventa y nueve por ciento del código real de tu plugin.
doc/
Contiene la ayuda en formato de manual. Tras generar los helptags, tu plugin se documenta con :help como cualquier parte del editor.
Hay un matiz sobre ftplugin/ que conviene fijar pronto porque explica muchos comportamientos desconcertantes: sus ficheros se ejecutan una vez por buffer, no una vez por sesión, y lo hacen para cada ruta del runtimepath que contenga un fichero con ese nombre. Es decir, varios plugins pueden aportar simultáneamente su propio fragmento para el mismo tipo de archivo, y todos se ejecutan en orden. De ahí que la regla sea inflexible: dentro de ftplugin/ solo opciones locales al buffer y atajos locales al buffer, nunca ajustes globales, porque un ajuste global escrito ahí se aplicará al abrir cierto tipo de fichero y persistirá para todos los demás.
Existen otros con propósito específico —colors/ para esquemas de color, syntax/ y queries/ para resaltado, autoload/ como reliquia de Vimscript, spell/, compiler/— pero para un plugin moderno escrito en Lua la lista relevante es la de arriba. Advierte la asimetría fundamental: plugin/ cuesta tiempo de arranque a todo el que instale tu plugin, lo use o no; lua/ no cuesta absolutamente nada hasta que alguien lo pide. Toda la disciplina de rendimiento del ecosistema se deriva de esa única frase.
El espacio de nombres es un contrato público
require no es un mecanismo especial de Neovim sino el de Lua, con un buscador añadido que traduce puntos en separadores de ruta y recorre cada entrada del runtimepath buscando dentro de su lua/. Como consecuencia, el nombre del módulo no es un detalle interno: es la única superficie por la que el mundo exterior tocará tu código, y colisiona globalmente con la de todos los demás plugins instalados.
-- Estructura minima de un plugin llamado pico.nvim
-- pico.nvim/
-- plugin/pico.lua -> se ejecuta solo, debe ser minimo
-- lua/pico/init.lua -> require("pico")
-- lua/pico/config.lua -> require("pico.config")
-- doc/pico.txt -> :help pico
-- plugin/pico.lua
if vim.g.loaded_pico then return end -- guarda contra doble ejecucion
vim.g.loaded_pico = true
vim.api.nvim_create_user_command("Pico", function(opts)
require("pico").abrir(opts.args) -- el require ocurre AQUI, no arriba
end, { nargs = "?", desc = "abre el bloc del proyecto" })
Tres reglas gobiernan este espacio. La primera: un único directorio raíz por plugin, con el nombre del proyecto sin sufijos, porque lua/utils/init.lua en dos plugins distintos es una colisión silenciosa que gana quien esté antes en la lista. La segunda: require memoriza en package.loaded, de modo que el fichero se ejecuta una sola vez y todo lo que escribas en el cuerpo del módulo es, en la práctica, un constructor global irrepetible. La tercera: la guarda con una variable global en plugin/ sigue siendo necesaria, porque nada impide que dos copias del plugin estén en el runtimepath a la vez.
Escribir local pico = require("pico") en la primera línea de plugin/pico.lua anula por completo la pereza: fuerza a cargar todo tu árbol de módulos en cada arranque, aunque el usuario no llegue a invocar el comando nunca. El require debe estar dentro del cuerpo de la función de retorno, no en el ámbito del fichero.
Documentar es parte de escribir el plugin
El fichero de ayuda no es un extra opcional sino la interfaz que hace descubrible tu trabajo dentro del editor. Su formato es texto plano con dos convenciones: las etiquetas se marcan entre asteriscos y las referencias entre barras verticales. Un índice generado a partir de esas etiquetas es lo que permite saltar con :help desde cualquier sitio.
*pico.txt* Bloc de notas por proyecto en ventana flotante
*pico-opciones*
Valores por defecto: >lua
require("pico").setup({
ancho = 0.6, -- fraccion del ancho de la pantalla
filetype = "markdown",
})
<
Ver tambien |pico-comandos| y |pico-eventos|.
# Genera el indice de etiquetas del directorio doc de tu plugin
nvim --headless -c "helptags ~/dev/pico.nvim/doc" -c q
# O, dentro del editor, para todas las rutas del runtimepath
:helptags ALL
Los gestores modernos ejecutan esa generación por ti al instalar, pero durante el desarrollo local tendrás que invocarla a mano cada vez que añadas una etiqueta nueva. Un fichero de ayuda decente cabe en cien líneas: una descripción de una frase, la tabla de opciones con sus valores por defecto, la lista de comandos, la de funciones públicas y una nota sobre requisitos. Escribirlo temprano tiene un efecto secundario valioso: obliga a formular la superficie pública antes de implementarla, y casi siempre revela que sobra la mitad.
Merece la pena detenerse en lo que este diseño no tiene, porque ahí está la lección transferible. No hay sistema de plugins. No hay clase base de la que heredar, ni interfaz que implementar, ni manifiesto que declare capacidades, ni contenedor que gestione el ciclo de vida, ni registro central que valide compatibilidades. Hay una lista de rutas, tres o cuatro nombres de directorio con semántica acordada y el mecanismo de módulos del lenguaje anfitrión sin modificar. Un plugin y una configuración personal son literalmente la misma clase de objeto; la única diferencia es quién escribió la ruta y en qué orden entró en la lista. Esa identidad tiene consecuencias que ningún sistema de plugins convencional puede ofrecer. Puedes convertir tu configuración en plugin moviendo una carpeta. Puedes leer el código de cualquier plugin instalado con el mismo require que usarías para el tuyo, e incluso reemplazar una de sus funciones en caliente desde tu propia configuración, porque no hay frontera de proceso ni encapsulación forzada entre ambos. Puedes depurar el ecosistema entero con las mismas herramientas con que depuras tu archivo de arranque. El precio de esa libertad es que nada te protege: no hay control de versiones de la interfaz, no hay aislamiento, un plugin puede romper a otro y el orden de la lista decide silenciosamente quién gana. Es exactamente el mismo intercambio que hace un sistema operativo tipo Unix al representarlo todo como ficheros en un árbol único de rutas, y por las mismas razones históricas. Aprender a escribir plugins no es, entonces, aprender un marco de trabajo: es interiorizar un espacio de nombres compartido y desarrollar la cortesía necesaria para habitarlo sin pisar a nadie. Toda la ingeniería de los próximos capítulos —el patrón de configuración, la pereza, la limpieza de recursos— consiste en construir voluntariamente las garantías que el mecanismo se negó deliberadamente a imponerte.
- Crea
~/dev/pico.nvimconplugin/,lua/pico/,doc/y uninit.luaque exporte una función que muestre un mensaje. - Añade la ruta al
runtimepathdesde tu configuración conprepend, reinicia y comprueba que:Picofunciona sin gestor de plugins de por medio. - Ejecuta
nvim --startuptimecon y sin unrequireen el ámbito de fichero deplugin/pico.luay anota la diferencia en milisegundos. - Provoca deliberadamente una colisión: crea
lua/utils/init.luaen tu plugin y comprueba connvim_get_runtime_filequé copia gana según el orden de la lista. - Escribe
doc/pico.txtcon una etiqueta, genera loshelptagsy verifica que:help picosalta a ella desde cualquier buffer.