From 9bd0ff20886842f69b567d9061811bbc840c18b0 Mon Sep 17 00:00:00 2001 From: Jonathan Apodaca Date: Thu, 12 Jun 2025 23:16:28 -0600 Subject: [PATCH] watch text-changes with signals --- examples/counter.lua | 19 ------- lua/u/renderer.lua | 115 ++++++++++++++++++------------------------- 2 files changed, 49 insertions(+), 85 deletions(-) diff --git a/examples/counter.lua b/examples/counter.lua index ebc3f73..fa303c7 100644 --- a/examples/counter.lua +++ b/examples/counter.lua @@ -22,29 +22,10 @@ tracker.create_effect(function() -- constructed with `h(...)` calls). To help organize the markup, text and -- tags can be nested in tables at any depth. Line breaks must be specified -- manually, with '\n'. - local text_ref = ui_buf.renderer:create_ref() ui_buf:render { 'Reactive Counter Example\n', '========================\n\n', - { - 'Text field: ', - h('text', { hl = 'DiffAdd', ref = text_ref }, { '[]' }), - '\n', - h('text', { - hl = 'DiffAdd', - nmap = { - [''] = function() - vim.notify(text_ref:text()) - return '' - end, - }, - }, ' Submit '), - }, - - '\n', - '\n', - { 'Counter: ', tostring(count), '\n' }, '\n', diff --git a/lua/u/renderer.lua b/lua/u/renderer.lua index 25329ef..8696be8 100644 --- a/lua/u/renderer.lua +++ b/lua/u/renderer.lua @@ -1,3 +1,5 @@ +local Signal = require('u.tracker').Signal + local M = {} local H = {} @@ -33,52 +35,6 @@ M.h = setmetatable({}, { end, }) --- TagRef {{{ ---- @class u.renderer.TagRef ---- @field tag? u.renderer.Tag ---- @field renderer u.renderer.Renderer -local TagRef = {} -TagRef.__index = TagRef - -function TagRef:extmark() - if not self.tag then return {} end - - --- @type u.renderer.RendererExtmark? - local extmark_inf = vim - .iter(self.renderer.curr.extmarks) - --- @param x u.renderer.RendererExtmark - :find(function(x) return x.tag == self.tag end) - if not extmark_inf then return {} end - - local extmark = vim.api.nvim_buf_get_extmark_by_id( - self.renderer.bufnr, - self.renderer.ns, - extmark_inf.id, - { details = true } - ) - return extmark -end - -function TagRef:range() - local extmark = self:extmark() - if not extmark then return nil end - local Range = require 'u.range' - return Range.from_extmark(self.renderer.bufnr, extmark) -end - -function TagRef:lines() - local range = self:range() - if not range then return {} end - return range:lines() -end - -function TagRef:text() - local range = self:range() - if not range then return '' end - return range:text() -end --- }}} - -- Renderer {{{ --- @class u.renderer.RendererExtmark --- @field id? number @@ -124,11 +80,15 @@ function Renderer.new(bufnr) -- {{{ old = { lines = {}, extmarks = {} }, curr = { lines = {}, extmarks = {} }, }, Renderer) + + vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI', 'TextChangedP' }, { + buffer = bufnr, + callback = function() self:_on_text_changed() end, + }) + return self end -- }}} -function Renderer:create_ref() return setmetatable({ renderer = self }, TagRef) end - --- @param opts { --- tree: u.renderer.Tree; --- on_tag?: fun(tag: u.renderer.Tag, start0: [number, number], stop0: [number, number]): any; @@ -276,14 +236,13 @@ function Renderer:render(tree) -- {{{ tag.attributes.extmark.hl_group = tag.attributes.extmark.hl_group or hl end - local extmark_opts = tag.attributes.extmark + local extmark_opts = tag.attributes.extmark or {} -- Set any necessary keymaps: for _, mode in ipairs { 'i', 'n', 'v', 'x', 'o' } do for lhs, _ in pairs(tag.attributes[mode .. 'map'] or {}) do -- Force creating an extmark if there are key handlers. To accurately -- sense the bounds of the text, we need an extmark: - extmark_opts = extmark_opts or {} vim.keymap.set( mode, lhs, @@ -293,22 +252,12 @@ function Renderer:render(tree) -- {{{ end end - -- Check for refs: - if tag.attributes.ref and getmetatable(tag.attributes.ref) == TagRef then - --- @type u.renderer.TagRef - local ref = tag.attributes.ref - ref.tag = tag - extmark_opts = extmark_opts or {} - end - - if extmark_opts then - table.insert(extmarks, { - start = start0, - stop = stop0, - opts = extmark_opts, - tag = tag, - }) - end + table.insert(extmarks, { + start = start0, + stop = stop0, + opts = extmark_opts, + tag = tag, + }) end end, -- }}} } @@ -404,6 +353,40 @@ function Renderer:_expr_map_callback(mode, lhs) -- {{{ return cancel and '' or lhs end -- }}} +function Renderer:_on_text_changed() + --- @type integer, integer + local l, c = unpack(vim.api.nvim_win_get_cursor(0)) + l = l - 1 -- make it actually 0-based + local pos_infos = self:get_pos_infos { l, c } + 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 extmark = + vim.api.nvim_buf_get_extmark_by_id(self.bufnr, self.ns, extmark_inf.id, { details = true }) + + --- @type integer, integer, vim.api.keyset.extmark_details + local start_row0, start_col0, details = unpack(extmark) + local end_row0, end_col0 = details.end_row, details.end_col + + if start_row0 == end_row0 and start_col0 == end_col0 then + -- Invalid extmark + else + local lines = vim.fn.getregion( + { self.bufnr, start_row0 + 1, start_col0 + 1 }, + { self.bufnr, end_row0 + 1, end_col0 }, + { type = 'v' } + ) + local text = table.concat(lines, '\n') + if text ~= signal:get() then signal:schedule_set(text) end + end + end + end +end + --- Returns pairs of extmarks and tags associate with said extmarks. The --- returned tags/extmarks are sorted smallest (innermost) to largest --- (outermost).