From 24dc69f2acd8609ea22631b53dcb5641aa9e246c Mon Sep 17 00:00:00 2001 From: Jonathan Apodaca Date: Sat, 14 Jun 2025 00:19:11 -0600 Subject: [PATCH] add example/form.lua --- examples/form.lua | 120 +++++++++++++++++++++++++++++++++++++++++++++ lua/u/renderer.lua | 14 ++++-- 2 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 examples/form.lua diff --git a/examples/form.lua b/examples/form.lua new file mode 100644 index 0000000..480400d --- /dev/null +++ b/examples/form.lua @@ -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) diff --git a/lua/u/renderer.lua b/lua/u/renderer.lua index 8696be8..9219115 100644 --- a/lua/u/renderer.lua +++ b/lua/u/renderer.lua @@ -5,7 +5,7 @@ local H = {} --- @alias u.renderer.TagEventHandler fun(tag: u.renderer.Tag, mode: string, lhs: string): string ---- @alias u.renderer.TagAttributes { [string]?: unknown; imap?: table; nmap?: table; vmap?: table; xmap?: table; omap?: table } +--- @alias u.renderer.TagAttributes { [string]?: unknown; imap?: table; nmap?: table; vmap?: table; xmap?: table; omap?: table, on_change?: fun(text: string): unknown } --- @class u.renderer.Tag --- @field kind 'tag' @@ -296,10 +296,15 @@ function Renderer:_reconcile() -- {{{ -- -- 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: vim.api.nvim_buf_clear_namespace(self.bufnr, self.ns, 0, -1) + -- Set current extmarks: for _, extmark in ipairs(self.curr.extmarks) do 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 local extmark_inf = pos_info.extmark 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 = 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' } ) local text = table.concat(lines, '\n') - if text ~= signal:get() then signal:schedule_set(text) end + on_change(text) end end end