Compatibilidad: capacidades, adaptadores y deprecación elegante
Soportar varias versiones de Neovim sin duplicar el plugin, por qué detectar capacidades vence a comparar números, dónde colocar las compuertas y los adaptadores, cómo declarar una política de soporte y cómo retirar algo sin romperle el día a nadie.
El día que publicas dejas de tener una versión de Neovim y pasas a tener todas las que usan tus usuarios: la del canal nocturno que estrena una API cada semana, la estable que salió hace dos meses, la que trae empaquetada una distribución conservadora y la que un administrador de sistemas congeló en una máquina remota a la que nadie quiere tocar. Tu plugin correrá en todas ellas sin que puedas probarlo en ninguna salvo la tuya. La compatibilidad no consiste en escribir código para el mínimo común denominador —eso es renunciar—, sino en construir un plugin que descubra en tiempo de carga qué tiene delante y se comporte lo mejor posible con lo que hay.
- Distinguir la comprobación de versión de la detección de capacidades y saber cuándo toca cada una.
- Aislar las diferencias entre versiones en adaptadores en lugar de esparcirlas por el código.
- Declarar y sostener una política explícita de versiones soportadas.
- Retirar funciones y opciones con avisos que informan una vez y no molestan nunca más.
Versiones frente a capacidades
La comprobación de versión es la respuesta obvia y funciona: el editor expone un predicado de características que entiende cadenas del estilo nvim-0.11 y devuelve uno si la versión es esa o superior. Sirve, y para algunas cosas es lo único que sirve, porque hay cambios de comportamiento que no se manifiestan como la existencia de un símbolo nuevo.
if vim.fn.has("nvim-0.11") == 1 then
-- comportamiento que solo es correcto a partir de esa version
end
El problema es que un número es una aproximación grosera a lo que realmente te importa, que es si esta función concreta existe y hace lo que necesito. Preguntar por la capacidad directamente es más preciso, más barato de leer y sobrevive a los casos raros: versiones de desarrollo entre dos publicaciones, compilaciones con parches, o funciones que se añadieron antes de que se anunciaran.
-- Capacidad, no version: funciona igual en 0.9 y en el canal nocturno
local uv = vim.uv or vim.loop
local islist = vim.islist or vim.tbl_islist
local hl = vim.hl or vim.highlight
-- Existe la API nueva de configuracion de servidores de lenguaje
local tiene_lsp_config = type(vim.lsp.config) == "function"
La regla práctica es sencilla: pregunta por el número solo cuando el cambio no se pueda observar. Si lo que cambió es que un símbolo apareció, pregunta por el símbolo. Si lo que cambió es que un símbolo existente empezó a comportarse de otro modo, o que un valor por defecto se invirtió, entonces sí, el número es la única señal disponible, y conviene comentarlo en el código para que dentro de dos años alguien entienda por qué está ahí.
Un símbolo puede faltar porque la versión es antigua o porque el usuario tiene una compilación sin esa característica, y también porque otro plugin lo ha sobrescrito con un envoltorio propio. Por eso la detección se hace una sola vez al cargar el módulo, se guarda en una variable local y no se repite en cada llamada: además de ser más rápido, evita que tu comportamiento cambie a mitad de sesión por culpa de un tercero.
La compuerta y el adaptador
Detectar es la mitad fácil. La difícil es dónde colocar el resultado, porque un plugin con quince comprobaciones repartidas por seis archivos es imposible de limpiar cuando por fin subes el mínimo. Solo hay dos patrones que envejecen bien y conviene usarlos con intención distinta.
Compuerta
Se comprueba una vez al cargar y decide si una característica entera se activa. Si falta, el plugin funciona con menos, no falla.
Adaptador
Un módulo único que expone un nombre estable y por dentro elige la implementación disponible. El resto del código nunca pregunta.
-- lua/mi-plugin/compat.lua : el unico archivo que sabe de versiones
local M = {}
M.uv = vim.uv or vim.loop
M.islist = vim.islist or vim.tbl_islist
-- Adaptador: una firma estable sobre dos APIs distintas
function M.rango_resaltado(buf, ns, grupo, desde, hasta)
local api = vim.hl or vim.highlight
return api.range(buf, ns, grupo, desde, hasta)
end
-- Compuerta: capacidad opcional, degradacion silenciosa
M.tiene_lsp_config = type(vim.lsp.config) == "function"
return M
Concentrarlo todo en un módulo llamado compat tiene un beneficio que se cobra el día que subes el mínimo soportado: borrar la compatibilidad con una versión antigua se convierte en editar un archivo y ejecutar las pruebas, en vez de en una cacería por todo el repositorio. La regla es que ningún otro módulo pregunta jamás por la versión; si lo hace, esa comprobación está mal ubicada.
Política de soporte
Soportar versiones cuesta, y ese coste hay que acotarlo de forma explícita antes de que lo acote el primer issue enfadado. La política habitual y razonable en este ecosistema es sostener la última versión estable y la inmediatamente anterior, más el canal nocturno en la medida de lo posible. Lo importante no es la política concreta sino que esté escrita en el README y en la ayuda, con dos consecuencias que se derivan de ella.
La primera es que el plugin debe fallar pronto y con claridad si la versión no llega al mínimo, en lugar de romperse a mitad de una operación con un error incomprensible sobre un campo nulo.
if vim.fn.has("nvim-0.10") == 0 then
vim.notify("mi-plugin requiere Neovim 0.10 o superior", vim.log.levels.ERROR)
return {}
end
La segunda es que la integración continua debe ejecutar tus pruebas en todas las versiones que dices soportar, no solo en la tuya. Una política declarada que nadie verifica es una intención, y las intenciones se rompen en silencio: el commit que usa sin querer una función de la versión nueva no avisa de nada hasta que un usuario abre un issue tres semanas después.
flowchart TB carga[Carga del plugin] --> min[Cumple la version minima] min --> no[Aviso claro y salida limpia] min --> si[Modulo compat detecta capacidades] si --> ad[Adaptadores con firma estable] si --> cp[Compuertas de caracteristicas opcionales] ad --> resto[El resto del codigo nunca pregunta version] cp --> resto resto --> ci[Integracion continua sobre cada version soportada] style si fill:#cba6f7,color:#11111b style resto fill:#a6e3a1,color:#11111b
Deprecar con elegancia
Retirar algo tiene una secuencia correcta y todas las demás producen usuarios molestos. Primero se publica la alternativa; después se marca lo viejo como obsoleto, pero sigue funcionando; se anuncia en qué versión desaparecerá; y solo entonces, en una versión mayor y no antes, se borra. Neovim ofrece una función pensada exactamente para el paso intermedio, que además respeta el ajuste global de avisos de obsolescencia del usuario.
function M.funcion_vieja(...)
vim.deprecate("mi-plugin.funcion_vieja", "mi-plugin.funcion_nueva", "2.0.0", "mi-plugin")
return M.funcion_nueva(...)
end
Para las claves de configuración el patrón es equivalente: aceptar el nombre antiguo, trasladar su valor al nuevo, avisar una vez y seguir adelante como si nada.
local renombradas = { color = "highlight", ancho = "ventana.ancho" }
local function migrar(opts)
for viejo, nuevo in pairs(renombradas) do
if opts[viejo] ~= nil then
vim.deprecate(("opts.%s"):format(viejo), ("opts.%s"):format(nuevo),
"2.0.0", "mi-plugin", false)
opts[nuevo] = opts[nuevo] == nil and opts[viejo] or opts[nuevo]
opts[viejo] = nil
end
end
return opts
end
Fíjate en que el valor nuevo gana si ambos están presentes: quien ya migró no debe verse arrastrado por un resto olvidado en su configuración. El aviso tiene tres requisitos que no son opcionales. Debe emitirse una sola vez por sesión, porque un mensaje repetido en cada pulsación no educa, entrena a ignorar. Debe decir exactamente qué escribir en su lugar, no solo que algo está obsoleto. Y debe indicar la versión de retirada, para que quien lo lea pueda decidir si tiene prisa o no. Un aviso que cumple los tres convierte una ruptura futura en una tarea de dos minutos programada por el usuario cuando le convenga.
Merece la pena detenerse en por qué la detección de capacidades vence a la comparación de números, porque la respuesta no es un detalle de estilo sino una diferencia epistemológica que reaparece en todos los sistemas distribuidos. Preguntar por la versión es un razonamiento indirecto: infieres la existencia de una función a partir de un número que la correlaciona, apoyándote en un mapa mental de qué se añadió cuándo que envejece, que no cubre las compilaciones parcheadas ni los estados intermedios del canal de desarrollo, y que se mantiene actualizado solo en tu cabeza. Preguntar por la capacidad es un razonamiento directo: consultas al entorno exactamente lo que necesitas saber, obtienes la respuesta verdadera para esa instalación concreta, y el código resultante dice literalmente de qué depende, con lo que cualquiera que lo lea entiende la condición sin conocer la historia del proyecto. La primera forma sustituye una observación por un modelo; la segunda observa. Y como todo modelo que sustituye a una observación, el número acierta casi siempre y falla justo en los casos raros, que son precisamente los que generan issues imposibles de reproducir. De aquí se sigue algo que trasciende a Neovim: el código que se adapta al entorno debe preguntar por propiedades, nunca por identidades. Preguntar quién eres —qué versión, qué sistema operativo, qué terminal— genera una lista de casos especiales que crece sin límite y se pudre; preguntar qué sabes hacer genera un conjunto de condiciones que se resuelven solas cuando el entorno mejora, y que se borran limpiamente cuando el mínimo soportado sube. Lo mismo vale para la deprecación, que es el caso temporal del mismo principio: no le comunicas al usuario que has cambiado de opinión, le comunicas qué propiedad concreta dejará de estar disponible, cuándo, y cuál la sustituye. Detectar capacidades y deprecar con aviso son, en el fondo, la misma virtud aplicada al espacio y al tiempo: no obligar a nadie a mantener en su cabeza un mapa de tu historia para poder usar tu software hoy.
Pregunta por capacidades siempre que el cambio sea observable y por el número solo cuando no lo sea. Detecta una vez al cargar y guarda el resultado. Concentra todo en un módulo compat con adaptadores de firma estable y compuertas para lo opcional. Declara tu política de versiones, falla pronto si no se cumple el mínimo, y verifica esa política en integración continua. Para retirar algo: publica el sustituto, avisa una vez, anuncia la versión de borrado y borra solo en una mayor.
- Busca en un plugin tuyo todas las llamadas a funciones que cambiaron de nombre entre versiones recientes y sustitúyelas por capacidades detectadas una sola vez.
- Crea el módulo
compaty muévele todas las comprobaciones, hasta que ningún otro archivo pregunte por la versión. - Añade la comprobación de versión mínima con aviso claro y salida limpia, y verifica el mensaje arrancando con una versión antigua.
- Marca una función como obsoleta con
vim.deprecate, comprueba que sigue funcionando y que el aviso aparece una sola vez por sesión. - Monta una matriz de integración continua con la versión estable, la anterior y el canal nocturno, y observa cuál se rompe primero.