Saltar al contenido principal

morph.nvim: React-like renderer para Neovim

· 6 min de lectura
Oscar Adrian Ortiz Bustos
Contando lecturas...

Introducción

Ultimamente me he estado metiendo mas de lleno al mundo de React, y creo que probablemente estés familiarizado con su modelo de componentes. ¿Qué pasaría si pudieras aplicar esos mismos conceptos dentro de Neovim para crear interfaces de usuario interactivas? Eso es exactamente lo que ofrece morph.nvim

WelcomeBanner

morph.nvim es un renderer inspirado en React que permite construir interfaces de usuario interactivas en buffers de Neovim usando componentes, estado y un sistema de reconciliación eficiente. En lugar de gestionar manualmente el contenido del buffer, extmarks y keymaps, puedes declarar tu UI y dejar que morph.nvim se encargue del resto.

¿Qué es morph.nvim?

morph.nvim transforma Neovim en un potente framework TUI (Terminal User Interface), permitiéndote crear buffers dinámicos e interactivos usando patrones familiares de React como componentes, estado y manejadores de eventos. Es perfecto para construir UIs personalizadas, formularios, dashboards, exploradores de archivos o cualquier interfaz basada en texto dentro del entorno de edición de Neovim.

Características principales

  • Arquitectura basada en componentes: Componentes reutilizables con props, estado y métodos de ciclo de vida
  • Reconciliación eficiente: Solo actualiza lo que cambió, usando un algoritmo similar al Virtual DOM de React
  • Estilizado de texto enriquecido: Aplica grupos de highlight y extmarks con atributos simples
  • Manejo de eventos interactivos: Responde a teclas con manejadores específicos por modo
  • Detección de cambios de texto: Detecta automáticamente cuando los usuarios editan texto dentro de tags

Ejemplo del mundo real

Uno de los ejemplos más interesantes en la documentación de morph.nvim es un formulario de búsqueda que demuestra varias características clave:

--- @param ctx morph.Ctx<{}, { query: string, results: table[] }>
local function SearchForm(ctx)
if ctx.phase == 'mount' then
ctx.state = { query = '', results = {} }
end

return {
h.Title({}, 'Search: '),
h('text', {
on_change = function(e)
ctx:update({
query = e.text,
results = performSearch(e.text)
})
end
}, ctx.state.query),
'\n\n',

-- Results
vim.tbl_map(function(result)
return {
h.Directory({
nmap = {
['<CR>'] = function()
vim.cmd('edit ' .. result.path)
return ''
end
}
}, result.name),
'\n'
}
end, ctx.state.results)
}
end

Este componente muestra:

  1. Estado local: query y results se mantienen en el estado del componente
  2. Eventos en tiempo real: on_change se dispara mientras el usuario escribe
  3. Manejo de teclas: <CR> abre el archivo correspondiente
  4. Renderizado condicional: Muestra resultados solo cuando existen

Sintaxis Hyperscript

morph.nvim usa una sintaxis similar a Hyperscript para crear elementos, parecida al JSX de React pero en Lua:

-- Elemento básico de texto
h('text', { hl = 'Comment' }, 'Hello world')

-- Abreviatura para grupos de highlight
h.Comment({}, 'Hello world') -- equivalente al anterior

-- Elementos anidados
h('text', {}, {
'Outer text ',
h.Keyword({}, 'highlighted'),
' more text'
})

-- Con manejadores de eventos
h.Directory({
nmap = {
['<CR>'] = function()
vim.cmd('edit ' .. filename)
return '' -- consume la tecla
end
}
}, filename)

Contexto de componentes

En lugar de hooks estilo React como useState y useEffect, morph.nvim usa un objeto de contexto (ctx) que persiste a través de los renders. Este enfoque es más simple y predecible:

--- @param ctx morph.Ctx<{ initial: number }, { count: number, history: number[] }>
local function StatefulCounter(ctx)
-- Inicializa estado solo en el primer render
if ctx.phase == 'mount' then
ctx.state = {
count = ctx.props.initial or 0,
history = {}
}
end

