Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
1bc1b2b
moved helpers from ui to core for separating core and ui
HarshK97 Feb 8, 2026
733be00
feat: enriching update actions with semantic info
HarshK97 Feb 8, 2026
f30a728
feat(core): added explicit rename action with metadata
HarshK97 Feb 8, 2026
e97ddb9
benchmark: updated the benchmark.lua to show the rename action time
HarshK97 Feb 8, 2026
e07fd8a
feat(core): normalizing internal actions
HarshK97 Feb 9, 2026
d5fc652
feat(core): added internal action summaries and count in actions.lua
HarshK97 Feb 9, 2026
c2eea43
feat(core): added query files to support languages via tree-sitter
HarshK97 Feb 10, 2026
db69c7f
feat(core): using query roles for semantic and action classification
HarshK97 Feb 10, 2026
7330f96
fix: highlighting anything not tagged as insert/delete
HarshK97 Feb 10, 2026
16ed432
feat(core): sorting nodes by position for deterministic ordering
HarshK97 Feb 11, 2026
cc91196
feat(core): sorting nodes ordering deterministically in bottom-up and…
HarshK97 Feb 11, 2026
913d3ea
feat(core): added refined rename query roles and semantic field checks
HarshK97 Feb 11, 2026
e244f21
feat(core): moved render payload construction to core engine
HarshK97 Feb 11, 2026
83dc744
refactor(ui): renderer now consumes core payload with new sign manager
HarshK97 Feb 12, 2026
4aa9914
refactor(ui): updated namespace highlights groups to Diffmantic*
HarshK97 Feb 12, 2026
6ddde75
feat(ui): added visual filler lines for improved diff alignment
HarshK97 Feb 13, 2026
ee3b7b2
updated the diff viewer image
HarshK97 Feb 13, 2026
a459f44
refactor(core): normalized action payload and move enrichment to anal…
HarshK97 Feb 13, 2026
a2944fb
refactor(core/ui): added rename supression and visual annotations for…
HarshK97 Feb 14, 2026
e554a4d
refactor(core): normalized action src/dst + metadata summary
HarshK97 Feb 14, 2026
f5ada37
delete(core): removed unused core/payload.lua and ui/helpers.lua
HarshK97 Feb 14, 2026
afc34a7
refactor(core): normalized analysis of payload and suppressed rename …
HarshK97 Feb 14, 2026
df1df3f
fix(core): using structural role captures for significance and matching
HarshK97 Feb 14, 2026
3414bdf
refactor(ui): consuming normalized action payload and sign precedence…
HarshK97 Feb 15, 2026
0bbfbd7
refactor(ui): simpilified filler logic and fixed non rendering hunk s…
HarshK97 Feb 15, 2026
f6ebcec
feat(core): supressing rename-only update actions
HarshK97 Feb 15, 2026
1d9de06
refactor(core): reduced rename detection noise in C/C++ fields
HarshK97 Feb 15, 2026
9edc481
refactor(core): suppress class member renames
HarshK97 Feb 16, 2026
a539675
refactor(core/ui): improved filler line calculation and removed base_…
HarshK97 Feb 16, 2026
9603a56
refactor(ui): removed filler cause it sucks right now
HarshK97 Feb 17, 2026
e0a9a04
feat(queries): add structural captures for C/C++/Go/JS/TS significance
HarshK97 Feb 17, 2026
4b24d8a
refactor(core): enforce parent-safe top-down behavior and expand rena…
HarshK97 Feb 17, 2026
5c331a1
refactor(core): deterministic left-biased recovery with immediate rec…
HarshK97 Feb 18, 2026
98af7a2
refactor(core): token-level asymmetric hunk classification and span a…
HarshK97 Feb 18, 2026
1a9841d
refactor(core): key-aware literal overrides and temp-result refactor …
HarshK97 Feb 19, 2026
7a35cca
refactor(ui): emphasize update-over-move with accent layer and highli…
HarshK97 Feb 19, 2026
f1376de
test(comparison): add cross-language before/after fixtures and sample…
HarshK97 Feb 20, 2026
21be528
perf(core): optimize preprocess hashing and parent-bucket top-down ma…
HarshK97 Feb 21, 2026
a942f32
perf(core): cache role/name resolution and speed bottom-up matching
HarshK97 Feb 21, 2026
0b653e9
perf(core): switch recovery to sparse worklist with bounded LCS
HarshK97 Feb 21, 2026
47bb9c5
perf(core): reuse role indexes and memoize action precompute ancestry
HarshK97 Feb 22, 2026
988bad2
test(perf): benchmark timing breakdown
HarshK97 Feb 22, 2026
3c9bac3
feat(ui/filler): introduce semantic filler pipeline and move-aware pl…
HarshK97 Feb 23, 2026
3caeced
fix(ui/filler): ignore render_as_change update hunks when deriving un…
HarshK97 Feb 23, 2026
7657c42
fix(ui/renderer): render insert/delete as change only via update hunk…
HarshK97 Feb 23, 2026
219f92a
refactor(core/analysis): refine single-line change hunks to minimal c…
HarshK97 Feb 24, 2026
96e4e3b
fix(core/analysis): collapse identical insert/delete hunk pairs to re…
HarshK97 Feb 24, 2026
74e1038
feat(ui): pass mappings to renderer and disable diagnostics in diff b…
HarshK97 Feb 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified images/python.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1,071 changes: 1,002 additions & 69 deletions lua/diffmantic/core/actions.lua

