LEVENSHTEIN FREAKING WORKS FOR TREE DIFFING
Some checks failed
NeoVim tests / code-quality (push) Failing after 1m16s
Some checks failed
NeoVim tests / code-quality (push) Failing after 1m16s
This commit is contained in:
parent
8a14b6b824
commit
5273fd743a
@ -272,8 +272,10 @@ function Renderer.patch_lines(bufnr, old_lines, new_lines) -- {{{
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Morph the text to the desired state:
|
-- Morph the text to the desired state:
|
||||||
local line_changes =
|
local line_changes = H.levenshtein {
|
||||||
H.levenshtein(old_lines or vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), new_lines)
|
from = old_lines or vim.api.nvim_buf_get_lines(bufnr, 0, -1, false),
|
||||||
|
to = new_lines,
|
||||||
|
}
|
||||||
for _, line_change in ipairs(line_changes) do
|
for _, line_change in ipairs(line_changes) do
|
||||||
local lnum0 = line_change.index - 1
|
local lnum0 = line_change.index - 1
|
||||||
|
|
||||||
@ -282,7 +284,7 @@ function Renderer.patch_lines(bufnr, old_lines, new_lines) -- {{{
|
|||||||
elseif line_change.kind == 'change' then
|
elseif line_change.kind == 'change' then
|
||||||
-- Compute inter-line diff, and apply:
|
-- Compute inter-line diff, and apply:
|
||||||
local col_changes =
|
local col_changes =
|
||||||
H.levenshtein(vim.split(line_change.from, ''), vim.split(line_change.to, ''))
|
H.levenshtein { from = vim.split(line_change.from, ''), to = vim.split(line_change.to, '') }
|
||||||
|
|
||||||
for _, col_change in ipairs(col_changes) do
|
for _, col_change in ipairs(col_changes) do
|
||||||
local cnum0 = col_change.index - 1
|
local cnum0 = col_change.index - 1
|
||||||
@ -644,23 +646,29 @@ function Renderer:get_tags_at(pos0, mode) -- {{{
|
|||||||
--- @type u.renderer.RendererExtmark[]
|
--- @type u.renderer.RendererExtmark[]
|
||||||
local mapped_extmarks = vim
|
local mapped_extmarks = vim
|
||||||
.iter(raw_overlapping_extmarks)
|
.iter(raw_overlapping_extmarks)
|
||||||
|
:map(
|
||||||
--- @return u.renderer.RendererExtmark
|
--- @return u.renderer.RendererExtmark
|
||||||
:map(function(ext)
|
function(ext)
|
||||||
--- @type number, number, number, { end_row?: number; end_col?: number }|nil
|
--- @type integer, integer, integer, { end_row?: number, end_col?: number }|nil
|
||||||
local id, line0, col0, details = unpack(ext)
|
local id, line0, col0, details = unpack(ext)
|
||||||
local start = { line0, col0 }
|
local start = { line0, col0 }
|
||||||
local stop = { line0, col0 }
|
local stop = { line0, col0 }
|
||||||
if details and details.end_row ~= nil and details.end_col ~= nil then
|
if details and details.end_row ~= nil and details.end_col ~= nil then
|
||||||
stop = { details.end_row, details.end_col }
|
stop = {
|
||||||
|
details.end_row --[[@as integer]],
|
||||||
|
details.end_col --[[@as integer]],
|
||||||
|
}
|
||||||
end
|
end
|
||||||
return { id = id, start = start, stop = stop, opts = details }
|
return { id = id, start = start, stop = stop, opts = details }
|
||||||
end)
|
end
|
||||||
|
)
|
||||||
:totable()
|
:totable()
|
||||||
|
|
||||||
local intersecting_extmarks = vim
|
local intersecting_extmarks = vim
|
||||||
.iter(mapped_extmarks)
|
.iter(mapped_extmarks)
|
||||||
|
:filter(
|
||||||
--- @param ext u.renderer.RendererExtmark
|
--- @param ext u.renderer.RendererExtmark
|
||||||
:filter(function(ext)
|
function(ext)
|
||||||
if ext.stop[1] ~= nil and ext.stop[2] ~= nil then
|
if ext.stop[1] ~= nil and ext.stop[2] ~= nil then
|
||||||
-- If we've "ciw" and "collapsed" an extmark onto the cursor,
|
-- If we've "ciw" and "collapsed" an extmark onto the cursor,
|
||||||
-- the cursor pos will equal the exmark's start AND end. In this
|
-- the cursor pos will equal the exmark's start AND end. In this
|
||||||
@ -695,7 +703,8 @@ function Renderer:get_tags_at(pos0, mode) -- {{{
|
|||||||
else
|
else
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
end)
|
end
|
||||||
|
)
|
||||||
:totable()
|
:totable()
|
||||||
|
|
||||||
-- Sort the tags into smallest (inner) to largest (outer):
|
-- Sort the tags into smallest (inner) to largest (outer):
|
||||||
@ -727,12 +736,14 @@ function Renderer:get_tags_at(pos0, mode) -- {{{
|
|||||||
--- @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)
|
||||||
|
:map(
|
||||||
--- @param ext u.renderer.RendererExtmark
|
--- @param ext u.renderer.RendererExtmark
|
||||||
:map(function(ext)
|
function(ext)
|
||||||
for _, extmark_cache in ipairs(self.text_content.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
|
||||||
|
)
|
||||||
:totable()
|
:totable()
|
||||||
|
|
||||||
return matching_tags
|
return matching_tags
|
||||||
@ -823,7 +834,10 @@ function Renderer:mount(tree) -- {{{
|
|||||||
|
|
||||||
component = function(NewC, new_tag)
|
component = function(NewC, new_tag)
|
||||||
--- @type { tag: u.renderer.Tag, ctx?: u.renderer.FnComponentContext } | nil
|
--- @type { tag: u.renderer.Tag, ctx?: u.renderer.FnComponentContext } | nil
|
||||||
local old_component_info = Renderer.tree_match(old_tree, { component = function(_, t) return { tag = t, ctx = t.ctx } end })
|
local old_component_info = Renderer.tree_match(
|
||||||
|
old_tree,
|
||||||
|
{ component = function(_, t) return { tag = t, ctx = t.ctx } end }
|
||||||
|
)
|
||||||
local old_tag = old_component_info and old_component_info.tag or nil
|
local old_tag = old_component_info and old_component_info.tag or nil
|
||||||
local ctx = old_component_info and old_component_info.ctx or nil
|
local ctx = old_component_info and old_component_info.ctx or nil
|
||||||
|
|
||||||
@ -862,52 +876,64 @@ function Renderer:mount(tree) -- {{{
|
|||||||
--- @param new_arr u.renderer.Node[]
|
--- @param new_arr u.renderer.Node[]
|
||||||
--- @return u.renderer.Node[]
|
--- @return u.renderer.Node[]
|
||||||
function H2.visit_array(old_arr, new_arr)
|
function H2.visit_array(old_arr, new_arr)
|
||||||
--- @type table<any, u.renderer.Node | nil>
|
-- We are going to hijack levenshtein in order to compute the
|
||||||
local old_by_key = {}
|
-- difference between elements/components. In this model, we need to
|
||||||
|
-- "update" all the nodes, so no nodes are equal. We will rely on
|
||||||
--- @param tree u.renderer.Tree
|
-- levenshtein to find the "shortest path" to conforming old => new via
|
||||||
--- @param idx integer
|
-- the cost.of_change function. That will provide the meat of modeling
|
||||||
local function get_or_set_key(tree, idx)
|
-- what effort it will take to morph one element into the new form.
|
||||||
|
-- What levenshtein gives us for free in this model is also informing
|
||||||
|
-- us what needs to be added (i.e., "mounted"), what needs to be
|
||||||
|
-- deleted ("unmounted") and what needs to be changed ("updated").
|
||||||
|
local changes = H.levenshtein {
|
||||||
|
from = old_arr or {},
|
||||||
|
to = new_arr or {},
|
||||||
|
are_equal = function() return false end,
|
||||||
|
cost = {
|
||||||
|
of_change = function(tree1, tree2, tree1_idx, tree2_idx)
|
||||||
|
--- @return string
|
||||||
|
local function verbose_tree_kind(tree, idx)
|
||||||
return Renderer.tree_match(tree, {
|
return Renderer.tree_match(tree, {
|
||||||
tag = function(t)
|
nil_ = function() return 'nil' end,
|
||||||
if not t.attributes.key then t.attributes.key = idx end
|
string = function() return 'string' end,
|
||||||
return t.attributes.key
|
boolean = function() return 'boolean' end,
|
||||||
|
array = function() return 'array' end,
|
||||||
|
tag = function(tag)
|
||||||
|
return ('tag-%s-%s'):format(tag.name, tostring(tag.attributes.key or idx))
|
||||||
end,
|
end,
|
||||||
component = function(_, t)
|
component = function(C, tag)
|
||||||
if not t.attributes.key then t.attributes.key = idx end
|
return ('component-%s-%s'):format(C, tostring(tag.attributes.key or idx))
|
||||||
return t.attributes.key
|
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
local tree1_inf = verbose_tree_kind(tree1, tree1_idx)
|
||||||
for idx, old in ipairs(old_arr or {}) do
|
local tree2_inf = verbose_tree_kind(tree2, tree2_idx)
|
||||||
local key = get_or_set_key(old, idx)
|
return tree1_inf == tree2_inf and 1 or 2
|
||||||
if key then old_by_key[key] = old end
|
end,
|
||||||
end
|
},
|
||||||
|
}
|
||||||
|
|
||||||
--- @type u.renderer.Node[]
|
--- @type u.renderer.Node[]
|
||||||
local resulting_nodes = {}
|
local resulting_nodes = {}
|
||||||
for idx, new in ipairs(new_arr) do
|
for _, change in ipairs(changes) do
|
||||||
local key = get_or_set_key(new, idx)
|
local resulting_node
|
||||||
local old = nil
|
if change.kind == 'add' then
|
||||||
if key then
|
resulting_node = H2.visit_tree(nil, change.item)
|
||||||
old = old_by_key[key]
|
elseif change.kind == 'delete' then
|
||||||
if old then old_by_key[key] = nil end
|
H2.visit_tree(change.item, nil)
|
||||||
|
elseif change.kind == 'change' then
|
||||||
|
resulting_node = H2.visit_tree(change.from, change.to)
|
||||||
end
|
end
|
||||||
table.insert(resulting_nodes, H2.visit_tree(old, new))
|
if resulting_node then table.insert(resulting_nodes, 1, resulting_node) end
|
||||||
end
|
end
|
||||||
|
|
||||||
log('Renderer.mount.visit_array', {
|
log('Renderer.mount.visit_array', {
|
||||||
old_arr = old_arr,
|
old_arr = old_arr,
|
||||||
new_arr = new_arr,
|
new_arr = new_arr,
|
||||||
|
changes = changes,
|
||||||
resulting_nodes = resulting_nodes,
|
resulting_nodes = resulting_nodes,
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Unmount whatever is left in the "old" map:
|
|
||||||
for _, n in pairs(old_by_key) do
|
|
||||||
H2.unmount(n)
|
|
||||||
end
|
|
||||||
|
|
||||||
return resulting_nodes
|
return resulting_nodes
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -990,11 +1016,18 @@ function TreeBuilder:tree() return self.nodes end
|
|||||||
--- @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 })
|
--- @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 opts {
|
||||||
--- @param y T[]
|
--- from: `T`[],
|
||||||
--- @param cost? { of_delete?: (fun(x: T): number), of_add?: (fun(x: T): number), of_change?: (fun(x: T, y: T): number) }
|
--- to: T[],
|
||||||
|
--- are_equal?: (fun(x: T, y: T, xidx: integer, yidx: integer): boolean),
|
||||||
|
--- cost?: {
|
||||||
|
--- of_delete?: (fun(x: T, idx: integer): number),
|
||||||
|
--- of_add?: (fun(x: T, idx: integer): number),
|
||||||
|
--- of_change?: (fun(x: T, y: T, xidx: integer, yidx: integer): 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(opts)
|
||||||
-- 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
|
||||||
-- same cost as Adds or Changes. I can imagine a future, however, where
|
-- same cost as Adds or Changes. I can imagine a future, however, where
|
||||||
-- fudging with the costs of operations produces a more optimized change-set
|
-- fudging with the costs of operations produces a more optimized change-set
|
||||||
@ -1004,12 +1037,13 @@ function H.levenshtein(x, y, cost)
|
|||||||
-- in here even though it's not actively used. Hopefully having this
|
-- in here even though it's not actively used. Hopefully having this
|
||||||
-- callback-based plumbing does not cause too much of a performance hit to
|
-- callback-based plumbing does not cause too much of a performance hit to
|
||||||
-- the renderer.
|
-- the renderer.
|
||||||
cost = cost or {}
|
if not opts.are_equal then opts.are_equal = function(x, y) return x == y end end
|
||||||
local cost_of_delete_f = cost.of_delete or function() return 1 end
|
if not opts.cost then opts.cost = {} end
|
||||||
local cost_of_add_f = cost.of_add or function() return 1 end
|
if not opts.cost.of_add then opts.cost.of_add = function() return 1 end end
|
||||||
local cost_of_change_f = cost.of_change or function() return 1 end
|
if not opts.cost.of_change then opts.cost.of_change = function() return 1 end end
|
||||||
|
if not opts.cost.of_delete then opts.cost.of_delete = function() return 1 end end
|
||||||
|
|
||||||
local m, n = #x, #y
|
local m, n = #opts.from, #opts.to
|
||||||
-- Initialize the distance matrix
|
-- Initialize the distance matrix
|
||||||
local dp = {}
|
local dp = {}
|
||||||
for i = 0, m do
|
for i = 0, m do
|
||||||
@ -1030,12 +1064,12 @@ function H.levenshtein(x, y, cost)
|
|||||||
-- Compute the Levenshtein distance dynamically
|
-- Compute the Levenshtein distance dynamically
|
||||||
for i = 1, m do
|
for i = 1, m do
|
||||||
for j = 1, n do
|
for j = 1, n do
|
||||||
if x[i] == y[j] then
|
if opts.are_equal(opts.from[i], opts.to[j], i, j) then
|
||||||
dp[i][j] = dp[i - 1][j - 1] -- no cost if items are the same
|
dp[i][j] = dp[i - 1][j - 1] -- no cost if items are the same
|
||||||
else
|
else
|
||||||
local cost_delete = dp[i - 1][j] + cost_of_delete_f(x[i])
|
local cost_delete = dp[i - 1][j] + opts.cost.of_delete(opts.from[i], i)
|
||||||
local cost_add = dp[i][j - 1] + cost_of_add_f(y[j])
|
local cost_add = dp[i][j - 1] + opts.cost.of_add(opts.to[j], j)
|
||||||
local cost_change = dp[i - 1][j - 1] + cost_of_change_f(x[i], y[j])
|
local cost_change = dp[i - 1][j - 1] + opts.cost.of_change(opts.from[i], opts.to[j], i, j)
|
||||||
dp[i][j] = math.min(cost_delete, cost_add, cost_change)
|
dp[i][j] = math.min(cost_delete, cost_add, cost_change)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -1060,9 +1094,9 @@ function H.levenshtein(x, y, cost)
|
|||||||
|
|
||||||
if is_first_min(cost_of_change, cost_of_add, cost_of_delete) then
|
if is_first_min(cost_of_change, cost_of_add, cost_of_delete) then
|
||||||
-- potential change
|
-- potential change
|
||||||
if x[i] ~= y[j] then
|
if not opts.are_equal(opts.from[i], opts.to[j]) then
|
||||||
--- @type u.renderer.LevenshteinChange
|
--- @type u.renderer.LevenshteinChange
|
||||||
local change = { kind = 'change', from = x[i], index = i, to = y[j] }
|
local change = { kind = 'change', from = opts.from[i], index = i, to = opts.to[j] }
|
||||||
table.insert(changes, change)
|
table.insert(changes, change)
|
||||||
end
|
end
|
||||||
i = i - 1
|
i = i - 1
|
||||||
@ -1070,13 +1104,13 @@ function H.levenshtein(x, y, cost)
|
|||||||
elseif is_first_min(cost_of_add, cost_of_change, cost_of_delete) then
|
elseif is_first_min(cost_of_add, cost_of_change, cost_of_delete) then
|
||||||
-- addition
|
-- addition
|
||||||
--- @type u.renderer.LevenshteinChange
|
--- @type u.renderer.LevenshteinChange
|
||||||
local change = { kind = 'add', item = y[j], index = i + 1 }
|
local change = { kind = 'add', item = opts.to[j], index = i + 1 }
|
||||||
table.insert(changes, change)
|
table.insert(changes, change)
|
||||||
j = j - 1
|
j = j - 1
|
||||||
elseif is_first_min(cost_of_delete, cost_of_change, cost_of_add) then
|
elseif is_first_min(cost_of_delete, cost_of_change, cost_of_add) then
|
||||||
-- deletion
|
-- deletion
|
||||||
--- @type u.renderer.LevenshteinChange
|
--- @type u.renderer.LevenshteinChange
|
||||||
local change = { kind = 'delete', item = x[i], index = i }
|
local change = { kind = 'delete', item = opts.from[i], index = i }
|
||||||
table.insert(changes, change)
|
table.insert(changes, change)
|
||||||
i = i - 1
|
i = i - 1
|
||||||
else
|
else
|
||||||
|
Loading…
x
Reference in New Issue
Block a user