add example/form.lua
Some checks failed
NeoVim tests / code-quality (push) Failing after 1m30s

This commit is contained in:
Jonathan Apodaca 2025-06-14 00:19:11 -06:00
parent ad2e579d1d
commit 24dc69f2ac
2 changed files with 129 additions and 5 deletions

120
examples/form.lua Normal file
View File

@ -0,0 +1,120 @@
-- form.lua:
--
-- This is a runnable example of a form. Open this file in Neovim, and execute
-- `:luafile %` to run it. It will create a new buffer to the side, and render
-- an interactive form. Edit the "inputs" between the `[...]` brackets, and
-- watch the buffer react immediately to your changes.
--
local Renderer = require('u.renderer').Renderer
local h = require('u.renderer').h
local tracker = require 'u.tracker'
-- Utility to trim brackets from strings:
local function trimb(s) return (s:gsub('^%[(.*)%]$', '%1')) end
-- Create a new, temporary, buffer to the side:
vim.cmd.vnew()
vim.bo.buftype = 'nofile'
vim.bo.bufhidden = 'wipe'
vim.bo.buflisted = false
local renderer = Renderer.new()
-- Create two signals:
local s_name = tracker.create_signal '[whoever-you-are]'
local s_age = tracker.create_signal '[ideally-a-number]'
-- We can create derived information from the signals above. Say we want to do
-- some validation on the input for `age`: we can do that with a memo:
local s_age_info = tracker.create_memo(function()
local age_raw = trimb(s_age:get())
local age_digits = age_raw:match '^%s*(%d+)%s*$'
local age_n = age_digits and tonumber(age_digits) or nil
return {
type = age_n and 'number' or 'string',
raw = age_raw,
n = age_n,
n1 = age_n and age_n + 1 or nil,
}
end)
-- This is the render effect that depends on the signals created above. This
-- will re-run every time one of the signals changes.
tracker.create_effect(function()
local name = s_name:get()
local age = s_age:get()
local age_info = s_age_info:get()
-- Each time the signals change, we re-render the buffer:
renderer:render {
h.Type({}, '# Form Example'),
'\n\n',
-- We can also listen for when specific locations in the buffer change, on
-- a tag-by-tag basis. This gives us two-way data-binding between the
-- buffer and the signals.
{
'Name: ',
h.Structure({
on_change = function(text) s_name:set(text) end,
}, name),
},
'\n',
{
'Age: ',
h.Structure({
on_change = function(text) s_age:set(text) end,
}, age),
},
'\n\n',
-- Show the values of the signals here, too, so that we can see the
-- reactivity in action. If you change the values in the tags above, you
-- can see the changes reflected here immediately.
{ 'Hello, "', trimb(name), '"!' },
--
-- A more complex example: we can do much more complex rendering, based on
-- the state. For example, if you type different values into the `age`
-- field, you can see not only the displayed information change, but also
-- the color of the highlights in this section will adapt to the type of
-- information that has been detected.
--
-- If string input is detected, values below are shown in the
-- `String`/`ErrorMsg` highlight groups.
--
-- If number input is detected, values below are shown in the `Number`
-- highlight group.
--
-- If a valid number is entered, then this section also displays how old
-- you willl be next year (`n + 1`).
--
'\n\n',
h.Type({}, '## Computed Information (derived from `age`)'),
'\n\n',
{
'Type: ',
h('text', {
hl = age_info.type == 'number' and 'Number' or 'String',
}, age_info.type),
},
{ '\nRaw input: ', h.String({}, '"' .. age_info.raw .. '"') },
{
'\nCurrent age: ',
age_info.n
-- Show the age:
and h.Number({}, tostring(age_info.n))
-- Show an error-placeholder if the age is invalid:
or h.ErrorMsg({}, '(?)'),
},
-- This part is shown conditionally, i.e., only if the age next year can be
-- computed:
age_info.n1 and {
'\nAge next year: ',
h.Number({}, tostring(age_info.n1)),
},
}
end)

View File

@ -5,7 +5,7 @@ local H = {}
--- @alias u.renderer.TagEventHandler fun(tag: u.renderer.Tag, mode: string, lhs: string): string --- @alias u.renderer.TagEventHandler fun(tag: u.renderer.Tag, mode: string, lhs: string): string
--- @alias u.renderer.TagAttributes { [string]?: unknown; imap?: table<string, u.renderer.TagEventHandler>; nmap?: table<string, u.renderer.TagEventHandler>; vmap?: table<string, u.renderer.TagEventHandler>; xmap?: table<string, u.renderer.TagEventHandler>; omap?: table<string, u.renderer.TagEventHandler> } --- @alias u.renderer.TagAttributes { [string]?: unknown; imap?: table<string, u.renderer.TagEventHandler>; nmap?: table<string, u.renderer.TagEventHandler>; vmap?: table<string, u.renderer.TagEventHandler>; xmap?: table<string, u.renderer.TagEventHandler>; omap?: table<string, u.renderer.TagEventHandler>, on_change?: fun(text: string): unknown }
--- @class u.renderer.Tag --- @class u.renderer.Tag
--- @field kind 'tag' --- @field kind 'tag'
@ -296,10 +296,15 @@ function Renderer:_reconcile() -- {{{
-- --
-- Step 2: reconcile extmarks: -- Step 2: reconcile extmarks:
-- You may be tempted to try to keep track of which extmarks are needed, and
-- only delete those that are not needed. However, each time a tree is
-- rendered, brand new extmarks are created. For simplicity, it is better to
-- just delete all extmarks, and recreate them.
-- --
-- Clear current extmarks: -- Clear current extmarks:
vim.api.nvim_buf_clear_namespace(self.bufnr, self.ns, 0, -1) vim.api.nvim_buf_clear_namespace(self.bufnr, self.ns, 0, -1)
-- Set current extmarks: -- Set current extmarks:
for _, extmark in ipairs(self.curr.extmarks) do for _, extmark in ipairs(self.curr.extmarks) do
extmark.id = vim.api.nvim_buf_set_extmark( extmark.id = vim.api.nvim_buf_set_extmark(
@ -361,10 +366,9 @@ function Renderer:_on_text_changed()
for _, pos_info in ipairs(pos_infos) do for _, pos_info in ipairs(pos_infos) do
local extmark_inf = pos_info.extmark local extmark_inf = pos_info.extmark
local tag = pos_info.tag local tag = pos_info.tag
if tag.attributes.signal and getmetatable(tag.attributes.signal) == Signal then
--- @type u.Signal
local signal = tag.attributes.signal
local on_change = tag.attributes.on_change
if on_change and type(on_change) == 'function' then
local extmark = local extmark =
vim.api.nvim_buf_get_extmark_by_id(self.bufnr, self.ns, extmark_inf.id, { details = true }) vim.api.nvim_buf_get_extmark_by_id(self.bufnr, self.ns, extmark_inf.id, { details = true })
@ -381,7 +385,7 @@ function Renderer:_on_text_changed()
{ type = 'v' } { type = 'v' }
) )
local text = table.concat(lines, '\n') local text = table.concat(lines, '\n')
if text ~= signal:get() then signal:schedule_set(text) end on_change(text)
end end
end end
end end