Large diffs are not rendered by default.

747 changes: 747 additions & 0 deletions lua/diffmantic/core/analysis.lua

Large diffs are not rendered by default.

319 changes: 258 additions & 61 deletions lua/diffmantic/core/bottom_up.lua

Large diffs are not rendered by default.

232 changes: 170 additions & 62 deletions lua/diffmantic/core/recovery.lua
Original file line number Diff line number Diff line change
@@ -1,91 +1,180 @@
local M = {}

-- Recovery matching: tries to match remaining unmapped nodes using LCS and unique type
function M.recovery_match(src_root, dst_root, mappings, src_info, dst_info, src_buf, dst_buf)
-- Build O(1) lookup tables
local function node_key(info)
if info.start_row ~= nil then
return info.start_row, info.start_col, info.end_row, info.end_col
end
local sr, sc, er, ec = info.node:range()
return sr, sc, er, ec
end

local function compare_info_order(a_info, b_info)
local asr, asc, aer, aec = node_key(a_info)
local bsr, bsc, ber, bec = node_key(b_info)
if asr ~= bsr then
return asr < bsr
end
if asc ~= bsc then
return asc < bsc
end
if aer ~= ber then
return aer < ber
end
if aec ~= bec then
return aec < bec
end
return a_info.type < b_info.type
end

-- Recovery matching: tries to match remaining unmapped nodes using LCS and unique type.
function M.recovery_match(src_root, dst_root, mappings, src_info, dst_info, src_buf, dst_buf, opts)
opts = opts or {}
local lcs_cell_limit = opts.recovery_lcs_cell_limit or 6000
local skip_unique_type_match = {
field_declaration = true,
}

-- Build O(1) lookup tables.
local src_to_dst = {}
local dst_to_src = {}
for _, m in ipairs(mappings) do
src_to_dst[m.src] = m.dst
dst_to_src[m.dst] = m.src
end

-- Longest Common Subsequence (LCS) for matching children
local function can_match(src_node, dst_node, hash_key)
local s = src_info[src_node:id()]
local d = dst_info[dst_node:id()]
if not s or not d then
return false
end
return s[hash_key] == d[hash_key] and src_node:type() == dst_node:type()
end

local function greedy_lcs(src_list, dst_list, hash_key)
local result = {}
local j = 1
for i = 1, #src_list do
local src_node = src_list[i]
while j <= #dst_list do
local dst_node = dst_list[j]
j = j + 1
if can_match(src_node, dst_node, hash_key) then
table.insert(result, { src = src_node, dst = dst_node })
break
end
end
end
return result
end