local state = ctx.state

return {
'Count: ', tostring(state.count), '\n',
'History: ', table.concat(state.history, ', '), '\n',
h.Keyword({
nmap = {
['<CR>'] = function()
-- Actualiza estado y dispara re-render
ctx:update({
count = state.count + 1,
history = vim.list_extend({}, state.history, { state.count })
})
return ''
end
}
}, '[Press Enter to increment]')
}
end

Propiedades clave del contexto:

  • ctx.props: Propiedades del componente (solo lectura, actualizadas por el padre)
  • ctx.state: Estado del componente (tus datos persistentes entre renders)
  • ctx.children: Elementos hijos pasados a este componente
  • ctx:update(new_state): Actualiza estado y dispara re-render
  • ctx.phase: Fase actual del ciclo de vida ('mount', 'update', 'unmount')

Instalación

Para autores de plugins

Neovim no tiene una buena solución para la gestión automática de dependencias de plugins. Se recomienda que los autores de bibliotecas incluyan morph.nvim dentro de su plugin. morph.nvim está implementado en un solo archivo, por lo que esto debería ser relativamente sencillo.

# En tu repositorio de plugin
git submodule add -- https://github.com/jrop/morph.nvim lua/my_plugin/morph
cd lua/my_plugin/morph/
git checkout artifact-v0.1.0 # usa la versión que quieras

Para usuarios que quieren usar morph.nvim en su configuración

lazy.nvim:

{ 'jrop/morph.nvim' }

packer.nvim:

use({ 'jrop/morph.nvim' })

¿Por qué morph.nvim?

Neovim ya es un editor de texto excepcional, pero morph.nvim desbloquea su potencial como un host de aplicaciones TUI con todas las funciones. En lugar de estar limitado a UIs tradicionales de plugins, puedes construir aplicaciones ricas e interactivas que se sientan nativas de la terminal mientras aprovechas las poderosas capacidades de manipulación de texto de Neovim.

Construir UIs interactivas en Neovim tradicionalmente requiere gestionar manualmente el contenido del buffer, extmarks, keymaps y autocmds. morph.nvim abstrae esta complejidad detrás de una API declarativa y basada en componentes que se siente familiar para los desarrolladores web mientras está optimizada para las capacidades únicas de Neovim.

MFP

Creo que no lo he dado a conocer lo suficiente, pero cuando trabajaba en una oficina generalmente había demasiado ruido, de hecho, a veces era difícil concentrarse. Para solucionar esto, siempre visitaba la pagina de Music For Programming y ponía música de fondo para ayudarme a concentrarme. La música de esta página es especialmente diseñada para mejorar la concentración y la productividad, lo que la convierte en una excelente opción para programadores.

Entonces lo que hice fue crear un cliente de terminal con Rust, para luego hacer su implementacion en Neovim usando morph.nvim, y así poder escuchar música de fondo mientras programaba sin tener que salir de mi editor. El resultado fue un plugin llamado mfp.nvim

Es un gran ejemplo de cómo morph.nvim puede ser utilizado para crear aplicaciones TUI completas dentro de Neovim, integrando funcionalidades como reproducción de música, controles interactivos y visualizaciones, todo sin salir del editor.

Proyectos similares

Conclusión

morph.nvim representa un paso significativo hacia la creación de aplicaciones TUI sofisticadas dentro de Neovim. Al traer los patrones de React al mundo de los editores de texto, abre nuevas posibilidades para plugins y herramientas que pueden ofrecer experiencias de usuario más ricas e interactivas.

Si ya estás familiarizado con React y usas Neovim, morph.nvim te permitirá aplicar tus conocimientos existentes para crear interfaces que antes requerían una gestión manual compleja. Es una herramienta poderosa que merece la pena explorar si estás interesado en llevar tus plugins de Neovim al siguiente nivel.

Escrito por un humano

Referencias: