This commit is contained in:
parent
72b6886838
commit
8fa2784bf7
@ -5,7 +5,9 @@ local ENABLE_LOG = false
|
|||||||
local function log(...)
|
local function log(...)
|
||||||
if not ENABLE_LOG then return end
|
if not ENABLE_LOG then return end
|
||||||
|
|
||||||
local f = assert(io.open(vim.fs.joinpath(vim.fn.stdpath 'log', 'u.renderer.log'), 'a+'))
|
local f = assert(
|
||||||
|
io.open(vim.fs.joinpath((vim.fn.stdpath 'log') --[[@as string]], 'u.renderer.log'), 'a+')
|
||||||
|
)
|
||||||
f:write(os.date() .. '\t' .. vim.inspect { ... } .. '\n')
|
f:write(os.date() .. '\t' .. vim.inspect { ... } .. '\n')
|
||||||
f:close()
|
f:close()
|
||||||
end
|
end
|
||||||
@ -15,11 +17,23 @@ 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>, on_change?: fun(text: string): unknown }
|
--- @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), key?: string|number }
|
||||||
|
|
||||||
|
--- @generic TProps
|
||||||
|
--- @generic TState
|
||||||
|
--- @class u.renderer.FnComponentContext<TProps, TState>
|
||||||
|
--- @field props TProps
|
||||||
|
--- @field state u.Signal<TState>
|
||||||
|
--- @field children u.renderer.Tree
|
||||||
|
--- @field phase 'mount'|'update'|'unmount'
|
||||||
|
--- @field private unsubscribe? fun(): any
|
||||||
|
--- @field private prev_tree u.renderer.Tree
|
||||||
|
|
||||||
|
--- @alias u.renderer.FnComponent<TProps, TState> fun(ctx: u.renderer.FnComponentContext<TProps, TState>): u.renderer.Tree
|
||||||
|
|
||||||
--- @class u.renderer.Tag
|
--- @class u.renderer.Tag
|
||||||
--- @field kind 'tag'
|
--- @field kind 'tag'
|
||||||
--- @field name string
|
--- @field name string | u.renderer.FnComponent<any, any>
|
||||||
--- @field attributes u.renderer.TagAttributes
|
--- @field attributes u.renderer.TagAttributes
|
||||||
--- @field children u.renderer.Tree
|
--- @field children u.renderer.Tree
|
||||||
|
|
||||||
@ -27,7 +41,6 @@ local H = {}
|
|||||||
--- @alias u.renderer.Tree u.renderer.Node | u.renderer.Node[]
|
--- @alias u.renderer.Tree u.renderer.Node | u.renderer.Node[]
|
||||||
|
|
||||||
-- luacheck: ignore
|
-- luacheck: ignore
|
||||||
--- @type table<string, fun(attributes: u.renderer.TagAttributes, children: u.renderer.Tree): u.renderer.Tag> & fun(name: string, attributes: u.renderer.TagAttributes, children: u.renderer.Tree): u.renderer.Tag>
|
|
||||||
M.h = setmetatable({}, {
|
M.h = setmetatable({}, {
|
||||||
__call = function(_, name, attributes, children)
|
__call = function(_, name, attributes, children)
|
||||||
return {
|
return {
|
||||||
@ -42,39 +55,77 @@ M.h = setmetatable({}, {
|
|||||||
return M.h('text', vim.tbl_deep_extend('force', { hl = name }, attributes), children)
|
return M.h('text', vim.tbl_deep_extend('force', { hl = name }, attributes), children)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
})
|
}) --[[@as table<string, fun(attributes: u.renderer.TagAttributes, children: u.renderer.Tree): u.renderer.Tag> & fun(name: string, attributes: u.renderer.TagAttributes, children: u.renderer.Tree): u.renderer.Tag>]]
|
||||||
|
|
||||||
-- Renderer {{{
|
-- Renderer {{{
|
||||||
--- @class u.renderer.RendererExtmark
|
--- @class u.renderer.RendererExtmark
|
||||||
--- @field id? number
|
--- @field id? integer
|
||||||
--- @field start [number, number]
|
--- @field start [integer, integer]
|
||||||
--- @field stop [number, number]
|
--- @field stop [integer, integer]
|
||||||
--- @field opts vim.api.keyset.set_extmark
|
--- @field opts vim.api.keyset.set_extmark
|
||||||
--- @field tag u.renderer.Tag
|
--- @field tag u.renderer.Tag
|
||||||
|
|
||||||
--- @class u.renderer.Renderer
|
--- @class u.renderer.Renderer
|
||||||
--- @field bufnr number
|
--- @field bufnr integer
|
||||||
--- @field ns number
|
--- @field ns integer
|
||||||
--- @field changedtick number
|
--- @field changedtick integer
|
||||||
--- @field old { lines: string[]; extmarks: u.renderer.RendererExtmark[] }
|
--- @field text_content { old: { lines: string[], extmarks: u.renderer.RendererExtmark[] }, curr: { lines: string[], extmarks: u.renderer.RendererExtmark[] } }
|
||||||
--- @field curr { lines: string[]; extmarks: u.renderer.RendererExtmark[] }
|
--- @field component_tree { old: u.renderer.Tree, ctx_by_node: table<u.renderer.Tree, u.renderer.FnComponentContext> }
|
||||||
local Renderer = {}
|
local Renderer = {}
|
||||||
Renderer.__index = Renderer
|
Renderer.__index = Renderer
|
||||||
M.Renderer = Renderer
|
M.Renderer = Renderer
|
||||||
|
|
||||||
--- @private
|
--- @private
|
||||||
--- @param x any
|
--- @param tree u.renderer.Tree
|
||||||
--- @return boolean
|
--- @param visitors {
|
||||||
function Renderer.is_tag(x) return type(x) == 'table' and x.kind == 'tag' end
|
--- nil_?: (fun(): any),
|
||||||
|
--- boolean?: (fun(b: boolean): any),
|
||||||
|
--- string?: (fun(s: string): any),
|
||||||
|
--- array?: (fun(tags: u.renderer.Node[]): any),
|
||||||
|
--- tag?: (fun(tag: u.renderer.Tag): any),
|
||||||
|
--- component?: (fun(component: u.renderer.FnComponent, tag: u.renderer.Tag): any),
|
||||||
|
--- unknown?: fun(tag: any): any
|
||||||
|
--- }
|
||||||
|
function Renderer.tree_match(tree, visitors)-- {{{
|
||||||
|
local function is_tag(x) return type(x) == 'table' and x.kind == 'tag' end
|
||||||
|
local function is_tag_arr(x) return type(x) == 'table' and not is_tag(x) end
|
||||||
|
|
||||||
|
if tree == nil then
|
||||||
|
return visitors.nil_ and visitors.nil_() or nil
|
||||||
|
elseif type(tree) == 'boolean' then
|
||||||
|
return visitors.boolean and visitors.boolean(tree) or nil
|
||||||
|
elseif type(tree) == 'string' then
|
||||||
|
return visitors.string and visitors.string(tree) or nil
|
||||||
|
elseif is_tag_arr(tree) then
|
||||||
|
return visitors.array and visitors.array(tree --[[@as any]]) or nil
|
||||||
|
elseif is_tag(tree) then
|
||||||
|
local tag = tree --[[@as u.renderer.Tag]]
|
||||||
|
if type(tag.name) == 'function' then
|
||||||
|
return visitors.component and visitors.component(tag.name --[[@as function]], tag) or nil
|
||||||
|
else
|
||||||
|
return visitors.tag and visitors.tag(tree --[[@as any]]) or nil
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return visitors.unknown and visitors.unknown(tree) or error 'unknown value: not a tag'
|
||||||
|
end
|
||||||
|
end-- }}}
|
||||||
|
|
||||||
--- @private
|
--- @private
|
||||||
--- @param x any
|
--- @param tree u.renderer.Tree
|
||||||
--- @return boolean
|
--- @return 'nil' | 'boolean' | 'string' | 'array' | 'tag' | u.renderer.FnComponent | 'unknown'
|
||||||
function Renderer.is_tag_arr(x)
|
function Renderer.tree_kind(tree) -- {{{
|
||||||
if type(x) ~= 'table' then return false end
|
return Renderer.tree_match(tree, {
|
||||||
return #x == 0 or not Renderer.is_tag(x)
|
nil_ = function() return 'nil' end,
|
||||||
end
|
boolean = function() return 'boolean' end,
|
||||||
--- @param bufnr number|nil
|
string = function() return 'string' end,
|
||||||
|
array = function() return 'array' end,
|
||||||
|
tag = function() return 'tag' end,
|
||||||
|
component = function(c) return c end,
|
||||||
|
unknown = function() return 'unknown' end,
|
||||||
|
}) --[[@as any]]
|
||||||
|
end -- }}}
|
||||||
|
|
||||||
|
--- @param bufnr integer|nil
|
||||||
function Renderer.new(bufnr) -- {{{
|
function Renderer.new(bufnr) -- {{{
|
||||||
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
||||||
|
|
||||||
@ -86,8 +137,15 @@ function Renderer.new(bufnr) -- {{{
|
|||||||
bufnr = bufnr,
|
bufnr = bufnr,
|
||||||
ns = vim.b[bufnr]._renderer_ns,
|
ns = vim.b[bufnr]._renderer_ns,
|
||||||
changedtick = 0,
|
changedtick = 0,
|
||||||
|
text_content = {
|
||||||
old = { lines = {}, extmarks = {} },
|
old = { lines = {}, extmarks = {} },
|
||||||
curr = { lines = {}, extmarks = {} },
|
curr = { lines = {}, extmarks = {} },
|
||||||
|
},
|
||||||
|
component_tree = {
|
||||||
|
old = nil,
|
||||||
|
curr = nil,
|
||||||
|
ctx_by_node = {},
|
||||||
|
},
|
||||||
}, Renderer)
|
}, Renderer)
|
||||||
|
|
||||||
vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI', 'TextChangedP' }, {
|
vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI', 'TextChangedP' }, {
|
||||||
@ -99,8 +157,8 @@ function Renderer.new(bufnr) -- {{{
|
|||||||
end -- }}}
|
end -- }}}
|
||||||
|
|
||||||
--- @param opts {
|
--- @param opts {
|
||||||
--- tree: u.renderer.Tree;
|
--- tree: u.renderer.Tree,
|
||||||
--- on_tag?: fun(tag: u.renderer.Tag, start0: [number, number], stop0: [number, number]): any;
|
--- on_tag?: fun(tag: u.renderer.Tag, start0: [number, number], stop0: [number, number]): any
|
||||||
--- }
|
--- }
|
||||||
function Renderer.markup_to_lines(opts) -- {{{
|
function Renderer.markup_to_lines(opts) -- {{{
|
||||||
--- @type string[]
|
--- @type string[]
|
||||||
@ -119,39 +177,31 @@ function Renderer.markup_to_lines(opts) -- {{{
|
|||||||
curr_col1 = 1
|
curr_col1 = 1
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param node u.renderer.Node
|
--- @param node u.renderer.Tree
|
||||||
local function visit(node) -- {{{
|
local function visit(node) -- {{{
|
||||||
if node == nil or type(node) == 'boolean' then return end
|
Renderer.tree_match(node, {
|
||||||
|
string = function(s_node)
|
||||||
if type(node) == 'string' then
|
local node_lines = vim.split(s_node, '\n')
|
||||||
local node_lines = vim.split(node, '\n')
|
|
||||||
for lnum, s in ipairs(node_lines) do
|
for lnum, s in ipairs(node_lines) do
|
||||||
if lnum > 1 then put_line() end
|
if lnum > 1 then put_line() end
|
||||||
put(s)
|
put(s)
|
||||||
end
|
end
|
||||||
elseif Renderer.is_tag(node) then
|
end,
|
||||||
|
array = function(ts)
|
||||||
|
for _, child in ipairs(ts) do
|
||||||
|
visit(child)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
tag = function(t)
|
||||||
|
assert(type(t.name) ~= 'function', 'markup_to_lines does not render components')
|
||||||
|
|
||||||
local start0 = { curr_line1 - 1, curr_col1 - 1 }
|
local start0 = { curr_line1 - 1, curr_col1 - 1 }
|
||||||
|
visit(t.children)
|
||||||
-- visit the children:
|
|
||||||
if Renderer.is_tag_arr(node.children) then
|
|
||||||
for _, child in
|
|
||||||
ipairs(node.children --[[@as u.renderer.Node[] ]])
|
|
||||||
do
|
|
||||||
-- newlines are not controlled by array entries, do NOT output a line here:
|
|
||||||
visit(child)
|
|
||||||
end
|
|
||||||
else -- luacheck: ignore
|
|
||||||
visit(node.children)
|
|
||||||
end
|
|
||||||
|
|
||||||
local stop0 = { curr_line1 - 1, curr_col1 - 1 }
|
local stop0 = { curr_line1 - 1, curr_col1 - 1 }
|
||||||
if opts.on_tag then opts.on_tag(node, start0, stop0) end
|
|
||||||
elseif Renderer.is_tag_arr(node) then
|
if opts.on_tag then opts.on_tag(t, start0, stop0) end
|
||||||
for _, child in ipairs(node) do
|
end,
|
||||||
-- newlines are not controlled by array entries, do NOT output a line here:
|
})
|
||||||
visit(child)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end -- }}}
|
end -- }}}
|
||||||
visit(opts.tree)
|
visit(opts.tree)
|
||||||
|
|
||||||
@ -159,12 +209,12 @@ function Renderer.markup_to_lines(opts) -- {{{
|
|||||||
end -- }}}
|
end -- }}}
|
||||||
|
|
||||||
--- @param opts {
|
--- @param opts {
|
||||||
--- tree: u.renderer.Tree;
|
--- tree: u.renderer.Tree,
|
||||||
--- format_tag?: fun(tag: u.renderer.Tag): string;
|
--- format_tag?: fun(tag: u.renderer.Tag): string
|
||||||
--- }
|
--- }
|
||||||
function Renderer.markup_to_string(opts) return table.concat(Renderer.markup_to_lines(opts), '\n') end
|
function Renderer.markup_to_string(opts) return table.concat(Renderer.markup_to_lines(opts), '\n') end
|
||||||
|
|
||||||
--- @param bufnr number
|
--- @param bufnr integer
|
||||||
--- @param old_lines string[] | nil
|
--- @param old_lines string[] | nil
|
||||||
--- @param new_lines string[]
|
--- @param new_lines string[]
|
||||||
function Renderer.patch_lines(bufnr, old_lines, new_lines) -- {{{
|
function Renderer.patch_lines(bufnr, old_lines, new_lines) -- {{{
|
||||||
@ -226,7 +276,8 @@ end -- }}}
|
|||||||
function Renderer:render(tree) -- {{{
|
function Renderer:render(tree) -- {{{
|
||||||
local changedtick = vim.b[self.bufnr].changedtick
|
local changedtick = vim.b[self.bufnr].changedtick
|
||||||
if changedtick ~= self.changedtick then
|
if changedtick ~= self.changedtick then
|
||||||
self.curr = { lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) }
|
self.text_content.curr =
|
||||||
|
{ extmarks = {}, lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) }
|
||||||
self.changedtick = changedtick
|
self.changedtick = changedtick
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -283,8 +334,8 @@ function Renderer:render(tree) -- {{{
|
|||||||
end, -- }}}
|
end, -- }}}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.old = self.curr
|
self.text_content.old = self.text_content.curr
|
||||||
self.curr = { lines = lines, extmarks = extmarks }
|
self.text_content.curr = { lines = lines, extmarks = extmarks }
|
||||||
self:_reconcile()
|
self:_reconcile()
|
||||||
vim.cmd.doautocmd { args = { 'User', 'Renderer:' .. tostring(self.bufnr) .. ':render' } }
|
vim.cmd.doautocmd { args = { 'User', 'Renderer:' .. tostring(self.bufnr) .. ':render' } }
|
||||||
end -- }}}
|
end -- }}}
|
||||||
@ -294,7 +345,7 @@ function Renderer:_reconcile() -- {{{
|
|||||||
--
|
--
|
||||||
-- Step 1: morph the text to the desired state:
|
-- Step 1: morph the text to the desired state:
|
||||||
--
|
--
|
||||||
Renderer.patch_lines(self.bufnr, self.old.lines, self.curr.lines)
|
Renderer.patch_lines(self.bufnr, self.text_content.old.lines, self.text_content.curr.lines)
|
||||||
self.changedtick = vim.b[self.bufnr].changedtick
|
self.changedtick = vim.b[self.bufnr].changedtick
|
||||||
|
|
||||||
--
|
--
|
||||||
@ -309,7 +360,7 @@ function Renderer:_reconcile() -- {{{
|
|||||||
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.text_content.curr.extmarks) do
|
||||||
extmark.id = vim.api.nvim_buf_set_extmark(
|
extmark.id = vim.api.nvim_buf_set_extmark(
|
||||||
self.bufnr,
|
self.bufnr,
|
||||||
self.ns,
|
self.ns,
|
||||||
@ -330,7 +381,7 @@ function Renderer:_reconcile() -- {{{
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
self.old = self.curr
|
self.text_content.old = self.text_content.curr
|
||||||
end -- }}}
|
end -- }}}
|
||||||
|
|
||||||
--- @private
|
--- @private
|
||||||
@ -492,7 +543,7 @@ function Renderer:_debug() -- {{{
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
computed = {
|
computed = {
|
||||||
extmarks = self.curr.extmarks,
|
extmarks = self.text_content.curr.extmarks,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
vim.api.nvim_buf_set_lines(info_bufnr, 0, -1, true, vim.split(vim.inspect(info), '\n'))
|
vim.api.nvim_buf_set_lines(info_bufnr, 0, -1, true, vim.split(vim.inspect(info), '\n'))
|
||||||
@ -534,9 +585,9 @@ end -- }}}
|
|||||||
--- (outermost).
|
--- (outermost).
|
||||||
---
|
---
|
||||||
--- @private (private for now)
|
--- @private (private for now)
|
||||||
--- @param pos0 [number; number]
|
--- @param pos0 [integer, integer]
|
||||||
--- @param mode string?
|
--- @param mode string?
|
||||||
--- @return { extmark: u.renderer.RendererExtmark; tag: u.renderer.Tag; }[]
|
--- @return { extmark: u.renderer.RendererExtmark, tag: u.renderer.Tag }[]
|
||||||
function Renderer:get_tags_at(pos0, mode) -- {{{
|
function Renderer:get_tags_at(pos0, mode) -- {{{
|
||||||
local cursor_line0, cursor_col0 = pos0[1], pos0[2]
|
local cursor_line0, cursor_col0 = pos0[1], pos0[2]
|
||||||
if not mode then mode = vim.api.nvim_get_mode().mode end
|
if not mode then mode = vim.api.nvim_get_mode().mode end
|
||||||
@ -637,15 +688,15 @@ function Renderer:get_tags_at(pos0, mode) -- {{{
|
|||||||
)
|
)
|
||||||
|
|
||||||
-- When we set the extmarks in the step above, we captured the IDs of the
|
-- When we set the extmarks in the step above, we captured the IDs of the
|
||||||
-- created extmarks in self.curr.extmarks, which also has which tag each
|
-- created extmarks in self.text_content.curr.extmarks, which also has which tag each
|
||||||
-- extmark is associated with. Cross-reference with that list to get a list
|
-- extmark is associated with. Cross-reference with that list to get a list
|
||||||
-- of tags that we need to fire events for:
|
-- of tags that we need to fire events for:
|
||||||
--- @type { extmark: u.renderer.RendererExtmark; tag: u.renderer.Tag }[]
|
--- @type { extmark: u.renderer.RendererExtmark, tag: u.renderer.Tag }[]
|
||||||
local matching_tags = vim
|
local matching_tags = vim
|
||||||
.iter(intersecting_extmarks)
|
.iter(intersecting_extmarks)
|
||||||
--- @param ext u.renderer.RendererExtmark
|
--- @param ext u.renderer.RendererExtmark
|
||||||
:map(function(ext)
|
:map(function(ext)
|
||||||
for _, extmark_cache in ipairs(self.curr.extmarks) do
|
for _, extmark_cache in ipairs(self.text_content.curr.extmarks) do
|
||||||
if extmark_cache.id == ext.id then return { extmark = ext, tag = extmark_cache.tag } end
|
if extmark_cache.id == ext.id then return { extmark = ext, tag = extmark_cache.tag } end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
@ -656,9 +707,9 @@ end -- }}}
|
|||||||
|
|
||||||
--- @private
|
--- @private
|
||||||
--- @param tag_or_id string | u.renderer.Tag
|
--- @param tag_or_id string | u.renderer.Tag
|
||||||
--- @return { start: [number, number]; stop: [number, number] } | nil
|
--- @return { start: [number, number], stop: [number, number] } | nil
|
||||||
function Renderer:get_tag_bounds(tag_or_id) -- {{{
|
function Renderer:get_tag_bounds(tag_or_id) -- {{{
|
||||||
for _, x in ipairs(self.curr.extmarks) do
|
for _, x in ipairs(self.text_content.curr.extmarks) do
|
||||||
local pos = { start = x.start, stop = x.stop }
|
local pos = { start = x.start, stop = x.stop }
|
||||||
local does_tag_match = type(tag_or_id) == 'string' and x.tag.attributes.id == tag_or_id
|
local does_tag_match = type(tag_or_id) == 'string' and x.tag.attributes.id == tag_or_id
|
||||||
or x.tag == tag_or_id
|
or x.tag == tag_or_id
|
||||||
@ -666,6 +717,175 @@ function Renderer:get_tag_bounds(tag_or_id) -- {{{
|
|||||||
end
|
end
|
||||||
end -- }}}
|
end -- }}}
|
||||||
|
|
||||||
|
--- @param tree u.renderer.Tree
|
||||||
|
function Renderer:mount(tree) -- {{{
|
||||||
|
local tracker = require 'u.tracker'
|
||||||
|
|
||||||
|
local function render()
|
||||||
|
local H2 = {}
|
||||||
|
|
||||||
|
--- @param tree u.renderer.Tree
|
||||||
|
function H2.unmount(tree)
|
||||||
|
--- @param tree u.renderer.Tree
|
||||||
|
local function unmount_one(tree)
|
||||||
|
Renderer.tree_match(tree, {
|
||||||
|
array = function(tags)
|
||||||
|
for _, tag in ipairs(tags) do
|
||||||
|
unmount_one(tag)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
tag = function(tag) unmount_one(tag.children) end,
|
||||||
|
component = function(Component, tag)
|
||||||
|
--- @type u.renderer.FnComponentContext
|
||||||
|
local ctx = assert(
|
||||||
|
self.component_tree.ctx_by_node[tree] --[[@as any]],
|
||||||
|
'could not find context for node'
|
||||||
|
)
|
||||||
|
|
||||||
|
-- depth-first:
|
||||||
|
unmount_one(ctx.children)
|
||||||
|
|
||||||
|
-- now unmount current:
|
||||||
|
ctx.phase = 'unmount'
|
||||||
|
Component(ctx)
|
||||||
|
local unsubscribe = assert(ctx.unsubscribe, 'unmount: unsubscribe is nil')
|
||||||
|
unsubscribe()
|
||||||
|
self.component_tree.ctx_by_node[tree] = nil
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
unmount_one(tree)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param old_tree u.renderer.Tree
|
||||||
|
--- @param new_tree u.renderer.Tree
|
||||||
|
--- @return u.renderer.Tree
|
||||||
|
function H2.visit_tree(old_tree, new_tree)
|
||||||
|
local old_tree_kind = Renderer.tree_kind(old_tree)
|
||||||
|
local new_tree_kind = Renderer.tree_kind(new_tree)
|
||||||
|
|
||||||
|
local new_tree_rendered = Renderer.tree_match(new_tree, {
|
||||||
|
string = function(s) return s end,
|
||||||
|
boolean = function(b) return b end,
|
||||||
|
nil_ = function() return nil end,
|
||||||
|
|
||||||
|
array = function(new_arr)
|
||||||
|
return H2.visit_array(old_tree --[[@as any]], new_arr)
|
||||||
|
end,
|
||||||
|
tag = function(new_tag)
|
||||||
|
local old_children = old_tree_kind == new_tree_kind and old_tree.children or nil
|
||||||
|
return M.h(
|
||||||
|
new_tag.name,
|
||||||
|
new_tag.attributes,
|
||||||
|
H2.visit_tree(old_children --[[@as any]], new_tag.children --[[@as any]])
|
||||||
|
)
|
||||||
|
end,
|
||||||
|
|
||||||
|
component = function(NewC, new_tag)
|
||||||
|
local old_ctx = old_tree_kind == new_tree_kind
|
||||||
|
and self.component_tree.ctx_by_node[old_tree]
|
||||||
|
or nil
|
||||||
|
if old_tree_kind == new_tree_kind then self.component_tree.ctx_by_node[old_tree] = nil end
|
||||||
|
|
||||||
|
--- @param new_tree u.renderer.Tree
|
||||||
|
--- @param ctx u.renderer.FnComponentContext
|
||||||
|
--- @param rendered_component u.renderer.Tree
|
||||||
|
local function update_ctx_and_recurse(new_tree, ctx, rendered_component)
|
||||||
|
self.component_tree.ctx_by_node[new_tree] = ctx
|
||||||
|
-- The rendered component could have other components in its tree:
|
||||||
|
-- recurse and simplify:
|
||||||
|
local new_tree_simplified = H2.visit_tree(ctx.prev_tree, rendered_component)
|
||||||
|
ctx.prev_tree = new_tree_simplified
|
||||||
|
return new_tree_simplified
|
||||||
|
end
|
||||||
|
|
||||||
|
if old_ctx then
|
||||||
|
old_ctx.phase = 'update'
|
||||||
|
old_ctx.props = new_tag.attributes
|
||||||
|
old_ctx.children = new_tag.children
|
||||||
|
return update_ctx_and_recurse(new_tree, old_ctx, NewC(old_ctx))
|
||||||
|
else
|
||||||
|
local state = tracker.create_signal(nil)
|
||||||
|
|
||||||
|
--- @type u.renderer.FnComponentContext
|
||||||
|
local ctx = {
|
||||||
|
phase = old_tree and 'update' or 'mount',
|
||||||
|
props = new_tag.attributes,
|
||||||
|
children = new_tag.children,
|
||||||
|
state = state --[[@as any]],
|
||||||
|
prev_tree = nil,
|
||||||
|
}
|
||||||
|
local rendered = NewC(ctx)
|
||||||
|
-- This is the main difference between this else, and the above if:
|
||||||
|
-- we subscribe _after_ first render. This makes it convenient for
|
||||||
|
-- first-renders to initialize the state without firing a
|
||||||
|
-- re-render:
|
||||||
|
ctx.unsubscribe = state:subscribe(render)
|
||||||
|
return update_ctx_and_recurse(new_tree, ctx, rendered)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
if old_tree_kind ~= new_tree_kind then H2.unmount(old_tree) end
|
||||||
|
|
||||||
|
return new_tree_rendered
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param old_arr u.renderer.Node[]
|
||||||
|
--- @param new_arr u.renderer.Node[]
|
||||||
|
--- @return u.renderer.Node[]
|
||||||
|
function H2.visit_array(old_arr, new_arr)
|
||||||
|
--- @type u.renderer.Node[]
|
||||||
|
local old_to_unmount = {}
|
||||||
|
--- @type table<any, u.renderer.Node>
|
||||||
|
local old_by_key = {}
|
||||||
|
for _, old in ipairs(old_arr or {}) do
|
||||||
|
local key = Renderer.tree_match(old, {
|
||||||
|
tag = function(t) return t.attributes.key end,
|
||||||
|
component = function(_, t) return t.attributes.key end,
|
||||||
|
})
|
||||||
|
if not key then
|
||||||
|
table.insert(old_to_unmount, old)
|
||||||
|
else
|
||||||
|
old_by_key[key] = old
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @type u.renderer.Node[]
|
||||||
|
local resulting_nodes = {}
|
||||||
|
for _, new in ipairs(new_arr) do
|
||||||
|
local key = Renderer.tree_match(new, {
|
||||||
|
tag = function(t) return t.attributes.key end,
|
||||||
|
component = function(_, t) return t.attributes.key end,
|
||||||
|
})
|
||||||
|
local old = nil
|
||||||
|
if key then
|
||||||
|
old = old_by_key[key]
|
||||||
|
if old then old_by_key[key] = nil end
|
||||||
|
end
|
||||||
|
table.insert(resulting_nodes, H2.visit_tree(old, new))
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, n in pairs(old_by_key) do
|
||||||
|
table.insert(old_to_unmount, n)
|
||||||
|
end
|
||||||
|
for _, n in pairs(old_to_unmount) do
|
||||||
|
H2.unmount(n)
|
||||||
|
end
|
||||||
|
|
||||||
|
return resulting_nodes
|
||||||
|
end
|
||||||
|
|
||||||
|
local renderable_tree = H2.visit_tree(self.component_tree.old, tree)
|
||||||
|
self.component_tree.old = tree
|
||||||
|
self:render(renderable_tree)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Kick off initial render:
|
||||||
|
render()
|
||||||
|
end -- }}}
|
||||||
|
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
-- TreeBuilder {{{
|
-- TreeBuilder {{{
|
||||||
@ -733,12 +953,12 @@ function TreeBuilder:tree() return self.nodes end
|
|||||||
|
|
||||||
-- Levenshtein utility {{{
|
-- Levenshtein utility {{{
|
||||||
-- luacheck: ignore
|
-- luacheck: ignore
|
||||||
--- @alias u.renderer.LevenshteinChange<T> ({ kind: 'add'; item: T; index: number; } | { kind: 'delete'; item: T; index: number; } | { kind: 'change'; from: T; to: T; index: number; })
|
--- @alias u.renderer.LevenshteinChange<T> ({ kind: 'add', item: T, index: integer } | { kind: 'delete', item: T, index: integer } | { kind: 'change', from: T, to: T, index: integer })
|
||||||
--- @private
|
--- @private
|
||||||
--- @generic T
|
--- @generic T
|
||||||
--- @param x `T`[]
|
--- @param x `T`[]
|
||||||
--- @param y T[]
|
--- @param y T[]
|
||||||
--- @param cost? { of_delete?: fun(x: T): number; of_add?: fun(x: T): number; of_change?: fun(x: T, y: T): number; }
|
--- @param cost? { of_delete?: (fun(x: T): number), of_add?: (fun(x: T): number), of_change?: (fun(x: T, y: T): number) }
|
||||||
--- @return u.renderer.LevenshteinChange<T>[] The changes, from last (greatest index) to first (smallest index).
|
--- @return u.renderer.LevenshteinChange<T>[] The changes, from last (greatest index) to first (smallest index).
|
||||||
function H.levenshtein(x, y, cost)
|
function H.levenshtein(x, y, cost)
|
||||||
-- At the moment, this whole `cost` plumbing is not used. Deletes have the
|
-- At the moment, this whole `cost` plumbing is not used. Deletes have the
|
||||||
|
Loading…
x
Reference in New Issue
Block a user