-- Longest Common Subsequence (LCS) for matching children.
-- Reconstructed left-to-right so duplicate-compatible nodes prefer earlier dst siblings.
local function lcs(src_list, dst_list, hash_key)
local m, n = #src_list, #dst_list
if m == 0 or n == 0 then
return {}
end
if (m * n) > lcs_cell_limit then
return greedy_lcs(src_list, dst_list, hash_key)
end

local dp = {}
for i = 0, m do
for i = 1, m + 1 do
dp[i] = {}
for j = 0, n do
for j = 1, n + 1 do
dp[i][j] = 0
end
end

for i = 1, m do
for j = 1, n do
local s, d = src_list[i], dst_list[j]
if src_info[s:id()][hash_key] == dst_info[d:id()][hash_key] and s:type() == d:type() then
dp[i][j] = dp[i - 1][j - 1] + 1
for i = m, 1, -1 do
for j = n, 1, -1 do
if can_match(src_list[i], dst_list[j], hash_key) then
dp[i][j] = dp[i + 1][j + 1] + 1
else
dp[i][j] = math.max(dp[i - 1][j], dp[i][j - 1])
dp[i][j] = math.max(dp[i + 1][j], dp[i][j + 1])
end
end
end

-- Backtrack to find matches
-- Deterministic left-biased reconstruction.
local result = {}
local i, j = m, n
while i > 0 and j > 0 do
local s, d = src_list[i], dst_list[j]
if src_info[s:id()][hash_key] == dst_info[d:id()][hash_key] and s:type() == d:type() then
table.insert(result, 1, { src = s, dst = d })
i, j = i - 1, j - 1
elseif dp[i - 1][j] > dp[i][j - 1] then
i = i - 1
local i, j = 1, 1
while i <= m and j <= n do
if can_match(src_list[i], dst_list[j], hash_key) and dp[i][j] == (dp[i + 1][j + 1] + 1) then
table.insert(result, { src = src_list[i], dst = dst_list[j] })
i = i + 1
j = j + 1
else
j = j - 1
local skip_src = dp[i + 1][j]
local skip_dst = dp[i][j + 1]
if skip_dst > skip_src then
j = j + 1
elseif skip_src > skip_dst then
i = i + 1
else
-- Tie: advance src to keep earlier destination candidates.
i = i + 1
end
end
end
return result
end

-- Helper to add a mapping and update lookup tables
local function add_mapping(src_id, dst_id)
table.insert(mappings, { src = src_id, dst = dst_id })
src_to_dst[src_id] = dst_id
dst_to_src[dst_id] = src_id
end

-- Try to match children using LCS and unique type
local function simple_recovery(src_node, dst_node)
local src_children, dst_children = {}, {}
local function has_unmatched_on_both_sides(src_node, dst_node)
local src_has = false
for child in src_node:iter_children() do
if not src_to_dst[child:id()] then
table.insert(src_children, child)
src_has = true
break
end
end
if not src_has then
return false
end
for child in dst_node:iter_children() do
if not dst_to_src[child:id()] then
table.insert(dst_children, child)
return true
end
end
if #src_children == 0 or #dst_children == 0 then
return false
end

local pending = {}
local queued = {}

local function queue_key(src_id, dst_id)
return tostring(src_id) .. ":" .. tostring(dst_id)
end

local function enqueue(src_id, dst_id)
local key = queue_key(src_id, dst_id)
if queued[key] then
return
end
queued[key] = true
pending[#pending + 1] = { src_id = src_id, dst_id = dst_id, key = key }
end

-- Step 1: match children with same hash (exact match)
for _, match in ipairs(lcs(src_children, dst_children, "hash")) do
if not src_to_dst[match.src:id()] and not dst_to_src[match.dst:id()] then
add_mapping(match.src:id(), match.dst:id())
end
-- Helper to add a mapping and update lookup tables.
local function add_mapping(src_id, dst_id)
if src_to_dst[src_id] or dst_to_src[dst_id] then
return false
end
table.insert(mappings, { src = src_id, dst = dst_id })
src_to_dst[src_id] = dst_id
dst_to_src[dst_id] = src_id
local src_entry = src_info[src_id]
local dst_entry = dst_info[dst_id]
if src_entry and dst_entry and has_unmatched_on_both_sides(src_entry.node, dst_entry.node) then
enqueue(src_id, dst_id)
end
return true
end

-- Step 2: match children with same structure_hash (for updates)
src_children, dst_children = {}, {}
local function unmatched_children(src_node, dst_node)
local src_children = {}
local dst_children = {}
for child in src_node:iter_children() do
if not src_to_dst[child:id()] then
table.insert(src_children, child)
Expand All @@ -96,25 +185,34 @@ function M.recovery_match(src_root, dst_root, mappings, src_info, dst_info, src_
table.insert(dst_children, child)
end
end
for _, match in ipairs(lcs(src_children, dst_children, "structure_hash")) do
if not src_to_dst[match.src:id()] and not dst_to_src[match.dst:id()] then
add_mapping(match.src:id(), match.dst:id())
end
return src_children, dst_children
end

-- Try to match children using LCS and unique type.
local function simple_recovery(src_node, dst_node)
local src_children, dst_children = unmatched_children(src_node, dst_node)
if #src_children == 0 or #dst_children == 0 then
return
end

-- Step 3: match children with unique type (type appears only once)
src_children, dst_children = {}, {}
for child in src_node:iter_children() do
if not src_to_dst[child:id()] then
table.insert(src_children, child)
-- Step 1: match children with same hash (exact match).
for _, match in ipairs(lcs(src_children, dst_children, "hash")) do
if add_mapping(match.src:id(), match.dst:id()) then
simple_recovery(match.src, match.dst)
end
end
for child in dst_node:iter_children() do
if not dst_to_src[child:id()] then
table.insert(dst_children, child)

-- Step 2: match children with same structure_hash (for updates).
src_children, dst_children = unmatched_children(src_node, dst_node)
for _, match in ipairs(lcs(src_children, dst_children, "structure_hash")) do
if match.src:type() ~= "field_declaration" and add_mapping(match.src:id(), match.dst:id()) then
simple_recovery(match.src, match.dst)
end
end

-- Step 3: match children with unique type (type appears only once).
src_children, dst_children = unmatched_children(src_node, dst_node)

local src_by_type, dst_by_type = {}, {}
local src_type_count, dst_type_count = {}, {}
for _, c in ipairs(src_children) do
Expand All @@ -129,21 +227,31 @@ function M.recovery_match(src_root, dst_root, mappings, src_info, dst_info, src_
end

for t, count in pairs(src_type_count) do
if count == 1 and dst_type_count[t] == 1 then
if count == 1 and dst_type_count[t] == 1 and not skip_unique_type_match[t] then
local s, d = src_by_type[t], dst_by_type[t]
if not src_to_dst[s:id()] and not dst_to_src[d:id()] then
add_mapping(s:id(), d:id())
if add_mapping(s:id(), d:id()) then
simple_recovery(s, d)
end
end
end
end

-- Apply recovery to all mapped nodes
for id, info in pairs(src_info) do
local dst_id = src_to_dst[id]
if dst_id then
simple_recovery(info.node, dst_info[dst_id].node)
-- Seed worklist only with mapped pairs that still have unmatched children on both sides.
for _, mapping in ipairs(mappings) do
local src_entry = src_info[mapping.src]
local dst_entry = dst_info[mapping.dst]
if src_entry and dst_entry and has_unmatched_on_both_sides(src_entry.node, dst_entry.node) then
enqueue(mapping.src, mapping.dst)
end
end

while #pending > 0 do
local pair = table.remove(pending)
queued[pair.key] = nil
local src_entry = src_info[pair.src_id]
local dst_entry = dst_info[pair.dst_id]
if src_entry and dst_entry and has_unmatched_on_both_sides(src_entry.node, dst_entry.node) then
simple_recovery(src_entry.node, dst_entry.node)
end
end

Expand Down
Loading