From 1bc1b2b0e52e466d7471bcc1303071febd9193e0 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sun, 8 Feb 2026 08:31:03 +0530 Subject: [PATCH 01/48] moved helpers from ui to core for separating core and ui --- lua/diffmantic/core/semantic.lua | 272 +++++++++++++++++++++++++++++++ lua/diffmantic/ui/helpers.lua | 230 +------------------------- 2 files changed, 280 insertions(+), 222 deletions(-) create mode 100644 lua/diffmantic/core/semantic.lua diff --git a/lua/diffmantic/core/semantic.lua b/lua/diffmantic/core/semantic.lua new file mode 100644 index 0000000..cc30c50 --- /dev/null +++ b/lua/diffmantic/core/semantic.lua @@ -0,0 +1,272 @@ +local M = {} + +-- Leaf-level diffs for small updates; otherwise return empty. +function M.find_leaf_changes(src_node, dst_node, src_buf, dst_buf) + local changes = {} + + local function get_all_leaves(node, bufnr) + local leaves = {} + local function traverse(n) + if n:child_count() == 0 then + table.insert(leaves, { + node = n, + text = vim.treesitter.get_node_text(n, bufnr), + type = n:type(), + }) + else + for child in n:iter_children() do + traverse(child) + end + end + end + traverse(node) + return leaves + end + + local src_leaves = get_all_leaves(src_node, src_buf) + local dst_leaves = get_all_leaves(dst_node, dst_buf) + + if #src_leaves ~= #dst_leaves then + return {} + end + + if math.abs(#src_leaves - #dst_leaves) > 2 then + return {} + end + + local min_len = math.min(#src_leaves, #dst_leaves) + local max_len = math.max(#src_leaves, #dst_leaves) + local same_count = 0 + + for i = 1, min_len do + local sl, dl = src_leaves[i], dst_leaves[i] + if sl.type == dl.type and sl.text == dl.text then + same_count = same_count + 1 + elseif sl.type == dl.type and sl.text ~= dl.text then + table.insert(changes, { + src_node = sl.node, + dst_node = dl.node, + src_text = sl.text, + dst_text = dl.text, + }) + end + end + + local similarity = same_count / max_len + + if similarity < 0.5 then + return {} + end + + if #changes > 5 then + return {} + end + + return changes +end + +function M.node_in_field(parent, field_name, node) + local nodes = parent:field(field_name) + if not nodes then + return false + end + for _, field_node in ipairs(nodes) do + if field_node:equal(node) or field_node:child_with_descendant(node) then + return true + end + end + return false +end + +function M.is_rename_identifier(node) + if not node then + return false + end + + local node_type = node:type() + if node_type ~= "identifier" and node_type ~= "type_identifier" and node_type ~= "field_identifier" then + return false + end + + local parent = node:parent() + if not parent then + return false + end + + local parent_type = parent:type() + if parent_type == "parameters" or parent_type == "parameter_list" or parent_type == "formal_parameters" then + return true + end + + if parent_type == "assignment" and M.node_in_field(parent, "left", node) then + return true + end + + if parent_type == "assignment_statement" and M.node_in_field(parent, "variable", node) then + return true + end + if parent_type == "variable_list" then + return true + end + + -- Language-specific name heuristics. + local current = node + while parent do + local ptype = parent:type() + if (ptype == "function_declaration" or ptype == "function_definition" or ptype == "class_definition" or ptype == "class_declaration") + and M.node_in_field(parent, "name", current) + then + return true + end + if (ptype == "class_specifier" or ptype == "struct_specifier" or ptype == "enum_specifier" or ptype == "union_specifier") + and (M.node_in_field(parent, "name", current) or M.node_in_field(parent, "tag", current)) + then + return true + end + if ptype == "function_declarator" then + return true + end + if ptype == "init_declarator" and M.node_in_field(parent, "declarator", current) then + return true + end + if ptype == "field_declaration" and M.node_in_field(parent, "declarator", current) then + return true + end + if ptype == "declarator" then + return true + end + if ptype == "field" then + return true + end + current = parent + parent = parent:parent() + end + + return false +end + +function M.is_value_node(node, text) + local node_type = node and node:type() or "" + if node_type:find("string") or node_type:find("number") or node_type:find("integer") or node_type:find("float") or node_type:find("boolean") then + return true + end + if node_type == "char_literal" or node_type:find("char") and node_type:find("literal") then + return true + end + if text then + if text:match("^%s*['\"].*['\"]%s*$") then + return true + end + if text:match("^%s*[%d%.]+%s*$") then + return true + end + if text == "true" or text == "false" or text == "nil" then + return true + end + end + return false +end + +local function expand_word_fragment(text, start_idx, end_idx) + local s = start_idx + local e = end_idx + while s > 1 and text:sub(s - 1, s - 1):match("[%w_]") do + s = s - 1 + end + while e <= #text and text:sub(e, e):match("[%w_]") do + e = e + 1 + end + return s, e - 1 +end + +local function expand_assignment_suffix(text, end_idx) + local i = end_idx + 1 + while i <= #text and text:sub(i, i):match("%s") do + i = i + 1 + end + if i <= #text and text:sub(i, i) == "=" then + local j = i + 1 + while j <= #text and text:sub(j, j):match("%s") do + j = j + 1 + end + return j - 1 + end + return end_idx +end + +function M.classify_text_change(old_text, new_text) + if old_text == new_text then + return nil + end + + local max_prefix = math.min(#old_text, #new_text) + local prefix = 0 + while prefix < max_prefix and old_text:sub(prefix + 1, prefix + 1) == new_text:sub(prefix + 1, prefix + 1) do + prefix = prefix + 1 + end + + local max_suffix = math.min(#old_text - prefix, #new_text - prefix) + local suffix = 0 + while suffix < max_suffix do + local o = old_text:sub(#old_text - suffix, #old_text - suffix) + local n = new_text:sub(#new_text - suffix, #new_text - suffix) + if o ~= n then + break + end + suffix = suffix + 1 + end + + local old_start = prefix + 1 + local old_end = #old_text - suffix + local new_start = prefix + 1 + local new_end = #new_text - suffix + + local has_old = old_start <= old_end + local has_new = new_start <= new_end + if not has_old and not has_new then + return nil + end + + if has_old then + old_start, old_end = expand_word_fragment(old_text, old_start, old_end) + old_end = expand_assignment_suffix(old_text, old_end) + end + if has_new then + new_start, new_end = expand_word_fragment(new_text, new_start, new_end) + new_end = expand_assignment_suffix(new_text, new_end) + end + + local kind = "replace" + if has_new and not has_old then + kind = "insert" + elseif has_old and not has_new then + kind = "delete" + end + + return { + kind = kind, + old_start = old_start, + old_end = old_end, + new_start = new_start, + new_end = new_end, + old_fragment = has_old and old_text:sub(old_start, old_end) or "", + new_fragment = has_new and new_text:sub(new_start, new_end) or "", + } +end + +function M.diff_fragment(old_text, new_text) + local change = M.classify_text_change(old_text, new_text) + if not change or change.kind ~= "replace" then + return nil + end + return { + old_start = change.old_start, + old_end = change.old_end, + new_start = change.new_start, + new_end = change.new_end, + old_fragment = change.old_fragment, + new_fragment = change.new_fragment, + } +end + +return M diff --git a/lua/diffmantic/ui/helpers.lua b/lua/diffmantic/ui/helpers.lua index f7ec8c6..6a98b6c 100644 --- a/lua/diffmantic/ui/helpers.lua +++ b/lua/diffmantic/ui/helpers.lua @@ -1,227 +1,13 @@ -local M = {} - --- Leaf-level diffs for small updates; otherwise return empty. -function M.find_leaf_changes(src_node, dst_node, src_buf, dst_buf) - local changes = {} - - local function get_all_leaves(node, bufnr) - local leaves = {} - local function traverse(n) - if n:child_count() == 0 then - table.insert(leaves, { - node = n, - text = vim.treesitter.get_node_text(n, bufnr), - type = n:type(), - }) - else - for child in n:iter_children() do - traverse(child) - end - end - end - traverse(node) - return leaves - end - - local src_leaves = get_all_leaves(src_node, src_buf) - local dst_leaves = get_all_leaves(dst_node, dst_buf) - - if #src_leaves ~= #dst_leaves then - return {} - end - - if math.abs(#src_leaves - #dst_leaves) > 2 then - return {} - end - - local min_len = math.min(#src_leaves, #dst_leaves) - local max_len = math.max(#src_leaves, #dst_leaves) - local same_count = 0 - - for i = 1, min_len do - local sl, dl = src_leaves[i], dst_leaves[i] - if sl.type == dl.type and sl.text == dl.text then - same_count = same_count + 1 - elseif sl.type == dl.type and sl.text ~= dl.text then - table.insert(changes, { - src_node = sl.node, - dst_node = dl.node, - src_text = sl.text, - dst_text = dl.text, - }) - end - end - - local similarity = same_count / max_len - - if similarity < 0.5 then - return {} - end - - if #changes > 5 then - return {} - end - - return changes -end - -function M.node_in_field(parent, field_name, node) - local nodes = parent:field(field_name) - if not nodes then - return false - end - for _, field_node in ipairs(nodes) do - if field_node:equal(node) or field_node:child_with_descendant(node) then - return true - end - end - return false -end - -function M.is_rename_identifier(node) - if not node then - return false - end - - local node_type = node:type() - if node_type ~= "identifier" and node_type ~= "type_identifier" and node_type ~= "field_identifier" then - return false - end - - local parent = node:parent() - if not parent then - return false - end - - local parent_type = parent:type() - if parent_type == "parameters" or parent_type == "parameter_list" or parent_type == "formal_parameters" then - return true - end - - if parent_type == "assignment" and M.node_in_field(parent, "left", node) then - return true - end - - if parent_type == "assignment_statement" and M.node_in_field(parent, "variable", node) then - return true - end - if parent_type == "variable_list" then - return true - end +local semantic = require("diffmantic.core.semantic") - -- Language-specific name heuristics. - local current = node - while parent do - local ptype = parent:type() - if (ptype == "function_declaration" or ptype == "function_definition" or ptype == "class_definition" or ptype == "class_declaration") - and M.node_in_field(parent, "name", current) - then - return true - end - if (ptype == "class_specifier" or ptype == "struct_specifier" or ptype == "enum_specifier" or ptype == "union_specifier") - and (M.node_in_field(parent, "name", current) or M.node_in_field(parent, "tag", current)) - then - return true - end - if ptype == "function_declarator" then - return true - end - if ptype == "init_declarator" and M.node_in_field(parent, "declarator", current) then - return true - end - if ptype == "field_declaration" and M.node_in_field(parent, "declarator", current) then - return true - end - if ptype == "declarator" then - return true - end - if ptype == "field" then - return true - end - current = parent - parent = parent:parent() - end - - return false -end - -function M.is_value_node(node, text) - local node_type = node and node:type() or "" - if node_type:find("string") or node_type:find("number") or node_type:find("integer") or node_type:find("float") or node_type:find("boolean") then - return true - end - if node_type == "char_literal" or node_type:find("char") and node_type:find("literal") then - return true - end - if text then - if text:match("^%s*['\"].*['\"]%s*$") then - return true - end - if text:match("^%s*[%d%.]+%s*$") then - return true - end - if text == "true" or text == "false" or text == "nil" then - return true - end - end - return false -end - -local function expand_word_fragment(text, start_idx, end_idx) - local s = start_idx - local e = end_idx - while s > 1 and text:sub(s - 1, s - 1):match("[%w_]") do - s = s - 1 - end - while e <= #text and text:sub(e, e):match("[%w_]") do - e = e + 1 - end - return s, e - 1 -end - -function M.diff_fragment(old_text, new_text) - if old_text == new_text then - return nil - end - - local max_prefix = math.min(#old_text, #new_text) - local prefix = 0 - while prefix < max_prefix and old_text:sub(prefix + 1, prefix + 1) == new_text:sub(prefix + 1, prefix + 1) do - prefix = prefix + 1 - end - - local max_suffix = math.min(#old_text - prefix, #new_text - prefix) - local suffix = 0 - while suffix < max_suffix do - local o = old_text:sub(#old_text - suffix, #old_text - suffix) - local n = new_text:sub(#new_text - suffix, #new_text - suffix) - if o ~= n then - break - end - suffix = suffix + 1 - end - - local old_start = prefix + 1 - local old_end = #old_text - suffix - local new_start = prefix + 1 - local new_end = #new_text - suffix - - if old_start > old_end or new_start > new_end then - return nil - end - - old_start, old_end = expand_word_fragment(old_text, old_start, old_end) - new_start, new_end = expand_word_fragment(new_text, new_start, new_end) +local M = {} - return { - old_start = old_start, - old_end = old_end, - new_start = new_start, - new_end = new_end, - old_fragment = old_text:sub(old_start, old_end), - new_fragment = new_text:sub(new_start, new_end), - } -end +M.find_leaf_changes = semantic.find_leaf_changes +M.node_in_field = semantic.node_in_field +M.is_rename_identifier = semantic.is_rename_identifier +M.is_value_node = semantic.is_value_node +M.classify_text_change = semantic.classify_text_change +M.diff_fragment = semantic.diff_fragment function M.set_inline_virt_text(buf, ns, row, col, text, hl) local opts = { From 733be00c9188072e5c3c0175a81d9c7277725eea Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sun, 8 Feb 2026 10:35:22 +0530 Subject: [PATCH 02/48] feat: enriching update actions with semantic info - added enrich_update_actions_with_semantic() to analyze update actions - detect leaf changes within updated nodes using semantic.find_leaf_change() - extracting rename pairs for identifier changes --- lua/diffmantic/core/actions.lua | 30 ++++++++++++++++++++++++++++++ lua/diffmantic/init.lua | 5 ++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/lua/diffmantic/core/actions.lua b/lua/diffmantic/core/actions.lua index f69567c..73d6126 100644 --- a/lua/diffmantic/core/actions.lua +++ b/lua/diffmantic/core/actions.lua @@ -1,4 +1,5 @@ local M = {} +local semantic = require("diffmantic.core.semantic") -- Generate edit actions from node mappings -- Actions describe what changed: insert, delete, update, move @@ -27,6 +28,31 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op timings[key] = (hrtime() - started_at) / 1e6 end + local function enrich_update_actions_with_semantics(actions_list) + local src_buf = opts and opts.src_buf or nil + local dst_buf = opts and opts.dst_buf or nil + if not src_buf or not dst_buf then + return + end + + for _, action in ipairs(actions_list) do + if action.type == "update" and action.node and action.target then + local leaf_changes = semantic.find_leaf_changes(action.node, action.target, src_buf, dst_buf) + local rename_pairs = {} + for _, change in ipairs(leaf_changes) do + if semantic.is_rename_identifier(change.src_node) or semantic.is_rename_identifier(change.dst_node) then + rename_pairs[change.src_text] = change.dst_text + end + end + + action.semantic = { + leaf_changes = leaf_changes, + rename_pairs = rename_pairs, + } + end + end + end + -- Build O(1) lookup tables local precompute_start = start_timer() local src_to_dst = {} @@ -284,6 +310,10 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end stop_timer(inserts_start, "inserts") + local semantic_start = start_timer() + enrich_update_actions_with_semantics(actions) + stop_timer(semantic_start, "semantic") + return actions, timings end diff --git a/lua/diffmantic/init.lua b/lua/diffmantic/init.lua index 4e43eeb..5723d1a 100644 --- a/lua/diffmantic/init.lua +++ b/lua/diffmantic/init.lua @@ -86,7 +86,10 @@ function M.diff(args) mappings = core.recovery_match(root1, root2, mappings, src_info, dst_info, buf1, buf2) -- debug_utils.print_recovery_mappings(mappings, before_recovery, src_info, dst_info, buf1, buf2) - local actions = core.generate_actions(root1, root2, mappings, src_info, dst_info) + local actions = core.generate_actions(root1, root2, mappings, src_info, dst_info, { + src_buf = buf1, + dst_buf = buf2, + }) -- debug_utils.print_actions(actions, buf1, buf2) -- debug_utils.print_mappings(mappings, src_info, dst_info, buf1, buf2) From f30a72890799183ad6797d0c0e72aea3ec2012d5 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sun, 8 Feb 2026 18:29:28 +0530 Subject: [PATCH 03/48] feat(core): added explicit rename action with metadata - added explicit rename action generation from update semantic leaf changes - added metadata on rename actions - added reusable range metadata helper, can be used for other actions to expose it to ui --- lua/diffmantic/core/actions.lua | 67 ++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/lua/diffmantic/core/actions.lua b/lua/diffmantic/core/actions.lua index 73d6126..c2ce27d 100644 --- a/lua/diffmantic/core/actions.lua +++ b/lua/diffmantic/core/actions.lua @@ -1,8 +1,23 @@ local M = {} local semantic = require("diffmantic.core.semantic") +local function range_metadata(node) + if not node then + return nil + end + local sr, sc, er, ec = node:range() + return { + start_row = sr, + start_col = sc, + end_row = er, + end_col = ec, + start_line = sr + 1, + end_line = er + 1, + } +end + -- Generate edit actions from node mappings --- Actions describe what changed: insert, delete, update, move +-- Actions describe what changed: insert, delete, update, move, rename function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, opts) local actions = {} local timings = nil @@ -53,6 +68,52 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end end + local function emit_rename_actions(actions_list) + local renames = {} + local seen = {} + + for _, action in ipairs(actions_list) do + if action.type == "update" and action.semantic and action.semantic.leaf_changes then + for _, change in ipairs(action.semantic.leaf_changes) do + local src_node = change.src_node + local dst_node = change.dst_node + if src_node and dst_node and change.src_text ~= change.dst_text then + local is_rename = semantic.is_rename_identifier(src_node) or semantic.is_rename_identifier(dst_node) + if is_rename then + local key = table.concat({ + tostring(src_node:id()), + tostring(dst_node:id()), + change.src_text, + change.dst_text, + }, ":") + + if not seen[key] then + seen[key] = true + table.insert(renames, { + type = "rename", + node = src_node, + target = dst_node, + from = change.src_text, + to = change.dst_text, + src_range = range_metadata(src_node), + dst_range = range_metadata(dst_node), + context = { + src_parent_type = action.node and action.node:type() or nil, + dst_parent_type = action.target and action.target:type() or nil, + }, + }) + end + end + end + end + end + end + + for _, rename_action in ipairs(renames) do + table.insert(actions_list, rename_action) + end + end + -- Build O(1) lookup tables local precompute_start = start_timer() local src_to_dst = {} @@ -314,6 +375,10 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op enrich_update_actions_with_semantics(actions) stop_timer(semantic_start, "semantic") + local rename_start = start_timer() + emit_rename_actions(actions) + stop_timer(rename_start, "renames") + return actions, timings end From e97ddb983884659de0b3d8ed00a3745064986f05 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sun, 8 Feb 2026 18:33:25 +0530 Subject: [PATCH 04/48] benchmark: updated the benchmark.lua to show the rename action time gives around 100 renames in 2 ms so pretty fast --- test/benchmark.lua | 62 +++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/test/benchmark.lua b/test/benchmark.lua index 2f267fa..1e77053 100644 --- a/test/benchmark.lua +++ b/test/benchmark.lua @@ -33,7 +33,7 @@ local function generate_lua_code(num_functions, vars_per_function) return table.concat(lines, "\n") end --- Generate modified version (swaps some functions, changes some values) +-- Generate modified version (swaps some functions, changes some values, renames symbols) local function generate_modified_lua(num_functions, vars_per_function, changes) local lines = {} table.insert(lines, "-- Auto-generated benchmark file (modified)") @@ -54,17 +54,37 @@ local function generate_modified_lua(num_functions, vars_per_function, changes) end end + local rename_fn_count = math.min(changes.renames or 0, num_functions) + for _, i in ipairs(order) do - table.insert(lines, string.format("function M.func_%d()", i)) + local function_name = string.format("func_%d", i) + local renamed_function_name = nil + if i <= rename_fn_count then + renamed_function_name = function_name .. "_renamed" + end + table.insert(lines, string.format("function M.%s()", renamed_function_name or function_name)) + + local first_var = string.format("var_%d_1", i) + local renamed_first_var = nil + if i <= rename_fn_count then + renamed_first_var = string.format("renamed_%d_1", i) + end + for j = 1, vars_per_function do + local var_name = string.format("var_%d_%d", i, j) + if j == 1 and renamed_first_var then + var_name = renamed_first_var + end local value = i * 100 + j -- Change some values if i <= (changes.updates or 0) and j == 1 then value = value + 1000 end - table.insert(lines, string.format(" local var_%d_%d = %d", i, j, value)) + table.insert(lines, string.format(" local %s = %d", var_name, value)) end - table.insert(lines, string.format(" return var_%d_1 + var_%d_%d", i, i, vars_per_function)) + + local return_first_var = renamed_first_var or first_var + table.insert(lines, string.format(" return %s + var_%d_%d", return_first_var, i, vars_per_function)) table.insert(lines, "end") table.insert(lines, "") end @@ -84,7 +104,7 @@ local function run_benchmark(name, num_functions, vars_per_function, changes) log(string.format("\n=== %s ===", name)) log(string.format("Source: %d lines, Dest: %d lines", #src_lines, #dst_lines)) log(string.format("Functions: %d, Vars/function: %d", num_functions, vars_per_function)) - log(string.format("Changes: %d swaps, %d updates", changes.swaps or 0, changes.updates or 0)) + log(string.format("Changes: %d swaps, %d updates, %d renames", changes.swaps or 0, changes.updates or 0, changes.renames or 0)) -- Create buffers local src_buf = vim.api.nvim_create_buf(false, true) @@ -122,18 +142,24 @@ local function run_benchmark(name, num_functions, vars_per_function, changes) -- Generate actions local start_actions = vim.loop.hrtime() - local actions, action_timings = core.generate_actions(src_root, dst_root, mappings, src_info, dst_info, { timings = true }) + local actions, action_timings = core.generate_actions(src_root, dst_root, mappings, src_info, dst_info, { + timings = true, + src_buf = src_buf, + dst_buf = dst_buf, + }) local actions_time = (vim.loop.hrtime() - start_actions) / 1e6 local total_time = (vim.loop.hrtime() - start_total) / 1e6 -- Count actions - local move_count, update_count, delete_count, insert_count = 0, 0, 0, 0 + local move_count, update_count, delete_count, insert_count, rename_count = 0, 0, 0, 0, 0 for _, action in ipairs(actions) do if action.type == "move" then move_count = move_count + 1 elseif action.type == "update" then update_count = update_count + 1 + elseif action.type == "rename" then + rename_count = rename_count + 1 elseif action.type == "delete" then delete_count = delete_count + 1 elseif action.type == "insert" then @@ -150,6 +176,7 @@ local function run_benchmark(name, num_functions, vars_per_function, changes) log(string.format(" Prep: %8.2f ms", action_timings.precompute or 0)) log(string.format(" Updates: %8.2f ms", action_timings.updates or 0)) log(string.format(" Moves: %8.2f ms", action_timings.moves or 0)) + log(string.format(" Renames: %8.2f ms", action_timings.renames or 0)) log(string.format(" Deletes: %8.2f ms", action_timings.deletes or 0)) log(string.format(" Inserts: %8.2f ms", action_timings.inserts or 0)) end @@ -159,9 +186,10 @@ local function run_benchmark(name, num_functions, vars_per_function, changes) log(string.format(" Mappings: %d", #mappings)) log( string.format( - " Actions: %d (moves=%d, updates=%d, deletes=%d, inserts=%d)", + " Actions: %d (moves=%d, renames=%d, updates=%d, deletes=%d, inserts=%d)", #actions, move_count, + rename_count, update_count, delete_count, insert_count @@ -194,15 +222,15 @@ local results = {} -- Test cases: (name, functions, vars_per_func, {swaps, updates}) local test_cases = { - { "~100 lines", 10, 5, { swaps = 2, updates = 2 } }, - { "~250 lines", 25, 5, { swaps = 5, updates = 5 } }, - { "~500 lines", 50, 5, { swaps = 10, updates = 10 } }, - { "~750 lines", 75, 5, { swaps = 15, updates = 15 } }, - { "~1000 lines", 100, 5, { swaps = 20, updates = 20 } }, - { "~2500 lines", 280, 5, { swaps = 50, updates = 50 } }, - { "~5000 lines", 560, 5, { swaps = 100, updates = 100 } }, - { "~7500 lines", 840, 5, { swaps = 150, updates = 150 } }, - { "~10000 lines", 1120, 5, { swaps = 200, updates = 200 } }, + { "~100 lines", 10, 5, { swaps = 2, updates = 2, renames = 2 } }, + { "~250 lines", 25, 5, { swaps = 5, updates = 5, renames = 5 } }, + { "~500 lines", 50, 5, { swaps = 10, updates = 10, renames = 10 } }, + { "~750 lines", 75, 5, { swaps = 15, updates = 15, renames = 15 } }, + { "~1000 lines", 100, 5, { swaps = 20, updates = 20, renames = 20 } }, + { "~2500 lines", 280, 5, { swaps = 50, updates = 50, renames = 50 } }, + { "~5000 lines", 560, 5, { swaps = 100, updates = 100, renames = 100 } }, + { "~7500 lines", 840, 5, { swaps = 150, updates = 150, renames = 150 } }, + { "~10000 lines", 1120, 5, { swaps = 200, updates = 200, renames = 200 } }, } for _, tc in ipairs(test_cases) do From e07fd8ae543cc3cce217b1c08da804a30369c676 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Mon, 9 Feb 2026 11:35:43 +0530 Subject: [PATCH 05/48] feat(core): normalizing internal actions - normalizing each actions with build_action function - which carries type, kind, src_node, dst_node, src_range, dst_range, and other temporary compatibility fields: nodes, target so the engine dont explode right now --- lua/diffmantic/core/actions.lua | 56 +++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/lua/diffmantic/core/actions.lua b/lua/diffmantic/core/actions.lua index c2ce27d..0ffb50d 100644 --- a/lua/diffmantic/core/actions.lua +++ b/lua/diffmantic/core/actions.lua @@ -16,6 +16,39 @@ local function range_metadata(node) } end +local function build_action(action_type, src_node, dst_node, extra) + local src_range = range_metadata(src_node) + local dst_range = range_metadata(dst_node) + + local action = { + type = action_type, + kind = action_type, + node = src_node or dst_node, + target = (src_node and dst_node) and dst_node or nil, + src_node = src_node, + dst_node = dst_node, + src_range = src_range, + dst_range = dst_range, + lines = { + from_line = src_range and src_range.start_line or nil, + to_line = dst_range and dst_range.start_line or nil, + }, + } + + if extra then + for key, value in pairs(extra) do + action[key] = value + end + end + + return action +end + + local function action_node_type(action) + local node = action.src_node or action.dst_node + return node and node:type() or nil + end + -- Generate edit actions from node mappings -- Actions describe what changed: insert, delete, update, move, rename function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, opts) @@ -51,8 +84,10 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end for _, action in ipairs(actions_list) do - if action.type == "update" and action.node and action.target then - local leaf_changes = semantic.find_leaf_changes(action.node, action.target, src_buf, dst_buf) + local src_node = action.src_node or action.node + local dst_node = action.dst_node or action.target + if action.type == "update" and src_node and dst_node then + local leaf_changes = semantic.find_leaf_changes(src_node, dst_node, src_buf, dst_buf) local rename_pairs = {} for _, change in ipairs(leaf_changes) do if semantic.is_rename_identifier(change.src_node) or semantic.is_rename_identifier(change.dst_node) then @@ -89,19 +124,14 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op if not seen[key] then seen[key] = true - table.insert(renames, { - type = "rename", - node = src_node, - target = dst_node, + table.insert(renames, build_action("rename", src_node, dst_node, { from = change.src_text, to = change.dst_text, - src_range = range_metadata(src_node), - dst_range = range_metadata(dst_node), context = { src_parent_type = action.node and action.node:type() or nil, dst_parent_type = action.target and action.target:type() or nil, }, - }) + })) end end end @@ -255,7 +285,7 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op if nodes_with_changes[m.src] and significant_types[s.type] then if not src_has_updated_sig_ancestor[m.src] then - table.insert(actions, { type = "update", node = s.node, target = d.node }) + table.insert(actions, build_action("update", s.node, d.node)) end end end @@ -342,7 +372,7 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op local s = src_info[pair.src_id] local d = dst_info[pair.dst_id] if s and d then - table.insert(actions, { type = "move", node = s.node, target = d.node }) + table.insert(actions, build_action("move", s.node, d.node)) end end end @@ -354,7 +384,7 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op for id, info in pairs(src_info) do if not src_to_dst[id] and significant_types[info.type] then if not src_has_unmapped_sig_ancestor[id] then - table.insert(actions, { type = "delete", node = info.node }) + table.insert(actions, build_action("delete", info.node, nil)) end end end @@ -365,7 +395,7 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op for id, info in pairs(dst_info) do if not dst_to_src[id] and significant_types[info.type] then if not dst_has_unmapped_sig_ancestor[id] then - table.insert(actions, { type = "insert", node = info.node }) + table.insert(actions, build_action("insert", nil, info.node)) end end end From d5fc6528be05e88919e2d49f019e0aeaabaa0c65 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Mon, 9 Feb 2026 18:27:24 +0530 Subject: [PATCH 06/48] feat(core): added internal action summaries and count in actions.lua - now gives internal summaries of actions - this includes, summary.counts, summary.moves, summary.renames, summary.inserts, summary.deletes, summary.updates - from_line and to_line, is include in move summary - now core.generate_actions(...) returns actions, timings, summary --- lua/diffmantic/core/actions.lua | 73 ++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/lua/diffmantic/core/actions.lua b/lua/diffmantic/core/actions.lua index 0ffb50d..c0e3ea8 100644 --- a/lua/diffmantic/core/actions.lua +++ b/lua/diffmantic/core/actions.lua @@ -44,11 +44,78 @@ local function build_action(action_type, src_node, dst_node, extra) return action end +local function build_summary(actions_list) + local summary = { + counts = { + move = 0, + rename = 0, + update = 0, + insert = 0, + delete = 0, + total = #actions_list, + }, + moves = {}, + renames = {}, + updates = {}, + inserts = {}, + deletes = {}, + } + local function action_node_type(action) local node = action.src_node or action.dst_node return node and node:type() or nil end + for _, action in ipairs(actions_list) do + local t = action.type + if summary.counts[t] ~= nil then + summary.counts[t] = summary.counts[t] + 1 + end + + if t == "move" then + table.insert(summary.moves, { + node_type = action_node_type(action), + from_line = action.lines and action.lines.from_line or nil, + to_line = action.lines and action.lines.to_line or nil, + src_range = action.src_range, + dst_range = action.dst_range, + }) + elseif t == "rename" then + table.insert(summary.renames, { + node_type = action_node_type(action), + from = action.from, + to = action.to, + from_line = action.lines and action.lines.from_line or nil, + to_line = action.lines and action.lines.to_line or nil, + src_range = action.src_range, + dst_range = action.dst_range, + }) + elseif t == "update" then + table.insert(summary.updates, { + node_type = action_node_type(action), + from_line = action.lines and action.lines.from_line or nil, + to_line = action.lines and action.lines.to_line or nil, + src_range = action.src_range, + dst_range = action.dst_range, + }) + elseif t == "insert" then + table.insert(summary.inserts, { + node_type = action_node_type(action), + line = action.lines and action.lines.to_line or nil, + dst_range = action.dst_range, + }) + elseif t == "delete" then + table.insert(summary.deletes, { + node_type = action_node_type(action), + line = action.lines and action.lines.from_line or nil, + src_range = action.src_range, + }) + end + end + + return summary +end + -- Generate edit actions from node mappings -- Actions describe what changed: insert, delete, update, move, rename function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, opts) @@ -409,7 +476,11 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op emit_rename_actions(actions) stop_timer(rename_start, "renames") - return actions, timings + local summary_start = start_timer() + local summary = build_summary(actions) + stop_timer(summary_start, "summary") + + return actions, timings, summary end return M From c2eea439ab59066d57fe23c079285ab8b9430fa0 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Tue, 10 Feb 2026 13:52:29 +0530 Subject: [PATCH 07/48] feat(core): added query files to support languages via tree-sitter instead of hard coding the language node types will use query files to get the node type and use them (learned the hard way) --- queries/c/diffmantic.scm | 30 ++++++++++++++++++++++++++++++ queries/cpp/diffmantic.scm | 26 ++++++++++++++++++++++++++ queries/fallback.scm | 1 + queries/go/diffmantic.scm | 28 ++++++++++++++++++++++++++++ queries/javascript/diffmantic.scm | 23 +++++++++++++++++++++++ queries/lua/diffmantic.scm | 22 ++++++++++++++++++++++ queries/python/diffmantic.scm | 17 +++++++++++++++++ queries/typescript/diffmantic.scm | 23 +++++++++++++++++++++++ 8 files changed, 170 insertions(+) create mode 100644 queries/c/diffmantic.scm create mode 100644 queries/cpp/diffmantic.scm create mode 100644 queries/fallback.scm create mode 100644 queries/go/diffmantic.scm create mode 100644 queries/javascript/diffmantic.scm create mode 100644 queries/lua/diffmantic.scm create mode 100644 queries/python/diffmantic.scm create mode 100644 queries/typescript/diffmantic.scm diff --git a/queries/c/diffmantic.scm b/queries/c/diffmantic.scm new file mode 100644 index 0000000..66b7a44 --- /dev/null +++ b/queries/c/diffmantic.scm @@ -0,0 +1,30 @@ +(function_definition + declarator: (function_declarator + declarator: (identifier) @diff.function.name) + body: (compound_statement) @diff.function.body) @diff.function.outer + +(struct_specifier + name: (type_identifier) @diff.class.name + body: (field_declaration_list) @diff.class.body) @diff.class.outer + +(union_specifier + name: (type_identifier) @diff.class.name + body: (field_declaration_list) @diff.class.body) @diff.class.outer + +(enum_specifier + name: (type_identifier) @diff.class.name + body: (enumerator_list) @diff.class.body) @diff.class.outer + +(init_declarator + declarator: [(identifier) (field_identifier)] @diff.variable.name) @diff.variable.outer + +(assignment_expression + left: (_) @diff.assignment.lhs + right: (_) @diff.assignment.rhs) @diff.assignment.outer + +(return_statement) @diff.return.outer +(preproc_include) @diff.preproc.outer +(preproc_def) @diff.preproc.outer +(preproc_function_def) @diff.preproc.outer + +[(identifier) (field_identifier) (type_identifier)] @diff.identifier.rename diff --git a/queries/cpp/diffmantic.scm b/queries/cpp/diffmantic.scm new file mode 100644 index 0000000..65caeab --- /dev/null +++ b/queries/cpp/diffmantic.scm @@ -0,0 +1,26 @@ +(function_definition + declarator: (function_declarator + declarator: (identifier) @diff.function.name) + body: (compound_statement) @diff.function.body) @diff.function.outer + +(class_specifier + name: (type_identifier) @diff.class.name + body: (field_declaration_list) @diff.class.body) @diff.class.outer + +(struct_specifier + name: (type_identifier) @diff.class.name + body: (field_declaration_list) @diff.class.body) @diff.class.outer + +(init_declarator + declarator: [(identifier) (field_identifier)] @diff.variable.name) @diff.variable.outer + +(assignment_expression + left: (_) @diff.assignment.lhs + right: (_) @diff.assignment.rhs) @diff.assignment.outer + +(return_statement) @diff.return.outer +(preproc_include) @diff.preproc.outer +(preproc_def) @diff.preproc.outer +(preproc_function_def) @diff.preproc.outer + +[(identifier) (field_identifier) (type_identifier)] @diff.identifier.rename diff --git a/queries/fallback.scm b/queries/fallback.scm new file mode 100644 index 0000000..506932f --- /dev/null +++ b/queries/fallback.scm @@ -0,0 +1 @@ +((_) @diff.fallback.node) diff --git a/queries/go/diffmantic.scm b/queries/go/diffmantic.scm new file mode 100644 index 0000000..9a7adf3 --- /dev/null +++ b/queries/go/diffmantic.scm @@ -0,0 +1,28 @@ +(function_declaration + name: (identifier) @diff.function.name + body: (block) @diff.function.body) @diff.function.outer + +(method_declaration + name: (field_identifier) @diff.function.name + body: (block) @diff.function.body) @diff.function.outer + +(type_declaration + (type_spec + name: (type_identifier) @diff.class.name + type: (struct_type) @diff.class.body)) @diff.class.outer + +(var_declaration + (var_spec + name: (identifier) @diff.variable.name)) @diff.variable.outer + +(short_var_declaration + left: (expression_list (identifier) @diff.variable.name)) @diff.variable.outer + +(assignment_statement + left: (_) @diff.assignment.lhs + right: (_) @diff.assignment.rhs) @diff.assignment.outer + +(import_declaration) @diff.import.outer +(return_statement) @diff.return.outer + +[(identifier) (field_identifier) (type_identifier)] @diff.identifier.rename diff --git a/queries/javascript/diffmantic.scm b/queries/javascript/diffmantic.scm new file mode 100644 index 0000000..2520639 --- /dev/null +++ b/queries/javascript/diffmantic.scm @@ -0,0 +1,23 @@ +(function_declaration + name: (identifier) @diff.function.name + body: (statement_block) @diff.function.body) @diff.function.outer + +(method_definition + name: (property_identifier) @diff.function.name + body: (statement_block) @diff.function.body) @diff.function.outer + +(class_declaration + name: (identifier) @diff.class.name + body: (class_body) @diff.class.body) @diff.class.outer + +(variable_declarator + name: [(identifier) (object_pattern) (array_pattern)] @diff.variable.name) @diff.variable.outer + +(assignment_expression + left: (_) @diff.assignment.lhs + right: (_) @diff.assignment.rhs) @diff.assignment.outer + +(import_statement) @diff.import.outer +(return_statement) @diff.return.outer + +[(identifier) (property_identifier)] @diff.identifier.rename diff --git a/queries/lua/diffmantic.scm b/queries/lua/diffmantic.scm new file mode 100644 index 0000000..1650921 --- /dev/null +++ b/queries/lua/diffmantic.scm @@ -0,0 +1,22 @@ +(function_declaration + name: [(identifier) (dot_index_expression) (method_index_expression)] @diff.function.name + body: (block) @diff.function.body) @diff.function.outer + +(variable_declaration + (assignment_statement + (variable_list name: (identifier) @diff.function.name) + (expression_list value: (function_definition + body: (block) @diff.function.body)))) @diff.function.outer + +(variable_declaration + (assignment_statement + (variable_list name: (_) @diff.variable.name) + (expression_list))) @diff.variable.outer + +(assignment_statement + (variable_list) @diff.assignment.lhs + (expression_list) @diff.assignment.rhs) @diff.assignment.outer + +(return_statement) @diff.return.outer + +(identifier) @diff.identifier.rename diff --git a/queries/python/diffmantic.scm b/queries/python/diffmantic.scm new file mode 100644 index 0000000..3a480ad --- /dev/null +++ b/queries/python/diffmantic.scm @@ -0,0 +1,17 @@ +(function_definition + name: (identifier) @diff.function.name + body: (block) @diff.function.body) @diff.function.outer + +(class_definition + name: (identifier) @diff.class.name + body: (block) @diff.class.body) @diff.class.outer + +(assignment + left: (_) @diff.assignment.lhs + right: (_) @diff.assignment.rhs) @diff.assignment.outer + +(import_statement) @diff.import.outer +(import_from_statement) @diff.import.outer +(return_statement) @diff.return.outer + +(identifier) @diff.identifier.rename diff --git a/queries/typescript/diffmantic.scm b/queries/typescript/diffmantic.scm new file mode 100644 index 0000000..2b35bad --- /dev/null +++ b/queries/typescript/diffmantic.scm @@ -0,0 +1,23 @@ +(function_declaration + name: (identifier) @diff.function.name + body: (statement_block) @diff.function.body) @diff.function.outer + +(method_definition + name: (property_identifier) @diff.function.name + body: (statement_block) @diff.function.body) @diff.function.outer + +(class_declaration + name: (type_identifier) @diff.class.name + body: (class_body) @diff.class.body) @diff.class.outer + +(variable_declarator + name: [(identifier) (object_pattern) (array_pattern)] @diff.variable.name) @diff.variable.outer + +(assignment_expression + left: (_) @diff.assignment.lhs + right: (_) @diff.assignment.rhs) @diff.assignment.outer + +(import_statement) @diff.import.outer +(return_statement) @diff.return.outer + +[(identifier) (property_identifier) (type_identifier)] @diff.identifier.rename From db69c7f9c785c853c4b0fd50421f1ed89a8a6834 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Tue, 10 Feb 2026 17:32:26 +0530 Subject: [PATCH 08/48] feat(core): using query roles for semantic and action classification using the query files now to detect the actions, but now everything which is not mapped together is shown inserted/deleted will need to fix that... somehow --- lua/diffmantic/core/actions.lua | 73 ++++++++++++--- lua/diffmantic/core/bottom_up.lua | 47 ++++++++-- lua/diffmantic/core/roles.lua | 144 ++++++++++++++++++++++++++++++ lua/diffmantic/core/semantic.lua | 7 +- 4 files changed, 254 insertions(+), 17 deletions(-) create mode 100644 lua/diffmantic/core/roles.lua diff --git a/lua/diffmantic/core/actions.lua b/lua/diffmantic/core/actions.lua index c0e3ea8..e86c940 100644 --- a/lua/diffmantic/core/actions.lua +++ b/lua/diffmantic/core/actions.lua @@ -1,5 +1,6 @@ local M = {} local semantic = require("diffmantic.core.semantic") +local roles = require("diffmantic.core.roles") local function range_metadata(node) if not node then @@ -129,6 +130,9 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end end + local src_role_index = nil + local dst_role_index = nil + local function start_timer() if not hrtime then return nil @@ -157,7 +161,10 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op local leaf_changes = semantic.find_leaf_changes(src_node, dst_node, src_buf, dst_buf) local rename_pairs = {} for _, change in ipairs(leaf_changes) do - if semantic.is_rename_identifier(change.src_node) or semantic.is_rename_identifier(change.dst_node) then + if + semantic.is_rename_identifier(change.src_node, src_role_index) + or semantic.is_rename_identifier(change.dst_node, dst_role_index) + then rename_pairs[change.src_text] = change.dst_text end end @@ -180,7 +187,8 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op local src_node = change.src_node local dst_node = change.dst_node if src_node and dst_node and change.src_text ~= change.dst_text then - local is_rename = semantic.is_rename_identifier(src_node) or semantic.is_rename_identifier(dst_node) + local is_rename = semantic.is_rename_identifier(src_node, src_role_index) + or semantic.is_rename_identifier(dst_node, dst_role_index) if is_rename then local key = table.concat({ tostring(src_node:id()), @@ -220,6 +228,15 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op dst_to_src[m.dst] = m.src end + local roles_start = start_timer() + local src_buf = opts and opts.src_buf or nil + local dst_buf = opts and opts.dst_buf or nil + if src_buf and dst_buf then + src_role_index = roles.build_index(src_root, src_buf) + dst_role_index = roles.build_index(dst_root, dst_buf) + end + stop_timer(roles_start, "roles") + local significant_types = { function_declaration = true, variable_declaration = true, @@ -264,6 +281,39 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op struct_specifier = true, } + local function has_kind(node, index, kind) + return index and roles.has_kind(node, index, kind) or false + end + + local function is_significant(info, index) + local node = info.node + if has_kind(node, index, "function") + or has_kind(node, index, "class") + or has_kind(node, index, "variable") + or has_kind(node, index, "assignment") + or has_kind(node, index, "import") + or has_kind(node, index, "return") + or has_kind(node, index, "preproc") + then + return true + end + return significant_types[info.type] or false + end + + local function is_transparent_update_ancestor(info, index) + if has_kind(info.node, index, "class") then + return true + end + return transparent_update_ancestors[info.type] or false + end + + local function is_movable(info, index) + if has_kind(info.node, index, "function") or has_kind(info.node, index, "class") then + return true + end + return movable_types[info.type] or false + end + -- Helper: check if node or any descendant has different content local function has_content_change(src_node, dst_node) local src_info_data = src_info[src_node:id()] @@ -292,7 +342,7 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op local p_id = current:id() local p_info = src_info[p_id] if p_info then - if not src_to_dst[p_id] and significant_types[p_info.type] then + if not src_to_dst[p_id] and is_significant(p_info, src_role_index) then src_has_unmapped_sig_ancestor[id] = true break end @@ -311,7 +361,7 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op local p_id = current:id() local p_info = dst_info[p_id] if p_info then - if not dst_to_src[p_id] and significant_types[p_info.type] then + if not dst_to_src[p_id] and is_significant(p_info, dst_role_index) then dst_has_unmapped_sig_ancestor[id] = true break end @@ -330,9 +380,10 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op local p_id = current:id() local p_info = src_info[p_id] if p_info then - if nodes_with_changes[p_id] - and significant_types[p_info.type] - and not transparent_update_ancestors[p_info.type] + if + nodes_with_changes[p_id] + and is_significant(p_info, src_role_index) + and not is_transparent_update_ancestor(p_info, src_role_index) then src_has_updated_sig_ancestor[id] = true break @@ -350,7 +401,7 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op for _, m in ipairs(mappings) do local s, d = src_info[m.src], dst_info[m.dst] - if nodes_with_changes[m.src] and significant_types[s.type] then + if nodes_with_changes[m.src] and is_significant(s, src_role_index) then if not src_has_updated_sig_ancestor[m.src] then table.insert(actions, build_action("update", s.node, d.node)) end @@ -364,7 +415,7 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op local movable_pairs = {} for _, m in ipairs(mappings) do local s = src_info[m.src] - if s and movable_types[s.type] then + if s and is_movable(s, src_role_index) then local src_parent_is_root = s.parent and s.parent:id() == src_root:id() local d = dst_info[m.dst] local dst_parent_is_root = d and d.parent and d.parent:id() == dst_root:id() @@ -449,7 +500,7 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op -- DELETES: unmapped source nodes local deletes_start = start_timer() for id, info in pairs(src_info) do - if not src_to_dst[id] and significant_types[info.type] then + if not src_to_dst[id] and is_significant(info, src_role_index) then if not src_has_unmapped_sig_ancestor[id] then table.insert(actions, build_action("delete", info.node, nil)) end @@ -460,7 +511,7 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op -- INSERTS: unmapped destination nodes local inserts_start = start_timer() for id, info in pairs(dst_info) do - if not dst_to_src[id] and significant_types[info.type] then + if not dst_to_src[id] and is_significant(info, dst_role_index) then if not dst_has_unmapped_sig_ancestor[id] then table.insert(actions, build_action("insert", nil, info.node)) end diff --git a/lua/diffmantic/core/bottom_up.lua b/lua/diffmantic/core/bottom_up.lua index 94caf90..1eef6ab 100644 --- a/lua/diffmantic/core/bottom_up.lua +++ b/lua/diffmantic/core/bottom_up.lua @@ -1,8 +1,12 @@ local M = {} +local roles = require("diffmantic.core.roles") -- Bottom-up matching: match nodes from leaves up, using parent mappings -- Tries to match nodes with the same type and label, and optionally name function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src_buf, dst_buf) + local src_role_index = roles.build_index(src_root, src_buf) + local dst_role_index = roles.build_index(dst_root, dst_buf) + -- Build O(1) lookup tables local src_to_dst = {} local dst_to_src = {} @@ -12,7 +16,22 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src end -- Get the name of a declaration node (function or variable) - local function get_declaration_name(node, bufnr) + local function get_declaration_name(node, bufnr, role_index) + local function_name = roles.get_kind_name_text(node, role_index, bufnr, "function") + if function_name and #function_name > 0 then + return function_name + end + + local class_name = roles.get_kind_name_text(node, role_index, bufnr, "class") + if class_name and #class_name > 0 then + return class_name + end + + local variable_name = roles.get_kind_name_text(node, role_index, bufnr, "variable") + if variable_name and #variable_name > 0 then + return variable_name + end + if node:type() == "class_specifier" or node:type() == "struct_specifier" or node:type() == "enum_specifier" or node:type() == "union_specifier" then local name_node = node:field("name")[1] or node:field("tag")[1] if name_node then @@ -240,6 +259,24 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src struct_specifier = true, } + local function is_identifier_type(info, role_index) + if roles.has_kind(info.node, role_index, "function") + or roles.has_kind(info.node, role_index, "class") + or roles.has_kind(info.node, role_index, "variable") + or roles.has_kind(info.node, role_index, "assignment") + then + return true + end + return identifier_types[info.type] or false + end + + local function is_unique_structure_fallback_type(info, role_index) + if roles.has_kind(info.node, role_index, "function") or roles.has_kind(info.node, role_index, "class") then + return true + end + return unique_structure_fallback_types[info.type] or false + end + -- Try to match unmapped nodes whose parent is mapped for id, info in pairs(src_info) do if not src_to_dst[id] then @@ -277,8 +314,8 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src end local src_name = nil - if identifier_types[info.type] then - src_name = get_declaration_name(info.node, src_buf) + if is_identifier_type(info, src_role_index) then + src_name = get_declaration_name(info.node, src_buf, src_role_index) end local src_value_hash = get_assignment_value_hash(info.node, src_info) @@ -290,7 +327,7 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src local d_info = dst_info[cand:id()] if d_info.type == info.type and d_info.label == info.label then if src_name then - local dst_name = get_declaration_name(cand, dst_buf) + local dst_name = get_declaration_name(cand, dst_buf, dst_role_index) if src_name == dst_name then table.insert(mappings, { src = id, dst = cand:id() }) src_to_dst[id] = cand:id() @@ -330,7 +367,7 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src table.insert(mappings, { src = id, dst = rename_candidate }) src_to_dst[id] = rename_candidate dst_to_src[rename_candidate] = id - elseif not src_to_dst[id] and unique_structure_fallback_types[info.type] then + elseif not src_to_dst[id] and is_unique_structure_fallback_type(info, src_role_index) then if #structure_candidates == 1 then local candidate_id = structure_candidates[1] table.insert(mappings, { src = id, dst = candidate_id }) diff --git a/lua/diffmantic/core/roles.lua b/lua/diffmantic/core/roles.lua new file mode 100644 index 0000000..e32e362 --- /dev/null +++ b/lua/diffmantic/core/roles.lua @@ -0,0 +1,144 @@ +local M = {} + +local CAPTURE_BY_KIND = { + ["function"] = { + "diff.function.outer", + "diff.function.name", + "diff.function.body", + }, + ["class"] = { + "diff.class.outer", + "diff.class.name", + "diff.class.body", + }, + ["variable"] = { + "diff.variable.outer", + "diff.variable.name", + }, + ["assignment"] = { + "diff.assignment.outer", + "diff.assignment.lhs", + "diff.assignment.rhs", + }, + ["import"] = { "diff.import.outer" }, + ["return"] = { "diff.return.outer" }, + ["preproc"] = { "diff.preproc.outer" }, + ["rename_identifier"] = { "diff.identifier.rename" }, +} + +local FALLBACK_QUERY = "((_) @diff.fallback.node)" + +local function add_capture(index, capture, node) + local id = node:id() + index.by_node[id] = index.by_node[id] or {} + index.by_node[id][capture] = true + + index.by_capture[capture] = index.by_capture[capture] or {} + index.by_capture[capture][id] = node +end + +local function resolve_lang(bufnr) + local ft = vim.bo[bufnr].filetype + if not ft or ft == "" then + return nil + end + return vim.treesitter.language.get_lang(ft) or ft +end + +local function get_query(lang) + local ok, query = pcall(vim.treesitter.query.get, lang, "diffmantic") + if ok and query then + return query + end + + local parsed_ok, parsed = pcall(vim.treesitter.query.parse, lang, FALLBACK_QUERY) + if parsed_ok and parsed then + return parsed + end + + return nil +end + +function M.build_index(root, bufnr) + local lang = resolve_lang(bufnr) + if not lang then + return nil + end + + local query = get_query(lang) + if not query then + return nil + end + + local index = { + lang = lang, + by_node = {}, + by_capture = {}, + } + + for id, node in query:iter_captures(root, bufnr, 0, -1) do + local capture = query.captures[id] + if capture then + add_capture(index, capture, node) + end + end + + return index +end + +function M.has_capture(node, index, capture) + if not node or not index then + return false + end + local by_node = index.by_node[node:id()] + return by_node and by_node[capture] or false +end + +function M.find_descendant_with_capture(node, index, capture) + if not node or not index then + return nil + end + + local by_capture = index.by_capture[capture] + if not by_capture then + return nil + end + + for _, captured in pairs(by_capture) do + if node:equal(captured) or node:child_with_descendant(captured) then + return captured + end + end + + return nil +end + +function M.has_kind(node, index, kind) + local captures = CAPTURE_BY_KIND[kind] + if not captures then + return false + end + + for _, capture in ipairs(captures) do + if M.has_capture(node, index, capture) or M.find_descendant_with_capture(node, index, capture) then + return true + end + end + + return false +end + +function M.get_kind_name_node(node, index, kind) + local capture = string.format("diff.%s.name", kind) + return M.find_descendant_with_capture(node, index, capture) +end + +function M.get_kind_name_text(node, index, bufnr, kind) + local name_node = M.get_kind_name_node(node, index, kind) + if not name_node then + return nil + end + return vim.treesitter.get_node_text(name_node, bufnr) +end + +return M diff --git a/lua/diffmantic/core/semantic.lua b/lua/diffmantic/core/semantic.lua index cc30c50..b9bbf30 100644 --- a/lua/diffmantic/core/semantic.lua +++ b/lua/diffmantic/core/semantic.lua @@ -1,4 +1,5 @@ local M = {} +local roles = require("diffmantic.core.roles") -- Leaf-level diffs for small updates; otherwise return empty. function M.find_leaf_changes(src_node, dst_node, src_buf, dst_buf) @@ -78,11 +79,15 @@ function M.node_in_field(parent, field_name, node) return false end -function M.is_rename_identifier(node) +function M.is_rename_identifier(node, role_index) if not node then return false end + if role_index and roles.has_kind(node, role_index, "rename_identifier") then + return true + end + local node_type = node:type() if node_type ~= "identifier" and node_type ~= "type_identifier" and node_type ~= "field_identifier" then return false From 7330f96993f339b33e7978d2bc338dde6f490740 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Tue, 10 Feb 2026 17:40:07 +0530 Subject: [PATCH 09/48] fix: highlighting anything not tagged as insert/delete i was having the highlight based on both if it was mapped and if it had any descendant which was mapped to making everything insert/delete --- lua/diffmantic/core/roles.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/diffmantic/core/roles.lua b/lua/diffmantic/core/roles.lua index e32e362..2f38f9f 100644 --- a/lua/diffmantic/core/roles.lua +++ b/lua/diffmantic/core/roles.lua @@ -120,7 +120,7 @@ function M.has_kind(node, index, kind) end for _, capture in ipairs(captures) do - if M.has_capture(node, index, capture) or M.find_descendant_with_capture(node, index, capture) then + if M.has_capture(node, index, capture) then return true end end From 16ed432ff763376c181d8e87da9ef2043abf5ae5 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Wed, 11 Feb 2026 17:15:39 +0530 Subject: [PATCH 10/48] feat(core): sorting nodes by position for deterministic ordering added a stable ordering for nodes at each height level in top down matching. now nodes are sorted by their source position(start row/col, end row/col) and then by type, this prevents non-deterministic matching regardless of tree traversal order --- lua/diffmantic/core/top_down.lua | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lua/diffmantic/core/top_down.lua b/lua/diffmantic/core/top_down.lua index 727c005..9b99f2f 100644 --- a/lua/diffmantic/core/top_down.lua +++ b/lua/diffmantic/core/top_down.lua @@ -2,6 +2,29 @@ local ts_utils = require("diffmantic.treesitter") local M = {} +local function node_key(node) + local sr, sc, er, ec = node:range() + return sr, sc, er, ec +end + +local function compare_node_order(a, b) + local asr, asc, aer, aec = node_key(a.node) + local bsr, bsc, ber, bec = node_key(b.node) + 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.type < b.type +end + -- Top-down matching: match nodes from the top of the tree downwards -- Matches nodes with the same hash at each height level function M.top_down_match(src_root, dst_root, src_buf, dst_buf) @@ -21,6 +44,11 @@ function M.top_down_match(src_root, dst_root, src_buf, dst_buf) end table.insert(by_height[data.height], data) end + + for _, nodes in pairs(by_height) do + table.sort(nodes, compare_node_order) + end + return by_height end local src_by_height = get_nodes_by_height(src_info) From cc9119696b82c5c6e37e0076d5410b4129026cd6 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Wed, 11 Feb 2026 17:27:59 +0530 Subject: [PATCH 11/48] feat(core): sorting nodes ordering deterministically in bottom-up and recovery passes similar to top-down ordering position deterministically, providing consistent order based on their source position. Bottom up pass sorts nodes by heights then position where recovery only sorts by position only --- lua/diffmantic/core/bottom_up.lua | 39 ++++++++++++++++++++++++++++++- lua/diffmantic/core/recovery.lua | 34 ++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/lua/diffmantic/core/bottom_up.lua b/lua/diffmantic/core/bottom_up.lua index 1eef6ab..8f4ff86 100644 --- a/lua/diffmantic/core/bottom_up.lua +++ b/lua/diffmantic/core/bottom_up.lua @@ -1,6 +1,29 @@ local M = {} local roles = require("diffmantic.core.roles") +local function node_key(node) + local sr, sc, er, ec = 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.node) + local bsr, bsc, ber, bec = node_key(b_info.node) + 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 + -- Bottom-up matching: match nodes from leaves up, using parent mappings -- Tries to match nodes with the same type and label, and optionally name function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src_buf, dst_buf) @@ -277,8 +300,22 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src return unique_structure_fallback_types[info.type] or false end + local src_ids = {} + for id in pairs(src_info) do + table.insert(src_ids, id) + end + table.sort(src_ids, function(a, b) + local ah = src_info[a].height or 0 + local bh = src_info[b].height or 0 + if ah == bh then + return compare_info_order(src_info[a], src_info[b]) + end + return ah < bh + end) + -- Try to match unmapped nodes whose parent is mapped - for id, info in pairs(src_info) do + for _, id in ipairs(src_ids) do + local info = src_info[id] if not src_to_dst[id] then local parent = info.parent local parent_mapped = false diff --git a/lua/diffmantic/core/recovery.lua b/lua/diffmantic/core/recovery.lua index b55d4b2..08a12f5 100644 --- a/lua/diffmantic/core/recovery.lua +++ b/lua/diffmantic/core/recovery.lua @@ -1,5 +1,28 @@ local M = {} +local function node_key(node) + local sr, sc, er, ec = 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.node) + local bsr, bsc, ber, bec = node_key(b_info.node) + 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) -- Build O(1) lookup tables @@ -140,7 +163,16 @@ function M.recovery_match(src_root, dst_root, mappings, src_info, dst_info, src_ end -- Apply recovery to all mapped nodes - for id, info in pairs(src_info) do + local src_ids = {} + for id in pairs(src_info) do + table.insert(src_ids, id) + end + table.sort(src_ids, function(a, b) + return compare_info_order(src_info[a], src_info[b]) + end) + + for _, id in ipairs(src_ids) do + local info = src_info[id] local dst_id = src_to_dst[id] if dst_id then simple_recovery(info.node, dst_info[dst_id].node) From 913d3eac74a9b080a711e0edc0738d052eeaff8c Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Wed, 11 Feb 2026 19:02:05 +0530 Subject: [PATCH 12/48] feat(core): added refined rename query roles and semantic field checks replaced the broad identifier rename captures with declaration focused captures across different languages, this tightens semantic rename check for field nodes to only accept name/key positions, this lets us reduce noisy rename detections from normal identifier usages and improves rename action quality --- lua/diffmantic/core/semantic.lua | 4 +++- queries/c/diffmantic.scm | 16 +++++++++++++++- queries/cpp/diffmantic.scm | 13 ++++++++++++- queries/go/diffmantic.scm | 17 ++++++++++++++++- queries/javascript/diffmantic.scm | 12 +++++++++++- queries/lua/diffmantic.scm | 15 ++++++++++++++- queries/python/diffmantic.scm | 9 ++++++++- queries/typescript/diffmantic.scm | 12 +++++++++++- 8 files changed, 90 insertions(+), 8 deletions(-) diff --git a/lua/diffmantic/core/semantic.lua b/lua/diffmantic/core/semantic.lua index b9bbf30..c26f075 100644 --- a/lua/diffmantic/core/semantic.lua +++ b/lua/diffmantic/core/semantic.lua @@ -141,7 +141,9 @@ function M.is_rename_identifier(node, role_index) return true end if ptype == "field" then - return true + if M.node_in_field(parent, "name", current) or M.node_in_field(parent, "key", current) then + return true + end end current = parent parent = parent:parent() diff --git a/queries/c/diffmantic.scm b/queries/c/diffmantic.scm index 66b7a44..06890e6 100644 --- a/queries/c/diffmantic.scm +++ b/queries/c/diffmantic.scm @@ -27,4 +27,18 @@ (preproc_def) @diff.preproc.outer (preproc_function_def) @diff.preproc.outer -[(identifier) (field_identifier) (type_identifier)] @diff.identifier.rename +(function_definition + declarator: (function_declarator + declarator: (identifier) @diff.identifier.rename)) + +(struct_specifier + name: (type_identifier) @diff.identifier.rename) + +(union_specifier + name: (type_identifier) @diff.identifier.rename) + +(enum_specifier + name: (type_identifier) @diff.identifier.rename) + +(init_declarator + declarator: [(identifier) (field_identifier)] @diff.identifier.rename) diff --git a/queries/cpp/diffmantic.scm b/queries/cpp/diffmantic.scm index 65caeab..53e0d96 100644 --- a/queries/cpp/diffmantic.scm +++ b/queries/cpp/diffmantic.scm @@ -23,4 +23,15 @@ (preproc_def) @diff.preproc.outer (preproc_function_def) @diff.preproc.outer -[(identifier) (field_identifier) (type_identifier)] @diff.identifier.rename +(function_definition + declarator: (function_declarator + declarator: (identifier) @diff.identifier.rename)) + +(class_specifier + name: (type_identifier) @diff.identifier.rename) + +(struct_specifier + name: (type_identifier) @diff.identifier.rename) + +(init_declarator + declarator: [(identifier) (field_identifier)] @diff.identifier.rename) diff --git a/queries/go/diffmantic.scm b/queries/go/diffmantic.scm index 9a7adf3..933458d 100644 --- a/queries/go/diffmantic.scm +++ b/queries/go/diffmantic.scm @@ -25,4 +25,19 @@ (import_declaration) @diff.import.outer (return_statement) @diff.return.outer -[(identifier) (field_identifier) (type_identifier)] @diff.identifier.rename +(function_declaration + name: (identifier) @diff.identifier.rename) + +(method_declaration + name: (field_identifier) @diff.identifier.rename) + +(type_declaration + (type_spec + name: (type_identifier) @diff.identifier.rename)) + +(var_declaration + (var_spec + name: (identifier) @diff.identifier.rename)) + +(short_var_declaration + left: (expression_list (identifier) @diff.identifier.rename)) diff --git a/queries/javascript/diffmantic.scm b/queries/javascript/diffmantic.scm index 2520639..c3a096d 100644 --- a/queries/javascript/diffmantic.scm +++ b/queries/javascript/diffmantic.scm @@ -20,4 +20,14 @@ (import_statement) @diff.import.outer (return_statement) @diff.return.outer -[(identifier) (property_identifier)] @diff.identifier.rename +(function_declaration + name: (identifier) @diff.identifier.rename) + +(method_definition + name: (property_identifier) @diff.identifier.rename) + +(class_declaration + name: (identifier) @diff.identifier.rename) + +(variable_declarator + name: (identifier) @diff.identifier.rename) diff --git a/queries/lua/diffmantic.scm b/queries/lua/diffmantic.scm index 1650921..5cf06a0 100644 --- a/queries/lua/diffmantic.scm +++ b/queries/lua/diffmantic.scm @@ -19,4 +19,17 @@ (return_statement) @diff.return.outer -(identifier) @diff.identifier.rename +(function_declaration + name: (identifier) @diff.identifier.rename) + +(variable_declaration + (assignment_statement + (variable_list name: (identifier) @diff.identifier.rename) + (expression_list))) + +(assignment_statement + (variable_list name: (identifier) @diff.identifier.rename) + (expression_list)) + +(field + name: (identifier) @diff.identifier.rename) diff --git a/queries/python/diffmantic.scm b/queries/python/diffmantic.scm index 3a480ad..9e84d3e 100644 --- a/queries/python/diffmantic.scm +++ b/queries/python/diffmantic.scm @@ -14,4 +14,11 @@ (import_from_statement) @diff.import.outer (return_statement) @diff.return.outer -(identifier) @diff.identifier.rename +(function_definition + name: (identifier) @diff.identifier.rename) + +(class_definition + name: (identifier) @diff.identifier.rename) + +(assignment + left: (identifier) @diff.identifier.rename) diff --git a/queries/typescript/diffmantic.scm b/queries/typescript/diffmantic.scm index 2b35bad..dff92f1 100644 --- a/queries/typescript/diffmantic.scm +++ b/queries/typescript/diffmantic.scm @@ -20,4 +20,14 @@ (import_statement) @diff.import.outer (return_statement) @diff.return.outer -[(identifier) (property_identifier) (type_identifier)] @diff.identifier.rename +(function_declaration + name: (identifier) @diff.identifier.rename) + +(method_definition + name: (property_identifier) @diff.identifier.rename) + +(class_declaration + name: (type_identifier) @diff.identifier.rename) + +(variable_declarator + name: (identifier) @diff.identifier.rename) From e244f21a050f9952c588b5d348edc9d8c4afd61d Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Wed, 11 Feb 2026 22:13:46 +0530 Subject: [PATCH 13/48] feat(core): moved render payload construction to core engine added a new core/payload.lua to build spans/signs/virt_text from normalized actions wired actions.lua to use payload.lua this keeps ui focused on consuming the payload instead of inferring semantic diffs itself --- lua/diffmantic/core/actions.lua | 373 +++++++++++++++++-- lua/diffmantic/core/payload.lua | 619 ++++++++++++++++++++++++++++++++ 2 files changed, 972 insertions(+), 20 deletions(-) create mode 100644 lua/diffmantic/core/payload.lua diff --git a/lua/diffmantic/core/actions.lua b/lua/diffmantic/core/actions.lua index e86c940..0c01fe4 100644 --- a/lua/diffmantic/core/actions.lua +++ b/lua/diffmantic/core/actions.lua @@ -1,6 +1,7 @@ local M = {} local semantic = require("diffmantic.core.semantic") local roles = require("diffmantic.core.roles") +local payload = require("diffmantic.core.payload") local function range_metadata(node) if not node then @@ -50,6 +51,7 @@ local function build_summary(actions_list) counts = { move = 0, rename = 0, + rename_suppressed = 0, update = 0, insert = 0, delete = 0, @@ -57,6 +59,7 @@ local function build_summary(actions_list) }, moves = {}, renames = {}, + suppressed_renames = {}, updates = {}, inserts = {}, deletes = {}, @@ -82,6 +85,10 @@ local function build_summary(actions_list) dst_range = action.dst_range, }) elseif t == "rename" then + local suppressed_usages = action.context and action.context.suppressed_usages or {} + local suppressed_count = #suppressed_usages + summary.counts.rename_suppressed = summary.counts.rename_suppressed + suppressed_count + table.insert(summary.renames, { node_type = action_node_type(action), from = action.from, @@ -90,7 +97,25 @@ local function build_summary(actions_list) to_line = action.lines and action.lines.to_line or nil, src_range = action.src_range, dst_range = action.dst_range, + suppressed_usage_count = suppressed_count, }) + + for _, usage in ipairs(suppressed_usages) do + table.insert(summary.suppressed_renames, { + from = usage.from, + to = usage.to, + from_line = usage.lines and usage.lines.from_line or nil, + to_line = usage.lines and usage.lines.to_line or nil, + src_range = usage.src_range, + dst_range = usage.dst_range, + suppressed_by = { + from = action.from, + to = action.to, + from_line = action.lines and action.lines.from_line or nil, + to_line = action.lines and action.lines.to_line or nil, + }, + }) + end elseif t == "update" then table.insert(summary.updates, { node_type = action_node_type(action), @@ -180,33 +205,198 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op local function emit_rename_actions(actions_list) local renames = {} local seen = {} + local src_buf = opts and opts.src_buf or nil + local dst_buf = opts and opts.dst_buf or nil + local is_buf_available = src_buf and dst_buf + + local function pair_key(from_text, to_text) + return tostring(from_text) .. "\x1f" .. tostring(to_text) + end + local function push_rename(src_node, dst_node, from_text, to_text, context) + if not src_node or not dst_node or not from_text or not to_text then + return + end + if from_text == to_text then + return + end + local key = table.concat({ + tostring(src_node:id()), + tostring(dst_node:id()), + from_text, + to_text, + }, ":") + if seen[key] then + return + end + seen[key] = true + table.insert(renames, build_action("rename", src_node, dst_node, { + from = from_text, + to = to_text, + context = context, + })) + end + + local function is_decl_rename(src_node, dst_node) + return semantic.is_rename_identifier(src_node, src_role_index) + and semantic.is_rename_identifier(dst_node, dst_role_index) + end + + local seed_pairs = {} + local function add_seed(from_text, to_text) + if from_text and to_text and from_text ~= to_text then + seed_pairs[pair_key(from_text, to_text)] = true + end + end + + local function collect_param_identifiers(node, bufnr) + if not node or not bufnr then + return {} + end + + local parameter_kinds = { + parameters = true, + parameter_list = true, + formal_parameters = true, + } + + local function find_parameter_node(n) + if parameter_kinds[n:type()] then + return n + end + for child in n:iter_children() do + local found = find_parameter_node(child) + if found then + return found + end + end + return nil + end + + local params_root = find_parameter_node(node) + if not params_root then + return {} + end + + local out = {} + local function walk(n) + if n:child_count() == 0 then + local t = n:type() + if t == "identifier" or t == "type_identifier" or t == "field_identifier" or t == "property_identifier" then + local text = vim.treesitter.get_node_text(n, bufnr) + if text and text:match("^[%a_][%w_]*$") then + table.insert(out, { node = n, text = text }) + end + end + return + end + for child in n:iter_children() do + walk(child) + end + end + + walk(params_root) + return out + end + + -- Pass 1a: high-confidence declaration-like rename seeds from semantic leaf changes. for _, action in ipairs(actions_list) do if action.type == "update" and action.semantic and action.semantic.leaf_changes then for _, change in ipairs(action.semantic.leaf_changes) do local src_node = change.src_node local dst_node = change.dst_node - if src_node and dst_node and change.src_text ~= change.dst_text then - local is_rename = semantic.is_rename_identifier(src_node, src_role_index) - or semantic.is_rename_identifier(dst_node, dst_role_index) - if is_rename then - local key = table.concat({ - tostring(src_node:id()), - tostring(dst_node:id()), - change.src_text, - change.dst_text, - }, ":") - - if not seen[key] then - seen[key] = true - table.insert(renames, build_action("rename", src_node, dst_node, { - from = change.src_text, - to = change.dst_text, - context = { + if src_node and dst_node and change.src_text ~= change.dst_text and is_decl_rename(src_node, dst_node) then + add_seed(change.src_text, change.dst_text) + end + end + end + end + + -- Pass 1a.2: positional parameter rename seeds/actions for updated functions. + if is_buf_available then + for _, action in ipairs(actions_list) do + if action.type == "update" and action.src_node and action.dst_node then + local src_params = collect_param_identifiers(action.src_node, src_buf) + local dst_params = collect_param_identifiers(action.dst_node, dst_buf) + if #src_params > 0 and #src_params == #dst_params and #src_params <= 16 then + for i = 1, #src_params do + local s = src_params[i] + local d = dst_params[i] + if s.text ~= d.text then + add_seed(s.text, d.text) + if is_decl_rename(s.node, d.node) then + push_rename(s.node, d.node, s.text, d.text, { src_parent_type = action.node and action.node:type() or nil, dst_parent_type = action.target and action.target:type() or nil, - }, - })) + source = "parameter_positional", + declaration = true, + }) + end + end + end + end + end + end + end + + -- Pass 1b: strict fallback seed collection from mapped leaf identifiers. + if is_buf_available then + local src_to_dst_local = {} + for _, m in ipairs(mappings) do + src_to_dst_local[m.src] = m.dst + end + + local function child_index(node) + local parent = node and node:parent() or nil + if not parent then + return -1 + end + local idx = 0 + for child in parent:iter_children() do + if child:equal(node) then + return idx + end + idx = idx + 1 + end + return -1 + end + + local function is_identifier_node(node) + if not node then + return false + end + local t = node:type() + return t == "identifier" or t == "type_identifier" or t == "field_identifier" or t == "property_identifier" + end + + local function is_identifier_like(text) + return text and text:match("^[%a_][%w_]*$") ~= nil + end + + for _, m in ipairs(mappings) do + local s = src_info[m.src] + local d = dst_info[m.dst] + if s and d and s.node and d.node then + local src_node = s.node + local dst_node = d.node + if src_node:child_count() == 0 and dst_node:child_count() == 0 then + local src_parent = src_node:parent() + local dst_parent = dst_node:parent() + if src_parent and dst_parent and src_to_dst_local[src_parent:id()] == dst_parent:id() then + if src_parent:type() == dst_parent:type() and child_index(src_node) == child_index(dst_node) then + local src_text = vim.treesitter.get_node_text(src_node, src_buf) + local dst_text = vim.treesitter.get_node_text(dst_node, dst_buf) + if src_text and dst_text and src_text ~= dst_text then + if + is_decl_rename(src_node, dst_node) + and is_identifier_node(src_node) + and is_identifier_node(dst_node) + and is_identifier_like(src_text) + and is_identifier_like(dst_text) + then + add_seed(src_text, dst_text) + end + end end end end @@ -214,7 +404,142 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end end + -- Pass 2: emit leaf rename actions gated by seeds (and declaration renames). + for _, action in ipairs(actions_list) do + if action.type == "update" and action.semantic and action.semantic.leaf_changes then + for _, change in ipairs(action.semantic.leaf_changes) do + local src_node = change.src_node + local dst_node = change.dst_node + local src_text = change.src_text + local dst_text = change.dst_text + if src_node and dst_node and src_text and dst_text and src_text ~= dst_text then + local is_decl = is_decl_rename(src_node, dst_node) + local key = pair_key(src_text, dst_text) + if seed_pairs[key] or is_decl then + push_rename(src_node, dst_node, src_text, dst_text, { + src_parent_type = action.node and action.node:type() or nil, + dst_parent_type = action.target and action.target:type() or nil, + declaration = is_decl, + }) + end + end + end + end + end + + -- Pass 3: emit strict mapped-leaf rename actions for seed pairs not present in leaf_changes. + if is_buf_available then + local src_to_dst_local = {} + for _, m in ipairs(mappings) do + src_to_dst_local[m.src] = m.dst + end + + local function child_index(node) + local parent = node and node:parent() or nil + if not parent then + return -1 + end + local idx = 0 + for child in parent:iter_children() do + if child:equal(node) then + return idx + end + idx = idx + 1 + end + return -1 + end + + local function is_identifier_node(node) + if not node then + return false + end + local t = node:type() + return t == "identifier" or t == "type_identifier" or t == "field_identifier" or t == "property_identifier" + end + + local function is_identifier_like(text) + return text and text:match("^[%a_][%w_]*$") ~= nil + end + + for _, m in ipairs(mappings) do + local s = src_info[m.src] + local d = dst_info[m.dst] + if s and d and s.node and d.node then + local src_node = s.node + local dst_node = d.node + if src_node:child_count() == 0 and dst_node:child_count() == 0 then + local src_parent = src_node:parent() + local dst_parent = dst_node:parent() + if src_parent and dst_parent and src_to_dst_local[src_parent:id()] == dst_parent:id() then + if src_parent:type() == dst_parent:type() and child_index(src_node) == child_index(dst_node) then + local src_text = vim.treesitter.get_node_text(src_node, src_buf) + local dst_text = vim.treesitter.get_node_text(dst_node, dst_buf) + if src_text and dst_text and src_text ~= dst_text then + if + is_decl_rename(src_node, dst_node) + and is_identifier_node(src_node) + and is_identifier_node(dst_node) + and is_identifier_like(src_text) + and is_identifier_like(dst_text) + then + local key = pair_key(src_text, dst_text) + if seed_pairs[key] then + push_rename(src_node, dst_node, src_text, dst_text, { + src_parent_type = src_parent:type(), + dst_parent_type = dst_parent:type(), + source = "mapping_seed", + declaration = true, + }) + end + end + end + end + end + end + end + end + end + + -- If a declaration rename exists for a pair, suppress usage-level duplicates for that pair. + local declaration_pairs = {} + for _, rename_action in ipairs(renames) do + if rename_action.context and rename_action.context.declaration then + declaration_pairs[pair_key(rename_action.from, rename_action.to)] = true + end + end + + local suppressed_by_pair = {} + local filtered_renames = {} for _, rename_action in ipairs(renames) do + local key = pair_key(rename_action.from, rename_action.to) + local is_declaration = rename_action.context and rename_action.context.declaration + if not declaration_pairs[key] or is_declaration then + table.insert(filtered_renames, rename_action) + else + suppressed_by_pair[key] = suppressed_by_pair[key] or {} + table.insert(suppressed_by_pair[key], { + from = rename_action.from, + to = rename_action.to, + src_range = rename_action.src_range, + dst_range = rename_action.dst_range, + lines = rename_action.lines, + context = rename_action.context, + }) + end + end + + for _, rename_action in ipairs(filtered_renames) do + if rename_action.context and rename_action.context.declaration then + local key = pair_key(rename_action.from, rename_action.to) + local suppressed_usages = suppressed_by_pair[key] + if suppressed_usages and #suppressed_usages > 0 then + rename_action.context.suppressed_usages = suppressed_usages + rename_action.context.suppressed_usage_count = #suppressed_usages + end + end + end + + for _, rename_action in ipairs(filtered_renames) do table.insert(actions_list, rename_action) end end @@ -253,7 +578,6 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op assignment_statement = true, for_statement = true, while_statement = true, - function_call = true, -- Python class_definition = true, import_statement = true, @@ -527,6 +851,15 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op emit_rename_actions(actions) stop_timer(rename_start, "renames") + local render_start = start_timer() + if src_buf and dst_buf then + payload.enrich(actions, { + src_buf = src_buf, + dst_buf = dst_buf, + }) + end + stop_timer(render_start, "payload") + local summary_start = start_timer() local summary = build_summary(actions) stop_timer(summary_start, "summary") diff --git a/lua/diffmantic/core/payload.lua b/lua/diffmantic/core/payload.lua new file mode 100644 index 0000000..cf66a24 --- /dev/null +++ b/lua/diffmantic/core/payload.lua @@ -0,0 +1,619 @@ +local semantic = require("diffmantic.core.semantic") + +local M = {} +local RENDER_UPDATE_TOKENS = true + +local function node_range(node) + if not node then + return nil + end + local ok, sr, sc, er, ec = pcall(function() + return node:range() + end) + if not ok then + return nil + end + return sr, sc, er, ec +end + +local function inclusive_end_row(sr, er, ec) + if er < sr then + return sr + end + if er == sr then + return sr + end + if ec == 0 then + return er - 1 + end + return er +end + +local function line_text(buf, row) + local lines = vim.api.nvim_buf_get_lines(buf, row, row + 1, false) + return lines[1] or "" +end + +local function append_span(spans, row, start_col, end_col, hl_group, buf) + if row < 0 or end_col <= start_col then + return false + end + + local text = line_text(buf, row) + local safe_start = math.max(start_col, 0) + local safe_end = math.min(end_col, #text) + if safe_end <= safe_start then + return false + end + + local segment = text:sub(safe_start + 1, safe_end) + local first = segment:find("%S") + if not first then + return false + end + local rev_last = segment:reverse():find("%S") + local last = #segment - rev_last + 1 + + table.insert(spans, { + row = row, + start_col = safe_start + first - 1, + end_row = row, + end_col = safe_start + last, + hl_group = hl_group, + }) + return true +end + +local function append_sign(signs, row, text, hl_group) + if row == nil or row < 0 then + return + end + if not text or text == "" then + return + end + table.insert(signs, { + row = row, + col = 0, + text = text, + hl_group = hl_group, + }) +end + +local function append_virt(virt, row, col, text, hl_group, pos) + if row == nil or row < 0 then + return + end + table.insert(virt, { + row = row, + col = col or 0, + text = text, + hl_group = hl_group or "Comment", + pos = pos or "eol", + }) +end + +local function append_node_tokens(spans, buf, node, hl_group, opts) + local sr, sc, er, ec = node_range(node) + if not sr then + return false + end + + local first_line_only = opts and opts.first_line_only or false + local end_row = first_line_only and sr or inclusive_end_row(sr, er, ec) + local highlighted = false + + for row = sr, end_row do + local text = line_text(buf, row) + local start_col = (row == sr) and sc or 0 + local end_col + if first_line_only then + end_col = #text + elseif row == end_row then + if er == sr then + end_col = ec + elseif row == er then + end_col = ec + else + end_col = #text + end + else + end_col = #text + end + highlighted = append_span(spans, row, start_col, end_col, hl_group, buf) or highlighted + end + + return highlighted +end + +local function tokenize_line(text) + local tokens = {} + local i = 1 + local len = #text + while i <= len do + local ch = text:sub(i, i) + if ch:match("%s") then + i = i + 1 + elseif ch:match("[%w_]") then + local j = i + 1 + while j <= len and text:sub(j, j):match("[%w_]") do + j = j + 1 + end + table.insert(tokens, { text = text:sub(i, j - 1), start_col = i, end_col = j - 1 }) + i = j + else + local j = i + 1 + while j <= len and not text:sub(j, j):match("[%w_%s]") do + j = j + 1 + end + table.insert(tokens, { text = text:sub(i, j - 1), start_col = i, end_col = j - 1 }) + i = j + end + end + return tokens +end + +local function tokens_equal(a, b, rename_map) + if a.text == b.text then + return true + end + if rename_map and rename_map[a.text] == b.text then + return true + end + return false +end + +local function lcs_matches(a, b, rename_map) + local n = #a + local m = #b + if n == 0 or m == 0 then + return {}, {} + end + + local dp = {} + for i = 0, n do + dp[i] = {} + dp[i][0] = 0 + end + for j = 1, m do + dp[0][j] = 0 + end + + for i = 1, n do + for j = 1, m do + if tokens_equal(a[i], b[j], rename_map) then + dp[i][j] = dp[i - 1][j - 1] + 1 + else + local up = dp[i - 1][j] + local left = dp[i][j - 1] + dp[i][j] = (up >= left) and up or left + end + end + end + + local match_a = {} + local match_b = {} + local i = n + local j = m + while i > 0 and j > 0 do + if tokens_equal(a[i], b[j], rename_map) then + match_a[i] = true + match_b[j] = true + i = i - 1 + j = j - 1 + else + local up = dp[i - 1][j] + local left = dp[i][j - 1] + if up >= left then + i = i - 1 + else + j = j - 1 + end + end + end + return match_a, match_b +end + +local function unmatched_token_spans(tokens, matched, line_text_value) + local spans = {} + local i = 1 + while i <= #tokens do + if matched[i] then + i = i + 1 + else + local start_col = tokens[i].start_col + local end_col = tokens[i].end_col + local j = i + 1 + while j <= #tokens and not matched[j] do + local gap_start = end_col + 1 + local gap_end = tokens[j].start_col - 1 + local gap = "" + if gap_start <= gap_end then + gap = line_text_value:sub(gap_start, gap_end) + end + if gap ~= "" and not gap:match("^%s+$") then + break + end + end_col = tokens[j].end_col + j = j + 1 + end + table.insert(spans, { start_col = start_col, end_col = end_col }) + i = j + end + end + return spans +end + +local function build_internal_diff(src_node, dst_node, src_buf, dst_buf, rename_map, only_changes) + local src_text = vim.treesitter.get_node_text(src_node, src_buf) + local dst_text = vim.treesitter.get_node_text(dst_node, dst_buf) + if not src_text or not dst_text or src_text == "" or dst_text == "" then + return nil + end + + local src_lines = vim.split(src_text, "\n", { plain = true }) + local dst_lines = vim.split(dst_text, "\n", { plain = true }) + + local ok, hunks = pcall(vim.text.diff, src_text, dst_text, { + result_type = "indices", + linematch = 60, + }) + + local sr, sc = src_node:range() + local tr, tc = dst_node:range() + + local out = { + src_spans = {}, + dst_spans = {}, + src_signs = {}, + dst_signs = {}, + } + + local function base_col_for_row(row, start_row, start_col) + if row == start_row then + return start_col + end + return 0 + end + + local function mark_line_tokens(buf, side_spans, row, line_value, base_col, hl_group) + if row < 0 or not line_value then + return false + end + local first = line_value:find("%S") + if not first then + return false + end + local rev_last = line_value:reverse():find("%S") + local last = #line_value - rev_last + 1 + return append_span(side_spans, row, base_col + first - 1, base_col + last, hl_group, buf) + end + + local function highlight_line_pair(src_row, dst_row, s_line, d_line) + local src_base = base_col_for_row(src_row, sr, sc) + local dst_base = base_col_for_row(dst_row, tr, tc) + + if s_line and d_line and s_line ~= d_line then + local tokens_src = tokenize_line(s_line) + local tokens_dst = tokenize_line(d_line) + if #tokens_src > 0 or #tokens_dst > 0 then + local match_src, match_dst = lcs_matches(tokens_src, tokens_dst, rename_map) + local did_src = false + local did_dst = false + + for _, span in ipairs(unmatched_token_spans(tokens_src, match_src, s_line)) do + did_src = append_span( + out.src_spans, + src_row, + src_base + span.start_col - 1, + src_base + span.end_col, + "DiffChangeText", + src_buf + ) or did_src + end + + for _, span in ipairs(unmatched_token_spans(tokens_dst, match_dst, d_line)) do + did_dst = append_span( + out.dst_spans, + dst_row, + dst_base + span.start_col - 1, + dst_base + span.end_col, + "DiffChangeText", + dst_buf + ) or did_dst + end + + if did_src then + append_sign(out.src_signs, src_row, "U", "DiffChangeText") + end + if did_dst then + append_sign(out.dst_signs, dst_row, "U", "DiffChangeText") + end + return did_src or did_dst + end + + local fragment = semantic.diff_fragment(s_line, d_line) + if fragment then + local did = false + did = append_span( + out.src_spans, + src_row, + src_base + fragment.old_start - 1, + src_base + fragment.old_end, + "DiffChangeText", + src_buf + ) or did + did = append_span( + out.dst_spans, + dst_row, + dst_base + fragment.new_start - 1, + dst_base + fragment.new_end, + "DiffChangeText", + dst_buf + ) or did + append_sign(out.src_signs, src_row, "U", "DiffChangeText") + append_sign(out.dst_signs, dst_row, "U", "DiffChangeText") + return did + end + + return false + end + + if s_line and not d_line then + if only_changes then + return false + end + local did = mark_line_tokens(src_buf, out.src_spans, src_row, s_line, src_base, "DiffDeleteText") + append_sign(out.src_signs, src_row, "-", "DiffDeleteText") + return did + elseif d_line and not s_line then + if only_changes then + return false + end + local did = mark_line_tokens(dst_buf, out.dst_spans, dst_row, d_line, dst_base, "DiffAddText") + append_sign(out.dst_signs, dst_row, "+", "DiffAddText") + return did + end + + return false + end + + local did_highlight = false + if ok and hunks and #hunks > 0 then + for _, h in ipairs(hunks) do + local start_a, count_a, start_b, count_b = h[1], h[2], h[3], h[4] + local overlap = math.min(count_a, count_b) + + for i = 0, overlap - 1 do + local src_row = sr + start_a - 1 + i + local dst_row = tr + start_b - 1 + i + local s_line = src_lines[start_a + i] + local d_line = dst_lines[start_b + i] + if highlight_line_pair(src_row, dst_row, s_line, d_line) then + did_highlight = true + end + end + + if count_a > overlap and not only_changes then + for i = overlap, count_a - 1 do + local src_row = sr + start_a - 1 + i + local s_line = src_lines[start_a + i] + local src_base = base_col_for_row(src_row, sr, sc) + append_sign(out.src_signs, src_row, "-", "DiffDeleteText") + did_highlight = mark_line_tokens(src_buf, out.src_spans, src_row, s_line, src_base, "DiffDeleteText") + or did_highlight + end + end + + if count_b > overlap and not only_changes then + for i = overlap, count_b - 1 do + local dst_row = tr + start_b - 1 + i + local d_line = dst_lines[start_b + i] + local dst_base = base_col_for_row(dst_row, tr, tc) + append_sign(out.dst_signs, dst_row, "+", "DiffAddText") + did_highlight = mark_line_tokens(dst_buf, out.dst_spans, dst_row, d_line, dst_base, "DiffAddText") + or did_highlight + end + end + end + else + local max_lines = math.max(#src_lines, #dst_lines) + for i = 1, max_lines do + local src_row = sr + i - 1 + local dst_row = tr + i - 1 + local s_line = src_lines[i] + local d_line = dst_lines[i] + if highlight_line_pair(src_row, dst_row, s_line, d_line) then + did_highlight = true + end + end + end + + if did_highlight or #out.src_spans > 0 or #out.dst_spans > 0 then + return out + end + return nil +end + +local function append_change_leaf(render, src_buf, dst_buf, src_node, dst_node, src_text, dst_text) + local sr, sc, er, ec = node_range(src_node) + local tr, tc, ter, tec = node_range(dst_node) + if not sr or not tr then + return false + end + + local highlighted = false + local fragment = semantic.diff_fragment(src_text or "", dst_text or "") + if fragment and er == sr and ter == tr then + local src_start = sc + math.max(fragment.old_start - 1, 0) + local src_end = sc + math.max(fragment.old_end, 0) + local dst_start = tc + math.max(fragment.new_start - 1, 0) + local dst_end = tc + math.max(fragment.new_end, 0) + + highlighted = append_span(render.src_spans, sr, src_start, src_end, "DiffChangeText", src_buf) or highlighted + highlighted = append_span(render.dst_spans, tr, dst_start, dst_end, "DiffChangeText", dst_buf) or highlighted + else + highlighted = append_node_tokens(render.src_spans, src_buf, src_node, "DiffChangeText", nil) or highlighted + highlighted = append_node_tokens(render.dst_spans, dst_buf, dst_node, "DiffChangeText", nil) or highlighted + end + + append_sign(render.src_signs, sr, "U", "DiffChangeText") + append_sign(render.dst_signs, tr, "U", "DiffChangeText") + + return highlighted +end + +function M.enrich(actions, opts) + local src_buf = opts and opts.src_buf or nil + local dst_buf = opts and opts.dst_buf or nil + if not src_buf or not dst_buf then + return + end + + local rename_map = {} + local rename_src_nodes = {} + local rename_dst_nodes = {} + + for _, action in ipairs(actions) do + if action.type == "rename" then + if action.from and action.to then + rename_map[action.from] = action.to + end + if action.src_node then + rename_src_nodes[action.src_node:id()] = true + end + if action.dst_node then + rename_dst_nodes[action.dst_node:id()] = true + end + end + end + + for _, action in ipairs(actions) do + local src_node = action.src_node + local dst_node = action.dst_node + local render = { + src_spans = {}, + dst_spans = {}, + src_signs = {}, + dst_signs = {}, + src_virt = {}, + dst_virt = {}, + } + local touched = false + + if action.type == "move" and src_node and dst_node then + local sr, sc = node_range(src_node) + local tr, tc = node_range(dst_node) + local src_line = action.lines and action.lines.from_line or (sr and sr + 1 or nil) + local dst_line = action.lines and action.lines.to_line or (tr and tr + 1 or nil) + + touched = append_node_tokens(render.src_spans, src_buf, src_node, "DiffMoveText", nil) or touched + touched = append_node_tokens(render.dst_spans, dst_buf, dst_node, "DiffMoveText", nil) or touched + + if sr and sc then + append_sign(render.src_signs, sr, "M", "DiffMoveText") + append_virt( + render.src_virt, + sr, + sc, + string.format(" ⤷ moved L%d → L%d", src_line or 0, dst_line or 0), + "Comment", + "eol" + ) + touched = true + end + if tr and tc then + append_sign(render.dst_signs, tr, "M", "DiffMoveText") + append_virt(render.dst_virt, tr, tc, string.format(" ⤶ from L%d", src_line or 0), "Comment", "eol") + touched = true + end + elseif action.type == "rename" and src_node and dst_node then + local sr, _, _, sec = node_range(src_node) + local tr, _, _, tec = node_range(dst_node) + + touched = append_node_tokens(render.src_spans, src_buf, src_node, "DiffRenameText", nil) or touched + touched = append_node_tokens(render.dst_spans, dst_buf, dst_node, "DiffRenameText", nil) or touched + + if sr and sec and action.to then + append_sign(render.src_signs, sr, "R", "DiffRenameText") + append_virt(render.src_virt, sr, sec, " -> " .. action.to, "Comment", "inline") + touched = true + end + if tr and tec and action.from then + append_sign(render.dst_signs, tr, "R", "DiffRenameText") + append_virt(render.dst_virt, tr, tec, string.format(" (was %s)", action.from), "Comment", "inline") + touched = true + end + elseif action.type == "update" and src_node and dst_node and RENDER_UPDATE_TOKENS then + local leaf_changes = action.semantic and action.semantic.leaf_changes or nil + local rename_pairs = action.semantic and action.semantic.rename_pairs or {} + local did_leaf_highlight = false + + if leaf_changes and #leaf_changes > 0 then + for _, change in ipairs(leaf_changes) do + local change_src = change.src_node + local change_dst = change.dst_node + if change_src and change_dst and change.src_text ~= change.dst_text then + local src_id = change_src:id() + local dst_id = change_dst:id() + local is_rename = rename_src_nodes[src_id] + or rename_dst_nodes[dst_id] + or (change.src_text and rename_pairs[change.src_text] == change.dst_text) + or (change.src_text and rename_map[change.src_text] == change.dst_text) + if not is_rename then + local highlighted = append_change_leaf( + render, + src_buf, + dst_buf, + change_src, + change_dst, + change.src_text, + change.dst_text + ) + did_leaf_highlight = highlighted or did_leaf_highlight + touched = highlighted or touched + end + end + end + end + + if not did_leaf_highlight then + local diff_ops = build_internal_diff(src_node, dst_node, src_buf, dst_buf, rename_map, false) + if diff_ops then + for _, span in ipairs(diff_ops.src_spans) do + table.insert(render.src_spans, span) + end + for _, span in ipairs(diff_ops.dst_spans) do + table.insert(render.dst_spans, span) + end + for _, sign in ipairs(diff_ops.src_signs) do + table.insert(render.src_signs, sign) + end + for _, sign in ipairs(diff_ops.dst_signs) do + table.insert(render.dst_signs, sign) + end + touched = true + end + end + elseif action.type == "delete" and src_node then + touched = append_node_tokens(render.src_spans, src_buf, src_node, "DiffDeleteText", nil) or touched + local sr = node_range(src_node) + if sr then + append_sign(render.src_signs, sr, "-", "DiffDeleteText") + touched = true + end + elseif action.type == "insert" and dst_node then + touched = append_node_tokens(render.dst_spans, dst_buf, dst_node, "DiffAddText", nil) or touched + local tr = node_range(dst_node) + if tr then + append_sign(render.dst_signs, tr, "+", "DiffAddText") + touched = true + end + end + + if touched then + action.render = render + end + end +end + +return M From 83dc744fa41a3beaba41b493152b842ce4f0f4e6 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Thu, 12 Feb 2026 15:44:41 +0530 Subject: [PATCH 14/48] refactor(ui): renderer now consumes core payload with new sign manager removed semantic/highlight diff logic from ui/renderer.lua and now renderer uses the core payload to show the highlights added ui/signs.lua for sign style/glyph/priority handling cleaned ui/helpers.lua by removing old semantic/internal diff helpers no longer needed in ui --- lua/diffmantic/ui/helpers.lua | 283 ------------------------- lua/diffmantic/ui/renderer.lua | 373 ++++++--------------------------- lua/diffmantic/ui/signs.lua | 88 ++++++++ 3 files changed, 151 insertions(+), 593 deletions(-) create mode 100644 lua/diffmantic/ui/signs.lua diff --git a/lua/diffmantic/ui/helpers.lua b/lua/diffmantic/ui/helpers.lua index 6a98b6c..d0c0714 100644 --- a/lua/diffmantic/ui/helpers.lua +++ b/lua/diffmantic/ui/helpers.lua @@ -1,14 +1,5 @@ -local semantic = require("diffmantic.core.semantic") - local M = {} -M.find_leaf_changes = semantic.find_leaf_changes -M.node_in_field = semantic.node_in_field -M.is_rename_identifier = semantic.is_rename_identifier -M.is_value_node = semantic.is_value_node -M.classify_text_change = semantic.classify_text_change -M.diff_fragment = semantic.diff_fragment - function M.set_inline_virt_text(buf, ns, row, col, text, hl) local opts = { virt_text = { { text, hl } }, @@ -22,278 +13,4 @@ function M.set_inline_virt_text(buf, ns, row, col, text, hl) pcall(vim.api.nvim_buf_set_extmark, buf, ns, row, col, opts) end -function M.highlight_internal_diff(src_node, dst_node, src_buf, dst_buf, ns, opts) - local src_text = vim.treesitter.get_node_text(src_node, src_buf) - local dst_text = vim.treesitter.get_node_text(dst_node, dst_buf) - if not src_text or not dst_text or src_text == "" or dst_text == "" then - return false - end - - local src_lines = vim.split(src_text, "\n", { plain = true }) - local dst_lines = vim.split(dst_text, "\n", { plain = true }) - - local ok, hunks = pcall(vim.text.diff, src_text, dst_text, { - result_type = "indices", - linematch = 60, - }) - - local sr, _, er, _ = src_node:range() - local tr, _, ter, _ = dst_node:range() - local src_end = er - 1 - local dst_end = ter - 1 - local signs_src = opts and opts.signs_src or nil - local signs_dst = opts and opts.signs_dst or nil - local rename_map = opts and opts.rename_map or nil - - local function mark_fragment(buf, row, start_col, end_col, hl_group) - if row < 0 or end_col <= start_col then - return false - end - return pcall(vim.api.nvim_buf_set_extmark, buf, ns, row, start_col, { - end_row = row, - end_col = end_col, - hl_group = hl_group, - }) - end - - local function mark_sign(buf, row, text, hl_group, sign_rows) - if row < 0 then - return false - end - if sign_rows and sign_rows[row] then - return false - end - local ok = pcall(vim.api.nvim_buf_set_extmark, buf, ns, row, 0, { - sign_text = text, - sign_hl_group = hl_group, - }) - if ok and sign_rows then - sign_rows[row] = true - end - return ok - end - - local function tokenize_line(text) - local tokens = {} - local i = 1 - local len = #text - while i <= len do - local ch = text:sub(i, i) - if ch:match("%s") then - i = i + 1 - elseif ch:match("[%w_]") then - local j = i + 1 - while j <= len and text:sub(j, j):match("[%w_]") do - j = j + 1 - end - table.insert(tokens, { text = text:sub(i, j - 1), start_col = i, end_col = j - 1 }) - i = j - else - local j = i + 1 - while j <= len and not text:sub(j, j):match("[%w_%s]") do - j = j + 1 - end - table.insert(tokens, { text = text:sub(i, j - 1), start_col = i, end_col = j - 1 }) - i = j - end - end - return tokens - end - - local function tokens_equal(a, b) - if a.text == b.text then - return true - end - if rename_map and rename_map[a.text] == b.text then - return true - end - return false - end - - local function lcs_matches(a, b) - local n = #a - local m = #b - if n == 0 or m == 0 then - return {}, {} - end - local dp = {} - for i = 0, n do - dp[i] = {} - dp[i][0] = 0 - end - for j = 1, m do - dp[0][j] = 0 - end - for i = 1, n do - for j = 1, m do - if tokens_equal(a[i], b[j]) then - dp[i][j] = dp[i - 1][j - 1] + 1 - else - local up = dp[i - 1][j] - local left = dp[i][j - 1] - dp[i][j] = (up >= left) and up or left - end - end - end - local match_a = {} - local match_b = {} - local i = n - local j = m - while i > 0 and j > 0 do - if tokens_equal(a[i], b[j]) then - match_a[i] = true - match_b[j] = true - i = i - 1 - j = j - 1 - else - local up = dp[i - 1][j] - local left = dp[i][j - 1] - if up >= left then - i = i - 1 - else - j = j - 1 - end - end - end - return match_a, match_b - end - - local function mark_full_line(buf, row, hl_group) - if row < 0 then - return false - end - return pcall(vim.api.nvim_buf_set_extmark, buf, ns, row, 0, { - end_row = row + 1, - end_col = 0, - hl_group = hl_group, - hl_eol = true, - }) - end - - local function highlight_line_pair(src_row, dst_row, s_line, d_line) - if s_line and d_line and s_line ~= d_line then - local tokens_src = tokenize_line(s_line) - local tokens_dst = tokenize_line(d_line) - if #tokens_src > 0 or #tokens_dst > 0 then - local match_src, match_dst = lcs_matches(tokens_src, tokens_dst) - local did_src = false - local did_dst = false - if src_row <= src_end then - for i, tok in ipairs(tokens_src) do - if not match_src[i] then - did_src = mark_fragment(src_buf, src_row, tok.start_col - 1, tok.end_col, "DiffChangeText") or did_src - end - end - if did_src then - mark_sign(src_buf, src_row, "U", "DiffChangeText", signs_src) - end - end - if dst_row <= dst_end then - for i, tok in ipairs(tokens_dst) do - if not match_dst[i] then - did_dst = mark_fragment(dst_buf, dst_row, tok.start_col - 1, tok.end_col, "DiffChangeText") or did_dst - end - end - if did_dst then - mark_sign(dst_buf, dst_row, "U", "DiffChangeText", signs_dst) - end - end - if did_src or did_dst then - return true - end - return false - end - local fragment = M.diff_fragment(s_line, d_line) - if fragment then - local did = false - if src_row <= src_end then - did = mark_fragment(src_buf, src_row, fragment.old_start - 1, fragment.old_end, "DiffChangeText") or did - mark_sign(src_buf, src_row, "U", "DiffChangeText", signs_src) - end - if dst_row <= dst_end then - did = mark_fragment(dst_buf, dst_row, fragment.new_start - 1, fragment.new_end, "DiffChangeText") or did - mark_sign(dst_buf, dst_row, "U", "DiffChangeText", signs_dst) - end - return did - end - local did = false - if src_row <= src_end then - did = mark_full_line(src_buf, src_row, "DiffChangeText") or did - mark_sign(src_buf, src_row, "U", "DiffChangeText", signs_src) - end - if dst_row <= dst_end then - did = mark_full_line(dst_buf, dst_row, "DiffChangeText") or did - mark_sign(dst_buf, dst_row, "U", "DiffChangeText", signs_dst) - end - return did - end - if s_line and not d_line then - if src_row <= src_end then - mark_sign(src_buf, src_row, "-", "DiffDeleteText", signs_src) - return mark_full_line(src_buf, src_row, "DiffDeleteText") - end - elseif d_line and not s_line then - if dst_row <= dst_end then - mark_sign(dst_buf, dst_row, "+", "DiffAddText", signs_dst) - return mark_full_line(dst_buf, dst_row, "DiffAddText") - end - end - return false - end - - local did_highlight = false - - if ok and hunks and #hunks > 0 then - for _, h in ipairs(hunks) do - local start_a, count_a, start_b, count_b = h[1], h[2], h[3], h[4] - local overlap = math.min(count_a, count_b) - - for i = 0, overlap - 1 do - local src_row = sr + start_a - 1 + i - local dst_row = tr + start_b - 1 + i - local s_line = src_lines[start_a + i] - local d_line = dst_lines[start_b + i] - if highlight_line_pair(src_row, dst_row, s_line, d_line) then - did_highlight = true - end - end - - if count_a > overlap then - for i = overlap, count_a - 1 do - local src_row = sr + start_a - 1 + i - if src_row <= src_end then - mark_sign(src_buf, src_row, "-", "DiffDeleteText", signs_src) - did_highlight = mark_full_line(src_buf, src_row, "DiffDeleteText") or did_highlight - end - end - end - - if count_b > overlap then - for i = overlap, count_b - 1 do - local dst_row = tr + start_b - 1 + i - if dst_row <= dst_end then - mark_sign(dst_buf, dst_row, "+", "DiffAddText", signs_dst) - did_highlight = mark_full_line(dst_buf, dst_row, "DiffAddText") or did_highlight - end - end - end - end - - return did_highlight - end - - local max_lines = math.max(#src_lines, #dst_lines) - for i = 1, max_lines do - local src_row = sr + i - 1 - local dst_row = tr + i - 1 - local s_line = src_lines[i] - local d_line = dst_lines[i] - if highlight_line_pair(src_row, dst_row, s_line, d_line) then - did_highlight = true - end - end - - return did_highlight -end - return M diff --git a/lua/diffmantic/ui/renderer.lua b/lua/diffmantic/ui/renderer.lua index df6b7f8..971582f 100644 --- a/lua/diffmantic/ui/renderer.lua +++ b/lua/diffmantic/ui/renderer.lua @@ -1,331 +1,84 @@ -local helpers = require("diffmantic.ui.helpers") +local signs = require("diffmantic.ui.signs") local M = {} -function M.render(src_buf, dst_buf, actions, ns) - -- Suppress insert/delete inside moved/updated ranges. - local src_suppress = {} - local dst_suppress = {} - local rename_map = {} - - local function add_range(ranges, node) - if not node then - return - end - local sr, _, er, _ = node:range() - table.insert(ranges, { start_row = sr, end_row = er }) +local HL_PRIORITY = { + DiffMoveText = 10, + DiffAddText = 20, + DiffDeleteText = 20, + DiffChangeText = 30, + DiffRenameText = 40, +} + +local function set_extmark(buf, ns, row, col, opts) + if opts and opts.hl_group and not opts.priority then + opts.priority = HL_PRIORITY[opts.hl_group] or 20 end + return pcall(vim.api.nvim_buf_set_extmark, buf, ns, row, col, opts) +end - for _, action in ipairs(actions) do - if action.type == "move" or action.type == "update" then - add_range(src_suppress, action.node) - add_range(dst_suppress, action.target) +local function apply_spans(buf, ns, spans) + if not spans then + return + end + for _, span in ipairs(spans) do + if span and span.row ~= nil and span.start_col ~= nil and span.end_col ~= nil and span.hl_group then + set_extmark(buf, ns, span.row, span.start_col, { + end_row = span.end_row or span.row, + end_col = span.end_col, + hl_group = span.hl_group, + }) end end +end - for _, action in ipairs(actions) do - if action.type == "update" then - local leaf_changes = helpers.find_leaf_changes(action.node, action.target, src_buf, dst_buf) - for _, change in ipairs(leaf_changes) do - if helpers.is_rename_identifier(change.src_node) or helpers.is_rename_identifier(change.dst_node) then - rename_map[change.src_text] = change.dst_text - end - end +local function apply_signs(buf, ns, list, seen_rows) + if not list then + return + end + for _, item in ipairs(list) do + if item and item.row ~= nil and item.text and item.hl_group then + signs.mark(buf, ns, item.row, item.col or 0, item.text, item.hl_group, seen_rows) end end +end - local function is_suppressed(ranges, node) - if not node then - return false - end - local sr, _, er, _ = node:range() - for _, range in ipairs(ranges) do - if sr >= range.start_row and er <= range.end_row then - return true +local function apply_virt(buf, ns, list) + if not list then + return + end + for _, item in ipairs(list) do + if item and item.row ~= nil and item.text then + local opts = { + virt_text = { { item.text, item.hl_group or "Comment" } }, + virt_text_pos = item.pos or "eol", + } + local ok = set_extmark(buf, ns, item.row, item.col or 0, opts) + if not ok and opts.virt_text_pos == "inline" then + opts.virt_text_pos = "eol" + set_extmark(buf, ns, item.row, item.col or 0, opts) end end - return false end +end - for _, action in ipairs(actions) do - local node = action.node - local sr, sc, er, ec = node:range() - - if action.type == "move" then - local target = action.target - local tr, tc, ter, tec = target:range() - local src_line = sr + 1 - local dst_line = tr + 1 - - pcall(vim.api.nvim_buf_set_extmark, src_buf, ns, sr, sc, { - end_row = er, - end_col = ec, - hl_group = "DiffMoveText", - virt_text = { { string.format(" ⤷ moved L%d → L%d", src_line, dst_line), "Comment" } }, - virt_text_pos = "eol", - sign_text = "M", - sign_hl_group = "DiffMoveText", - }) - pcall(vim.api.nvim_buf_set_extmark, dst_buf, ns, tr, tc, { - end_row = ter, - end_col = tec, - hl_group = "DiffMoveText", - virt_text = { { string.format(" ⤶ from L%d", src_line), "Comment" } }, - virt_text_pos = "eol", - sign_text = "M", - sign_hl_group = "DiffMoveText", - }) - elseif action.type == "update" then - local target = action.target - local tr, tc, ter, tec = target:range() - - local leaf_changes = helpers.find_leaf_changes(node, target, src_buf, dst_buf) - - local signs_src = {} - local signs_dst = {} - if #leaf_changes > 0 then - local rename_signs = {} - local rename_signs_src = {} - local rename_inline_src = {} - local rename_inline_dst = {} - local update_signs_dst = {} - local update_signs_src = {} - local rename_pairs = {} - - for src_text, dst_text in pairs(rename_map) do - rename_pairs[src_text] = dst_text - end - - for _, change in ipairs(leaf_changes) do - local src_node = change.src_node - local dst_node = change.dst_node - if helpers.is_rename_identifier(src_node) or helpers.is_rename_identifier(dst_node) then - rename_pairs[change.src_text] = change.dst_text - end - end - - for _, change in ipairs(leaf_changes) do - local src_node = change.src_node - local dst_node = change.dst_node - local ctr, ctc, cter, ctec = dst_node:range() - local csr, csc, cser, csec = src_node:range() - - local is_rename_ref = false - if not (helpers.is_rename_identifier(src_node) or helpers.is_rename_identifier(dst_node)) then - local src_type = src_node:type() - local dst_type = dst_node:type() - if - ( - src_type == "identifier" - or src_type == "field_identifier" - or src_type == "type_identifier" - ) - and (dst_type == "identifier" or dst_type == "field_identifier" or dst_type == "type_identifier") - and ( - rename_pairs[change.src_text] == change.dst_text - or rename_map[change.src_text] == change.dst_text - ) - then - is_rename_ref = true - end - end - - if is_rename_ref then - -- Identifier usage changed only due to rename; ignore to avoid noise. - goto continue_leaf - end - - if helpers.is_rename_identifier(src_node) or helpers.is_rename_identifier(dst_node) then - -- Rename: highlight identifier with inline "was"/"->". - if not rename_signs_src[csr] then - rename_signs_src[csr] = true - signs_src[csr] = true - pcall(vim.api.nvim_buf_set_extmark, src_buf, ns, csr, csc, { - end_row = cser, - end_col = csec, - hl_group = "DiffRenameText", - sign_text = "R", - sign_hl_group = "DiffRenameText", - }) - else - pcall(vim.api.nvim_buf_set_extmark, src_buf, ns, csr, csc, { - end_row = cser, - end_col = csec, - hl_group = "DiffChangeText", - }) - end - - if not rename_signs[ctr] then - rename_signs[ctr] = true - signs_dst[ctr] = true - pcall(vim.api.nvim_buf_set_extmark, dst_buf, ns, ctr, ctc, { - end_row = cter, - end_col = ctec, - hl_group = "DiffRenameText", - sign_text = "R", - sign_hl_group = "DiffRenameText", - }) - else - signs_dst[ctr] = true - pcall(vim.api.nvim_buf_set_extmark, dst_buf, ns, ctr, ctc, { - end_row = cter, - end_col = ctec, - hl_group = "DiffChangeText", - }) - end - local src_key = tostring(src_node:id()) - if not rename_inline_src[src_key] then - rename_inline_src[src_key] = true - helpers.set_inline_virt_text(src_buf, ns, csr, csec, " -> " .. change.dst_text, "Comment") - end - local dst_key = tostring(dst_node:id()) - if not rename_inline_dst[dst_key] then - rename_inline_dst[dst_key] = true - helpers.set_inline_virt_text( - dst_buf, - ns, - ctr, - ctec, - string.format(" (was %s)", change.src_text), - "Comment" - ) - end - elseif - helpers.is_value_node(src_node, change.src_text) - or helpers.is_value_node(dst_node, change.dst_text) - then - -- Value change: micro-diff only (no virtual text). - local fragment = helpers.diff_fragment(change.src_text, change.dst_text) - if fragment then - local rel_start = fragment.new_start - 1 - local rel_end = fragment.new_end - if cter == ctr then - pcall(vim.api.nvim_buf_set_extmark, dst_buf, ns, ctr, ctc + rel_start, { - end_row = cter, - end_col = ctc + rel_end, - hl_group = "DiffChange", - }) - end - if cser == csr then - pcall(vim.api.nvim_buf_set_extmark, src_buf, ns, csr, csc + fragment.old_start - 1, { - end_row = cser, - end_col = csc + fragment.old_end, - hl_group = "DiffChange", - }) - end - - if not update_signs_dst[ctr] then - update_signs_dst[ctr] = true - signs_dst[ctr] = true - pcall(vim.api.nvim_buf_set_extmark, dst_buf, ns, ctr, ctc + rel_end, { - sign_text = "U", - sign_hl_group = "DiffChangeText", - }) - end - if not update_signs_src[csr] then - update_signs_src[csr] = true - signs_src[csr] = true - pcall(vim.api.nvim_buf_set_extmark, src_buf, ns, csr, csc + fragment.old_end, { - sign_text = "U", - sign_hl_group = "DiffChangeText", - }) - end - else - pcall(vim.api.nvim_buf_set_extmark, dst_buf, ns, ctr, ctc, { - end_row = cter, - end_col = ctec, - hl_group = "DiffChange", - sign_text = "U", - sign_hl_group = "DiffChangeText", - }) - if cser == csr then - if not update_signs_src[csr] then - update_signs_src[csr] = true - signs_src[csr] = true - pcall(vim.api.nvim_buf_set_extmark, src_buf, ns, csr, csc, { - end_row = cser, - end_col = csec, - hl_group = "DiffChange", - sign_text = "U", - sign_hl_group = "DiffChangeText", - }) - else - pcall(vim.api.nvim_buf_set_extmark, src_buf, ns, csr, csc, { - end_row = cser, - end_col = csec, - hl_group = "DiffChange", - }) - end - end - end - else - if cser >= csr then - pcall(vim.api.nvim_buf_set_extmark, src_buf, ns, csr, csc, { - end_row = cser, - end_col = csec, - hl_group = "DiffChangeText", - }) - end - if cter >= ctr then - pcall(vim.api.nvim_buf_set_extmark, dst_buf, ns, ctr, ctc, { - end_row = cter, - end_col = ctec, - hl_group = "DiffChangeText", - }) - end - if not update_signs_src[csr] then - update_signs_src[csr] = true - signs_src[csr] = true - pcall(vim.api.nvim_buf_set_extmark, src_buf, ns, csr, csc, { - sign_text = "U", - sign_hl_group = "DiffChangeText", - }) - end - if not update_signs_dst[ctr] then - update_signs_dst[ctr] = true - signs_dst[ctr] = true - pcall(vim.api.nvim_buf_set_extmark, dst_buf, ns, ctr, ctc, { - sign_text = "U", - sign_hl_group = "DiffChangeText", - }) - end - end +function M.render(src_buf, dst_buf, actions, ns) + local src_sign_rows = {} + local dst_sign_rows = {} - ::continue_leaf:: - end - else - helpers.highlight_internal_diff(node, target, src_buf, dst_buf, ns, { - signs_src = signs_src, - signs_dst = signs_dst, - rename_map = rename_map, - }) - end - elseif action.type == "delete" then - if is_suppressed(src_suppress, node) and node:type() ~= "field_declaration" then - goto continue_action - end - pcall(vim.api.nvim_buf_set_extmark, src_buf, ns, sr, sc, { - end_row = er, - end_col = ec, - hl_group = "DiffDeleteText", - sign_text = "-", - sign_hl_group = "DiffDeleteText", - }) - elseif action.type == "insert" then - if is_suppressed(dst_suppress, node) and node:type() ~= "field_declaration" then - goto continue_action - end - pcall(vim.api.nvim_buf_set_extmark, dst_buf, ns, sr, sc, { - end_row = er, - end_col = ec, - hl_group = "DiffAddText", - sign_text = "+", - sign_hl_group = "DiffAddText", - }) + for _, action in ipairs(actions) do + local render = action.render + if render then + apply_spans(src_buf, ns, render.src_spans) + apply_spans(dst_buf, ns, render.dst_spans) + apply_signs(src_buf, ns, render.src_signs, src_sign_rows) + apply_signs(dst_buf, ns, render.dst_signs, dst_sign_rows) + apply_virt(src_buf, ns, render.src_virt) + apply_virt(dst_buf, ns, render.dst_virt) end - - ::continue_action:: end + end return M + diff --git a/lua/diffmantic/ui/signs.lua b/lua/diffmantic/ui/signs.lua new file mode 100644 index 0000000..fff6a9c --- /dev/null +++ b/lua/diffmantic/ui/signs.lua @@ -0,0 +1,88 @@ +local M = {} + +local SIGN_GROUP_BY_TEXT_GROUP = { + DiffAddText = "DiffAddSign", + DiffDeleteText = "DiffDeleteSign", + DiffChangeText = "DiffChangeSign", + DiffMoveText = "DiffMoveSign", + DiffRenameText = "DiffRenameSign", +} + +local SIGN_PRIORITY_BY_TEXT_GROUP = { + DiffAddText = 10, + DiffDeleteText = 10, + DiffChangeText = 20, + DiffMoveText = 30, + DiffRenameText = 40, +} + +function M.glyph() + return vim.g.diffmantic_side_sign_glyph or "▎" +end + +function M.style() + return vim.g.diffmantic_sign_style or "both" +end + +local function normalize_sign_char(text) + if not text or text == "" then + return nil + end + return text:sub(1, 1) +end + +function M.sign_text(text) + local sign_char = normalize_sign_char(text) + if not sign_char then + return nil + end + + local style = M.style() + if style == "letter" then + return sign_char + end + if style == "gutter" then + return M.glyph() + end + return M.glyph() .. sign_char +end + +function M.group_for_hl(hl_group) + return SIGN_GROUP_BY_TEXT_GROUP[hl_group] or hl_group +end + +function M.priority_for_hl(hl_group) + return SIGN_PRIORITY_BY_TEXT_GROUP[hl_group] or 0 +end + +function M.mark(buf, ns, row, col, text, hl_group, sign_rows) + if row == nil or row < 0 then + return false + end + if not text or text == "" then + return false + end + + local priority = M.priority_for_hl(hl_group) + local existing_priority = -1 + if sign_rows and sign_rows[row] then + local existing = sign_rows[row] + existing_priority = type(existing) == "number" and existing or 0 + end + if existing_priority >= priority then + return false + end + + local ok = pcall(vim.api.nvim_buf_set_extmark, buf, ns, row, col or 0, { + sign_text = M.sign_text(text), + sign_hl_group = M.group_for_hl(hl_group), + priority = priority, + }) + + if ok and sign_rows then + sign_rows[row] = priority + end + return ok +end + +return M From 4aa9914a4dae4d83df445f1387a396160f02a8fb Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Thu, 12 Feb 2026 18:30:54 +0530 Subject: [PATCH 15/48] refactor(ui): updated namespace highlights groups to Diffmantic* renamed internal diff highlight group from Diff*Text/Sign to Diffmantic* --- lua/diffmantic/core/payload.lua | 68 ++++++++++++------------ lua/diffmantic/init.lua | 92 ++++++++++++++++++++++++--------- lua/diffmantic/ui/renderer.lua | 10 ++-- lua/diffmantic/ui/signs.lua | 20 +++---- 4 files changed, 118 insertions(+), 72 deletions(-) diff --git a/lua/diffmantic/core/payload.lua b/lua/diffmantic/core/payload.lua index cf66a24..d671bb3 100644 --- a/lua/diffmantic/core/payload.lua +++ b/lua/diffmantic/core/payload.lua @@ -306,7 +306,7 @@ local function build_internal_diff(src_node, dst_node, src_buf, dst_buf, rename_ src_row, src_base + span.start_col - 1, src_base + span.end_col, - "DiffChangeText", + "DiffmanticChange", src_buf ) or did_src end @@ -317,16 +317,16 @@ local function build_internal_diff(src_node, dst_node, src_buf, dst_buf, rename_ dst_row, dst_base + span.start_col - 1, dst_base + span.end_col, - "DiffChangeText", + "DiffmanticChange", dst_buf ) or did_dst end if did_src then - append_sign(out.src_signs, src_row, "U", "DiffChangeText") + append_sign(out.src_signs, src_row, "U", "DiffmanticChange") end if did_dst then - append_sign(out.dst_signs, dst_row, "U", "DiffChangeText") + append_sign(out.dst_signs, dst_row, "U", "DiffmanticChange") end return did_src or did_dst end @@ -339,7 +339,7 @@ local function build_internal_diff(src_node, dst_node, src_buf, dst_buf, rename_ src_row, src_base + fragment.old_start - 1, src_base + fragment.old_end, - "DiffChangeText", + "DiffmanticChange", src_buf ) or did did = append_span( @@ -347,11 +347,11 @@ local function build_internal_diff(src_node, dst_node, src_buf, dst_buf, rename_ dst_row, dst_base + fragment.new_start - 1, dst_base + fragment.new_end, - "DiffChangeText", + "DiffmanticChange", dst_buf ) or did - append_sign(out.src_signs, src_row, "U", "DiffChangeText") - append_sign(out.dst_signs, dst_row, "U", "DiffChangeText") + append_sign(out.src_signs, src_row, "U", "DiffmanticChange") + append_sign(out.dst_signs, dst_row, "U", "DiffmanticChange") return did end @@ -362,15 +362,15 @@ local function build_internal_diff(src_node, dst_node, src_buf, dst_buf, rename_ if only_changes then return false end - local did = mark_line_tokens(src_buf, out.src_spans, src_row, s_line, src_base, "DiffDeleteText") - append_sign(out.src_signs, src_row, "-", "DiffDeleteText") + local did = mark_line_tokens(src_buf, out.src_spans, src_row, s_line, src_base, "DiffmanticDelete") + append_sign(out.src_signs, src_row, "-", "DiffmanticDelete") return did elseif d_line and not s_line then if only_changes then return false end - local did = mark_line_tokens(dst_buf, out.dst_spans, dst_row, d_line, dst_base, "DiffAddText") - append_sign(out.dst_signs, dst_row, "+", "DiffAddText") + local did = mark_line_tokens(dst_buf, out.dst_spans, dst_row, d_line, dst_base, "DiffmanticAdd") + append_sign(out.dst_signs, dst_row, "+", "DiffmanticAdd") return did end @@ -398,8 +398,8 @@ local function build_internal_diff(src_node, dst_node, src_buf, dst_buf, rename_ local src_row = sr + start_a - 1 + i local s_line = src_lines[start_a + i] local src_base = base_col_for_row(src_row, sr, sc) - append_sign(out.src_signs, src_row, "-", "DiffDeleteText") - did_highlight = mark_line_tokens(src_buf, out.src_spans, src_row, s_line, src_base, "DiffDeleteText") + append_sign(out.src_signs, src_row, "-", "DiffmanticDelete") + did_highlight = mark_line_tokens(src_buf, out.src_spans, src_row, s_line, src_base, "DiffmanticDelete") or did_highlight end end @@ -409,8 +409,8 @@ local function build_internal_diff(src_node, dst_node, src_buf, dst_buf, rename_ local dst_row = tr + start_b - 1 + i local d_line = dst_lines[start_b + i] local dst_base = base_col_for_row(dst_row, tr, tc) - append_sign(out.dst_signs, dst_row, "+", "DiffAddText") - did_highlight = mark_line_tokens(dst_buf, out.dst_spans, dst_row, d_line, dst_base, "DiffAddText") + append_sign(out.dst_signs, dst_row, "+", "DiffmanticAdd") + did_highlight = mark_line_tokens(dst_buf, out.dst_spans, dst_row, d_line, dst_base, "DiffmanticAdd") or did_highlight end end @@ -449,15 +449,15 @@ local function append_change_leaf(render, src_buf, dst_buf, src_node, dst_node, local dst_start = tc + math.max(fragment.new_start - 1, 0) local dst_end = tc + math.max(fragment.new_end, 0) - highlighted = append_span(render.src_spans, sr, src_start, src_end, "DiffChangeText", src_buf) or highlighted - highlighted = append_span(render.dst_spans, tr, dst_start, dst_end, "DiffChangeText", dst_buf) or highlighted + highlighted = append_span(render.src_spans, sr, src_start, src_end, "DiffmanticChange", src_buf) or highlighted + highlighted = append_span(render.dst_spans, tr, dst_start, dst_end, "DiffmanticChange", dst_buf) or highlighted else - highlighted = append_node_tokens(render.src_spans, src_buf, src_node, "DiffChangeText", nil) or highlighted - highlighted = append_node_tokens(render.dst_spans, dst_buf, dst_node, "DiffChangeText", nil) or highlighted + highlighted = append_node_tokens(render.src_spans, src_buf, src_node, "DiffmanticChange", nil) or highlighted + highlighted = append_node_tokens(render.dst_spans, dst_buf, dst_node, "DiffmanticChange", nil) or highlighted end - append_sign(render.src_signs, sr, "U", "DiffChangeText") - append_sign(render.dst_signs, tr, "U", "DiffChangeText") + append_sign(render.src_signs, sr, "U", "DiffmanticChange") + append_sign(render.dst_signs, tr, "U", "DiffmanticChange") return highlighted end @@ -506,11 +506,11 @@ function M.enrich(actions, opts) local src_line = action.lines and action.lines.from_line or (sr and sr + 1 or nil) local dst_line = action.lines and action.lines.to_line or (tr and tr + 1 or nil) - touched = append_node_tokens(render.src_spans, src_buf, src_node, "DiffMoveText", nil) or touched - touched = append_node_tokens(render.dst_spans, dst_buf, dst_node, "DiffMoveText", nil) or touched + touched = append_node_tokens(render.src_spans, src_buf, src_node, "DiffmanticMove", nil) or touched + touched = append_node_tokens(render.dst_spans, dst_buf, dst_node, "DiffmanticMove", nil) or touched if sr and sc then - append_sign(render.src_signs, sr, "M", "DiffMoveText") + append_sign(render.src_signs, sr, "M", "DiffmanticMove") append_virt( render.src_virt, sr, @@ -522,7 +522,7 @@ function M.enrich(actions, opts) touched = true end if tr and tc then - append_sign(render.dst_signs, tr, "M", "DiffMoveText") + append_sign(render.dst_signs, tr, "M", "DiffmanticMove") append_virt(render.dst_virt, tr, tc, string.format(" ⤶ from L%d", src_line or 0), "Comment", "eol") touched = true end @@ -530,16 +530,16 @@ function M.enrich(actions, opts) local sr, _, _, sec = node_range(src_node) local tr, _, _, tec = node_range(dst_node) - touched = append_node_tokens(render.src_spans, src_buf, src_node, "DiffRenameText", nil) or touched - touched = append_node_tokens(render.dst_spans, dst_buf, dst_node, "DiffRenameText", nil) or touched + touched = append_node_tokens(render.src_spans, src_buf, src_node, "DiffmanticRename", nil) or touched + touched = append_node_tokens(render.dst_spans, dst_buf, dst_node, "DiffmanticRename", nil) or touched if sr and sec and action.to then - append_sign(render.src_signs, sr, "R", "DiffRenameText") + append_sign(render.src_signs, sr, "R", "DiffmanticRename") append_virt(render.src_virt, sr, sec, " -> " .. action.to, "Comment", "inline") touched = true end if tr and tec and action.from then - append_sign(render.dst_signs, tr, "R", "DiffRenameText") + append_sign(render.dst_signs, tr, "R", "DiffmanticRename") append_virt(render.dst_virt, tr, tec, string.format(" (was %s)", action.from), "Comment", "inline") touched = true end @@ -595,17 +595,17 @@ function M.enrich(actions, opts) end end elseif action.type == "delete" and src_node then - touched = append_node_tokens(render.src_spans, src_buf, src_node, "DiffDeleteText", nil) or touched + touched = append_node_tokens(render.src_spans, src_buf, src_node, "DiffmanticDelete", nil) or touched local sr = node_range(src_node) if sr then - append_sign(render.src_signs, sr, "-", "DiffDeleteText") + append_sign(render.src_signs, sr, "-", "DiffmanticDelete") touched = true end elseif action.type == "insert" and dst_node then - touched = append_node_tokens(render.dst_spans, dst_buf, dst_node, "DiffAddText", nil) or touched + touched = append_node_tokens(render.dst_spans, dst_buf, dst_node, "DiffmanticAdd", nil) or touched local tr = node_range(dst_node) if tr then - append_sign(render.dst_signs, tr, "+", "DiffAddText") + append_sign(render.dst_signs, tr, "+", "DiffmanticAdd") touched = true end end diff --git a/lua/diffmantic/init.lua b/lua/diffmantic/init.lua index 5723d1a..21bd850 100644 --- a/lua/diffmantic/init.lua +++ b/lua/diffmantic/init.lua @@ -3,23 +3,71 @@ local core = require("diffmantic.core") local ui = require("diffmantic.ui") local debug_utils = require("diffmantic.debug_utils") +local function hl(name) + local ok, value = pcall(vim.api.nvim_get_hl, 0, { name = name, link = false }) + if not ok then + return {} + end + return value or {} +end + +local function pick_bg(names) + for _, name in ipairs(names) do + local value = hl(name).bg + if value then + return value + end + end + return nil +end + +local function pick_fg(names, fallback) + for _, name in ipairs(names) do + local value = hl(name).fg + if value then + return value + end + end + return fallback +end + local function setup_highlights() - local add_fg = vim.api.nvim_get_hl(0, { name = "DiffAdd" }).fg or 0xa6e3a1 - local delete_fg = vim.api.nvim_get_hl(0, { name = "DiffDelete" }).fg or 0xf38ba8 - local change_fg = vim.api.nvim_get_hl(0, { name = "DiffChange" }).fg or 0xf9e2af - - vim.api.nvim_set_hl(0, "DiffAddText", { fg = add_fg, bg = "NONE", ctermbg = "NONE" }) - vim.api.nvim_set_hl(0, "DiffDeleteText", { fg = delete_fg, bg = "NONE", ctermbg = "NONE" }) - vim.api.nvim_set_hl(0, "DiffChangeText", { fg = change_fg, bg = "NONE", ctermbg = "NONE" }) - vim.api.nvim_set_hl(0, "DiffMoveText", { fg = 0x89b4fa, bg = "NONE", ctermbg = "NONE" }) - vim.api.nvim_set_hl(0, "DiffRenameText", { fg = change_fg, bg = "NONE", ctermbg = "NONE", underline = true }) + local add_bg = pick_bg({ "DiffAdd", "DiffText" }) + local delete_bg = pick_bg({ "DiffDelete", "DiffText" }) + local change_bg = pick_bg({ "DiffText", "DiffChange" }) + local move_bg = pick_bg({ "DiffText", "DiffChange" }) + + local add_sign_fg = pick_fg({ "DiffAdd" }, 0x49D17D) + local delete_sign_fg = pick_fg({ "DiffDelete" }, 0xFF6B6B) + local change_sign_fg = pick_fg({ "DiagnosticWarn", "DiffChange" }, 0xE8C95A) + local move_sign_fg = pick_fg({ "DiagnosticInfo", "DiffText" }, 0x5AA2FF) + + vim.api.nvim_set_hl(0, "DiffmanticAdd", { fg = add_sign_fg, bg = add_bg }) + vim.api.nvim_set_hl(0, "DiffmanticDelete", { fg = delete_sign_fg, bg = delete_bg }) + vim.api.nvim_set_hl(0, "DiffmanticChange", { fg = change_sign_fg, bg = change_bg }) + vim.api.nvim_set_hl(0, "DiffmanticMove", { fg = move_sign_fg, bg = move_bg }) + vim.api.nvim_set_hl(0, "DiffmanticRename", { fg = change_sign_fg, underline = true, bold = true, italic = true }) + + vim.api.nvim_set_hl(0, "DiffmanticAddSign", { fg = add_sign_fg, bg = "NONE" }) + vim.api.nvim_set_hl(0, "DiffmanticDeleteSign", { fg = delete_sign_fg, bg = "NONE" }) + vim.api.nvim_set_hl(0, "DiffmanticChangeSign", { fg = change_sign_fg, bg = "NONE" }) + vim.api.nvim_set_hl(0, "DiffmanticMoveSign", { fg = move_sign_fg, bg = "NONE" }) + vim.api.nvim_set_hl(0, "DiffmanticRenameSign", { fg = change_sign_fg, bg = "NONE" }) + end function M.setup(opts) setup_highlights() + + local aug = vim.api.nvim_create_augroup("diffmantic_highlights", { clear = true }) + vim.api.nvim_create_autocmd("ColorScheme", { + group = aug, + callback = setup_highlights, + }) end function M.diff(args) + setup_highlights() local parts = vim.split(args, " ", { trimempty = true }) if #parts == 0 then print("Please provide one or two files paths to compare.") @@ -28,36 +76,27 @@ function M.diff(args) local file1, file2 = parts[1], parts[2] local buf1, buf2 + local win1, win2 if file2 then -- Case: 2 files provided. Open them in split. vim.cmd("tabnew") vim.cmd("edit " .. file1) buf1 = vim.api.nvim_get_current_buf() - local win1 = vim.api.nvim_get_current_win() + win1 = vim.api.nvim_get_current_win() vim.cmd("vsplit " .. file2) buf2 = vim.api.nvim_get_current_buf() - local win2 = vim.api.nvim_get_current_win() - - vim.wo[win1].scrollbind = true - vim.wo[win1].cursorbind = true - vim.wo[win2].scrollbind = true - vim.wo[win2].cursorbind = true + win2 = vim.api.nvim_get_current_win() else -- Case: 1 file provided. Compare current buffer vs file. buf1 = vim.api.nvim_get_current_buf() - local win1 = vim.api.nvim_get_current_win() + win1 = vim.api.nvim_get_current_win() local expanded_path = vim.fn.expand(file1) vim.cmd("vsplit " .. expanded_path) buf2 = vim.api.nvim_get_current_buf() - local win2 = vim.api.nvim_get_current_win() - - vim.wo[win1].scrollbind = true - vim.wo[win1].cursorbind = true - vim.wo[win2].scrollbind = true - vim.wo[win2].cursorbind = true + win2 = vim.api.nvim_get_current_win() end local lang = vim.treesitter.language.get_lang(vim.bo[buf1].filetype) @@ -94,6 +133,13 @@ function M.diff(args) -- debug_utils.print_actions(actions, buf1, buf2) -- debug_utils.print_mappings(mappings, src_info, dst_info, buf1, buf2) ui.apply_highlights(buf1, buf2, actions) + + vim.api.nvim_win_set_cursor(win1, { 1, 0 }) + vim.api.nvim_win_set_cursor(win2, { 1, 0 }) + vim.wo[win1].scrollbind = true + vim.wo[win1].cursorbind = true + vim.wo[win2].scrollbind = true + vim.wo[win2].cursorbind = true end return M diff --git a/lua/diffmantic/ui/renderer.lua b/lua/diffmantic/ui/renderer.lua index 971582f..54d12a2 100644 --- a/lua/diffmantic/ui/renderer.lua +++ b/lua/diffmantic/ui/renderer.lua @@ -3,11 +3,11 @@ local signs = require("diffmantic.ui.signs") local M = {} local HL_PRIORITY = { - DiffMoveText = 10, - DiffAddText = 20, - DiffDeleteText = 20, - DiffChangeText = 30, - DiffRenameText = 40, + DiffmanticMove = 10, + DiffmanticAdd = 20, + DiffmanticDelete = 20, + DiffmanticChange = 30, + DiffmanticRename = 40, } local function set_extmark(buf, ns, row, col, opts) diff --git a/lua/diffmantic/ui/signs.lua b/lua/diffmantic/ui/signs.lua index fff6a9c..6fd5848 100644 --- a/lua/diffmantic/ui/signs.lua +++ b/lua/diffmantic/ui/signs.lua @@ -1,19 +1,19 @@ local M = {} local SIGN_GROUP_BY_TEXT_GROUP = { - DiffAddText = "DiffAddSign", - DiffDeleteText = "DiffDeleteSign", - DiffChangeText = "DiffChangeSign", - DiffMoveText = "DiffMoveSign", - DiffRenameText = "DiffRenameSign", + DiffmanticAdd = "DiffmanticAddSign", + DiffmanticDelete = "DiffmanticDeleteSign", + DiffmanticChange = "DiffmanticChangeSign", + DiffmanticMove = "DiffmanticMoveSign", + DiffmanticRename = "DiffmanticRenameSign", } local SIGN_PRIORITY_BY_TEXT_GROUP = { - DiffAddText = 10, - DiffDeleteText = 10, - DiffChangeText = 20, - DiffMoveText = 30, - DiffRenameText = 40, + DiffmanticAdd = 10, + DiffmanticDelete = 10, + DiffmanticChange = 20, + DiffmanticMove = 30, + DiffmanticRename = 40, } function M.glyph() From 6ddde7597cd65452b7f5c5e299ae32026fce8c8f Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Fri, 13 Feb 2026 11:01:13 +0530 Subject: [PATCH 16/48] feat(ui): added visual filler lines for improved diff alignment - added src_fillers/dst_fillers to payload output structure - generating filler entries for deletions/insertion/movement - handle trailing blank lines and top level nodes - applied fillers as virtual text as the native filler lines seem to be a native diff feature --- lua/diffmantic/core/payload.lua | 64 ++++++++++ lua/diffmantic/init.lua | 4 + lua/diffmantic/ui/filler.lua | 201 ++++++++++++++++++++++++++++++++ lua/diffmantic/ui/renderer.lua | 25 ++++ 4 files changed, 294 insertions(+) create mode 100644 lua/diffmantic/ui/filler.lua diff --git a/lua/diffmantic/core/payload.lua b/lua/diffmantic/core/payload.lua index d671bb3..ab7fcc4 100644 --- a/lua/diffmantic/core/payload.lua +++ b/lua/diffmantic/core/payload.lua @@ -266,6 +266,8 @@ local function build_internal_diff(src_node, dst_node, src_buf, dst_buf, rename_ dst_spans = {}, src_signs = {}, dst_signs = {}, + src_fillers = {}, + dst_fillers = {}, } local function base_col_for_row(row, start_row, start_col) @@ -394,6 +396,7 @@ local function build_internal_diff(src_node, dst_node, src_buf, dst_buf, rename_ end if count_a > overlap and not only_changes then + local del_count = count_a - overlap for i = overlap, count_a - 1 do local src_row = sr + start_a - 1 + i local s_line = src_lines[start_a + i] @@ -402,9 +405,18 @@ local function build_internal_diff(src_node, dst_node, src_buf, dst_buf, rename_ did_highlight = mark_line_tokens(src_buf, out.src_spans, src_row, s_line, src_base, "DiffmanticDelete") or did_highlight end + local dst_anchor = count_b > 0 + and (tr + start_b - 1 + overlap) + or (tr + start_b) + table.insert(out.dst_fillers, { + row = dst_anchor, + count = del_count, + hl_group = "DiffmanticDeleteFiller", + }) end if count_b > overlap and not only_changes then + local ins_count = count_b - overlap for i = overlap, count_b - 1 do local dst_row = tr + start_b - 1 + i local d_line = dst_lines[start_b + i] @@ -413,6 +425,14 @@ local function build_internal_diff(src_node, dst_node, src_buf, dst_buf, rename_ did_highlight = mark_line_tokens(dst_buf, out.dst_spans, dst_row, d_line, dst_base, "DiffmanticAdd") or did_highlight end + local src_anchor = count_a > 0 + and (sr + start_a - 1 + overlap) + or (sr + start_a) + table.insert(out.src_fillers, { + row = src_anchor, + count = ins_count, + hl_group = "DiffmanticAddFiller", + }) end end else @@ -472,6 +492,8 @@ function M.enrich(actions, opts) local rename_map = {} local rename_src_nodes = {} local rename_dst_nodes = {} + local moved_src_ranges = {} -- { {sr, er}, ... } + local moved_dst_ranges = {} -- { {sr, er}, ... } for _, action in ipairs(actions) do if action.type == "rename" then @@ -484,6 +506,15 @@ function M.enrich(actions, opts) if action.dst_node then rename_dst_nodes[action.dst_node:id()] = true end + elseif action.type == "move" then + if action.src_node then + local sr, _, er, _ = action.src_node:range() + table.insert(moved_src_ranges, { sr, er }) + end + if action.dst_node then + local sr, _, er, _ = action.dst_node:range() + table.insert(moved_dst_ranges, { sr, er }) + end end end @@ -591,6 +622,39 @@ function M.enrich(actions, opts) for _, sign in ipairs(diff_ops.dst_signs) do table.insert(render.dst_signs, sign) end + local is_moved = false + if src_node then + local nsr, _, ner, _ = src_node:range() + for _, r in ipairs(moved_src_ranges) do + if nsr >= r[1] and ner <= r[2] then + is_moved = true + break + end + end + end + if not is_moved and dst_node then + local nsr, _, ner, _ = dst_node:range() + for _, r in ipairs(moved_dst_ranges) do + if nsr >= r[1] and ner <= r[2] then + is_moved = true + break + end + end + end + if not is_moved then + if diff_ops.src_fillers then + render.src_fillers = render.src_fillers or {} + for _, f in ipairs(diff_ops.src_fillers) do + table.insert(render.src_fillers, f) + end + end + if diff_ops.dst_fillers then + render.dst_fillers = render.dst_fillers or {} + for _, f in ipairs(diff_ops.dst_fillers) do + table.insert(render.dst_fillers, f) + end + end + end touched = true end end diff --git a/lua/diffmantic/init.lua b/lua/diffmantic/init.lua index 21bd850..2397ee2 100644 --- a/lua/diffmantic/init.lua +++ b/lua/diffmantic/init.lua @@ -54,6 +54,10 @@ local function setup_highlights() vim.api.nvim_set_hl(0, "DiffmanticMoveSign", { fg = move_sign_fg, bg = "NONE" }) vim.api.nvim_set_hl(0, "DiffmanticRenameSign", { fg = change_sign_fg, bg = "NONE" }) + vim.api.nvim_set_hl(0, "DiffmanticAddFiller", { fg = add_sign_fg, bg = add_bg }) + vim.api.nvim_set_hl(0, "DiffmanticDeleteFiller", { fg = delete_sign_fg, bg = delete_bg }) + vim.api.nvim_set_hl(0, "DiffmanticMoveFiller", { fg = move_sign_fg, bg = move_bg }) + end function M.setup(opts) diff --git a/lua/diffmantic/ui/filler.lua b/lua/diffmantic/ui/filler.lua new file mode 100644 index 0000000..d77b446 --- /dev/null +++ b/lua/diffmantic/ui/filler.lua @@ -0,0 +1,201 @@ +local M = {} + +local VIRT_LINE_LEN = 300 + +local function make_virt_line(hl_group, char) + return { { string.rep(char or "╱", VIRT_LINE_LEN), hl_group } } +end + +local function trailing_blanks(buf, end_row, end_col) + local last_occupied = end_col > 0 and end_row or (end_row - 1) + local buf_count = vim.api.nvim_buf_line_count(buf) + local count = 0 + local row = last_occupied + 1 + while row < buf_count do + local lines = vim.api.nvim_buf_get_lines(buf, row, row + 1, false) + if lines[1] and lines[1]:match("^%s*$") then + count = count + 1 + row = row + 1 + else + break + end + end + return count +end + +local function is_top_level(node) + if not node then + return false + end + local parent = node:parent() + return parent ~= nil and parent:parent() == nil +end + +function M.compute(actions, src_buf, dst_buf) + local src_fillers = {} + local dst_fillers = {} + + for _, action in ipairs(actions) do + local t = action.type + + if t == "insert" and action.dst_node and is_top_level(action.dst_node) then + local sr, _, er, ec = action.dst_node:range() + local count = er - sr + if ec > 0 then + count = count + 1 + end + count = count + trailing_blanks(dst_buf, er, ec) + if count > 0 then + table.insert(src_fillers, { + row = sr, + count = count, + hl_group = "DiffmanticAddFiller", + }) + end + elseif t == "delete" and action.src_node and is_top_level(action.src_node) then + local sr, _, er, ec = action.src_node:range() + local count = er - sr + if ec > 0 then + count = count + 1 + end + count = count + trailing_blanks(src_buf, er, ec) + if count > 0 then + table.insert(dst_fillers, { + row = sr, + count = count, + hl_group = "DiffmanticDeleteFiller", + }) + end + elseif t == "move" and action.src_node and action.dst_node and is_top_level(action.src_node) then + local ssr, _, ser, sec = action.src_node:range() + local src_body = ser - ssr + if sec > 0 then + src_body = src_body + 1 + end + local src_trailing = trailing_blanks(src_buf, ser, sec) + local src_count = src_body + src_trailing + + local dsr, _, der, dec = action.dst_node:range() + local dst_body = der - dsr + if dec > 0 then + dst_body = dst_body + 1 + end + local dst_trailing = trailing_blanks(dst_buf, der, dec) + local dst_count = dst_body + dst_trailing + + local src_text = vim.treesitter.get_node_text(action.src_node, src_buf) + local dst_text = vim.treesitter.get_node_text(action.dst_node, dst_buf) + local ok, hunks = pcall(vim.text.diff, src_text, dst_text, { + result_type = "indices", + linematch = 60, + }) + + local dst_inserted = {} + local src_deleted = {} + if ok and hunks then + for _, h in ipairs(hunks) do + local start_a, count_a, start_b, count_b = h[1], h[2], h[3], h[4] + local overlap = math.min(count_a, count_b) + if count_b > overlap then + for i = start_b + overlap, start_b + count_b - 1 do + dst_inserted[i] = true + end + end + if count_a > overlap then + for i = start_a + overlap, start_a + count_a - 1 do + src_deleted[i] = true + end + end + end + end + + if dst_count > 0 then + local lines = {} + for i = 1, dst_body do + lines[i] = dst_inserted[i] and "DiffmanticAddFiller" or "DiffmanticMoveFiller" + end + for i = dst_body + 1, dst_count do + lines[i] = "DiffmanticMoveFiller" + end + table.insert(src_fillers, { + row = dsr, + lines = lines, + }) + end + + if src_count > 0 then + local src_block_end = sec > 0 and (ser + 1) or ser + local best_dst_row = nil + local best_src_row = math.huge + for _, other in ipairs(actions) do + if other ~= action and other.src_node and other.dst_node then + local osr = select(1, other.src_node:range()) + local odr = select(1, other.dst_node:range()) + if osr >= src_block_end and osr < best_src_row then + best_src_row = osr + best_dst_row = odr + end + end + end + if best_dst_row then + local lines = {} + for i = 1, src_body do + lines[i] = src_deleted[i] and "DiffmanticDeleteFiller" or "DiffmanticMoveFiller" + end + for i = src_body + 1, src_count do + lines[i] = "DiffmanticMoveFiller" + end + table.insert(dst_fillers, { + row = best_dst_row, + lines = lines, + }) + end + end + end + end + + table.sort(src_fillers, function(a, b) + return a.row < b.row + end) + table.sort(dst_fillers, function(a, b) + return a.row < b.row + end) + + return src_fillers, dst_fillers +end + +function M.apply(buf, ns, fillers) + if not fillers or #fillers == 0 then + return + end + + local line_count = vim.api.nvim_buf_line_count(buf) + + for _, filler in ipairs(fillers) do + local row = filler.row + if row >= line_count then + row = line_count - 1 + end + if row < 0 then + row = 0 + end + + local virt_lines = {} + if filler.lines then + for i, hl in ipairs(filler.lines) do + virt_lines[i] = make_virt_line(hl) + end + else + for i = 1, filler.count do + virt_lines[i] = make_virt_line(filler.hl_group) + end + end + + pcall(vim.api.nvim_buf_set_extmark, buf, ns, row, 0, { + virt_lines = virt_lines, + virt_lines_above = true, + }) + end +end + +return M diff --git a/lua/diffmantic/ui/renderer.lua b/lua/diffmantic/ui/renderer.lua index 54d12a2..2499a5b 100644 --- a/lua/diffmantic/ui/renderer.lua +++ b/lua/diffmantic/ui/renderer.lua @@ -1,4 +1,5 @@ local signs = require("diffmantic.ui.signs") +local filler = require("diffmantic.ui.filler") local M = {} @@ -65,6 +66,8 @@ end function M.render(src_buf, dst_buf, actions, ns) local src_sign_rows = {} local dst_sign_rows = {} + local extra_src_fillers = {} + local extra_dst_fillers = {} for _, action in ipairs(actions) do local render = action.render @@ -75,9 +78,31 @@ function M.render(src_buf, dst_buf, actions, ns) apply_signs(dst_buf, ns, render.dst_signs, dst_sign_rows) apply_virt(src_buf, ns, render.src_virt) apply_virt(dst_buf, ns, render.dst_virt) + + if render.src_fillers then + for _, f in ipairs(render.src_fillers) do + table.insert(extra_src_fillers, f) + end + end + if render.dst_fillers then + for _, f in ipairs(render.dst_fillers) do + table.insert(extra_dst_fillers, f) + end + end end end + local src_fillers, dst_fillers = filler.compute(actions, src_buf, dst_buf) + + for _, f in ipairs(extra_src_fillers) do + table.insert(src_fillers, f) + end + for _, f in ipairs(extra_dst_fillers) do + table.insert(dst_fillers, f) + end + + filler.apply(src_buf, ns, src_fillers) + filler.apply(dst_buf, ns, dst_fillers) end return M From ee3b7b2cd4d480dcd6ed54cf6c4337bacfc045b2 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Fri, 13 Feb 2026 11:08:39 +0530 Subject: [PATCH 17/48] updated the diff viewer image --- images/python.png | Bin 123424 -> 325076 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/images/python.png b/images/python.png index c451cb38b8b79d8b8382f91ee2258335daeb54cf..4c3d104200c154b3a0d972d2ec6bbabc1bafcf7b 100644 GIT binary patch literal 325076 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 z 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

by;v6((TMdpA8dy8XLf|p=R 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

>pv)7qJx_=<(zrn2U& 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 From a459f4431c554addffa46cffb53bfc046e497f8c Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Fri, 13 Feb 2026 17:30:17 +0530 Subject: [PATCH 18/48] refactor(core): normalized action payload and move enrichment to analysis.lua Normalized action objects around src/dst/metadata so actions.lua becomes core payload source. Replaced the update semantic storage from action.semantic to action.analysis --- lua/diffmantic/core/actions.lua | 148 +++++++----- lua/diffmantic/core/analysis.lua | 384 ++++++++++++++++++++++++++++++ lua/diffmantic/core/bottom_up.lua | 10 +- 3 files changed, 478 insertions(+), 64 deletions(-) create mode 100644 lua/diffmantic/core/analysis.lua diff --git a/lua/diffmantic/core/actions.lua b/lua/diffmantic/core/actions.lua index 0c01fe4..57020ec 100644 --- a/lua/diffmantic/core/actions.lua +++ b/lua/diffmantic/core/actions.lua @@ -1,7 +1,7 @@ local M = {} local semantic = require("diffmantic.core.semantic") local roles = require("diffmantic.core.roles") -local payload = require("diffmantic.core.payload") +local analysis = require("diffmantic.core.analysis") local function range_metadata(node) if not node then @@ -21,19 +21,29 @@ end local function build_action(action_type, src_node, dst_node, extra) local src_range = range_metadata(src_node) local dst_range = range_metadata(dst_node) + local node = src_node or dst_node + local from_line = src_range and src_range.start_line or nil + local to_line = dst_range and dst_range.start_line or nil local action = { type = action_type, - kind = action_type, - node = src_node or dst_node, - target = (src_node and dst_node) and dst_node or nil, src_node = src_node, dst_node = dst_node, src_range = src_range, dst_range = dst_range, + src = src_range and vim.tbl_extend("force", {}, src_range, { text = nil }) or nil, + dst = dst_range and vim.tbl_extend("force", {}, dst_range, { text = nil }) or nil, lines = { - from_line = src_range and src_range.start_line or nil, - to_line = dst_range and dst_range.start_line or nil, + from_line = from_line, + to_line = to_line, + }, + metadata = { + node_type = node and node:type() or nil, + old_name = nil, + new_name = nil, + from_line = from_line, + to_line = to_line, + suppressed_renames = nil, }, } @@ -41,6 +51,13 @@ local function build_action(action_type, src_node, dst_node, extra) for key, value in pairs(extra) do action[key] = value end + action.metadata.old_name = extra.old_name or extra.from or action.metadata.old_name + action.metadata.new_name = extra.new_name or extra.to or action.metadata.new_name + action.metadata.from_line = extra.from_line or action.metadata.from_line + action.metadata.to_line = extra.to_line or action.metadata.to_line + if extra.context and extra.context.suppressed_usages then + action.metadata.suppressed_renames = extra.context.suppressed_usages + end end return action @@ -65,11 +82,6 @@ local function build_summary(actions_list) deletes = {}, } - local function action_node_type(action) - local node = action.src_node or action.dst_node - return node and node:type() or nil - end - for _, action in ipairs(actions_list) do local t = action.type if summary.counts[t] ~= nil then @@ -78,25 +90,26 @@ local function build_summary(actions_list) if t == "move" then table.insert(summary.moves, { - node_type = action_node_type(action), - from_line = action.lines and action.lines.from_line or nil, - to_line = action.lines and action.lines.to_line or nil, - src_range = action.src_range, - dst_range = action.dst_range, + node_type = action.metadata and action.metadata.node_type or nil, + from_line = action.metadata and action.metadata.from_line or (action.lines and action.lines.from_line or nil), + to_line = action.metadata and action.metadata.to_line or (action.lines and action.lines.to_line or nil), + src_range = action.src or action.src_range, + dst_range = action.dst or action.dst_range, }) elseif t == "rename" then - local suppressed_usages = action.context and action.context.suppressed_usages or {} + local suppressed_usages = action.metadata and action.metadata.suppressed_renames + or (action.context and action.context.suppressed_usages or {}) local suppressed_count = #suppressed_usages summary.counts.rename_suppressed = summary.counts.rename_suppressed + suppressed_count table.insert(summary.renames, { - node_type = action_node_type(action), - from = action.from, - to = action.to, - from_line = action.lines and action.lines.from_line or nil, - to_line = action.lines and action.lines.to_line or nil, - src_range = action.src_range, - dst_range = action.dst_range, + node_type = action.metadata and action.metadata.node_type or nil, + from = action.metadata and action.metadata.old_name or action.from, + to = action.metadata and action.metadata.new_name or action.to, + from_line = action.metadata and action.metadata.from_line or (action.lines and action.lines.from_line or nil), + to_line = action.metadata and action.metadata.to_line or (action.lines and action.lines.to_line or nil), + src_range = action.src or action.src_range, + dst_range = action.dst or action.dst_range, suppressed_usage_count = suppressed_count, }) @@ -106,35 +119,35 @@ local function build_summary(actions_list) to = usage.to, from_line = usage.lines and usage.lines.from_line or nil, to_line = usage.lines and usage.lines.to_line or nil, - src_range = usage.src_range, - dst_range = usage.dst_range, + src_range = usage.src or usage.src_range, + dst_range = usage.dst or usage.dst_range, suppressed_by = { - from = action.from, - to = action.to, - from_line = action.lines and action.lines.from_line or nil, - to_line = action.lines and action.lines.to_line or nil, + from = action.metadata and action.metadata.old_name or action.from, + to = action.metadata and action.metadata.new_name or action.to, + from_line = action.metadata and action.metadata.from_line or (action.lines and action.lines.from_line or nil), + to_line = action.metadata and action.metadata.to_line or (action.lines and action.lines.to_line or nil), }, }) end elseif t == "update" then table.insert(summary.updates, { - node_type = action_node_type(action), - from_line = action.lines and action.lines.from_line or nil, - to_line = action.lines and action.lines.to_line or nil, - src_range = action.src_range, - dst_range = action.dst_range, + node_type = action.metadata and action.metadata.node_type or nil, + from_line = action.metadata and action.metadata.from_line or (action.lines and action.lines.from_line or nil), + to_line = action.metadata and action.metadata.to_line or (action.lines and action.lines.to_line or nil), + src_range = action.src or action.src_range, + dst_range = action.dst or action.dst_range, }) elseif t == "insert" then table.insert(summary.inserts, { - node_type = action_node_type(action), - line = action.lines and action.lines.to_line or nil, - dst_range = action.dst_range, + node_type = action.metadata and action.metadata.node_type or nil, + line = action.metadata and action.metadata.to_line or (action.lines and action.lines.to_line or nil), + dst_range = action.dst or action.dst_range, }) elseif t == "delete" then table.insert(summary.deletes, { - node_type = action_node_type(action), - line = action.lines and action.lines.from_line or nil, - src_range = action.src_range, + node_type = action.metadata and action.metadata.node_type or nil, + line = action.metadata and action.metadata.from_line or (action.lines and action.lines.from_line or nil), + src_range = action.src or action.src_range, }) end end @@ -180,8 +193,8 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end for _, action in ipairs(actions_list) do - local src_node = action.src_node or action.node - local dst_node = action.dst_node or action.target + local src_node = action.src_node + local dst_node = action.dst_node if action.type == "update" and src_node and dst_node then local leaf_changes = semantic.find_leaf_changes(src_node, dst_node, src_buf, dst_buf) local rename_pairs = {} @@ -194,7 +207,7 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end end - action.semantic = { + action.analysis = { leaf_changes = leaf_changes, rename_pairs = rename_pairs, } @@ -301,8 +314,8 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op -- Pass 1a: high-confidence declaration-like rename seeds from semantic leaf changes. for _, action in ipairs(actions_list) do - if action.type == "update" and action.semantic and action.semantic.leaf_changes then - for _, change in ipairs(action.semantic.leaf_changes) do + if action.type == "update" and action.analysis and action.analysis.leaf_changes then + for _, change in ipairs(action.analysis.leaf_changes) do local src_node = change.src_node local dst_node = change.dst_node if src_node and dst_node and change.src_text ~= change.dst_text and is_decl_rename(src_node, dst_node) then @@ -326,8 +339,8 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op add_seed(s.text, d.text) if is_decl_rename(s.node, d.node) then push_rename(s.node, d.node, s.text, d.text, { - src_parent_type = action.node and action.node:type() or nil, - dst_parent_type = action.target and action.target:type() or nil, + src_parent_type = action.src_node and action.src_node:type() or nil, + dst_parent_type = action.dst_node and action.dst_node:type() or nil, source = "parameter_positional", declaration = true, }) @@ -406,8 +419,8 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op -- Pass 2: emit leaf rename actions gated by seeds (and declaration renames). for _, action in ipairs(actions_list) do - if action.type == "update" and action.semantic and action.semantic.leaf_changes then - for _, change in ipairs(action.semantic.leaf_changes) do + if action.type == "update" and action.analysis and action.analysis.leaf_changes then + for _, change in ipairs(action.analysis.leaf_changes) do local src_node = change.src_node local dst_node = change.dst_node local src_text = change.src_text @@ -417,8 +430,8 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op local key = pair_key(src_text, dst_text) if seed_pairs[key] or is_decl then push_rename(src_node, dst_node, src_text, dst_text, { - src_parent_type = action.node and action.node:type() or nil, - dst_parent_type = action.target and action.target:type() or nil, + src_parent_type = action.src_node and action.src_node:type() or nil, + dst_parent_type = action.dst_node and action.dst_node:type() or nil, declaration = is_decl, }) end @@ -503,25 +516,31 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op -- If a declaration rename exists for a pair, suppress usage-level duplicates for that pair. local declaration_pairs = {} for _, rename_action in ipairs(renames) do + local from_name = rename_action.metadata and rename_action.metadata.old_name or rename_action.from + local to_name = rename_action.metadata and rename_action.metadata.new_name or rename_action.to if rename_action.context and rename_action.context.declaration then - declaration_pairs[pair_key(rename_action.from, rename_action.to)] = true + declaration_pairs[pair_key(from_name, to_name)] = true end end local suppressed_by_pair = {} local filtered_renames = {} for _, rename_action in ipairs(renames) do - local key = pair_key(rename_action.from, rename_action.to) + local from_name = rename_action.metadata and rename_action.metadata.old_name or rename_action.from + local to_name = rename_action.metadata and rename_action.metadata.new_name or rename_action.to + local key = pair_key(from_name, to_name) local is_declaration = rename_action.context and rename_action.context.declaration if not declaration_pairs[key] or is_declaration then table.insert(filtered_renames, rename_action) else suppressed_by_pair[key] = suppressed_by_pair[key] or {} table.insert(suppressed_by_pair[key], { - from = rename_action.from, - to = rename_action.to, - src_range = rename_action.src_range, - dst_range = rename_action.dst_range, + from = from_name, + to = to_name, + src = rename_action.src, + dst = rename_action.dst, + src_range = rename_action.src or rename_action.src_range, + dst_range = rename_action.dst or rename_action.dst_range, lines = rename_action.lines, context = rename_action.context, }) @@ -530,11 +549,16 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op for _, rename_action in ipairs(filtered_renames) do if rename_action.context and rename_action.context.declaration then - local key = pair_key(rename_action.from, rename_action.to) + local from_name = rename_action.metadata and rename_action.metadata.old_name or rename_action.from + local to_name = rename_action.metadata and rename_action.metadata.new_name or rename_action.to + local key = pair_key(from_name, to_name) local suppressed_usages = suppressed_by_pair[key] if suppressed_usages and #suppressed_usages > 0 then rename_action.context.suppressed_usages = suppressed_usages rename_action.context.suppressed_usage_count = #suppressed_usages + if rename_action.metadata then + rename_action.metadata.suppressed_renames = suppressed_usages + end end end end @@ -851,14 +875,14 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op emit_rename_actions(actions) stop_timer(rename_start, "renames") - local render_start = start_timer() + local analysis_start = start_timer() if src_buf and dst_buf then - payload.enrich(actions, { + analysis.enrich(actions, { src_buf = src_buf, dst_buf = dst_buf, }) end - stop_timer(render_start, "payload") + stop_timer(analysis_start, "analysis") local summary_start = start_timer() local summary = build_summary(actions) diff --git a/lua/diffmantic/core/analysis.lua b/lua/diffmantic/core/analysis.lua new file mode 100644 index 0000000..4d52539 --- /dev/null +++ b/lua/diffmantic/core/analysis.lua @@ -0,0 +1,384 @@ +local semantic = require("diffmantic.core.semantic") + +local M = {} + +local function range_metadata(node) + if not node then + return nil + end + local sr, sc, er, ec = node:range() + return { + start_row = sr, + start_col = sc, + end_row = er, + end_col = ec, + start_line = sr + 1, + end_line = er + 1, + } +end + +local function clone_range(r) + if not r then + return nil + end + return { + start_row = r.start_row, + start_col = r.start_col, + end_row = r.end_row, + end_col = r.end_col, + start_line = r.start_line, + end_line = r.end_line, + } +end + +local function line_text(buf, row) + local lines = vim.api.nvim_buf_get_lines(buf, row, row + 1, false) + return lines[1] or "" +end + +local function base_col_for_row(row, start_row, start_col) + if row == start_row then + return start_col + end + return 0 +end + +local function tokenize_line(text) + local tokens = {} + local i = 1 + local len = #text + while i <= len do + local ch = text:sub(i, i) + if ch:match("%s") then + i = i + 1 + elseif ch:match("[%w_]") then + local j = i + 1 + while j <= len and text:sub(j, j):match("[%w_]") do + j = j + 1 + end + table.insert(tokens, { text = text:sub(i, j - 1), start_col = i, end_col = j - 1 }) + i = j + else + local j = i + 1 + while j <= len and not text:sub(j, j):match("[%w_%s]") do + j = j + 1 + end + table.insert(tokens, { text = text:sub(i, j - 1), start_col = i, end_col = j - 1 }) + i = j + end + end + return tokens +end + +local function tokens_equal(a, b, rename_map) + if a.text == b.text then + return true + end + if rename_map and rename_map[a.text] == b.text then + return true + end + return false +end + +local function lcs_matches(a, b, rename_map) + local n = #a + local m = #b + if n == 0 or m == 0 then + return {}, {} + end + + local dp = {} + for i = 0, n do + dp[i] = {} + dp[i][0] = 0 + end + for j = 1, m do + dp[0][j] = 0 + end + + for i = 1, n do + for j = 1, m do + if tokens_equal(a[i], b[j], rename_map) then + dp[i][j] = dp[i - 1][j - 1] + 1 + else + local up = dp[i - 1][j] + local left = dp[i][j - 1] + dp[i][j] = (up >= left) and up or left + end + end + end + + local match_a = {} + local match_b = {} + local i = n + local j = m + while i > 0 and j > 0 do + if tokens_equal(a[i], b[j], rename_map) then + match_a[i] = true + match_b[j] = true + i = i - 1 + j = j - 1 + else + local up = dp[i - 1][j] + local left = dp[i][j - 1] + if up >= left then + i = i - 1 + else + j = j - 1 + end + end + end + return match_a, match_b +end + +local function unmatched_token_spans(tokens, matched, source_line) + local spans = {} + local i = 1 + while i <= #tokens do + if matched[i] then + i = i + 1 + else + local start_col = tokens[i].start_col + local end_col = tokens[i].end_col + local j = i + 1 + while j <= #tokens and not matched[j] do + local gap_start = end_col + 1 + local gap_end = tokens[j].start_col - 1 + local gap = "" + if gap_start <= gap_end then + gap = source_line:sub(gap_start, gap_end) + end + if gap ~= "" and not gap:match("^%s+$") then + break + end + end_col = tokens[j].end_col + j = j + 1 + end + table.insert(spans, { start_col = start_col, end_col = end_col }) + i = j + end + end + return spans +end + +local function make_range(row, start_col, end_col) + if row == nil or start_col == nil or end_col == nil or end_col <= start_col then + return nil + end + return { + start_row = row, + start_col = start_col, + end_row = row, + end_col = end_col, + start_line = row + 1, + end_line = row + 1, + } +end + +local function trimmed_range_for_line(buf, row, base_col, line_value) + if not line_value then + return nil + end + local first = line_value:find("%S") + if not first then + return nil + end + local rev_last = line_value:reverse():find("%S") + local last = #line_value - rev_last + 1 + return make_range(row, base_col + first - 1, base_col + last) +end + +local function hunk_change(src_range, dst_range) + return { kind = "change", src = src_range, dst = dst_range } +end + +local function hunk_insert(dst_range) + return { kind = "insert", src = nil, dst = dst_range } +end + +local function hunk_delete(src_range) + return { kind = "delete", src = src_range, dst = nil } +end + +local function fallback_hunks_from_diff(src_node, dst_node, src_buf, dst_buf, rename_pairs) + local src_text = vim.treesitter.get_node_text(src_node, src_buf) + local dst_text = vim.treesitter.get_node_text(dst_node, dst_buf) + if not src_text or not dst_text then + return {} + end + + local src_lines = vim.split(src_text, "\n", { plain = true }) + local dst_lines = vim.split(dst_text, "\n", { plain = true }) + local ok, hunks = pcall(vim.text.diff, src_text, dst_text, { + result_type = "indices", + linematch = 60, + }) + if not ok or not hunks then + return {} + end + + local rename_map = rename_pairs or {} + local sr, sc = src_node:range() + local tr, tc = dst_node:range() + local out = {} + + local function push_change_line(src_row, dst_row, src_line, dst_line) + local src_base = base_col_for_row(src_row, sr, sc) + local dst_base = base_col_for_row(dst_row, tr, tc) + if not src_line or not dst_line or src_line == dst_line then + return + end + + local tokens_src = tokenize_line(src_line) + local tokens_dst = tokenize_line(dst_line) + if #tokens_src > 0 or #tokens_dst > 0 then + local match_src, match_dst = lcs_matches(tokens_src, tokens_dst, rename_map) + local src_spans = unmatched_token_spans(tokens_src, match_src, src_line) + local dst_spans = unmatched_token_spans(tokens_dst, match_dst, dst_line) + local count = math.min(#src_spans, #dst_spans) + for i = 1, count do + local src_range = make_range( + src_row, + src_base + src_spans[i].start_col - 1, + src_base + src_spans[i].end_col + ) + local dst_range = make_range( + dst_row, + dst_base + dst_spans[i].start_col - 1, + dst_base + dst_spans[i].end_col + ) + if src_range and dst_range then + table.insert(out, hunk_change(src_range, dst_range)) + end + end + return + end + + local fragment = semantic.diff_fragment(src_line, dst_line) + if fragment then + local src_range = make_range( + src_row, + src_base + math.max(fragment.old_start - 1, 0), + src_base + math.max(fragment.old_end, 0) + ) + local dst_range = make_range( + dst_row, + dst_base + math.max(fragment.new_start - 1, 0), + dst_base + math.max(fragment.new_end, 0) + ) + if src_range and dst_range then + table.insert(out, hunk_change(src_range, dst_range)) + end + return + end + + local src_range = trimmed_range_for_line(src_buf, src_row, src_base, src_line) + local dst_range = trimmed_range_for_line(dst_buf, dst_row, dst_base, dst_line) + if src_range and dst_range then + table.insert(out, hunk_change(src_range, dst_range)) + end + end + + for _, h in ipairs(hunks) do + local start_a, count_a, start_b, count_b = h[1], h[2], h[3], h[4] + local overlap = math.min(count_a, count_b) + + for i = 0, overlap - 1 do + local src_row = sr + start_a - 1 + i + local dst_row = tr + start_b - 1 + i + push_change_line(src_row, dst_row, src_lines[start_a + i], dst_lines[start_b + i]) + end + + for i = overlap, count_a - 1 do + local src_row = sr + start_a - 1 + i + local src_line = src_lines[start_a + i] + local src_base = base_col_for_row(src_row, sr, sc) + local src_range = trimmed_range_for_line(src_buf, src_row, src_base, src_line) + if src_range then + table.insert(out, hunk_delete(src_range)) + end + end + + for i = overlap, count_b - 1 do + local dst_row = tr + start_b - 1 + i + local dst_line = dst_lines[start_b + i] + local dst_base = base_col_for_row(dst_row, tr, tc) + local dst_range = trimmed_range_for_line(dst_buf, dst_row, dst_base, dst_line) + if dst_range then + table.insert(out, hunk_insert(dst_range)) + end + end + end + + return out +end + +function M.enrich(actions, opts) + local src_buf = opts and opts.src_buf or nil + local dst_buf = opts and opts.dst_buf or nil + if not src_buf or not dst_buf then + return + end + + for _, action in ipairs(actions) do + if action.type == "update" and action.src_node and action.dst_node then + local raw_leaf_changes = action.analysis and action.analysis.leaf_changes or {} + local rename_pairs = action.analysis and action.analysis.rename_pairs or {} + local normalized_leaf = {} + local hunks = {} + + for _, change in ipairs(raw_leaf_changes) do + local src_range = range_metadata(change.src_node) + local dst_range = range_metadata(change.dst_node) + table.insert(normalized_leaf, { + src = clone_range(src_range), + dst = clone_range(dst_range), + src_text = change.src_text, + dst_text = change.dst_text, + }) + + if change.src_text ~= change.dst_text and rename_pairs[change.src_text] ~= change.dst_text then + local hunk_src = src_range + local hunk_dst = dst_range + + if + src_range + and dst_range + and src_range.start_row == src_range.end_row + and dst_range.start_row == dst_range.end_row + then + local fragment = semantic.diff_fragment(change.src_text or "", change.dst_text or "") + if fragment then + hunk_src = make_range( + src_range.start_row, + src_range.start_col + math.max(fragment.old_start - 1, 0), + src_range.start_col + math.max(fragment.old_end, 0) + ) + hunk_dst = make_range( + dst_range.start_row, + dst_range.start_col + math.max(fragment.new_start - 1, 0), + dst_range.start_col + math.max(fragment.new_end, 0) + ) + end + end + + if hunk_src and hunk_dst then + table.insert(hunks, hunk_change(hunk_src, hunk_dst)) + end + end + end + + if #hunks == 0 then + hunks = fallback_hunks_from_diff(action.src_node, action.dst_node, src_buf, dst_buf, rename_pairs) + end + + action.analysis = { + leaf_changes = normalized_leaf, + rename_pairs = rename_pairs, + hunks = hunks, + } + end + end +end + +return M diff --git a/lua/diffmantic/core/bottom_up.lua b/lua/diffmantic/core/bottom_up.lua index 8f4ff86..948a8b7 100644 --- a/lua/diffmantic/core/bottom_up.lua +++ b/lua/diffmantic/core/bottom_up.lua @@ -55,7 +55,12 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src return variable_name end - if node:type() == "class_specifier" or node:type() == "struct_specifier" or node:type() == "enum_specifier" or node:type() == "union_specifier" then + if + node:type() == "class_specifier" + or node:type() == "struct_specifier" + or node:type() == "enum_specifier" + or node:type() == "union_specifier" + then local name_node = node:field("name")[1] or node:field("tag")[1] if name_node then return vim.treesitter.get_node_text(name_node, bufnr) @@ -283,7 +288,8 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src } local function is_identifier_type(info, role_index) - if roles.has_kind(info.node, role_index, "function") + if + roles.has_kind(info.node, role_index, "function") or roles.has_kind(info.node, role_index, "class") or roles.has_kind(info.node, role_index, "variable") or roles.has_kind(info.node, role_index, "assignment") From a2944fbe681e63133c8874da1de2cf4ba3d4ebfd Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sat, 14 Feb 2026 07:42:52 +0530 Subject: [PATCH 19/48] refactor(core/ui): added rename supression and visual annotations for move/rename operations - Add range_contains() to check if one range is within another - Add collect_suppressed_rename_pairs() to track renames within declarations and suppress redundant annotations - Add visual text annotations for move operations showing source/destination line numbers with arrows - Add visual text annotations for renames showing old/new names - Remove unused extra_src_fillers and extra_dst_fillers handling - Clean up trailing whitespace --- lua/diffmantic/core/analysis.lua | 126 ++++++++++++++- lua/diffmantic/ui/filler.lua | 266 +++++++++++++++++++++++++++---- lua/diffmantic/ui/renderer.lua | 175 +++++++++++++------- 3 files changed, 478 insertions(+), 89 deletions(-) diff --git a/lua/diffmantic/core/analysis.lua b/lua/diffmantic/core/analysis.lua index 4d52539..af2e290 100644 --- a/lua/diffmantic/core/analysis.lua +++ b/lua/diffmantic/core/analysis.lua @@ -200,6 +200,99 @@ local function hunk_delete(src_range) return { kind = "delete", src = src_range, dst = nil } end +local function range_contains(outer, inner) + if not outer or not inner then + return false + end + if outer.start_row == nil or outer.end_row == nil or outer.start_col == nil or outer.end_col == nil then + return false + end + if inner.start_row == nil or inner.end_row == nil or inner.start_col == nil or inner.end_col == nil then + return false + end + if inner.start_row < outer.start_row or inner.end_row > outer.end_row then + return false + end + if inner.start_row == outer.start_row and inner.start_col < outer.start_col then + return false + end + if inner.end_row == outer.end_row and inner.end_col > outer.end_col then + return false + end + return true +end + +local function clone_range_like(r) + return clone_range(r) +end + +local function collect_suppressed_rename_pairs(actions) + local pairs = {} + local declaration_pairs = {} + for _, action in ipairs(actions) do + if action.type == "rename" then + local old_name = action.metadata and action.metadata.old_name or action.from + local new_name = action.metadata and action.metadata.new_name or action.to + if action.context and action.context.declaration and old_name and new_name and old_name ~= new_name then + declaration_pairs[old_name] = new_name + end + + local suppressed = action.metadata and action.metadata.suppressed_renames or nil + if suppressed then + for _, usage in ipairs(suppressed) do + local src = clone_range_like(usage.src or usage.src_range) + local dst = clone_range_like(usage.dst or usage.dst_range) + if src and dst then + table.insert(pairs, { src = src, dst = dst }) + end + end + end + end + end + return pairs, declaration_pairs +end + +local function text_for_range(buf, range) + if not buf or not range then + return nil + end + if range.start_row == nil or range.end_row == nil or range.start_col == nil or range.end_col == nil then + return nil + end + if range.start_row ~= range.end_row then + return nil + end + local line = line_text(buf, range.start_row) + if not line or line == "" then + return nil + end + local start_col = range.start_col + 1 + local end_col = range.end_col + if end_col < start_col then + return nil + end + return line:sub(start_col, end_col) +end + +local function is_suppressed_change_hunk(hunk_src, hunk_dst, suppressed_pairs, declaration_pairs, src_buf, dst_buf) + if not hunk_src or not hunk_dst then + return false + end + for _, pair in ipairs(suppressed_pairs) do + if range_contains(hunk_src, pair.src) and range_contains(hunk_dst, pair.dst) then + return true + end + end + if declaration_pairs and next(declaration_pairs) then + local src_text = text_for_range(src_buf, hunk_src) + local dst_text = text_for_range(dst_buf, hunk_dst) + if src_text and dst_text and declaration_pairs[src_text] == dst_text then + return true + end + end + return false +end + local function fallback_hunks_from_diff(src_node, dst_node, src_buf, dst_buf, rename_pairs) local src_text = vim.treesitter.get_node_text(src_node, src_buf) local dst_text = vim.treesitter.get_node_text(dst_node, dst_buf) @@ -319,6 +412,7 @@ function M.enrich(actions, opts) if not src_buf or not dst_buf then return end + local suppressed_pairs, declaration_pairs = collect_suppressed_rename_pairs(actions) for _, action in ipairs(actions) do if action.type == "update" and action.src_node and action.dst_node then @@ -362,7 +456,18 @@ function M.enrich(actions, opts) end end - if hunk_src and hunk_dst then + if + hunk_src + and hunk_dst + and not is_suppressed_change_hunk( + hunk_src, + hunk_dst, + suppressed_pairs, + declaration_pairs, + src_buf, + dst_buf + ) + then table.insert(hunks, hunk_change(hunk_src, hunk_dst)) end end @@ -371,6 +476,25 @@ function M.enrich(actions, opts) if #hunks == 0 then hunks = fallback_hunks_from_diff(action.src_node, action.dst_node, src_buf, dst_buf, rename_pairs) end + if #suppressed_pairs > 0 and #hunks > 0 then + local filtered = {} + for _, hunk in ipairs(hunks) do + if + hunk.kind ~= "change" + or not is_suppressed_change_hunk( + hunk.src, + hunk.dst, + suppressed_pairs, + declaration_pairs, + src_buf, + dst_buf + ) + then + table.insert(filtered, hunk) + end + end + hunks = filtered + end action.analysis = { leaf_changes = normalized_leaf, diff --git a/lua/diffmantic/ui/filler.lua b/lua/diffmantic/ui/filler.lua index d77b446..ff0759a 100644 --- a/lua/diffmantic/ui/filler.lua +++ b/lua/diffmantic/ui/filler.lua @@ -6,6 +6,59 @@ local function make_virt_line(hl_group, char) return { { string.rep(char or "╱", VIRT_LINE_LEN), hl_group } } end +local function span_line_count(range) + if not range then + return 0 + end + local sr = range.start_row + local er = range.end_row + local ec = range.end_col + if sr == nil or er == nil or ec == nil then + return 0 + end + local count = er - sr + if ec > 0 then + count = count + 1 + end + if count <= 0 then + count = 1 + end + return count +end + +local function line_trim_bounds(line) + if not line then + return nil, nil + end + local first = line:find("%S") + if not first then + return nil, nil + end + local rev_last = line:reverse():find("%S") + local last = #line - rev_last + 1 + return first - 1, last +end + +local function is_whole_line_span(buf, range) + if not range then + return false + end + if range.start_row == nil or range.end_row == nil then + return false + end + if range.start_row ~= range.end_row then + return true + end + local row = range.start_row + local lines = vim.api.nvim_buf_get_lines(buf, row, row + 1, false) + local line = lines[1] or "" + local first_col, last_col_exclusive = line_trim_bounds(line) + if not first_col then + return false + end + return range.start_col <= first_col and range.end_col >= last_col_exclusive +end + local function trailing_blanks(buf, end_row, end_col) local last_occupied = end_col > 0 and end_row or (end_row - 1) local buf_count = vim.api.nvim_buf_line_count(buf) @@ -23,50 +76,158 @@ local function trailing_blanks(buf, end_row, end_col) return count end -local function is_top_level(node) - if not node then +local function range_within(inner, outer) + if not inner or not outer then + return false + end + if inner.start_row == nil or inner.end_row == nil or inner.start_col == nil or inner.end_col == nil then return false end - local parent = node:parent() - return parent ~= nil and parent:parent() == nil + if outer.start_row == nil or outer.end_row == nil or outer.start_col == nil or outer.end_col == nil then + return false + end + if inner.start_row < outer.start_row or inner.end_row > outer.end_row then + return false + end + if inner.start_row == outer.start_row and inner.start_col < outer.start_col then + return false + end + if inner.end_row == outer.end_row and inner.end_col > outer.end_col then + return false + end + return true +end + +local function add_filler(dst_list, seen, row, count, hl_group) + if row == nil or count == nil or count <= 0 or not hl_group then + return + end + local key = table.concat({ tostring(row), tostring(count), hl_group }, ":") + if seen[key] then + return + end + seen[key] = true + table.insert(dst_list, { + row = row, + count = count, + hl_group = hl_group, + }) +end + +local function mirror_dst_row_into_src(update_src, update_dst, dst_row) + if not update_src or not update_dst or dst_row == nil then + return dst_row + end + local offset = math.max(0, dst_row - update_dst.start_row) + return update_src.start_row + offset +end + +local function mirror_src_row_into_dst(update_src, update_dst, src_row) + if not update_src or not update_dst or src_row == nil then + return src_row + end + local offset = math.max(0, src_row - update_src.start_row) + return update_dst.start_row + offset +end + +local function collect_runs(ranges) + if not ranges or #ranges == 0 then + return {} + end + table.sort(ranges, function(a, b) + return a.start_row < b.start_row + end) + local runs = {} + for _, range in ipairs(ranges) do + local count = span_line_count(range) + local current = runs[#runs] + if current and range.start_row <= (current.end_row + 1) then + current.end_row = math.max(current.end_row, range.end_row) + current.count = current.count + count + else + table.insert(runs, { + start_row = range.start_row, + end_row = range.end_row, + count = count, + }) + end + end + return runs end function M.compute(actions, src_buf, dst_buf) local src_fillers = {} local dst_fillers = {} + local seen_src = {} + local seen_dst = {} + local move_src_ranges = {} + local move_dst_ranges = {} + + for _, action in ipairs(actions) do + if action.type == "move" then + local src = action.src or action.src_range + local dst = action.dst or action.dst_range + if src then + table.insert(move_src_ranges, src) + end + if dst then + table.insert(move_dst_ranges, dst) + end + end + end + + local function inside_move_src(range) + if not range then + return false + end + for _, moved in ipairs(move_src_ranges) do + if range_within(range, moved) then + return true + end + end + return false + end + + local function inside_move_dst(range) + if not range then + return false + end + for _, moved in ipairs(move_dst_ranges) do + if range_within(range, moved) then + return true + end + end + return false + end for _, action in ipairs(actions) do local t = action.type + local src = action.src or action.src_range + local dst = action.dst or action.dst_range - if t == "insert" and action.dst_node and is_top_level(action.dst_node) then - local sr, _, er, ec = action.dst_node:range() - local count = er - sr - if ec > 0 then - count = count + 1 + if t == "insert" and dst and dst.start_row ~= nil then + if inside_move_dst(dst) then + goto continue end - count = count + trailing_blanks(dst_buf, er, ec) - if count > 0 then - table.insert(src_fillers, { - row = sr, - count = count, - hl_group = "DiffmanticAddFiller", - }) + if not is_whole_line_span(dst_buf, dst) then + goto continue end - elseif t == "delete" and action.src_node and is_top_level(action.src_node) then - local sr, _, er, ec = action.src_node:range() - local count = er - sr - if ec > 0 then - count = count + 1 - end - count = count + trailing_blanks(src_buf, er, ec) - if count > 0 then - table.insert(dst_fillers, { - row = sr, - count = count, - hl_group = "DiffmanticDeleteFiller", - }) + local count = span_line_count(dst) + if count >= 1 then + add_filler(src_fillers, seen_src, dst.start_row, count, "DiffmanticAddFiller") + end + elseif t == "delete" and src and src.start_row ~= nil then + if inside_move_src(src) then + goto continue end - elseif t == "move" and action.src_node and action.dst_node and is_top_level(action.src_node) then + if not is_whole_line_span(src_buf, src) then + goto continue + end + local count = span_line_count(src) + if count >= 1 then + add_filler(dst_fillers, seen_dst, src.start_row, count, "DiffmanticDeleteFiller") + end + elseif t == "move" and action.src_node and action.dst_node then local ssr, _, ser, sec = action.src_node:range() local src_body = ser - ssr if sec > 0 then @@ -151,7 +312,52 @@ function M.compute(actions, src_buf, dst_buf) }) end end + elseif t == "update" and action.analysis and action.analysis.hunks then + local update_src = action.src or action.src_range + local update_dst = action.dst or action.dst_range + local move_related_update = inside_move_src(update_src) or inside_move_dst(update_dst) + if move_related_update then + goto continue + end + + local insert_ranges = {} + local delete_ranges = {} + for _, hunk in ipairs(action.analysis.hunks) do + if hunk.kind == "insert" and hunk.dst then + table.insert(insert_ranges, hunk.dst) + elseif hunk.kind == "delete" and hunk.src then + table.insert(delete_ranges, hunk.src) + end + end + + local insert_runs = collect_runs(insert_ranges) + local delete_runs = collect_runs(delete_ranges) + + for _, run in ipairs(insert_runs) do + local anchor = mirror_dst_row_into_src(update_src, update_dst, run.start_row) + local deleted_before = 0 + for _, d in ipairs(delete_runs) do + if d.start_row <= anchor then + deleted_before = deleted_before + d.count + end + end + anchor = anchor + deleted_before + add_filler(src_fillers, seen_src, anchor, run.count, "DiffmanticAddFiller") + end + + for _, run in ipairs(delete_runs) do + local anchor = mirror_src_row_into_dst(update_src, update_dst, run.start_row) + local inserted_before = 0 + for _, ins in ipairs(insert_runs) do + if ins.start_row <= anchor then + inserted_before = inserted_before + ins.count + end + end + anchor = anchor + inserted_before + add_filler(dst_fillers, seen_dst, anchor, run.count, "DiffmanticDeleteFiller") + end end + ::continue:: end table.sort(src_fillers, function(a, b) diff --git a/lua/diffmantic/ui/renderer.lua b/lua/diffmantic/ui/renderer.lua index 2499a5b..a202586 100644 --- a/lua/diffmantic/ui/renderer.lua +++ b/lua/diffmantic/ui/renderer.lua @@ -18,92 +18,151 @@ local function set_extmark(buf, ns, row, col, opts) return pcall(vim.api.nvim_buf_set_extmark, buf, ns, row, col, opts) end -local function apply_spans(buf, ns, spans) - if not spans then +local function apply_span(buf, ns, range, hl_group) + if not range or not hl_group then return end - for _, span in ipairs(spans) do - if span and span.row ~= nil and span.start_col ~= nil and span.end_col ~= nil and span.hl_group then - set_extmark(buf, ns, span.row, span.start_col, { - end_row = span.end_row or span.row, - end_col = span.end_col, - hl_group = span.hl_group, - }) - end + local sr = range.start_row + local sc = range.start_col + local er = range.end_row or sr + local ec = range.end_col + if sr == nil or sc == nil or ec == nil then + return end + if sr == er and ec <= sc then + ec = sc + 1 + end + set_extmark(buf, ns, sr, sc, { + end_row = er, + end_col = ec, + hl_group = hl_group, + }) end -local function apply_signs(buf, ns, list, seen_rows) - if not list then +local function apply_sign(buf, ns, row, text, hl_group, seen_rows) + if row == nil or not text or not hl_group then return end - for _, item in ipairs(list) do - if item and item.row ~= nil and item.text and item.hl_group then - signs.mark(buf, ns, item.row, item.col or 0, item.text, item.hl_group, seen_rows) - end - end + signs.mark(buf, ns, row, 0, text, hl_group, seen_rows) end -local function apply_virt(buf, ns, list) - if not list then +local function apply_virt(buf, ns, row, col, text, hl_group, pos) + if row == nil or not text then return end - for _, item in ipairs(list) do - if item and item.row ~= nil and item.text then - local opts = { - virt_text = { { item.text, item.hl_group or "Comment" } }, - virt_text_pos = item.pos or "eol", - } - local ok = set_extmark(buf, ns, item.row, item.col or 0, opts) - if not ok and opts.virt_text_pos == "inline" then - opts.virt_text_pos = "eol" - set_extmark(buf, ns, item.row, item.col or 0, opts) - end - end + local opts = { + virt_text = { { text, hl_group or "Comment" } }, + virt_text_pos = pos or "eol", + } + local ok = set_extmark(buf, ns, row, col or 0, opts) + if not ok and opts.virt_text_pos == "inline" then + opts.virt_text_pos = "eol" + set_extmark(buf, ns, row, col or 0, opts) + end +end + +local TYPE_STYLE = { + move = { hl = "DiffmanticMove", sign = "M" }, + rename = { hl = "DiffmanticRename", sign = "R" }, + update = { hl = "DiffmanticChange", sign = "U" }, + insert = { hl = "DiffmanticAdd", sign = "+" }, + delete = { hl = "DiffmanticDelete", sign = "-" }, +} + +local HUNK_STYLE = { + change = { + src_hl = "DiffmanticChange", + dst_hl = "DiffmanticChange", + src_sign = "U", + dst_sign = "U", + }, + insert = { + src_hl = nil, + dst_hl = "DiffmanticAdd", + src_sign = nil, + dst_sign = "+", + }, + delete = { + src_hl = "DiffmanticDelete", + dst_hl = nil, + src_sign = "-", + dst_sign = nil, + }, +} + +local function move_to_arrow(from_line, to_line) + if type(from_line) ~= "number" or type(to_line) ~= "number" then + return "⤴" end + if to_line > from_line then + return "⤵" + end + return "⤴" end function M.render(src_buf, dst_buf, actions, ns) local src_sign_rows = {} local dst_sign_rows = {} - local extra_src_fillers = {} - local extra_dst_fillers = {} for _, action in ipairs(actions) do - local render = action.render - if render then - apply_spans(src_buf, ns, render.src_spans) - apply_spans(dst_buf, ns, render.dst_spans) - apply_signs(src_buf, ns, render.src_signs, src_sign_rows) - apply_signs(dst_buf, ns, render.dst_signs, dst_sign_rows) - apply_virt(src_buf, ns, render.src_virt) - apply_virt(dst_buf, ns, render.dst_virt) - - if render.src_fillers then - for _, f in ipairs(render.src_fillers) do - table.insert(extra_src_fillers, f) + local style = TYPE_STYLE[action.type] + if style then + local src = action.src or action.src_range + local dst = action.dst or action.dst_range + local meta = action.metadata or {} + + if action.type == "update" and action.analysis and action.analysis.hunks then + for _, hunk in ipairs(action.analysis.hunks) do + local hstyle = HUNK_STYLE[hunk.kind] or HUNK_STYLE.change + if hunk.src and hstyle.src_hl then + apply_span(src_buf, ns, hunk.src, hstyle.src_hl) + apply_sign(src_buf, ns, hunk.src.start_row, hstyle.src_sign, hstyle.src_hl, src_sign_rows) + end + if hunk.dst and hstyle.dst_hl then + apply_span(dst_buf, ns, hunk.dst, hstyle.dst_hl) + apply_sign(dst_buf, ns, hunk.dst.start_row, hstyle.dst_sign, hstyle.dst_hl, dst_sign_rows) + end end - end - if render.dst_fillers then - for _, f in ipairs(render.dst_fillers) do - table.insert(extra_dst_fillers, f) + + if src and src.start_row ~= nil then + apply_sign(src_buf, ns, src.start_row, "U", "DiffmanticChange", src_sign_rows) + end + if dst and dst.start_row ~= nil then + apply_sign(dst_buf, ns, dst.start_row, "U", "DiffmanticChange", dst_sign_rows) + end + else + if src then + apply_span(src_buf, ns, src, style.hl) + apply_sign(src_buf, ns, src.start_row, style.sign, style.hl, src_sign_rows) + end + if dst then + apply_span(dst_buf, ns, dst, style.hl) + apply_sign(dst_buf, ns, dst.start_row, style.sign, style.hl, dst_sign_rows) + end + + if action.type == "move" then + if src and meta.to_line then + local arrow = move_to_arrow(meta.from_line, meta.to_line) + apply_virt(src_buf, ns, src.start_row, src.end_col or 0, string.format(" %s moved to L%d", arrow, meta.to_line), "Comment", "eol") + end + if dst and meta.from_line then + apply_virt(dst_buf, ns, dst.start_row, dst.end_col or 0, string.format(" ⤶ from L%d", meta.from_line), "Comment", "eol") + end + elseif action.type == "rename" then + if src and meta.new_name then + apply_virt(src_buf, ns, src.start_row, src.end_col or 0, " -> " .. meta.new_name, "Comment", "inline") + end + if dst and meta.old_name then + apply_virt(dst_buf, ns, dst.start_row, dst.end_col or 0, string.format(" (was %s)", meta.old_name), "Comment", "inline") + end end end end end local src_fillers, dst_fillers = filler.compute(actions, src_buf, dst_buf) - - for _, f in ipairs(extra_src_fillers) do - table.insert(src_fillers, f) - end - for _, f in ipairs(extra_dst_fillers) do - table.insert(dst_fillers, f) - end - filler.apply(src_buf, ns, src_fillers) filler.apply(dst_buf, ns, dst_fillers) end return M - From e554a4df7f7f2c33cbc5a17e002c1fcd8afd0fe1 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sat, 14 Feb 2026 11:41:10 +0530 Subject: [PATCH 20/48] refactor(core): normalized action src/dst + metadata summary - action range construction now uses normalized src/dst fields - summary tables read metadata/src/dst directly inside of legacy code --- lua/diffmantic/core/actions.lua | 93 ++++++++++++++++----------------- 1 file changed, 46 insertions(+), 47 deletions(-) diff --git a/lua/diffmantic/core/actions.lua b/lua/diffmantic/core/actions.lua index 57020ec..fa22832 100644 --- a/lua/diffmantic/core/actions.lua +++ b/lua/diffmantic/core/actions.lua @@ -19,24 +19,18 @@ local function range_metadata(node) end local function build_action(action_type, src_node, dst_node, extra) - local src_range = range_metadata(src_node) - local dst_range = range_metadata(dst_node) + local src = range_metadata(src_node) + local dst = range_metadata(dst_node) local node = src_node or dst_node - local from_line = src_range and src_range.start_line or nil - local to_line = dst_range and dst_range.start_line or nil + local from_line = src and src.start_line or nil + local to_line = dst and dst.start_line or nil local action = { type = action_type, src_node = src_node, dst_node = dst_node, - src_range = src_range, - dst_range = dst_range, - src = src_range and vim.tbl_extend("force", {}, src_range, { text = nil }) or nil, - dst = dst_range and vim.tbl_extend("force", {}, dst_range, { text = nil }) or nil, - lines = { - from_line = from_line, - to_line = to_line, - }, + src = src and vim.tbl_extend("force", {}, src, { text = nil }) or nil, + dst = dst and vim.tbl_extend("force", {}, dst, { text = nil }) or nil, metadata = { node_type = node and node:type() or nil, old_name = nil, @@ -89,65 +83,70 @@ local function build_summary(actions_list) end if t == "move" then + local metadata = action.metadata or {} table.insert(summary.moves, { - node_type = action.metadata and action.metadata.node_type or nil, - from_line = action.metadata and action.metadata.from_line or (action.lines and action.lines.from_line or nil), - to_line = action.metadata and action.metadata.to_line or (action.lines and action.lines.to_line or nil), - src_range = action.src or action.src_range, - dst_range = action.dst or action.dst_range, + node_type = metadata.node_type, + from_line = metadata.from_line, + to_line = metadata.to_line, + src_range = action.src, + dst_range = action.dst, }) elseif t == "rename" then - local suppressed_usages = action.metadata and action.metadata.suppressed_renames - or (action.context and action.context.suppressed_usages or {}) + local metadata = action.metadata or {} + local suppressed_usages = metadata.suppressed_renames or (action.context and action.context.suppressed_usages or {}) local suppressed_count = #suppressed_usages summary.counts.rename_suppressed = summary.counts.rename_suppressed + suppressed_count table.insert(summary.renames, { - node_type = action.metadata and action.metadata.node_type or nil, - from = action.metadata and action.metadata.old_name or action.from, - to = action.metadata and action.metadata.new_name or action.to, - from_line = action.metadata and action.metadata.from_line or (action.lines and action.lines.from_line or nil), - to_line = action.metadata and action.metadata.to_line or (action.lines and action.lines.to_line or nil), - src_range = action.src or action.src_range, - dst_range = action.dst or action.dst_range, + node_type = metadata.node_type, + from = metadata.old_name, + to = metadata.new_name, + from_line = metadata.from_line, + to_line = metadata.to_line, + src_range = action.src, + dst_range = action.dst, suppressed_usage_count = suppressed_count, }) for _, usage in ipairs(suppressed_usages) do + local usage_meta = usage.metadata or {} table.insert(summary.suppressed_renames, { - from = usage.from, - to = usage.to, - from_line = usage.lines and usage.lines.from_line or nil, - to_line = usage.lines and usage.lines.to_line or nil, - src_range = usage.src or usage.src_range, - dst_range = usage.dst or usage.dst_range, + from = usage_meta.old_name, + to = usage_meta.new_name, + from_line = usage_meta.from_line, + to_line = usage_meta.to_line, + src_range = usage.src, + dst_range = usage.dst, suppressed_by = { - from = action.metadata and action.metadata.old_name or action.from, - to = action.metadata and action.metadata.new_name or action.to, - from_line = action.metadata and action.metadata.from_line or (action.lines and action.lines.from_line or nil), - to_line = action.metadata and action.metadata.to_line or (action.lines and action.lines.to_line or nil), + from = metadata.old_name, + to = metadata.new_name, + from_line = metadata.from_line, + to_line = metadata.to_line, }, }) end elseif t == "update" then + local metadata = action.metadata or {} table.insert(summary.updates, { - node_type = action.metadata and action.metadata.node_type or nil, - from_line = action.metadata and action.metadata.from_line or (action.lines and action.lines.from_line or nil), - to_line = action.metadata and action.metadata.to_line or (action.lines and action.lines.to_line or nil), - src_range = action.src or action.src_range, - dst_range = action.dst or action.dst_range, + node_type = metadata.node_type, + from_line = metadata.from_line, + to_line = metadata.to_line, + src_range = action.src, + dst_range = action.dst, }) elseif t == "insert" then + local metadata = action.metadata or {} table.insert(summary.inserts, { - node_type = action.metadata and action.metadata.node_type or nil, - line = action.metadata and action.metadata.to_line or (action.lines and action.lines.to_line or nil), - dst_range = action.dst or action.dst_range, + node_type = metadata.node_type, + line = metadata.to_line, + dst_range = action.dst, }) elseif t == "delete" then + local metadata = action.metadata or {} table.insert(summary.deletes, { - node_type = action.metadata and action.metadata.node_type or nil, - line = action.metadata and action.metadata.from_line or (action.lines and action.lines.from_line or nil), - src_range = action.src or action.src_range, + node_type = metadata.node_type, + line = metadata.from_line, + src_range = action.src, }) end end From f5ada370615cbbaea75270e6b97c6c8a927d76c1 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sat, 14 Feb 2026 11:57:45 +0530 Subject: [PATCH 21/48] delete(core): removed unused core/payload.lua and ui/helpers.lua --- lua/diffmantic/core/payload.lua | 683 -------------------------------- lua/diffmantic/ui/helpers.lua | 16 - 2 files changed, 699 deletions(-) delete mode 100644 lua/diffmantic/core/payload.lua delete mode 100644 lua/diffmantic/ui/helpers.lua diff --git a/lua/diffmantic/core/payload.lua b/lua/diffmantic/core/payload.lua deleted file mode 100644 index ab7fcc4..0000000 --- a/lua/diffmantic/core/payload.lua +++ /dev/null @@ -1,683 +0,0 @@ -local semantic = require("diffmantic.core.semantic") - -local M = {} -local RENDER_UPDATE_TOKENS = true - -local function node_range(node) - if not node then - return nil - end - local ok, sr, sc, er, ec = pcall(function() - return node:range() - end) - if not ok then - return nil - end - return sr, sc, er, ec -end - -local function inclusive_end_row(sr, er, ec) - if er < sr then - return sr - end - if er == sr then - return sr - end - if ec == 0 then - return er - 1 - end - return er -end - -local function line_text(buf, row) - local lines = vim.api.nvim_buf_get_lines(buf, row, row + 1, false) - return lines[1] or "" -end - -local function append_span(spans, row, start_col, end_col, hl_group, buf) - if row < 0 or end_col <= start_col then - return false - end - - local text = line_text(buf, row) - local safe_start = math.max(start_col, 0) - local safe_end = math.min(end_col, #text) - if safe_end <= safe_start then - return false - end - - local segment = text:sub(safe_start + 1, safe_end) - local first = segment:find("%S") - if not first then - return false - end - local rev_last = segment:reverse():find("%S") - local last = #segment - rev_last + 1 - - table.insert(spans, { - row = row, - start_col = safe_start + first - 1, - end_row = row, - end_col = safe_start + last, - hl_group = hl_group, - }) - return true -end - -local function append_sign(signs, row, text, hl_group) - if row == nil or row < 0 then - return - end - if not text or text == "" then - return - end - table.insert(signs, { - row = row, - col = 0, - text = text, - hl_group = hl_group, - }) -end - -local function append_virt(virt, row, col, text, hl_group, pos) - if row == nil or row < 0 then - return - end - table.insert(virt, { - row = row, - col = col or 0, - text = text, - hl_group = hl_group or "Comment", - pos = pos or "eol", - }) -end - -local function append_node_tokens(spans, buf, node, hl_group, opts) - local sr, sc, er, ec = node_range(node) - if not sr then - return false - end - - local first_line_only = opts and opts.first_line_only or false - local end_row = first_line_only and sr or inclusive_end_row(sr, er, ec) - local highlighted = false - - for row = sr, end_row do - local text = line_text(buf, row) - local start_col = (row == sr) and sc or 0 - local end_col - if first_line_only then - end_col = #text - elseif row == end_row then - if er == sr then - end_col = ec - elseif row == er then - end_col = ec - else - end_col = #text - end - else - end_col = #text - end - highlighted = append_span(spans, row, start_col, end_col, hl_group, buf) or highlighted - end - - return highlighted -end - -local function tokenize_line(text) - local tokens = {} - local i = 1 - local len = #text - while i <= len do - local ch = text:sub(i, i) - if ch:match("%s") then - i = i + 1 - elseif ch:match("[%w_]") then - local j = i + 1 - while j <= len and text:sub(j, j):match("[%w_]") do - j = j + 1 - end - table.insert(tokens, { text = text:sub(i, j - 1), start_col = i, end_col = j - 1 }) - i = j - else - local j = i + 1 - while j <= len and not text:sub(j, j):match("[%w_%s]") do - j = j + 1 - end - table.insert(tokens, { text = text:sub(i, j - 1), start_col = i, end_col = j - 1 }) - i = j - end - end - return tokens -end - -local function tokens_equal(a, b, rename_map) - if a.text == b.text then - return true - end - if rename_map and rename_map[a.text] == b.text then - return true - end - return false -end - -local function lcs_matches(a, b, rename_map) - local n = #a - local m = #b - if n == 0 or m == 0 then - return {}, {} - end - - local dp = {} - for i = 0, n do - dp[i] = {} - dp[i][0] = 0 - end - for j = 1, m do - dp[0][j] = 0 - end - - for i = 1, n do - for j = 1, m do - if tokens_equal(a[i], b[j], rename_map) then - dp[i][j] = dp[i - 1][j - 1] + 1 - else - local up = dp[i - 1][j] - local left = dp[i][j - 1] - dp[i][j] = (up >= left) and up or left - end - end - end - - local match_a = {} - local match_b = {} - local i = n - local j = m - while i > 0 and j > 0 do - if tokens_equal(a[i], b[j], rename_map) then - match_a[i] = true - match_b[j] = true - i = i - 1 - j = j - 1 - else - local up = dp[i - 1][j] - local left = dp[i][j - 1] - if up >= left then - i = i - 1 - else - j = j - 1 - end - end - end - return match_a, match_b -end - -local function unmatched_token_spans(tokens, matched, line_text_value) - local spans = {} - local i = 1 - while i <= #tokens do - if matched[i] then - i = i + 1 - else - local start_col = tokens[i].start_col - local end_col = tokens[i].end_col - local j = i + 1 - while j <= #tokens and not matched[j] do - local gap_start = end_col + 1 - local gap_end = tokens[j].start_col - 1 - local gap = "" - if gap_start <= gap_end then - gap = line_text_value:sub(gap_start, gap_end) - end - if gap ~= "" and not gap:match("^%s+$") then - break - end - end_col = tokens[j].end_col - j = j + 1 - end - table.insert(spans, { start_col = start_col, end_col = end_col }) - i = j - end - end - return spans -end - -local function build_internal_diff(src_node, dst_node, src_buf, dst_buf, rename_map, only_changes) - local src_text = vim.treesitter.get_node_text(src_node, src_buf) - local dst_text = vim.treesitter.get_node_text(dst_node, dst_buf) - if not src_text or not dst_text or src_text == "" or dst_text == "" then - return nil - end - - local src_lines = vim.split(src_text, "\n", { plain = true }) - local dst_lines = vim.split(dst_text, "\n", { plain = true }) - - local ok, hunks = pcall(vim.text.diff, src_text, dst_text, { - result_type = "indices", - linematch = 60, - }) - - local sr, sc = src_node:range() - local tr, tc = dst_node:range() - - local out = { - src_spans = {}, - dst_spans = {}, - src_signs = {}, - dst_signs = {}, - src_fillers = {}, - dst_fillers = {}, - } - - local function base_col_for_row(row, start_row, start_col) - if row == start_row then - return start_col - end - return 0 - end - - local function mark_line_tokens(buf, side_spans, row, line_value, base_col, hl_group) - if row < 0 or not line_value then - return false - end - local first = line_value:find("%S") - if not first then - return false - end - local rev_last = line_value:reverse():find("%S") - local last = #line_value - rev_last + 1 - return append_span(side_spans, row, base_col + first - 1, base_col + last, hl_group, buf) - end - - local function highlight_line_pair(src_row, dst_row, s_line, d_line) - local src_base = base_col_for_row(src_row, sr, sc) - local dst_base = base_col_for_row(dst_row, tr, tc) - - if s_line and d_line and s_line ~= d_line then - local tokens_src = tokenize_line(s_line) - local tokens_dst = tokenize_line(d_line) - if #tokens_src > 0 or #tokens_dst > 0 then - local match_src, match_dst = lcs_matches(tokens_src, tokens_dst, rename_map) - local did_src = false - local did_dst = false - - for _, span in ipairs(unmatched_token_spans(tokens_src, match_src, s_line)) do - did_src = append_span( - out.src_spans, - src_row, - src_base + span.start_col - 1, - src_base + span.end_col, - "DiffmanticChange", - src_buf - ) or did_src - end - - for _, span in ipairs(unmatched_token_spans(tokens_dst, match_dst, d_line)) do - did_dst = append_span( - out.dst_spans, - dst_row, - dst_base + span.start_col - 1, - dst_base + span.end_col, - "DiffmanticChange", - dst_buf - ) or did_dst - end - - if did_src then - append_sign(out.src_signs, src_row, "U", "DiffmanticChange") - end - if did_dst then - append_sign(out.dst_signs, dst_row, "U", "DiffmanticChange") - end - return did_src or did_dst - end - - local fragment = semantic.diff_fragment(s_line, d_line) - if fragment then - local did = false - did = append_span( - out.src_spans, - src_row, - src_base + fragment.old_start - 1, - src_base + fragment.old_end, - "DiffmanticChange", - src_buf - ) or did - did = append_span( - out.dst_spans, - dst_row, - dst_base + fragment.new_start - 1, - dst_base + fragment.new_end, - "DiffmanticChange", - dst_buf - ) or did - append_sign(out.src_signs, src_row, "U", "DiffmanticChange") - append_sign(out.dst_signs, dst_row, "U", "DiffmanticChange") - return did - end - - return false - end - - if s_line and not d_line then - if only_changes then - return false - end - local did = mark_line_tokens(src_buf, out.src_spans, src_row, s_line, src_base, "DiffmanticDelete") - append_sign(out.src_signs, src_row, "-", "DiffmanticDelete") - return did - elseif d_line and not s_line then - if only_changes then - return false - end - local did = mark_line_tokens(dst_buf, out.dst_spans, dst_row, d_line, dst_base, "DiffmanticAdd") - append_sign(out.dst_signs, dst_row, "+", "DiffmanticAdd") - return did - end - - return false - end - - local did_highlight = false - if ok and hunks and #hunks > 0 then - for _, h in ipairs(hunks) do - local start_a, count_a, start_b, count_b = h[1], h[2], h[3], h[4] - local overlap = math.min(count_a, count_b) - - for i = 0, overlap - 1 do - local src_row = sr + start_a - 1 + i - local dst_row = tr + start_b - 1 + i - local s_line = src_lines[start_a + i] - local d_line = dst_lines[start_b + i] - if highlight_line_pair(src_row, dst_row, s_line, d_line) then - did_highlight = true - end - end - - if count_a > overlap and not only_changes then - local del_count = count_a - overlap - for i = overlap, count_a - 1 do - local src_row = sr + start_a - 1 + i - local s_line = src_lines[start_a + i] - local src_base = base_col_for_row(src_row, sr, sc) - append_sign(out.src_signs, src_row, "-", "DiffmanticDelete") - did_highlight = mark_line_tokens(src_buf, out.src_spans, src_row, s_line, src_base, "DiffmanticDelete") - or did_highlight - end - local dst_anchor = count_b > 0 - and (tr + start_b - 1 + overlap) - or (tr + start_b) - table.insert(out.dst_fillers, { - row = dst_anchor, - count = del_count, - hl_group = "DiffmanticDeleteFiller", - }) - end - - if count_b > overlap and not only_changes then - local ins_count = count_b - overlap - for i = overlap, count_b - 1 do - local dst_row = tr + start_b - 1 + i - local d_line = dst_lines[start_b + i] - local dst_base = base_col_for_row(dst_row, tr, tc) - append_sign(out.dst_signs, dst_row, "+", "DiffmanticAdd") - did_highlight = mark_line_tokens(dst_buf, out.dst_spans, dst_row, d_line, dst_base, "DiffmanticAdd") - or did_highlight - end - local src_anchor = count_a > 0 - and (sr + start_a - 1 + overlap) - or (sr + start_a) - table.insert(out.src_fillers, { - row = src_anchor, - count = ins_count, - hl_group = "DiffmanticAddFiller", - }) - end - end - else - local max_lines = math.max(#src_lines, #dst_lines) - for i = 1, max_lines do - local src_row = sr + i - 1 - local dst_row = tr + i - 1 - local s_line = src_lines[i] - local d_line = dst_lines[i] - if highlight_line_pair(src_row, dst_row, s_line, d_line) then - did_highlight = true - end - end - end - - if did_highlight or #out.src_spans > 0 or #out.dst_spans > 0 then - return out - end - return nil -end - -local function append_change_leaf(render, src_buf, dst_buf, src_node, dst_node, src_text, dst_text) - local sr, sc, er, ec = node_range(src_node) - local tr, tc, ter, tec = node_range(dst_node) - if not sr or not tr then - return false - end - - local highlighted = false - local fragment = semantic.diff_fragment(src_text or "", dst_text or "") - if fragment and er == sr and ter == tr then - local src_start = sc + math.max(fragment.old_start - 1, 0) - local src_end = sc + math.max(fragment.old_end, 0) - local dst_start = tc + math.max(fragment.new_start - 1, 0) - local dst_end = tc + math.max(fragment.new_end, 0) - - highlighted = append_span(render.src_spans, sr, src_start, src_end, "DiffmanticChange", src_buf) or highlighted - highlighted = append_span(render.dst_spans, tr, dst_start, dst_end, "DiffmanticChange", dst_buf) or highlighted - else - highlighted = append_node_tokens(render.src_spans, src_buf, src_node, "DiffmanticChange", nil) or highlighted - highlighted = append_node_tokens(render.dst_spans, dst_buf, dst_node, "DiffmanticChange", nil) or highlighted - end - - append_sign(render.src_signs, sr, "U", "DiffmanticChange") - append_sign(render.dst_signs, tr, "U", "DiffmanticChange") - - return highlighted -end - -function M.enrich(actions, opts) - local src_buf = opts and opts.src_buf or nil - local dst_buf = opts and opts.dst_buf or nil - if not src_buf or not dst_buf then - return - end - - local rename_map = {} - local rename_src_nodes = {} - local rename_dst_nodes = {} - local moved_src_ranges = {} -- { {sr, er}, ... } - local moved_dst_ranges = {} -- { {sr, er}, ... } - - for _, action in ipairs(actions) do - if action.type == "rename" then - if action.from and action.to then - rename_map[action.from] = action.to - end - if action.src_node then - rename_src_nodes[action.src_node:id()] = true - end - if action.dst_node then - rename_dst_nodes[action.dst_node:id()] = true - end - elseif action.type == "move" then - if action.src_node then - local sr, _, er, _ = action.src_node:range() - table.insert(moved_src_ranges, { sr, er }) - end - if action.dst_node then - local sr, _, er, _ = action.dst_node:range() - table.insert(moved_dst_ranges, { sr, er }) - end - end - end - - for _, action in ipairs(actions) do - local src_node = action.src_node - local dst_node = action.dst_node - local render = { - src_spans = {}, - dst_spans = {}, - src_signs = {}, - dst_signs = {}, - src_virt = {}, - dst_virt = {}, - } - local touched = false - - if action.type == "move" and src_node and dst_node then - local sr, sc = node_range(src_node) - local tr, tc = node_range(dst_node) - local src_line = action.lines and action.lines.from_line or (sr and sr + 1 or nil) - local dst_line = action.lines and action.lines.to_line or (tr and tr + 1 or nil) - - touched = append_node_tokens(render.src_spans, src_buf, src_node, "DiffmanticMove", nil) or touched - touched = append_node_tokens(render.dst_spans, dst_buf, dst_node, "DiffmanticMove", nil) or touched - - if sr and sc then - append_sign(render.src_signs, sr, "M", "DiffmanticMove") - append_virt( - render.src_virt, - sr, - sc, - string.format(" ⤷ moved L%d → L%d", src_line or 0, dst_line or 0), - "Comment", - "eol" - ) - touched = true - end - if tr and tc then - append_sign(render.dst_signs, tr, "M", "DiffmanticMove") - append_virt(render.dst_virt, tr, tc, string.format(" ⤶ from L%d", src_line or 0), "Comment", "eol") - touched = true - end - elseif action.type == "rename" and src_node and dst_node then - local sr, _, _, sec = node_range(src_node) - local tr, _, _, tec = node_range(dst_node) - - touched = append_node_tokens(render.src_spans, src_buf, src_node, "DiffmanticRename", nil) or touched - touched = append_node_tokens(render.dst_spans, dst_buf, dst_node, "DiffmanticRename", nil) or touched - - if sr and sec and action.to then - append_sign(render.src_signs, sr, "R", "DiffmanticRename") - append_virt(render.src_virt, sr, sec, " -> " .. action.to, "Comment", "inline") - touched = true - end - if tr and tec and action.from then - append_sign(render.dst_signs, tr, "R", "DiffmanticRename") - append_virt(render.dst_virt, tr, tec, string.format(" (was %s)", action.from), "Comment", "inline") - touched = true - end - elseif action.type == "update" and src_node and dst_node and RENDER_UPDATE_TOKENS then - local leaf_changes = action.semantic and action.semantic.leaf_changes or nil - local rename_pairs = action.semantic and action.semantic.rename_pairs or {} - local did_leaf_highlight = false - - if leaf_changes and #leaf_changes > 0 then - for _, change in ipairs(leaf_changes) do - local change_src = change.src_node - local change_dst = change.dst_node - if change_src and change_dst and change.src_text ~= change.dst_text then - local src_id = change_src:id() - local dst_id = change_dst:id() - local is_rename = rename_src_nodes[src_id] - or rename_dst_nodes[dst_id] - or (change.src_text and rename_pairs[change.src_text] == change.dst_text) - or (change.src_text and rename_map[change.src_text] == change.dst_text) - if not is_rename then - local highlighted = append_change_leaf( - render, - src_buf, - dst_buf, - change_src, - change_dst, - change.src_text, - change.dst_text - ) - did_leaf_highlight = highlighted or did_leaf_highlight - touched = highlighted or touched - end - end - end - end - - if not did_leaf_highlight then - local diff_ops = build_internal_diff(src_node, dst_node, src_buf, dst_buf, rename_map, false) - if diff_ops then - for _, span in ipairs(diff_ops.src_spans) do - table.insert(render.src_spans, span) - end - for _, span in ipairs(diff_ops.dst_spans) do - table.insert(render.dst_spans, span) - end - for _, sign in ipairs(diff_ops.src_signs) do - table.insert(render.src_signs, sign) - end - for _, sign in ipairs(diff_ops.dst_signs) do - table.insert(render.dst_signs, sign) - end - local is_moved = false - if src_node then - local nsr, _, ner, _ = src_node:range() - for _, r in ipairs(moved_src_ranges) do - if nsr >= r[1] and ner <= r[2] then - is_moved = true - break - end - end - end - if not is_moved and dst_node then - local nsr, _, ner, _ = dst_node:range() - for _, r in ipairs(moved_dst_ranges) do - if nsr >= r[1] and ner <= r[2] then - is_moved = true - break - end - end - end - if not is_moved then - if diff_ops.src_fillers then - render.src_fillers = render.src_fillers or {} - for _, f in ipairs(diff_ops.src_fillers) do - table.insert(render.src_fillers, f) - end - end - if diff_ops.dst_fillers then - render.dst_fillers = render.dst_fillers or {} - for _, f in ipairs(diff_ops.dst_fillers) do - table.insert(render.dst_fillers, f) - end - end - end - touched = true - end - end - elseif action.type == "delete" and src_node then - touched = append_node_tokens(render.src_spans, src_buf, src_node, "DiffmanticDelete", nil) or touched - local sr = node_range(src_node) - if sr then - append_sign(render.src_signs, sr, "-", "DiffmanticDelete") - touched = true - end - elseif action.type == "insert" and dst_node then - touched = append_node_tokens(render.dst_spans, dst_buf, dst_node, "DiffmanticAdd", nil) or touched - local tr = node_range(dst_node) - if tr then - append_sign(render.dst_signs, tr, "+", "DiffmanticAdd") - touched = true - end - end - - if touched then - action.render = render - end - end -end - -return M diff --git a/lua/diffmantic/ui/helpers.lua b/lua/diffmantic/ui/helpers.lua deleted file mode 100644 index d0c0714..0000000 --- a/lua/diffmantic/ui/helpers.lua +++ /dev/null @@ -1,16 +0,0 @@ -local M = {} - -function M.set_inline_virt_text(buf, ns, row, col, text, hl) - local opts = { - virt_text = { { text, hl } }, - virt_text_pos = "inline", - } - local ok = pcall(vim.api.nvim_buf_set_extmark, buf, ns, row, col, opts) - if ok then - return - end - opts.virt_text_pos = "eol" - pcall(vim.api.nvim_buf_set_extmark, buf, ns, row, col, opts) -end - -return M From afc34a73a995dee73dcb537a83ebab1274acf61c Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sat, 14 Feb 2026 17:09:27 +0530 Subject: [PATCH 22/48] refactor(core): normalized analysis of payload and suppressed rename metadata - build_action now accepts structural analysis/metadata/context inputs - suppressed rename usage entries store normalized src/dst + metadata - analysis enrichment reads normalized rename metadata now --- lua/diffmantic/core/actions.lua | 39 +++++++++++++++++++++----------- lua/diffmantic/core/analysis.lua | 9 ++++---- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/lua/diffmantic/core/actions.lua b/lua/diffmantic/core/actions.lua index fa22832..0892e18 100644 --- a/lua/diffmantic/core/actions.lua +++ b/lua/diffmantic/core/actions.lua @@ -39,16 +39,25 @@ local function build_action(action_type, src_node, dst_node, extra) to_line = to_line, suppressed_renames = nil, }, + analysis = nil, } if extra then - for key, value in pairs(extra) do - action[key] = value + if extra.context then + action.context = extra.context + end + if extra.analysis then + action.analysis = extra.analysis + end + if extra.metadata then + action.metadata = vim.tbl_extend("force", action.metadata, extra.metadata) end action.metadata.old_name = extra.old_name or extra.from or action.metadata.old_name action.metadata.new_name = extra.new_name or extra.to or action.metadata.new_name action.metadata.from_line = extra.from_line or action.metadata.from_line action.metadata.to_line = extra.to_line or action.metadata.to_line + action.metadata.node_type = extra.node_type or action.metadata.node_type + action.metadata.suppressed_renames = extra.suppressed_renames or action.metadata.suppressed_renames if extra.context and extra.context.suppressed_usages then action.metadata.suppressed_renames = extra.context.suppressed_usages end @@ -515,8 +524,9 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op -- If a declaration rename exists for a pair, suppress usage-level duplicates for that pair. local declaration_pairs = {} for _, rename_action in ipairs(renames) do - local from_name = rename_action.metadata and rename_action.metadata.old_name or rename_action.from - local to_name = rename_action.metadata and rename_action.metadata.new_name or rename_action.to + local metadata = rename_action.metadata or {} + local from_name = metadata.old_name + local to_name = metadata.new_name if rename_action.context and rename_action.context.declaration then declaration_pairs[pair_key(from_name, to_name)] = true end @@ -525,8 +535,9 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op local suppressed_by_pair = {} local filtered_renames = {} for _, rename_action in ipairs(renames) do - local from_name = rename_action.metadata and rename_action.metadata.old_name or rename_action.from - local to_name = rename_action.metadata and rename_action.metadata.new_name or rename_action.to + local metadata = rename_action.metadata or {} + local from_name = metadata.old_name + local to_name = metadata.new_name local key = pair_key(from_name, to_name) local is_declaration = rename_action.context and rename_action.context.declaration if not declaration_pairs[key] or is_declaration then @@ -534,13 +545,14 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op else suppressed_by_pair[key] = suppressed_by_pair[key] or {} table.insert(suppressed_by_pair[key], { - from = from_name, - to = to_name, src = rename_action.src, dst = rename_action.dst, - src_range = rename_action.src or rename_action.src_range, - dst_range = rename_action.dst or rename_action.dst_range, - lines = rename_action.lines, + metadata = { + old_name = from_name, + new_name = to_name, + from_line = metadata.from_line, + to_line = metadata.to_line, + }, context = rename_action.context, }) end @@ -548,8 +560,9 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op for _, rename_action in ipairs(filtered_renames) do if rename_action.context and rename_action.context.declaration then - local from_name = rename_action.metadata and rename_action.metadata.old_name or rename_action.from - local to_name = rename_action.metadata and rename_action.metadata.new_name or rename_action.to + local metadata = rename_action.metadata or {} + local from_name = metadata.old_name + local to_name = metadata.new_name local key = pair_key(from_name, to_name) local suppressed_usages = suppressed_by_pair[key] if suppressed_usages and #suppressed_usages > 0 then diff --git a/lua/diffmantic/core/analysis.lua b/lua/diffmantic/core/analysis.lua index af2e290..f006caa 100644 --- a/lua/diffmantic/core/analysis.lua +++ b/lua/diffmantic/core/analysis.lua @@ -231,8 +231,9 @@ local function collect_suppressed_rename_pairs(actions) local declaration_pairs = {} for _, action in ipairs(actions) do if action.type == "rename" then - local old_name = action.metadata and action.metadata.old_name or action.from - local new_name = action.metadata and action.metadata.new_name or action.to + local metadata = action.metadata or {} + local old_name = metadata.old_name + local new_name = metadata.new_name if action.context and action.context.declaration and old_name and new_name and old_name ~= new_name then declaration_pairs[old_name] = new_name end @@ -240,8 +241,8 @@ local function collect_suppressed_rename_pairs(actions) local suppressed = action.metadata and action.metadata.suppressed_renames or nil if suppressed then for _, usage in ipairs(suppressed) do - local src = clone_range_like(usage.src or usage.src_range) - local dst = clone_range_like(usage.dst or usage.dst_range) + local src = clone_range_like(usage.src) + local dst = clone_range_like(usage.dst) if src and dst then table.insert(pairs, { src = src, dst = dst }) end From df1df3f59c33a46c0333465a57cf669ce03cc79c Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sat, 14 Feb 2026 19:23:10 +0530 Subject: [PATCH 23/48] fix(core): using structural role captures for significance and matching - added structural role checks in core/roles.lua (outer captures only) - using structural checks in actions - removed false insert/delete noise from lhs/rhs leaf captures --- lua/diffmantic/core/actions.lua | 29 ++++++++++++++++++++++++++++- lua/diffmantic/core/bottom_up.lua | 13 ++++++++----- lua/diffmantic/core/roles.lua | 25 +++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/lua/diffmantic/core/actions.lua b/lua/diffmantic/core/actions.lua index 0892e18..b2a8864 100644 --- a/lua/diffmantic/core/actions.lua +++ b/lua/diffmantic/core/actions.lua @@ -642,10 +642,26 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op } local function has_kind(node, index, kind) - return index and roles.has_kind(node, index, kind) or false + return index and roles.has_structural_kind(node, index, kind) or false end + local function has_role_captures(index) + if not index or not index.by_capture then + return false + end + for capture, _ in pairs(index.by_capture) do + if capture ~= "diff.fallback.node" then + return true + end + end + return false + end + + local src_uses_roles = has_role_captures(src_role_index) + local dst_uses_roles = has_role_captures(dst_role_index) + local function is_significant(info, index) + local uses_roles = (index == src_role_index and src_uses_roles) or (index == dst_role_index and dst_uses_roles) local node = info.node if has_kind(node, index, "function") or has_kind(node, index, "class") @@ -657,20 +673,31 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op then return true end + if uses_roles then + return false + end return significant_types[info.type] or false end local function is_transparent_update_ancestor(info, index) + local uses_roles = (index == src_role_index and src_uses_roles) or (index == dst_role_index and dst_uses_roles) if has_kind(info.node, index, "class") then return true end + if uses_roles then + return false + end return transparent_update_ancestors[info.type] or false end local function is_movable(info, index) + local uses_roles = (index == src_role_index and src_uses_roles) or (index == dst_role_index and dst_uses_roles) if has_kind(info.node, index, "function") or has_kind(info.node, index, "class") then return true end + if uses_roles then + return false + end return movable_types[info.type] or false end diff --git a/lua/diffmantic/core/bottom_up.lua b/lua/diffmantic/core/bottom_up.lua index 948a8b7..12ee9d1 100644 --- a/lua/diffmantic/core/bottom_up.lua +++ b/lua/diffmantic/core/bottom_up.lua @@ -289,10 +289,10 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src local function is_identifier_type(info, role_index) if - roles.has_kind(info.node, role_index, "function") - or roles.has_kind(info.node, role_index, "class") - or roles.has_kind(info.node, role_index, "variable") - or roles.has_kind(info.node, role_index, "assignment") + roles.has_structural_kind(info.node, role_index, "function") + or roles.has_structural_kind(info.node, role_index, "class") + or roles.has_structural_kind(info.node, role_index, "variable") + or roles.has_structural_kind(info.node, role_index, "assignment") then return true end @@ -300,7 +300,10 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src end local function is_unique_structure_fallback_type(info, role_index) - if roles.has_kind(info.node, role_index, "function") or roles.has_kind(info.node, role_index, "class") then + if + roles.has_structural_kind(info.node, role_index, "function") + or roles.has_structural_kind(info.node, role_index, "class") + then return true end return unique_structure_fallback_types[info.type] or false diff --git a/lua/diffmantic/core/roles.lua b/lua/diffmantic/core/roles.lua index 2f38f9f..d3e82c5 100644 --- a/lua/diffmantic/core/roles.lua +++ b/lua/diffmantic/core/roles.lua @@ -26,6 +26,16 @@ local CAPTURE_BY_KIND = { ["rename_identifier"] = { "diff.identifier.rename" }, } +local STRUCTURAL_CAPTURE_BY_KIND = { + ["function"] = { "diff.function.outer" }, + ["class"] = { "diff.class.outer" }, + ["variable"] = { "diff.variable.outer" }, + ["assignment"] = { "diff.assignment.outer" }, + ["import"] = { "diff.import.outer" }, + ["return"] = { "diff.return.outer" }, + ["preproc"] = { "diff.preproc.outer" }, +} + local FALLBACK_QUERY = "((_) @diff.fallback.node)" local function add_capture(index, capture, node) @@ -128,6 +138,21 @@ function M.has_kind(node, index, kind) return false end +function M.has_structural_kind(node, index, kind) + local captures = STRUCTURAL_CAPTURE_BY_KIND[kind] + if not captures then + return M.has_kind(node, index, kind) + end + + for _, capture in ipairs(captures) do + if M.has_capture(node, index, capture) then + return true + end + end + + return false +end + function M.get_kind_name_node(node, index, kind) local capture = string.format("diff.%s.name", kind) return M.find_descendant_with_capture(node, index, capture) From 3414bdfd7ff3896bbf5ba95d6a45de9b3a0c9473 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sun, 15 Feb 2026 09:26:46 +0530 Subject: [PATCH 24/48] refactor(ui): consuming normalized action payload and sign precedence stabilized - renderer reads action.src/action.dst/action.metadata/action.analysis only - render pipeline applies filler layers before spans/signs/virt texts --- lua/diffmantic/ui/renderer.lua | 18 ++++++++++-------- lua/diffmantic/ui/signs.lua | 8 ++++---- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lua/diffmantic/ui/renderer.lua b/lua/diffmantic/ui/renderer.lua index a202586..59b181a 100644 --- a/lua/diffmantic/ui/renderer.lua +++ b/lua/diffmantic/ui/renderer.lua @@ -103,31 +103,37 @@ end function M.render(src_buf, dst_buf, actions, ns) local src_sign_rows = {} local dst_sign_rows = {} + local src_fillers, dst_fillers = filler.compute(actions, src_buf, dst_buf) + filler.apply(src_buf, ns, src_fillers) + filler.apply(dst_buf, ns, dst_fillers) for _, action in ipairs(actions) do local style = TYPE_STYLE[action.type] if style then - local src = action.src or action.src_range - local dst = action.dst or action.dst_range + local src = action.src + local dst = action.dst local meta = action.metadata or {} if action.type == "update" and action.analysis and action.analysis.hunks then + local rendered_hunk = false for _, hunk in ipairs(action.analysis.hunks) do local hstyle = HUNK_STYLE[hunk.kind] or HUNK_STYLE.change if hunk.src and hstyle.src_hl then apply_span(src_buf, ns, hunk.src, hstyle.src_hl) apply_sign(src_buf, ns, hunk.src.start_row, hstyle.src_sign, hstyle.src_hl, src_sign_rows) + rendered_hunk = true end if hunk.dst and hstyle.dst_hl then apply_span(dst_buf, ns, hunk.dst, hstyle.dst_hl) apply_sign(dst_buf, ns, hunk.dst.start_row, hstyle.dst_sign, hstyle.dst_hl, dst_sign_rows) + rendered_hunk = true end end - if src and src.start_row ~= nil then + if rendered_hunk and src and src.start_row ~= nil then apply_sign(src_buf, ns, src.start_row, "U", "DiffmanticChange", src_sign_rows) end - if dst and dst.start_row ~= nil then + if rendered_hunk and dst and dst.start_row ~= nil then apply_sign(dst_buf, ns, dst.start_row, "U", "DiffmanticChange", dst_sign_rows) end else @@ -159,10 +165,6 @@ function M.render(src_buf, dst_buf, actions, ns) end end end - - local src_fillers, dst_fillers = filler.compute(actions, src_buf, dst_buf) - filler.apply(src_buf, ns, src_fillers) - filler.apply(dst_buf, ns, dst_fillers) end return M diff --git a/lua/diffmantic/ui/signs.lua b/lua/diffmantic/ui/signs.lua index 6fd5848..e194fe5 100644 --- a/lua/diffmantic/ui/signs.lua +++ b/lua/diffmantic/ui/signs.lua @@ -9,11 +9,11 @@ local SIGN_GROUP_BY_TEXT_GROUP = { } local SIGN_PRIORITY_BY_TEXT_GROUP = { - DiffmanticAdd = 10, - DiffmanticDelete = 10, + DiffmanticAdd = 40, + DiffmanticDelete = 40, DiffmanticChange = 20, - DiffmanticMove = 30, - DiffmanticRename = 40, + DiffmanticMove = 10, + DiffmanticRename = 30, } function M.glyph() From 0bbfbd7ed8b29cd1650556d9429f416b24ae305c Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sun, 15 Feb 2026 15:23:11 +0530 Subject: [PATCH 25/48] refactor(ui): simpilified filler logic and fixed non rendering hunk styling - renamed span_line_count to line_count for clarity - replaced line_trim_bounds and is_whole_line_span with trailing_blanks helper - removed unused functions: line_trim_bounds and is_whole_line_span - applied span and size styling to non-rendering hunks instead of rendering ones - using style.sign and style.hl consistently when applying signs --- lua/diffmantic/ui/filler.lua | 458 ++++++++++++++++----------------- lua/diffmantic/ui/renderer.lua | 15 +- 2 files changed, 238 insertions(+), 235 deletions(-) diff --git a/lua/diffmantic/ui/filler.lua b/lua/diffmantic/ui/filler.lua index ff0759a..d21b8e6 100644 --- a/lua/diffmantic/ui/filler.lua +++ b/lua/diffmantic/ui/filler.lua @@ -6,7 +6,7 @@ local function make_virt_line(hl_group, char) return { { string.rep(char or "╱", VIRT_LINE_LEN), hl_group } } end -local function span_line_count(range) +local function line_count(range) if not range then return 0 end @@ -26,53 +26,34 @@ local function span_line_count(range) return count end -local function line_trim_bounds(line) - if not line then - return nil, nil - end - local first = line:find("%S") - if not first then - return nil, nil +local function trailing_blanks(buf, range) + if not buf or not range or range.end_row == nil then + return 0 end - local rev_last = line:reverse():find("%S") - local last = #line - rev_last + 1 - return first - 1, last -end -local function is_whole_line_span(buf, range) - if not range then - return false - end - if range.start_row == nil or range.end_row == nil then - return false + local last_occupied = range.end_row + if (range.end_col or 0) == 0 then + last_occupied = last_occupied - 1 end - if range.start_row ~= range.end_row then - return true + if last_occupied < (range.start_row or 0) then + last_occupied = range.start_row or 0 end - local row = range.start_row - local lines = vim.api.nvim_buf_get_lines(buf, row, row + 1, false) - local line = lines[1] or "" - local first_col, last_col_exclusive = line_trim_bounds(line) - if not first_col then - return false - end - return range.start_col <= first_col and range.end_col >= last_col_exclusive -end -local function trailing_blanks(buf, end_row, end_col) - local last_occupied = end_col > 0 and end_row or (end_row - 1) - local buf_count = vim.api.nvim_buf_line_count(buf) - local count = 0 + local buf_lines = vim.api.nvim_buf_line_count(buf) local row = last_occupied + 1 - while row < buf_count do + local count = 0 + + while row < buf_lines do local lines = vim.api.nvim_buf_get_lines(buf, row, row + 1, false) - if lines[1] and lines[1]:match("^%s*$") then + local line = lines[1] or "" + if line:match("^%s*$") then count = count + 1 row = row + 1 else break end end + return count end @@ -98,36 +79,113 @@ local function range_within(inner, outer) return true end -local function add_filler(dst_list, seen, row, count, hl_group) - if row == nil or count == nil or count <= 0 or not hl_group then +local function ensure_lines(entry, count, default_hl) + for i = #entry.lines + 1, count do + entry.lines[i] = default_hl + end +end + +local function mark_lines(entry, start_idx, count, hl_group) + if not entry or start_idx == nil or count == nil or count <= 0 then return end - local key = table.concat({ tostring(row), tostring(count), hl_group }, ":") - if seen[key] then + ensure_lines(entry, start_idx + count - 1, "DiffmanticMoveFiller") + for i = start_idx, start_idx + count - 1 do + entry.lines[i] = hl_group + end +end + +local function add_simple(fillers, row, count, hl_group) + if row == nil or count == nil or count <= 0 or not hl_group then return end - seen[key] = true - table.insert(dst_list, { + table.insert(fillers, { row = row, count = count, hl_group = hl_group, }) end -local function mirror_dst_row_into_src(update_src, update_dst, dst_row) - if not update_src or not update_dst or dst_row == nil then - return dst_row +local function coalesce_simple(fillers) + local simple = {} + local rich = {} + for _, filler in ipairs(fillers) do + if filler.lines then + table.insert(rich, filler) + else + table.insert(simple, filler) + end end - local offset = math.max(0, dst_row - update_dst.start_row) - return update_src.start_row + offset + + table.sort(simple, function(a, b) + return a.row < b.row + end) + + local merged = {} + for _, filler in ipairs(simple) do + local prev = merged[#merged] + if prev and prev.hl_group == filler.hl_group and filler.row <= (prev.row + prev.count) then + local prev_end = prev.row + prev.count + local this_end = filler.row + filler.count + prev.count = math.max(prev_end, this_end) - prev.row + else + table.insert(merged, { + row = filler.row, + count = filler.count, + hl_group = filler.hl_group, + }) + end + end + + for _, filler in ipairs(rich) do + table.insert(merged, filler) + end + + table.sort(merged, function(a, b) + return a.row < b.row + end) + return merged +end + +local function count_filler_lines_before(fillers, row) + if row == nil or not fillers then + return 0 + end + local total = 0 + for _, filler in ipairs(fillers) do + if filler.row and filler.row < row then + if filler.lines then + total = total + (filler.base_count or #filler.lines) + elseif filler.count then + total = total + filler.count + end + end + end + return total +end + +local function find_move_region_for_src(move_regions, src_range) + if not src_range then + return nil + end + for _, region in ipairs(move_regions) do + if range_within(src_range, region.src) then + return region + end + end + return nil end -local function mirror_src_row_into_dst(update_src, update_dst, src_row) - if not update_src or not update_dst or src_row == nil then - return src_row +local function find_move_region_for_dst(move_regions, dst_range) + if not dst_range then + return nil + end + for _, region in ipairs(move_regions) do + if range_within(dst_range, region.dst) then + return region + end end - local offset = math.max(0, src_row - update_src.start_row) - return update_dst.start_row + offset + return nil end local function collect_runs(ranges) @@ -139,7 +197,7 @@ local function collect_runs(ranges) end) local runs = {} for _, range in ipairs(ranges) do - local count = span_line_count(range) + local count = line_count(range) local current = runs[#runs] if current and range.start_row <= (current.end_row + 1) then current.end_row = math.max(current.end_row, range.end_row) @@ -148,6 +206,8 @@ local function collect_runs(ranges) table.insert(runs, { start_row = range.start_row, end_row = range.end_row, + start_col = range.start_col, + end_col = range.end_col, count = count, }) end @@ -155,171 +215,100 @@ local function collect_runs(ranges) return runs end -function M.compute(actions, src_buf, dst_buf) - local src_fillers = {} - local dst_fillers = {} - local seen_src = {} - local seen_dst = {} - local move_src_ranges = {} - local move_dst_ranges = {} +local function range_from_run(run) + return { + start_row = run.start_row, + start_col = run.start_col or 0, + end_row = run.end_row, + end_col = run.end_col or 1, + } +end - for _, action in ipairs(actions) do - if action.type == "move" then - local src = action.src or action.src_range - local dst = action.dst or action.dst_range - if src then - table.insert(move_src_ranges, src) - end - if dst then - table.insert(move_dst_ranges, dst) - end - end +local function move_anchor_after_blank_run(buf, row) + if not buf or row == nil or row < 0 then + return row end - - local function inside_move_src(range) - if not range then - return false - end - for _, moved in ipairs(move_src_ranges) do - if range_within(range, moved) then - return true - end + local max_row = vim.api.nvim_buf_line_count(buf) - 1 + local cursor = math.min(row, max_row) + + while cursor <= max_row do + local lines = vim.api.nvim_buf_get_lines(buf, cursor, cursor + 1, false) + local line = lines[1] or "" + if line:match("^%s*$") then + cursor = cursor + 1 + else + break end - return false end - local function inside_move_dst(range) - if not range then - return false - end - for _, moved in ipairs(move_dst_ranges) do - if range_within(range, moved) then - return true - end - end - return false + if cursor > max_row then + return row end + return cursor +end - for _, action in ipairs(actions) do - local t = action.type - local src = action.src or action.src_range - local dst = action.dst or action.dst_range - - if t == "insert" and dst and dst.start_row ~= nil then - if inside_move_dst(dst) then - goto continue - end - if not is_whole_line_span(dst_buf, dst) then - goto continue - end - local count = span_line_count(dst) - if count >= 1 then - add_filler(src_fillers, seen_src, dst.start_row, count, "DiffmanticAddFiller") - end - elseif t == "delete" and src and src.start_row ~= nil then - if inside_move_src(src) then - goto continue - end - if not is_whole_line_span(src_buf, src) then - goto continue - end - local count = span_line_count(src) - if count >= 1 then - add_filler(dst_fillers, seen_dst, src.start_row, count, "DiffmanticDeleteFiller") - end - elseif t == "move" and action.src_node and action.dst_node then - local ssr, _, ser, sec = action.src_node:range() - local src_body = ser - ssr - if sec > 0 then - src_body = src_body + 1 - end - local src_trailing = trailing_blanks(src_buf, ser, sec) - local src_count = src_body + src_trailing +function M.compute(actions, src_buf, dst_buf) + local src_fillers = {} + local dst_fillers = {} + local move_regions = {} - local dsr, _, der, dec = action.dst_node:range() - local dst_body = der - dsr - if dec > 0 then - dst_body = dst_body + 1 - end - local dst_trailing = trailing_blanks(dst_buf, der, dec) - local dst_count = dst_body + dst_trailing - - local src_text = vim.treesitter.get_node_text(action.src_node, src_buf) - local dst_text = vim.treesitter.get_node_text(action.dst_node, dst_buf) - local ok, hunks = pcall(vim.text.diff, src_text, dst_text, { - result_type = "indices", - linematch = 60, + for _, action in ipairs(actions) do + if action.type == "move" and action.src and action.dst then + local src_base_count = line_count(action.src) + local dst_base_count = line_count(action.dst) + local src_count = src_base_count + trailing_blanks(src_buf, action.src) + local dst_count = dst_base_count + trailing_blanks(dst_buf, action.dst) + + local src_entry = { + row = action.dst.start_row, + lines = {}, + base_count = dst_base_count, + } + ensure_lines(src_entry, dst_count, "DiffmanticMoveFiller") + + local dst_entry = { + row = action.src.start_row, + lines = {}, + base_count = src_base_count, + } + ensure_lines(dst_entry, src_count, "DiffmanticMoveFiller") + + table.insert(src_fillers, src_entry) + table.insert(dst_fillers, dst_entry) + + table.insert(move_regions, { + src = action.src, + dst = action.dst, + src_entry = src_entry, + dst_entry = dst_entry, }) + end + end - local dst_inserted = {} - local src_deleted = {} - if ok and hunks then - for _, h in ipairs(hunks) do - local start_a, count_a, start_b, count_b = h[1], h[2], h[3], h[4] - local overlap = math.min(count_a, count_b) - if count_b > overlap then - for i = start_b + overlap, start_b + count_b - 1 do - dst_inserted[i] = true - end - end - if count_a > overlap then - for i = start_a + overlap, start_a + count_a - 1 do - src_deleted[i] = true - end - end - end - end - - if dst_count > 0 then - local lines = {} - for i = 1, dst_body do - lines[i] = dst_inserted[i] and "DiffmanticAddFiller" or "DiffmanticMoveFiller" - end - for i = dst_body + 1, dst_count do - lines[i] = "DiffmanticMoveFiller" + for _, action in ipairs(actions) do + if action.type == "insert" and action.dst then + local count = line_count(action.dst) + if count > 0 then + local region = find_move_region_for_dst(move_regions, action.dst) + if region then + local start_idx = (action.dst.start_row - region.dst.start_row) + 1 + mark_lines(region.src_entry, start_idx, count, "DiffmanticAddFiller") + else + add_simple(src_fillers, action.dst.start_row, count, "DiffmanticAddFiller") end - table.insert(src_fillers, { - row = dsr, - lines = lines, - }) end - - if src_count > 0 then - local src_block_end = sec > 0 and (ser + 1) or ser - local best_dst_row = nil - local best_src_row = math.huge - for _, other in ipairs(actions) do - if other ~= action and other.src_node and other.dst_node then - local osr = select(1, other.src_node:range()) - local odr = select(1, other.dst_node:range()) - if osr >= src_block_end and osr < best_src_row then - best_src_row = osr - best_dst_row = odr - end - end - end - if best_dst_row then - local lines = {} - for i = 1, src_body do - lines[i] = src_deleted[i] and "DiffmanticDeleteFiller" or "DiffmanticMoveFiller" - end - for i = src_body + 1, src_count do - lines[i] = "DiffmanticMoveFiller" - end - table.insert(dst_fillers, { - row = best_dst_row, - lines = lines, - }) + elseif action.type == "delete" and action.src then + local count = line_count(action.src) + if count > 0 then + local region = find_move_region_for_src(move_regions, action.src) + if region then + local start_idx = (action.src.start_row - region.src.start_row) + 1 + mark_lines(region.dst_entry, start_idx, count, "DiffmanticDeleteFiller") + else + add_simple(dst_fillers, action.src.start_row, count, "DiffmanticDeleteFiller") end end - elseif t == "update" and action.analysis and action.analysis.hunks then - local update_src = action.src or action.src_range - local update_dst = action.dst or action.dst_range - local move_related_update = inside_move_src(update_src) or inside_move_dst(update_dst) - if move_related_update then - goto continue - end - + elseif action.type == "update" and action.src and action.dst and action.analysis and action.analysis.hunks then local insert_ranges = {} local delete_ranges = {} for _, hunk in ipairs(action.analysis.hunks) do @@ -334,40 +323,52 @@ function M.compute(actions, src_buf, dst_buf) local delete_runs = collect_runs(delete_ranges) for _, run in ipairs(insert_runs) do - local anchor = mirror_dst_row_into_src(update_src, update_dst, run.start_row) - local deleted_before = 0 - for _, d in ipairs(delete_runs) do - if d.start_row <= anchor then - deleted_before = deleted_before + d.count + local run_range = range_from_run(run) + local dst_move_region = find_move_region_for_dst(move_regions, run_range) + if dst_move_region then + local start_idx = (run.start_row - dst_move_region.dst.start_row) + 1 + mark_lines(dst_move_region.src_entry, start_idx, run.count, "DiffmanticAddFiller") + else + local anchor = action.src.start_row + math.max(0, run.start_row - action.dst.start_row) + local deleted_before = 0 + for _, d in ipairs(delete_runs) do + if d.start_row <= anchor then + deleted_before = deleted_before + d.count + end end + anchor = anchor + deleted_before + add_simple(src_fillers, anchor, run.count, "DiffmanticAddFiller") end - anchor = anchor + deleted_before - add_filler(src_fillers, seen_src, anchor, run.count, "DiffmanticAddFiller") end for _, run in ipairs(delete_runs) do - local anchor = mirror_src_row_into_dst(update_src, update_dst, run.start_row) - local inserted_before = 0 - for _, ins in ipairs(insert_runs) do - if ins.start_row <= anchor then - inserted_before = inserted_before + ins.count + local run_range = range_from_run(run) + local src_move_region = find_move_region_for_src(move_regions, run_range) + if src_move_region then + local start_idx = (run.start_row - src_move_region.src.start_row) + 1 + mark_lines(src_move_region.dst_entry, start_idx, run.count, "DiffmanticDeleteFiller") + else + local anchor = action.dst.start_row + math.max(0, run.start_row - action.src.start_row) + local inserted_before = 0 + for _, ins in ipairs(insert_runs) do + if ins.start_row <= anchor then + inserted_before = inserted_before + ins.count + end end + anchor = anchor + inserted_before + add_simple(dst_fillers, anchor, run.count, "DiffmanticDeleteFiller") end - anchor = anchor + inserted_before - add_filler(dst_fillers, seen_dst, anchor, run.count, "DiffmanticDeleteFiller") end end - ::continue:: end - table.sort(src_fillers, function(a, b) - return a.row < b.row - end) - table.sort(dst_fillers, function(a, b) - return a.row < b.row - end) + for _, region in ipairs(move_regions) do + region.src_entry.row = region.src_entry.row + count_filler_lines_before(dst_fillers, region.src_entry.row) + region.dst_entry.row = region.dst_entry.row + count_filler_lines_before(src_fillers, region.dst_entry.row) + region.dst_entry.row = move_anchor_after_blank_run(dst_buf, region.dst_entry.row) + end - return src_fillers, dst_fillers + return coalesce_simple(src_fillers), coalesce_simple(dst_fillers) end function M.apply(buf, ns, fillers) @@ -375,12 +376,11 @@ function M.apply(buf, ns, fillers) return end - local line_count = vim.api.nvim_buf_line_count(buf) - + local line_count_buf = vim.api.nvim_buf_line_count(buf) for _, filler in ipairs(fillers) do local row = filler.row - if row >= line_count then - row = line_count - 1 + if row >= line_count_buf then + row = line_count_buf - 1 end if row < 0 then row = 0 diff --git a/lua/diffmantic/ui/renderer.lua b/lua/diffmantic/ui/renderer.lua index 59b181a..e19a200 100644 --- a/lua/diffmantic/ui/renderer.lua +++ b/lua/diffmantic/ui/renderer.lua @@ -129,12 +129,15 @@ function M.render(src_buf, dst_buf, actions, ns) rendered_hunk = true end end - - if rendered_hunk and src and src.start_row ~= nil then - apply_sign(src_buf, ns, src.start_row, "U", "DiffmanticChange", src_sign_rows) - end - if rendered_hunk and dst and dst.start_row ~= nil then - apply_sign(dst_buf, ns, dst.start_row, "U", "DiffmanticChange", dst_sign_rows) + if not rendered_hunk then + if src then + apply_span(src_buf, ns, src, style.hl) + apply_sign(src_buf, ns, src.start_row, style.sign, style.hl, src_sign_rows) + end + if dst then + apply_span(dst_buf, ns, dst, style.hl) + apply_sign(dst_buf, ns, dst.start_row, style.sign, style.hl, dst_sign_rows) + end end else if src then From f6ebcece6f0ac9235e58ebc831b5827e3b4ddf5d Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sun, 15 Feb 2026 16:11:04 +0530 Subject: [PATCH 26/48] feat(core): supressing rename-only update actions detects and filters out update actions that only rename variables without any actual code changes. Reduces the noise in diff view. - Tracks has_non_rename_change flag during leaf change analysis - calculates rename_only when changes exists but are all renames - skips fallback hunk generation for rename-only updates - added update_suppress timing instrumentation --- lua/diffmantic/core/actions.lua | 17 +++++++++++++++++ lua/diffmantic/core/analysis.lua | 7 ++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lua/diffmantic/core/actions.lua b/lua/diffmantic/core/actions.lua index b2a8864..f16c2e1 100644 --- a/lua/diffmantic/core/actions.lua +++ b/lua/diffmantic/core/actions.lua @@ -923,6 +923,23 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end stop_timer(analysis_start, "analysis") + local update_suppress_start = start_timer() + local filtered_actions = {} + for _, action in ipairs(actions) do + if action.type == "update" then + local action_analysis = action.analysis or {} + local has_hunks = action_analysis.hunks and #action_analysis.hunks > 0 + local has_rename_pairs = action_analysis.rename_pairs and next(action_analysis.rename_pairs) ~= nil + if action_analysis.rename_only and has_rename_pairs and not has_hunks then + goto continue + end + end + table.insert(filtered_actions, action) + ::continue:: + end + actions = filtered_actions + stop_timer(update_suppress_start, "update_suppress") + local summary_start = start_timer() local summary = build_summary(actions) stop_timer(summary_start, "summary") diff --git a/lua/diffmantic/core/analysis.lua b/lua/diffmantic/core/analysis.lua index f006caa..336846a 100644 --- a/lua/diffmantic/core/analysis.lua +++ b/lua/diffmantic/core/analysis.lua @@ -421,6 +421,7 @@ function M.enrich(actions, opts) local rename_pairs = action.analysis and action.analysis.rename_pairs or {} local normalized_leaf = {} local hunks = {} + local has_non_rename_change = false for _, change in ipairs(raw_leaf_changes) do local src_range = range_metadata(change.src_node) @@ -433,6 +434,7 @@ function M.enrich(actions, opts) }) if change.src_text ~= change.dst_text and rename_pairs[change.src_text] ~= change.dst_text then + has_non_rename_change = true local hunk_src = src_range local hunk_dst = dst_range @@ -474,7 +476,9 @@ function M.enrich(actions, opts) end end - if #hunks == 0 then + local rename_only = (#normalized_leaf > 0) and (next(rename_pairs) ~= nil) and not has_non_rename_change + + if #hunks == 0 and not rename_only then hunks = fallback_hunks_from_diff(action.src_node, action.dst_node, src_buf, dst_buf, rename_pairs) end if #suppressed_pairs > 0 and #hunks > 0 then @@ -501,6 +505,7 @@ function M.enrich(actions, opts) leaf_changes = normalized_leaf, rename_pairs = rename_pairs, hunks = hunks, + rename_only = rename_only, } end end From 1d9de06b24ba59dd5c0860b5e895ae4d50031dcd Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sun, 15 Feb 2026 17:37:12 +0530 Subject: [PATCH 27/48] refactor(core): reduced rename detection noise in C/C++ fields Simplify parameter identifier extraction and suppress noisy field declaration renames that lack semantic context. - Change walk() to first_identifier() - extract only first identifier per parameter instead of recursively collecting all - Remove type_identifier from identifier matching - Remove Pass 1b strict fallback seed collection logic - Disable rename detection for field_declaration declarators - Update C/C++ queries to exclude field_identifier from init_declarator --- lua/diffmantic/core/actions.lua | 161 ++++--------------------------- lua/diffmantic/core/semantic.lua | 7 +- queries/c/diffmantic.scm | 2 +- queries/cpp/diffmantic.scm | 2 +- 4 files changed, 25 insertions(+), 147 deletions(-) diff --git a/lua/diffmantic/core/actions.lua b/lua/diffmantic/core/actions.lua index f16c2e1..1cf5b10 100644 --- a/lua/diffmantic/core/actions.lua +++ b/lua/diffmantic/core/actions.lua @@ -300,23 +300,34 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end local out = {} - local function walk(n) + local function first_identifier(n) if n:child_count() == 0 then local t = n:type() - if t == "identifier" or t == "type_identifier" or t == "field_identifier" or t == "property_identifier" then + if t == "identifier" or t == "field_identifier" or t == "property_identifier" then local text = vim.treesitter.get_node_text(n, bufnr) if text and text:match("^[%a_][%w_]*$") then - table.insert(out, { node = n, text = text }) + return { node = n, text = text } end end - return + return nil end for child in n:iter_children() do - walk(child) + local found = first_identifier(child) + if found then + return found + end end + return nil end - walk(params_root) + for child in params_root:iter_children() do + if child:named() then + local found = first_identifier(child) + if found then + table.insert(out, found) + end + end + end return out end @@ -360,71 +371,6 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end end - -- Pass 1b: strict fallback seed collection from mapped leaf identifiers. - if is_buf_available then - local src_to_dst_local = {} - for _, m in ipairs(mappings) do - src_to_dst_local[m.src] = m.dst - end - - local function child_index(node) - local parent = node and node:parent() or nil - if not parent then - return -1 - end - local idx = 0 - for child in parent:iter_children() do - if child:equal(node) then - return idx - end - idx = idx + 1 - end - return -1 - end - - local function is_identifier_node(node) - if not node then - return false - end - local t = node:type() - return t == "identifier" or t == "type_identifier" or t == "field_identifier" or t == "property_identifier" - end - - local function is_identifier_like(text) - return text and text:match("^[%a_][%w_]*$") ~= nil - end - - for _, m in ipairs(mappings) do - local s = src_info[m.src] - local d = dst_info[m.dst] - if s and d and s.node and d.node then - local src_node = s.node - local dst_node = d.node - if src_node:child_count() == 0 and dst_node:child_count() == 0 then - local src_parent = src_node:parent() - local dst_parent = dst_node:parent() - if src_parent and dst_parent and src_to_dst_local[src_parent:id()] == dst_parent:id() then - if src_parent:type() == dst_parent:type() and child_index(src_node) == child_index(dst_node) then - local src_text = vim.treesitter.get_node_text(src_node, src_buf) - local dst_text = vim.treesitter.get_node_text(dst_node, dst_buf) - if src_text and dst_text and src_text ~= dst_text then - if - is_decl_rename(src_node, dst_node) - and is_identifier_node(src_node) - and is_identifier_node(dst_node) - and is_identifier_like(src_text) - and is_identifier_like(dst_text) - then - add_seed(src_text, dst_text) - end - end - end - end - end - end - end - end - -- Pass 2: emit leaf rename actions gated by seeds (and declaration renames). for _, action in ipairs(actions_list) do if action.type == "update" and action.analysis and action.analysis.leaf_changes then @@ -448,79 +394,6 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end end - -- Pass 3: emit strict mapped-leaf rename actions for seed pairs not present in leaf_changes. - if is_buf_available then - local src_to_dst_local = {} - for _, m in ipairs(mappings) do - src_to_dst_local[m.src] = m.dst - end - - local function child_index(node) - local parent = node and node:parent() or nil - if not parent then - return -1 - end - local idx = 0 - for child in parent:iter_children() do - if child:equal(node) then - return idx - end - idx = idx + 1 - end - return -1 - end - - local function is_identifier_node(node) - if not node then - return false - end - local t = node:type() - return t == "identifier" or t == "type_identifier" or t == "field_identifier" or t == "property_identifier" - end - - local function is_identifier_like(text) - return text and text:match("^[%a_][%w_]*$") ~= nil - end - - for _, m in ipairs(mappings) do - local s = src_info[m.src] - local d = dst_info[m.dst] - if s and d and s.node and d.node then - local src_node = s.node - local dst_node = d.node - if src_node:child_count() == 0 and dst_node:child_count() == 0 then - local src_parent = src_node:parent() - local dst_parent = dst_node:parent() - if src_parent and dst_parent and src_to_dst_local[src_parent:id()] == dst_parent:id() then - if src_parent:type() == dst_parent:type() and child_index(src_node) == child_index(dst_node) then - local src_text = vim.treesitter.get_node_text(src_node, src_buf) - local dst_text = vim.treesitter.get_node_text(dst_node, dst_buf) - if src_text and dst_text and src_text ~= dst_text then - if - is_decl_rename(src_node, dst_node) - and is_identifier_node(src_node) - and is_identifier_node(dst_node) - and is_identifier_like(src_text) - and is_identifier_like(dst_text) - then - local key = pair_key(src_text, dst_text) - if seed_pairs[key] then - push_rename(src_node, dst_node, src_text, dst_text, { - src_parent_type = src_parent:type(), - dst_parent_type = dst_parent:type(), - source = "mapping_seed", - declaration = true, - }) - end - end - end - end - end - end - end - end - end - -- If a declaration rename exists for a pair, suppress usage-level duplicates for that pair. local declaration_pairs = {} for _, rename_action in ipairs(renames) do diff --git a/lua/diffmantic/core/semantic.lua b/lua/diffmantic/core/semantic.lua index c26f075..de7bdb2 100644 --- a/lua/diffmantic/core/semantic.lua +++ b/lua/diffmantic/core/semantic.lua @@ -135,9 +135,14 @@ function M.is_rename_identifier(node, role_index) return true end if ptype == "field_declaration" and M.node_in_field(parent, "declarator", current) then - return true + -- C/C++ member fields often have no value context; rename inference here is noisy. + return false end if ptype == "declarator" then + local grandparent = parent:parent() + if grandparent and grandparent:type() == "field_declaration" then + return false + end return true end if ptype == "field" then diff --git a/queries/c/diffmantic.scm b/queries/c/diffmantic.scm index 06890e6..5b1ed94 100644 --- a/queries/c/diffmantic.scm +++ b/queries/c/diffmantic.scm @@ -41,4 +41,4 @@ name: (type_identifier) @diff.identifier.rename) (init_declarator - declarator: [(identifier) (field_identifier)] @diff.identifier.rename) + declarator: (identifier) @diff.identifier.rename) diff --git a/queries/cpp/diffmantic.scm b/queries/cpp/diffmantic.scm index 53e0d96..e9544a7 100644 --- a/queries/cpp/diffmantic.scm +++ b/queries/cpp/diffmantic.scm @@ -34,4 +34,4 @@ name: (type_identifier) @diff.identifier.rename) (init_declarator - declarator: [(identifier) (field_identifier)] @diff.identifier.rename) + declarator: (identifier) @diff.identifier.rename) From 9edc481e2cd5b5411c692dd2f9528dbe48b27a97 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Mon, 16 Feb 2026 16:02:25 +0530 Subject: [PATCH 28/48] refactor(core): suppress class member renames prevents noisy rename detection for identifiers within class bodies that are not the class name itself suppressing update actions for class bodies in favour of move/insert/delete - added is_class_like_context check to exclude non-name identifiers in class bodies from rename detection - added should_emit_update to suppress update for class/struct/types bodies --- lua/diffmantic/core/actions.lua | 59 ++++++++++++++++++++++++++++++- lua/diffmantic/core/bottom_up.lua | 27 +++++++++++--- lua/diffmantic/core/semantic.lua | 25 +++++++++++++ queries/c/diffmantic.scm | 31 +++++++++++++--- queries/cpp/diffmantic.scm | 17 ++++++--- queries/go/diffmantic.scm | 8 +++++ queries/typescript/diffmantic.scm | 20 +++++++++++ 7 files changed, 173 insertions(+), 14 deletions(-) diff --git a/lua/diffmantic/core/actions.lua b/lua/diffmantic/core/actions.lua index 1cf5b10..ecf9e62 100644 --- a/lua/diffmantic/core/actions.lua +++ b/lua/diffmantic/core/actions.lua @@ -259,6 +259,30 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end local function is_decl_rename(src_node, dst_node) + local function is_class_name_node(node, role_index) + return roles.has_capture(node, role_index, "diff.class.name") + end + + local function is_class_like_context(node, role_index) + local cur = node + while cur do + if roles.has_kind(cur, role_index, "class") then + return true + end + cur = cur:parent() + end + return false + end + + local src_is_class_name = is_class_name_node(src_node, src_role_index) + local dst_is_class_name = is_class_name_node(dst_node, dst_role_index) + if + (not src_is_class_name and is_class_like_context(src_node, src_role_index)) + or (not dst_is_class_name and is_class_like_context(dst_node, dst_role_index)) + then + return false + end + return semantic.is_rename_identifier(src_node, src_role_index) and semantic.is_rename_identifier(dst_node, dst_role_index) end @@ -574,6 +598,17 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op return movable_types[info.type] or false end + local function should_emit_update(info, index) + if not is_significant(info, index) then + return false + end + -- Type/struct/class bodies are better represented by insert/delete or move+rename. + if has_kind(info.node, index, "class") then + return false + end + return true + end + -- Helper: check if node or any descendant has different content local function has_content_change(src_node, dst_node) local src_info_data = src_info[src_node:id()] @@ -661,7 +696,7 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op for _, m in ipairs(mappings) do local s, d = src_info[m.src], dst_info[m.dst] - if nodes_with_changes[m.src] and is_significant(s, src_role_index) then + if nodes_with_changes[m.src] and should_emit_update(s, src_role_index) then if not src_has_updated_sig_ancestor[m.src] then table.insert(actions, build_action("update", s.node, d.node)) end @@ -757,6 +792,28 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end stop_timer(moves_start, "moves") + -- Updates for nodes that are also classified as moves are noisy and duplicate intent. + local moved_pairs = {} + for _, action in ipairs(actions) do + if action.type == "move" and action.src_node and action.dst_node then + moved_pairs[action.src_node:id() .. ":" .. action.dst_node:id()] = true + end + end + if next(moved_pairs) ~= nil then + local filtered = {} + for _, action in ipairs(actions) do + if action.type == "update" and action.src_node and action.dst_node then + local key = action.src_node:id() .. ":" .. action.dst_node:id() + if moved_pairs[key] then + goto continue + end + end + table.insert(filtered, action) + ::continue:: + end + actions = filtered + end + -- DELETES: unmapped source nodes local deletes_start = start_timer() for id, info in pairs(src_info) do diff --git a/lua/diffmantic/core/bottom_up.lua b/lua/diffmantic/core/bottom_up.lua index 12ee9d1..bfff02d 100644 --- a/lua/diffmantic/core/bottom_up.lua +++ b/lua/diffmantic/core/bottom_up.lua @@ -40,6 +40,26 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src -- Get the name of a declaration node (function or variable) local function get_declaration_name(node, bufnr, role_index) + local function find_first_identifier(n) + if not n then + return nil + end + if n:child_count() == 0 then + local t = n:type() + if t == "identifier" or t == "field_identifier" or t == "property_identifier" then + return n + end + return nil + end + for child in n:iter_children() do + local found = find_first_identifier(child) + if found then + return found + end + end + return nil + end + local function_name = roles.get_kind_name_text(node, role_index, bufnr, "function") if function_name and #function_name > 0 then return function_name @@ -133,10 +153,9 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src if node:type() == "function_definition" then for child in node:iter_children() do if child:type() == "function_declarator" then - for subchild in child:iter_children() do - if subchild:type() == "identifier" then - return vim.treesitter.get_node_text(subchild, bufnr) - end + local found = find_first_identifier(child) + if found then + return vim.treesitter.get_node_text(found, bufnr) end end end diff --git a/lua/diffmantic/core/semantic.lua b/lua/diffmantic/core/semantic.lua index de7bdb2..58fdd81 100644 --- a/lua/diffmantic/core/semantic.lua +++ b/lua/diffmantic/core/semantic.lua @@ -84,7 +84,29 @@ function M.is_rename_identifier(node, role_index) return false end + local function in_class_like_context(n, index) + if not index then + return false + end + local cur = n + while cur do + if roles.has_kind(cur, index, "class") then + return true + end + cur = cur:parent() + end + return false + end + + local function is_class_name_node(n, index) + return index and roles.has_capture(n, index, "diff.class.name") or false + end + if role_index and roles.has_kind(node, role_index, "rename_identifier") then + -- Allow renaming struct/type/class declaration names, but not members inside them. + if in_class_like_context(node, role_index) and not is_class_name_node(node, role_index) then + return false + end return true end @@ -102,6 +124,9 @@ function M.is_rename_identifier(node, role_index) if parent_type == "parameters" or parent_type == "parameter_list" or parent_type == "formal_parameters" then return true end + if parent_type == "parameter_declaration" and node_type == "identifier" then + return true + end if parent_type == "assignment" and M.node_in_field(parent, "left", node) then return true diff --git a/queries/c/diffmantic.scm b/queries/c/diffmantic.scm index 5b1ed94..43c4996 100644 --- a/queries/c/diffmantic.scm +++ b/queries/c/diffmantic.scm @@ -1,8 +1,14 @@ (function_definition - declarator: (function_declarator - declarator: (identifier) @diff.function.name) + declarator: (function_declarator) body: (compound_statement) @diff.function.body) @diff.function.outer +(function_declarator + declarator: (identifier) @diff.function.name) + +(function_declarator + declarator: (pointer_declarator + declarator: (identifier) @diff.function.name)) + (struct_specifier name: (type_identifier) @diff.class.name body: (field_declaration_list) @diff.class.body) @diff.class.outer @@ -16,7 +22,19 @@ body: (enumerator_list) @diff.class.body) @diff.class.outer (init_declarator - declarator: [(identifier) (field_identifier)] @diff.variable.name) @diff.variable.outer + declarator: [ + (identifier) @diff.variable.name + (pointer_declarator + declarator: (identifier) @diff.variable.name) + (array_declarator + declarator: (identifier) @diff.variable.name) + (pointer_declarator + declarator: (array_declarator + declarator: (identifier) @diff.variable.name)) + (array_declarator + declarator: (pointer_declarator + declarator: (identifier) @diff.variable.name)) + ]) @diff.variable.outer (assignment_expression left: (_) @diff.assignment.lhs @@ -27,8 +45,11 @@ (preproc_def) @diff.preproc.outer (preproc_function_def) @diff.preproc.outer -(function_definition - declarator: (function_declarator +(function_declarator + declarator: (identifier) @diff.identifier.rename) + +(function_declarator + declarator: (pointer_declarator declarator: (identifier) @diff.identifier.rename)) (struct_specifier diff --git a/queries/cpp/diffmantic.scm b/queries/cpp/diffmantic.scm index e9544a7..5287afb 100644 --- a/queries/cpp/diffmantic.scm +++ b/queries/cpp/diffmantic.scm @@ -1,8 +1,14 @@ (function_definition - declarator: (function_declarator - declarator: (identifier) @diff.function.name) + declarator: (function_declarator) body: (compound_statement) @diff.function.body) @diff.function.outer +(function_declarator + declarator: (identifier) @diff.function.name) + +(function_declarator + declarator: (pointer_declarator + declarator: (identifier) @diff.function.name)) + (class_specifier name: (type_identifier) @diff.class.name body: (field_declaration_list) @diff.class.body) @diff.class.outer @@ -23,8 +29,11 @@ (preproc_def) @diff.preproc.outer (preproc_function_def) @diff.preproc.outer -(function_definition - declarator: (function_declarator +(function_declarator + declarator: (identifier) @diff.identifier.rename) + +(function_declarator + declarator: (pointer_declarator declarator: (identifier) @diff.identifier.rename)) (class_specifier diff --git a/queries/go/diffmantic.scm b/queries/go/diffmantic.scm index 933458d..e33856a 100644 --- a/queries/go/diffmantic.scm +++ b/queries/go/diffmantic.scm @@ -15,6 +15,10 @@ (var_spec name: (identifier) @diff.variable.name)) @diff.variable.outer +(const_declaration + (const_spec + name: (identifier) @diff.variable.name)) @diff.variable.outer + (short_var_declaration left: (expression_list (identifier) @diff.variable.name)) @diff.variable.outer @@ -39,5 +43,9 @@ (var_spec name: (identifier) @diff.identifier.rename)) +(const_declaration + (const_spec + name: (identifier) @diff.identifier.rename)) + (short_var_declaration left: (expression_list (identifier) @diff.identifier.rename)) diff --git a/queries/typescript/diffmantic.scm b/queries/typescript/diffmantic.scm index dff92f1..10055f8 100644 --- a/queries/typescript/diffmantic.scm +++ b/queries/typescript/diffmantic.scm @@ -10,9 +10,20 @@ name: (type_identifier) @diff.class.name body: (class_body) @diff.class.body) @diff.class.outer +(type_alias_declaration + name: (type_identifier) @diff.class.name + value: (object_type) @diff.class.body) @diff.class.outer + +(interface_declaration + name: (type_identifier) @diff.class.name + body: (interface_body) @diff.class.body) @diff.class.outer + (variable_declarator name: [(identifier) (object_pattern) (array_pattern)] @diff.variable.name) @diff.variable.outer +(property_signature + name: (property_identifier) @diff.variable.name) @diff.variable.outer + (assignment_expression left: (_) @diff.assignment.lhs right: (_) @diff.assignment.rhs) @diff.assignment.outer @@ -29,5 +40,14 @@ (class_declaration name: (type_identifier) @diff.identifier.rename) +(type_alias_declaration + name: (type_identifier) @diff.identifier.rename) + +(interface_declaration + name: (type_identifier) @diff.identifier.rename) + (variable_declarator name: (identifier) @diff.identifier.rename) + +(property_signature + name: (property_identifier) @diff.identifier.rename) From a5396753fe5417936a965e67885df6643694f640 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Mon, 16 Feb 2026 21:21:20 +0530 Subject: [PATCH 29/48] refactor(core/ui): improved filler line calculation and removed base_count tracking - added range_text() helper to extract text from buffer ranges - added action_pair_key() to generate unique keys for action pairs - removed base_count field from filler entries (unused) - removed move_anchor_after_blank_run() in favor of more precise count_filler_lines_at() calculation --- lua/diffmantic/core/actions.lua | 113 +++++++++++++++++++++++++------- lua/diffmantic/ui/filler.lua | 57 ++++++++-------- 2 files changed, 116 insertions(+), 54 deletions(-) diff --git a/lua/diffmantic/core/actions.lua b/lua/diffmantic/core/actions.lua index ecf9e62..b9033f4 100644 --- a/lua/diffmantic/core/actions.lua +++ b/lua/diffmantic/core/actions.lua @@ -3,6 +3,73 @@ local semantic = require("diffmantic.core.semantic") local roles = require("diffmantic.core.roles") local analysis = require("diffmantic.core.analysis") +local function range_text(buf, range) + if not buf or not range then + return nil + end + if range.start_row == nil or range.end_row == nil or range.start_col == nil or range.end_col == nil then + return nil + end + if range.start_row ~= range.end_row then + return nil + end + local line = vim.api.nvim_buf_get_lines(buf, range.start_row, range.start_row + 1, false)[1] or "" + if line == "" then + return nil + end + local start_col = range.start_col + 1 + local end_col = range.end_col + if end_col < start_col then + return nil + end + return line:sub(start_col, end_col) +end + +local function action_pair_key(action) + if action and action.src_node and action.dst_node then + return action.src_node:id() .. ":" .. action.dst_node:id() + end + if action and action.src and action.dst then + return table.concat({ + tostring(action.src.start_row), + tostring(action.src.start_col), + tostring(action.src.end_row), + tostring(action.src.end_col), + tostring(action.dst.start_row), + tostring(action.dst.start_col), + tostring(action.dst.end_row), + tostring(action.dst.end_col), + }, ":") + end + return nil +end + +local function has_effective_update_hunks(action, src_buf, dst_buf) + local action_analysis = action and action.analysis or nil + local hunks = action_analysis and action_analysis.hunks or nil + if not hunks or #hunks == 0 then + return false + end + + local rename_pairs = action_analysis.rename_pairs or {} + for _, hunk in ipairs(hunks) do + if hunk.kind == "insert" or hunk.kind == "delete" then + return true + end + if hunk.kind == "change" then + local src_text = range_text(src_buf, hunk.src) + local dst_text = range_text(dst_buf, hunk.dst) + if not src_text or not dst_text then + return true + end + if src_text ~= dst_text and rename_pairs[src_text] ~= dst_text then + return true + end + end + end + return false +end + local function range_metadata(node) if not node then return nil @@ -792,28 +859,6 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end stop_timer(moves_start, "moves") - -- Updates for nodes that are also classified as moves are noisy and duplicate intent. - local moved_pairs = {} - for _, action in ipairs(actions) do - if action.type == "move" and action.src_node and action.dst_node then - moved_pairs[action.src_node:id() .. ":" .. action.dst_node:id()] = true - end - end - if next(moved_pairs) ~= nil then - local filtered = {} - for _, action in ipairs(actions) do - if action.type == "update" and action.src_node and action.dst_node then - local key = action.src_node:id() .. ":" .. action.dst_node:id() - if moved_pairs[key] then - goto continue - end - end - table.insert(filtered, action) - ::continue:: - end - actions = filtered - end - -- DELETES: unmapped source nodes local deletes_start = start_timer() for id, info in pairs(src_info) do @@ -854,15 +899,37 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op stop_timer(analysis_start, "analysis") local update_suppress_start = start_timer() + local moved_pairs = {} + local declaration_rename_pairs = {} + for _, action in ipairs(actions) do + local key = action_pair_key(action) + if key then + if action.type == "move" then + moved_pairs[key] = true + elseif action.type == "rename" and action.context and action.context.declaration then + declaration_rename_pairs[key] = true + end + end + end + local filtered_actions = {} for _, action in ipairs(actions) do if action.type == "update" then + local key = action_pair_key(action) local action_analysis = action.analysis or {} - local has_hunks = action_analysis.hunks and #action_analysis.hunks > 0 + local has_hunks = has_effective_update_hunks(action, src_buf, dst_buf) local has_rename_pairs = action_analysis.rename_pairs and next(action_analysis.rename_pairs) ~= nil + local overlaps_decl_rename = key and declaration_rename_pairs[key] or false + local overlaps_move = key and moved_pairs[key] or false if action_analysis.rename_only and has_rename_pairs and not has_hunks then goto continue end + if overlaps_decl_rename and not has_hunks then + goto continue + end + if overlaps_move and not has_hunks then + goto continue + end end table.insert(filtered_actions, action) ::continue:: diff --git a/lua/diffmantic/ui/filler.lua b/lua/diffmantic/ui/filler.lua index d21b8e6..9f4588e 100644 --- a/lua/diffmantic/ui/filler.lua +++ b/lua/diffmantic/ui/filler.lua @@ -147,15 +147,32 @@ local function coalesce_simple(fillers) return merged end -local function count_filler_lines_before(fillers, row) +local function count_filler_lines_before(fillers, row, exclude) if row == nil or not fillers then return 0 end local total = 0 for _, filler in ipairs(fillers) do - if filler.row and filler.row < row then + if filler ~= exclude and filler.row and filler.row < row then if filler.lines then - total = total + (filler.base_count or #filler.lines) + total = total + #filler.lines + elseif filler.count then + total = total + filler.count + end + end + end + return total +end + +local function count_filler_lines_at(fillers, row, exclude) + if row == nil or not fillers then + return 0 + end + local total = 0 + for _, filler in ipairs(fillers) do + if filler ~= exclude and filler.row and filler.row == row then + if filler.lines then + total = total + #filler.lines elseif filler.count then total = total + filler.count end @@ -224,29 +241,6 @@ local function range_from_run(run) } end -local function move_anchor_after_blank_run(buf, row) - if not buf or row == nil or row < 0 then - return row - end - local max_row = vim.api.nvim_buf_line_count(buf) - 1 - local cursor = math.min(row, max_row) - - while cursor <= max_row do - local lines = vim.api.nvim_buf_get_lines(buf, cursor, cursor + 1, false) - local line = lines[1] or "" - if line:match("^%s*$") then - cursor = cursor + 1 - else - break - end - end - - if cursor > max_row then - return row - end - return cursor -end - function M.compute(actions, src_buf, dst_buf) local src_fillers = {} local dst_fillers = {} @@ -262,14 +256,12 @@ function M.compute(actions, src_buf, dst_buf) local src_entry = { row = action.dst.start_row, lines = {}, - base_count = dst_base_count, } ensure_lines(src_entry, dst_count, "DiffmanticMoveFiller") local dst_entry = { row = action.src.start_row, lines = {}, - base_count = src_base_count, } ensure_lines(dst_entry, src_count, "DiffmanticMoveFiller") @@ -363,9 +355,12 @@ function M.compute(actions, src_buf, dst_buf) end for _, region in ipairs(move_regions) do - region.src_entry.row = region.src_entry.row + count_filler_lines_before(dst_fillers, region.src_entry.row) - region.dst_entry.row = region.dst_entry.row + count_filler_lines_before(src_fillers, region.dst_entry.row) - region.dst_entry.row = move_anchor_after_blank_run(dst_buf, region.dst_entry.row) + region.src_entry.row = region.src_entry.row + + count_filler_lines_before(dst_fillers, region.src_entry.row, region.src_entry) + + count_filler_lines_at(dst_fillers, region.src_entry.row, region.src_entry) + region.dst_entry.row = region.dst_entry.row + + count_filler_lines_before(src_fillers, region.dst_entry.row, region.dst_entry) + + count_filler_lines_at(src_fillers, region.dst_entry.row, region.dst_entry) end return coalesce_simple(src_fillers), coalesce_simple(dst_fillers) From 9603a56985293b73e0f0601308f8efcbe47178f4 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Tue, 17 Feb 2026 13:42:21 +0530 Subject: [PATCH 30/48] refactor(ui): removed filler cause it sucks right now --- lua/diffmantic/ui/filler.lua | 402 --------------------------------- lua/diffmantic/ui/renderer.lua | 79 +++++-- 2 files changed, 65 insertions(+), 416 deletions(-) delete mode 100644 lua/diffmantic/ui/filler.lua diff --git a/lua/diffmantic/ui/filler.lua b/lua/diffmantic/ui/filler.lua deleted file mode 100644 index 9f4588e..0000000 --- a/lua/diffmantic/ui/filler.lua +++ /dev/null @@ -1,402 +0,0 @@ -local M = {} - -local VIRT_LINE_LEN = 300 - -local function make_virt_line(hl_group, char) - return { { string.rep(char or "╱", VIRT_LINE_LEN), hl_group } } -end - -local function line_count(range) - if not range then - return 0 - end - local sr = range.start_row - local er = range.end_row - local ec = range.end_col - if sr == nil or er == nil or ec == nil then - return 0 - end - local count = er - sr - if ec > 0 then - count = count + 1 - end - if count <= 0 then - count = 1 - end - return count -end - -local function trailing_blanks(buf, range) - if not buf or not range or range.end_row == nil then - return 0 - end - - local last_occupied = range.end_row - if (range.end_col or 0) == 0 then - last_occupied = last_occupied - 1 - end - if last_occupied < (range.start_row or 0) then - last_occupied = range.start_row or 0 - end - - local buf_lines = vim.api.nvim_buf_line_count(buf) - local row = last_occupied + 1 - local count = 0 - - while row < buf_lines do - local lines = vim.api.nvim_buf_get_lines(buf, row, row + 1, false) - local line = lines[1] or "" - if line:match("^%s*$") then - count = count + 1 - row = row + 1 - else - break - end - end - - return count -end - -local function range_within(inner, outer) - if not inner or not outer then - return false - end - if inner.start_row == nil or inner.end_row == nil or inner.start_col == nil or inner.end_col == nil then - return false - end - if outer.start_row == nil or outer.end_row == nil or outer.start_col == nil or outer.end_col == nil then - return false - end - if inner.start_row < outer.start_row or inner.end_row > outer.end_row then - return false - end - if inner.start_row == outer.start_row and inner.start_col < outer.start_col then - return false - end - if inner.end_row == outer.end_row and inner.end_col > outer.end_col then - return false - end - return true -end - -local function ensure_lines(entry, count, default_hl) - for i = #entry.lines + 1, count do - entry.lines[i] = default_hl - end -end - -local function mark_lines(entry, start_idx, count, hl_group) - if not entry or start_idx == nil or count == nil or count <= 0 then - return - end - ensure_lines(entry, start_idx + count - 1, "DiffmanticMoveFiller") - for i = start_idx, start_idx + count - 1 do - entry.lines[i] = hl_group - end -end - -local function add_simple(fillers, row, count, hl_group) - if row == nil or count == nil or count <= 0 or not hl_group then - return - end - table.insert(fillers, { - row = row, - count = count, - hl_group = hl_group, - }) -end - -local function coalesce_simple(fillers) - local simple = {} - local rich = {} - for _, filler in ipairs(fillers) do - if filler.lines then - table.insert(rich, filler) - else - table.insert(simple, filler) - end - end - - table.sort(simple, function(a, b) - return a.row < b.row - end) - - local merged = {} - for _, filler in ipairs(simple) do - local prev = merged[#merged] - if prev and prev.hl_group == filler.hl_group and filler.row <= (prev.row + prev.count) then - local prev_end = prev.row + prev.count - local this_end = filler.row + filler.count - prev.count = math.max(prev_end, this_end) - prev.row - else - table.insert(merged, { - row = filler.row, - count = filler.count, - hl_group = filler.hl_group, - }) - end - end - - for _, filler in ipairs(rich) do - table.insert(merged, filler) - end - - table.sort(merged, function(a, b) - return a.row < b.row - end) - return merged -end - -local function count_filler_lines_before(fillers, row, exclude) - if row == nil or not fillers then - return 0 - end - local total = 0 - for _, filler in ipairs(fillers) do - if filler ~= exclude and filler.row and filler.row < row then - if filler.lines then - total = total + #filler.lines - elseif filler.count then - total = total + filler.count - end - end - end - return total -end - -local function count_filler_lines_at(fillers, row, exclude) - if row == nil or not fillers then - return 0 - end - local total = 0 - for _, filler in ipairs(fillers) do - if filler ~= exclude and filler.row and filler.row == row then - if filler.lines then - total = total + #filler.lines - elseif filler.count then - total = total + filler.count - end - end - end - return total -end - -local function find_move_region_for_src(move_regions, src_range) - if not src_range then - return nil - end - for _, region in ipairs(move_regions) do - if range_within(src_range, region.src) then - return region - end - end - return nil -end - -local function find_move_region_for_dst(move_regions, dst_range) - if not dst_range then - return nil - end - for _, region in ipairs(move_regions) do - if range_within(dst_range, region.dst) then - return region - end - end - return nil -end - -local function collect_runs(ranges) - if not ranges or #ranges == 0 then - return {} - end - table.sort(ranges, function(a, b) - return a.start_row < b.start_row - end) - local runs = {} - for _, range in ipairs(ranges) do - local count = line_count(range) - local current = runs[#runs] - if current and range.start_row <= (current.end_row + 1) then - current.end_row = math.max(current.end_row, range.end_row) - current.count = current.count + count - else - table.insert(runs, { - start_row = range.start_row, - end_row = range.end_row, - start_col = range.start_col, - end_col = range.end_col, - count = count, - }) - end - end - return runs -end - -local function range_from_run(run) - return { - start_row = run.start_row, - start_col = run.start_col or 0, - end_row = run.end_row, - end_col = run.end_col or 1, - } -end - -function M.compute(actions, src_buf, dst_buf) - local src_fillers = {} - local dst_fillers = {} - local move_regions = {} - - for _, action in ipairs(actions) do - if action.type == "move" and action.src and action.dst then - local src_base_count = line_count(action.src) - local dst_base_count = line_count(action.dst) - local src_count = src_base_count + trailing_blanks(src_buf, action.src) - local dst_count = dst_base_count + trailing_blanks(dst_buf, action.dst) - - local src_entry = { - row = action.dst.start_row, - lines = {}, - } - ensure_lines(src_entry, dst_count, "DiffmanticMoveFiller") - - local dst_entry = { - row = action.src.start_row, - lines = {}, - } - ensure_lines(dst_entry, src_count, "DiffmanticMoveFiller") - - table.insert(src_fillers, src_entry) - table.insert(dst_fillers, dst_entry) - - table.insert(move_regions, { - src = action.src, - dst = action.dst, - src_entry = src_entry, - dst_entry = dst_entry, - }) - end - end - - for _, action in ipairs(actions) do - if action.type == "insert" and action.dst then - local count = line_count(action.dst) - if count > 0 then - local region = find_move_region_for_dst(move_regions, action.dst) - if region then - local start_idx = (action.dst.start_row - region.dst.start_row) + 1 - mark_lines(region.src_entry, start_idx, count, "DiffmanticAddFiller") - else - add_simple(src_fillers, action.dst.start_row, count, "DiffmanticAddFiller") - end - end - elseif action.type == "delete" and action.src then - local count = line_count(action.src) - if count > 0 then - local region = find_move_region_for_src(move_regions, action.src) - if region then - local start_idx = (action.src.start_row - region.src.start_row) + 1 - mark_lines(region.dst_entry, start_idx, count, "DiffmanticDeleteFiller") - else - add_simple(dst_fillers, action.src.start_row, count, "DiffmanticDeleteFiller") - end - end - elseif action.type == "update" and action.src and action.dst and action.analysis and action.analysis.hunks then - local insert_ranges = {} - local delete_ranges = {} - for _, hunk in ipairs(action.analysis.hunks) do - if hunk.kind == "insert" and hunk.dst then - table.insert(insert_ranges, hunk.dst) - elseif hunk.kind == "delete" and hunk.src then - table.insert(delete_ranges, hunk.src) - end - end - - local insert_runs = collect_runs(insert_ranges) - local delete_runs = collect_runs(delete_ranges) - - for _, run in ipairs(insert_runs) do - local run_range = range_from_run(run) - local dst_move_region = find_move_region_for_dst(move_regions, run_range) - if dst_move_region then - local start_idx = (run.start_row - dst_move_region.dst.start_row) + 1 - mark_lines(dst_move_region.src_entry, start_idx, run.count, "DiffmanticAddFiller") - else - local anchor = action.src.start_row + math.max(0, run.start_row - action.dst.start_row) - local deleted_before = 0 - for _, d in ipairs(delete_runs) do - if d.start_row <= anchor then - deleted_before = deleted_before + d.count - end - end - anchor = anchor + deleted_before - add_simple(src_fillers, anchor, run.count, "DiffmanticAddFiller") - end - end - - for _, run in ipairs(delete_runs) do - local run_range = range_from_run(run) - local src_move_region = find_move_region_for_src(move_regions, run_range) - if src_move_region then - local start_idx = (run.start_row - src_move_region.src.start_row) + 1 - mark_lines(src_move_region.dst_entry, start_idx, run.count, "DiffmanticDeleteFiller") - else - local anchor = action.dst.start_row + math.max(0, run.start_row - action.src.start_row) - local inserted_before = 0 - for _, ins in ipairs(insert_runs) do - if ins.start_row <= anchor then - inserted_before = inserted_before + ins.count - end - end - anchor = anchor + inserted_before - add_simple(dst_fillers, anchor, run.count, "DiffmanticDeleteFiller") - end - end - end - end - - for _, region in ipairs(move_regions) do - region.src_entry.row = region.src_entry.row - + count_filler_lines_before(dst_fillers, region.src_entry.row, region.src_entry) - + count_filler_lines_at(dst_fillers, region.src_entry.row, region.src_entry) - region.dst_entry.row = region.dst_entry.row - + count_filler_lines_before(src_fillers, region.dst_entry.row, region.dst_entry) - + count_filler_lines_at(src_fillers, region.dst_entry.row, region.dst_entry) - end - - return coalesce_simple(src_fillers), coalesce_simple(dst_fillers) -end - -function M.apply(buf, ns, fillers) - if not fillers or #fillers == 0 then - return - end - - local line_count_buf = vim.api.nvim_buf_line_count(buf) - for _, filler in ipairs(fillers) do - local row = filler.row - if row >= line_count_buf then - row = line_count_buf - 1 - end - if row < 0 then - row = 0 - end - - local virt_lines = {} - if filler.lines then - for i, hl in ipairs(filler.lines) do - virt_lines[i] = make_virt_line(hl) - end - else - for i = 1, filler.count do - virt_lines[i] = make_virt_line(filler.hl_group) - end - end - - pcall(vim.api.nvim_buf_set_extmark, buf, ns, row, 0, { - virt_lines = virt_lines, - virt_lines_above = true, - }) - end -end - -return M diff --git a/lua/diffmantic/ui/renderer.lua b/lua/diffmantic/ui/renderer.lua index e19a200..e7f283e 100644 --- a/lua/diffmantic/ui/renderer.lua +++ b/lua/diffmantic/ui/renderer.lua @@ -1,5 +1,4 @@ local signs = require("diffmantic.ui.signs") -local filler = require("diffmantic.ui.filler") local M = {} @@ -90,6 +89,62 @@ local HUNK_STYLE = { }, } +local function range_text(buf, range) + if not buf or not range then + return nil + end + if range.start_row == nil or range.end_row == nil or range.start_col == nil or range.end_col == nil then + return nil + end + if range.start_row ~= range.end_row then + return nil + end + local line = vim.api.nvim_buf_get_lines(buf, range.start_row, range.start_row + 1, false)[1] or "" + if line == "" then + return nil + end + local start_col = range.start_col + 1 + local end_col = range.end_col + if end_col < start_col then + return nil + end + return line:sub(start_col, end_col) +end + +local function hunk_is_effective_non_rename(hunk, rename_pairs, src_buf, dst_buf) + if not hunk then + return false + end + if hunk.kind == "insert" or hunk.kind == "delete" then + return true + end + if hunk.kind ~= "change" then + return false + end + local src_text = range_text(src_buf, hunk.src) + local dst_text = range_text(dst_buf, hunk.dst) + if not src_text or not dst_text then + return true + end + return src_text ~= dst_text and rename_pairs[src_text] ~= dst_text +end + +local function effective_update_hunks(action, src_buf, dst_buf) + local analysis = action and action.analysis or nil + local hunks = analysis and analysis.hunks or nil + if not hunks or #hunks == 0 then + return {} + end + local rename_pairs = analysis.rename_pairs or {} + local effective = {} + for _, hunk in ipairs(hunks) do + if hunk_is_effective_non_rename(hunk, rename_pairs, src_buf, dst_buf) then + table.insert(effective, hunk) + end + end + return effective +end + local function move_to_arrow(from_line, to_line) if type(from_line) ~= "number" or type(to_line) ~= "number" then return "⤴" @@ -103,9 +158,7 @@ end function M.render(src_buf, dst_buf, actions, ns) local src_sign_rows = {} local dst_sign_rows = {} - local src_fillers, dst_fillers = filler.compute(actions, src_buf, dst_buf) - filler.apply(src_buf, ns, src_fillers) - filler.apply(dst_buf, ns, dst_fillers) + for _, action in ipairs(actions) do local style = TYPE_STYLE[action.type] @@ -114,9 +167,13 @@ function M.render(src_buf, dst_buf, actions, ns) local dst = action.dst local meta = action.metadata or {} - if action.type == "update" and action.analysis and action.analysis.hunks then + if action.type == "update" then + local effective_hunks = effective_update_hunks(action, src_buf, dst_buf) + if #effective_hunks == 0 then + goto continue + end local rendered_hunk = false - for _, hunk in ipairs(action.analysis.hunks) do + for _, hunk in ipairs(effective_hunks) do local hstyle = HUNK_STYLE[hunk.kind] or HUNK_STYLE.change if hunk.src and hstyle.src_hl then apply_span(src_buf, ns, hunk.src, hstyle.src_hl) @@ -130,14 +187,7 @@ function M.render(src_buf, dst_buf, actions, ns) end end if not rendered_hunk then - if src then - apply_span(src_buf, ns, src, style.hl) - apply_sign(src_buf, ns, src.start_row, style.sign, style.hl, src_sign_rows) - end - if dst then - apply_span(dst_buf, ns, dst, style.hl) - apply_sign(dst_buf, ns, dst.start_row, style.sign, style.hl, dst_sign_rows) - end + goto continue end else if src then @@ -166,6 +216,7 @@ function M.render(src_buf, dst_buf, actions, ns) end end end + ::continue:: end end end From e0a9a04019af84965aaad915bf24742c0c9451c7 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Tue, 17 Feb 2026 17:55:28 +0530 Subject: [PATCH 31/48] feat(queries): add structural captures for C/C++/Go/JS/TS significance --- queries/c/diffmantic.scm | 53 ++++++++++++++++++++++++++++++- queries/cpp/diffmantic.scm | 44 +++++++++++++++++++++++-- queries/go/diffmantic.scm | 15 ++++++++- queries/javascript/diffmantic.scm | 8 +++++ queries/typescript/diffmantic.scm | 8 +++++ 5 files changed, 124 insertions(+), 4 deletions(-) diff --git a/queries/c/diffmantic.scm b/queries/c/diffmantic.scm index 43c4996..732c65b 100644 --- a/queries/c/diffmantic.scm +++ b/queries/c/diffmantic.scm @@ -2,6 +2,11 @@ declarator: (function_declarator) body: (compound_statement) @diff.function.body) @diff.function.outer +(function_definition + declarator: (pointer_declarator + declarator: (function_declarator)) + body: (compound_statement) @diff.function.body) @diff.function.outer + (function_declarator declarator: (identifier) @diff.function.name) @@ -24,6 +29,8 @@ (init_declarator declarator: [ (identifier) @diff.variable.name + (parenthesized_declarator + (identifier) @diff.variable.name) (pointer_declarator declarator: (identifier) @diff.variable.name) (array_declarator @@ -36,6 +43,36 @@ declarator: (identifier) @diff.variable.name)) ]) @diff.variable.outer +(field_declaration + declarator: [ + (field_identifier) @diff.variable.name + (pointer_declarator + declarator: (field_identifier) @diff.variable.name) + (array_declarator + declarator: (field_identifier) @diff.variable.name) + (pointer_declarator + declarator: (array_declarator + declarator: (field_identifier) @diff.variable.name)) + (array_declarator + declarator: (pointer_declarator + declarator: (field_identifier) @diff.variable.name)) + ]) @diff.variable.outer + +(field_declaration + [ + (field_identifier) @diff.variable.name + (pointer_declarator + declarator: (field_identifier) @diff.variable.name) + (array_declarator + declarator: (field_identifier) @diff.variable.name) + (pointer_declarator + declarator: (array_declarator + declarator: (field_identifier) @diff.variable.name)) + (array_declarator + declarator: (pointer_declarator + declarator: (field_identifier) @diff.variable.name)) + ]) @diff.variable.outer + (assignment_expression left: (_) @diff.assignment.lhs right: (_) @diff.assignment.rhs) @diff.assignment.outer @@ -62,4 +99,18 @@ name: (type_identifier) @diff.identifier.rename) (init_declarator - declarator: (identifier) @diff.identifier.rename) + declarator: [ + (identifier) @diff.identifier.rename + (parenthesized_declarator + (identifier) @diff.identifier.rename) + (pointer_declarator + declarator: (identifier) @diff.identifier.rename) + (array_declarator + declarator: (identifier) @diff.identifier.rename) + (pointer_declarator + declarator: (array_declarator + declarator: (identifier) @diff.identifier.rename)) + (array_declarator + declarator: (pointer_declarator + declarator: (identifier) @diff.identifier.rename)) + ]) diff --git a/queries/cpp/diffmantic.scm b/queries/cpp/diffmantic.scm index 5287afb..9526208 100644 --- a/queries/cpp/diffmantic.scm +++ b/queries/cpp/diffmantic.scm @@ -2,6 +2,11 @@ declarator: (function_declarator) body: (compound_statement) @diff.function.body) @diff.function.outer +(function_definition + declarator: (pointer_declarator + declarator: (function_declarator)) + body: (compound_statement) @diff.function.body) @diff.function.outer + (function_declarator declarator: (identifier) @diff.function.name) @@ -18,7 +23,40 @@ body: (field_declaration_list) @diff.class.body) @diff.class.outer (init_declarator - declarator: [(identifier) (field_identifier)] @diff.variable.name) @diff.variable.outer + declarator: [ + (identifier) @diff.variable.name + (field_identifier) @diff.variable.name + ]) @diff.variable.outer + +(field_declaration + declarator: [ + (field_identifier) @diff.variable.name + (pointer_declarator + declarator: (field_identifier) @diff.variable.name) + (array_declarator + declarator: (field_identifier) @diff.variable.name) + (pointer_declarator + declarator: (array_declarator + declarator: (field_identifier) @diff.variable.name)) + (array_declarator + declarator: (pointer_declarator + declarator: (field_identifier) @diff.variable.name)) + ]) @diff.variable.outer + +(field_declaration + [ + (field_identifier) @diff.variable.name + (pointer_declarator + declarator: (field_identifier) @diff.variable.name) + (array_declarator + declarator: (field_identifier) @diff.variable.name) + (pointer_declarator + declarator: (array_declarator + declarator: (field_identifier) @diff.variable.name)) + (array_declarator + declarator: (pointer_declarator + declarator: (field_identifier) @diff.variable.name)) + ]) @diff.variable.outer (assignment_expression left: (_) @diff.assignment.lhs @@ -43,4 +81,6 @@ name: (type_identifier) @diff.identifier.rename) (init_declarator - declarator: (identifier) @diff.identifier.rename) + declarator: [ + (identifier) @diff.identifier.rename + ]) diff --git a/queries/go/diffmantic.scm b/queries/go/diffmantic.scm index e33856a..f8ede2d 100644 --- a/queries/go/diffmantic.scm +++ b/queries/go/diffmantic.scm @@ -9,7 +9,10 @@ (type_declaration (type_spec name: (type_identifier) @diff.class.name - type: (struct_type) @diff.class.body)) @diff.class.outer + type: [ + (struct_type) @diff.class.body + (interface_type) @diff.class.body + ])) @diff.class.outer (var_declaration (var_spec @@ -22,10 +25,20 @@ (short_var_declaration left: (expression_list (identifier) @diff.variable.name)) @diff.variable.outer +(field_declaration + name: (field_identifier) @diff.variable.name) @diff.variable.outer + +(field_declaration + (field_identifier) @diff.variable.name) @diff.variable.outer + (assignment_statement left: (_) @diff.assignment.lhs right: (_) @diff.assignment.rhs) @diff.assignment.outer +(keyed_element + key: (_) @diff.assignment.lhs + value: (_) @diff.assignment.rhs) @diff.assignment.outer + (import_declaration) @diff.import.outer (return_statement) @diff.return.outer diff --git a/queries/javascript/diffmantic.scm b/queries/javascript/diffmantic.scm index c3a096d..6477d72 100644 --- a/queries/javascript/diffmantic.scm +++ b/queries/javascript/diffmantic.scm @@ -13,10 +13,18 @@ (variable_declarator name: [(identifier) (object_pattern) (array_pattern)] @diff.variable.name) @diff.variable.outer +(lexical_declaration + (variable_declarator + name: (identifier) @diff.variable.name)) @diff.variable.outer + (assignment_expression left: (_) @diff.assignment.lhs right: (_) @diff.assignment.rhs) @diff.assignment.outer +(pair + key: (_) @diff.assignment.lhs + value: (_) @diff.assignment.rhs) @diff.assignment.outer + (import_statement) @diff.import.outer (return_statement) @diff.return.outer diff --git a/queries/typescript/diffmantic.scm b/queries/typescript/diffmantic.scm index 10055f8..0d5c7e4 100644 --- a/queries/typescript/diffmantic.scm +++ b/queries/typescript/diffmantic.scm @@ -21,6 +21,10 @@ (variable_declarator name: [(identifier) (object_pattern) (array_pattern)] @diff.variable.name) @diff.variable.outer +(lexical_declaration + (variable_declarator + name: (identifier) @diff.variable.name)) @diff.variable.outer + (property_signature name: (property_identifier) @diff.variable.name) @diff.variable.outer @@ -28,6 +32,10 @@ left: (_) @diff.assignment.lhs right: (_) @diff.assignment.rhs) @diff.assignment.outer +(pair + key: (_) @diff.assignment.lhs + value: (_) @diff.assignment.rhs) @diff.assignment.outer + (import_statement) @diff.import.outer (return_statement) @diff.return.outer From 4b24d8af93be0e206581b6bd89197f0d49dc7ff3 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Tue, 17 Feb 2026 19:08:47 +0530 Subject: [PATCH 32/48] refactor(core): enforce parent-safe top-down behavior and expand rename heuristics --- lua/diffmantic/core/semantic.lua | 107 ++++++++++++++++++++++++------- lua/diffmantic/core/top_down.lua | 40 +++++++++--- 2 files changed, 116 insertions(+), 31 deletions(-) diff --git a/lua/diffmantic/core/semantic.lua b/lua/diffmantic/core/semantic.lua index 58fdd81..ada78dd 100644 --- a/lua/diffmantic/core/semantic.lua +++ b/lua/diffmantic/core/semantic.lua @@ -1,8 +1,66 @@ local M = {} local roles = require("diffmantic.core.roles") +local function in_class_like_context(node, role_index) + if not node then + return false + end + local cur = node + while cur do + local ctype = cur:type() + if ctype == "class_specifier" or ctype == "struct_specifier" or ctype == "union_specifier" or ctype == "class_definition" then + return true + end + if role_index and roles.has_kind(cur, role_index, "class") then + return true + end + cur = cur:parent() + end + return false +end + +function M.is_ambiguous_member_change(node, role_index) + if not node then + return false + end + local node_type = node:type() + if node_type ~= "identifier" and node_type ~= "field_identifier" and node_type ~= "type_identifier" then + return false + end + + local cur = node + local parent = node:parent() + while parent do + local ptype = parent:type() + if ptype == "field_declaration" or ptype == "field_declarator" then + return true + end + if ptype == "declarator" then + local grandparent = parent:parent() + if grandparent and (grandparent:type() == "field_declaration" or grandparent:type() == "field_declarator") then + return true + end + end + if (ptype == "class_specifier" or ptype == "struct_specifier" or ptype == "union_specifier" or ptype == "enum_specifier") + and not M.node_in_field(parent, "name", cur) + and not M.node_in_field(parent, "tag", cur) + and node_type == "field_identifier" + then + return true + end + cur = parent + parent = parent:parent() + end + + if role_index and in_class_like_context(node, role_index) and node_type == "field_identifier" then + return true + end + + return false +end + -- Leaf-level diffs for small updates; otherwise return empty. -function M.find_leaf_changes(src_node, dst_node, src_buf, dst_buf) +function M.find_leaf_changes(src_node, dst_node, src_buf, dst_buf, src_role_index, dst_role_index) local changes = {} local function get_all_leaves(node, bufnr) @@ -44,12 +102,19 @@ function M.find_leaf_changes(src_node, dst_node, src_buf, dst_buf) if sl.type == dl.type and sl.text == dl.text then same_count = same_count + 1 elseif sl.type == dl.type and sl.text ~= dl.text then - table.insert(changes, { - src_node = sl.node, - dst_node = dl.node, - src_text = sl.text, - dst_text = dl.text, - }) + local src_ambiguous = M.is_ambiguous_member_change(sl.node, src_role_index) + local dst_ambiguous = M.is_ambiguous_member_change(dl.node, dst_role_index) + if src_ambiguous or dst_ambiguous then + -- Member declaration internals are structurally noisy; avoid rename/update inference. + same_count = same_count + 1 + else + table.insert(changes, { + src_node = sl.node, + dst_node = dl.node, + src_text = sl.text, + dst_text = dl.text, + }) + end end end @@ -84,24 +149,14 @@ function M.is_rename_identifier(node, role_index) return false end - local function in_class_like_context(n, index) - if not index then - return false - end - local cur = n - while cur do - if roles.has_kind(cur, index, "class") then - return true - end - cur = cur:parent() - end - return false - end - local function is_class_name_node(n, index) return index and roles.has_capture(n, index, "diff.class.name") or false end + if M.is_ambiguous_member_change(node, role_index) then + return false + end + if role_index and roles.has_kind(node, role_index, "rename_identifier") then -- Allow renaming struct/type/class declaration names, but not members inside them. if in_class_like_context(node, role_index) and not is_class_name_node(node, role_index) then @@ -121,7 +176,15 @@ function M.is_rename_identifier(node, role_index) end local parent_type = parent:type() - if parent_type == "parameters" or parent_type == "parameter_list" or parent_type == "formal_parameters" then + local parameter_parent_kinds = { + parameters = true, + parameter_list = true, + formal_parameters = true, + required_parameter = true, + optional_parameter = true, + rest_parameter = true, + } + if parameter_parent_kinds[parent_type] then return true end if parent_type == "parameter_declaration" and node_type == "identifier" then diff --git a/lua/diffmantic/core/top_down.lua b/lua/diffmantic/core/top_down.lua index 9b99f2f..c9cee4e 100644 --- a/lua/diffmantic/core/top_down.lua +++ b/lua/diffmantic/core/top_down.lua @@ -34,6 +34,7 @@ function M.top_down_match(src_root, dst_root, src_buf, dst_buf) local src_mapped = {} local dst_mapped = {} + local src_to_dst = {} -- Group nodes by their height in the tree local function get_nodes_by_height(info) @@ -54,6 +55,26 @@ function M.top_down_match(src_root, dst_root, src_buf, dst_buf) local src_by_height = get_nodes_by_height(src_info) local dst_by_height = get_nodes_by_height(dst_info) + local function parents_are_compatible(src_data, dst_data) + local src_parent = src_data.parent + local dst_parent = dst_data.parent + + if not src_parent and not dst_parent then + return true + end + if not src_parent or not dst_parent then + return false + end + + local src_parent_id = src_parent:id() + local dst_parent_id = dst_parent:id() + if src_parent_id == src_root:id() and dst_parent_id == dst_root:id() then + return true + end + + return src_to_dst[src_parent_id] == dst_parent_id + end + -- Find the maximum height in both trees local max_h = 0 for h in pairs(src_by_height) do @@ -85,17 +106,18 @@ function M.top_down_match(src_root, dst_root, src_buf, dst_buf) for _, s in ipairs(s_nodes) do if not src_mapped[s.id] then local candidates = dst_by_hash[s.hash] - if candidates then - for i, d in ipairs(candidates) do - if not dst_mapped[d.id] then - table.insert(mappings, { src = s.id, dst = d.id }) - src_mapped[s.id] = true - dst_mapped[d.id] = true - table.remove(candidates, i) - break + if candidates then + for i, d in ipairs(candidates) do + if not dst_mapped[d.id] and parents_are_compatible(s, d) then + table.insert(mappings, { src = s.id, dst = d.id }) + src_mapped[s.id] = true + dst_mapped[d.id] = true + src_to_dst[s.id] = d.id + table.remove(candidates, i) + break + end end end - end end end end From 5c331a152a5286f3060d537afa479b077ad1e56d Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Wed, 18 Feb 2026 10:02:19 +0530 Subject: [PATCH 33/48] refactor(core): deterministic left-biased recovery with immediate recursion --- lua/diffmantic/core/recovery.lua | 127 +++++++++++++++++-------------- 1 file changed, 69 insertions(+), 58 deletions(-) diff --git a/lua/diffmantic/core/recovery.lua b/lua/diffmantic/core/recovery.lua index 08a12f5..738a86a 100644 --- a/lua/diffmantic/core/recovery.lua +++ b/lua/diffmantic/core/recovery.lua @@ -23,9 +23,13 @@ local function compare_info_order(a_info, b_info) return a_info.type < b_info.type end --- Recovery matching: tries to match remaining unmapped nodes using LCS and unique type +-- 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 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 @@ -33,7 +37,17 @@ function M.recovery_match(src_root, dst_root, mappings, src_info, dst_info, src_ 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 + + -- 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 @@ -41,51 +55,61 @@ function M.recovery_match(src_root, dst_root, mappings, src_info, dst_info, src_ 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 + -- 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 + return true 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 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) @@ -96,47 +120,35 @@ function M.recovery_match(src_root, dst_root, mappings, src_info, dst_info, src_ table.insert(dst_children, child) end 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 1: match children with same hash (exact match) + -- 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()) + if add_mapping(match.src:id(), match.dst:id()) then + -- Recurse immediately so children of newly matched nodes are not skipped by outer ordering. + simple_recovery(match.src, match.dst) end end - -- Step 2: match children with same structure_hash (for updates) - 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) - end - end - for child in dst_node:iter_children() do - if not dst_to_src[child:id()] then - table.insert(dst_children, child) - end - end + -- 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 not src_to_dst[match.src:id()] and not dst_to_src[match.dst:id()] then - add_mapping(match.src:id(), match.dst:id()) + if match.src:type() ~= "field_declaration" and add_mapping(match.src:id(), match.dst:id()) then + -- Recurse immediately for the same reason as Step 1. + simple_recovery(match.src, match.dst) end 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) - end - end - for child in dst_node:iter_children() do - if not dst_to_src[child:id()] then - table.insert(dst_children, child) - 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 = {}, {} @@ -152,17 +164,16 @@ 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 + -- Apply recovery to all mapped nodes. local src_ids = {} for id in pairs(src_info) do table.insert(src_ids, id) From 98af7a2377138e8e82c0502e43c0d4d9c9c8a533 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Wed, 18 Feb 2026 19:26:53 +0530 Subject: [PATCH 34/48] refactor(core): token-level asymmetric hunk classification and span alignment --- lua/diffmantic/core/analysis.lua | 261 ++++++++++++++++++++++++------- 1 file changed, 203 insertions(+), 58 deletions(-) diff --git a/lua/diffmantic/core/analysis.lua b/lua/diffmantic/core/analysis.lua index 336846a..894eb73 100644 --- a/lua/diffmantic/core/analysis.lua +++ b/lua/diffmantic/core/analysis.lua @@ -47,28 +47,26 @@ local function tokenize_line(text) local tokens = {} local i = 1 local len = #text - while i <= len do - local ch = text:sub(i, i) - if ch:match("%s") then - i = i + 1 - elseif ch:match("[%w_]") then - local j = i + 1 - while j <= len and text:sub(j, j):match("[%w_]") do - j = j + 1 - end - table.insert(tokens, { text = text:sub(i, j - 1), start_col = i, end_col = j - 1 }) - i = j - else - local j = i + 1 - while j <= len and not text:sub(j, j):match("[%w_%s]") do - j = j + 1 + while i <= len do + local ch = text:sub(i, i) + if ch:match("%s") then + i = i + 1 + elseif ch:match("[%w_]") then + local j = i + 1 + while j <= len and text:sub(j, j):match("[%w_]") do + j = j + 1 + end + table.insert(tokens, { text = text:sub(i, j - 1), start_col = i, end_col = j - 1 }) + i = j + else + -- Keep punctuation/granular symbols as single-char tokens so we can + -- align shared delimiters (e.g. closing quote) and avoid off-by-one spans. + table.insert(tokens, { text = ch, start_col = i, end_col = i }) + i = i + 1 end - table.insert(tokens, { text = text:sub(i, j - 1), start_col = i, end_col = j - 1 }) - i = j end + return tokens end - return tokens -end local function tokens_equal(a, b, rename_map) if a.text == b.text then @@ -88,43 +86,45 @@ local function lcs_matches(a, b, rename_map) end local dp = {} - for i = 0, n do + for i = 1, n + 1 do dp[i] = {} - dp[i][0] = 0 - end - for j = 1, m do - dp[0][j] = 0 + for j = 1, m + 1 do + dp[i][j] = 0 + end end - for i = 1, n do - for j = 1, m do + for i = n, 1, -1 do + for j = m, 1, -1 do if tokens_equal(a[i], b[j], rename_map) then - dp[i][j] = dp[i - 1][j - 1] + 1 + dp[i][j] = dp[i + 1][j + 1] + 1 else - local up = dp[i - 1][j] - local left = dp[i][j - 1] - dp[i][j] = (up >= left) and up or left + local skip_src = dp[i + 1][j] + local skip_dst = dp[i][j + 1] + dp[i][j] = (skip_src >= skip_dst) and skip_src or skip_dst end end end local match_a = {} local match_b = {} - local i = n - local j = m - while i > 0 and j > 0 do - if tokens_equal(a[i], b[j], rename_map) then + local i = 1 + local j = 1 + while i <= n and j <= m do + if tokens_equal(a[i], b[j], rename_map) and dp[i][j] == (dp[i + 1][j + 1] + 1) then match_a[i] = true match_b[j] = true - i = i - 1 - j = j - 1 + i = i + 1 + j = j + 1 else - local up = dp[i - 1][j] - local left = dp[i][j - 1] - if up >= left then - i = i - 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 - j = j - 1 + -- Tie: keep destination earlier by advancing source. + i = i + 1 end end end @@ -294,6 +294,112 @@ local function is_suppressed_change_hunk(hunk_src, hunk_dst, suppressed_pairs, d return false end +local function whitespace_between(buf, row, left_end_col, right_start_col) + if not buf or row == nil or left_end_col == nil or right_start_col == nil then + return false + end + if right_start_col < left_end_col then + return false + end + if right_start_col == left_end_col then + return true + end + local line = line_text(buf, row) + if not line then + return false + end + local gap = line:sub(left_end_col + 1, right_start_col) + return gap:match("^%s*$") ~= nil +end + +local function can_merge_ranges(prev, cur, buf) + if not prev or not cur then + return false + end + if prev.start_row ~= prev.end_row or cur.start_row ~= cur.end_row then + return false + end + if prev.start_row ~= cur.start_row then + return false + end + if cur.start_col < prev.end_col then + return false + end + return whitespace_between(buf, prev.start_row, prev.end_col, cur.start_col) +end + +local function clone_hunk(h) + return { + kind = h.kind, + src = clone_range(h.src), + dst = clone_range(h.dst), + } +end + +local function hunk_start(h) + local r = h.src or h.dst + if not r then + return math.huge, math.huge + end + return r.start_row or math.huge, r.start_col or math.huge +end + +local function extend_range(base, incoming) + if not base or not incoming then + return + end + if incoming.end_row > base.end_row or (incoming.end_row == base.end_row and incoming.end_col > base.end_col) then + base.end_row = incoming.end_row + base.end_col = incoming.end_col + base.end_line = incoming.end_line + end +end + +local function can_merge_hunks(prev, cur, src_buf, dst_buf) + if not prev or not cur or prev.kind ~= cur.kind then + return false + end + if prev.kind == "change" then + return can_merge_ranges(prev.src, cur.src, src_buf) and can_merge_ranges(prev.dst, cur.dst, dst_buf) + end + if prev.kind == "insert" then + return can_merge_ranges(prev.dst, cur.dst, dst_buf) + end + if prev.kind == "delete" then + return can_merge_ranges(prev.src, cur.src, src_buf) + end + return false +end + +local function merge_adjacent_hunks(hunks, src_buf, dst_buf) + if not hunks or #hunks <= 1 then + return hunks or {} + end + local ordered = {} + for _, h in ipairs(hunks) do + table.insert(ordered, clone_hunk(h)) + end + table.sort(ordered, function(a, b) + local ar, ac = hunk_start(a) + local br, bc = hunk_start(b) + if ar == br then + return ac < bc + end + return ar < br + end) + local merged = {} + for _, h in ipairs(ordered) do + local prev = merged[#merged] + if prev and can_merge_hunks(prev, h, src_buf, dst_buf) then + extend_range(prev.src, h.src) + extend_range(prev.dst, h.dst) + else + table.insert(merged, h) + end + end + return merged +end + local function fallback_hunks_from_diff(src_node, dst_node, src_buf, dst_buf, rename_pairs) local src_text = vim.treesitter.get_node_text(src_node, src_buf) local dst_text = vim.treesitter.get_node_text(dst_node, dst_buf) @@ -329,23 +435,55 @@ local function fallback_hunks_from_diff(src_node, dst_node, src_buf, dst_buf, re local match_src, match_dst = lcs_matches(tokens_src, tokens_dst, rename_map) local src_spans = unmatched_token_spans(tokens_src, match_src, src_line) local dst_spans = unmatched_token_spans(tokens_dst, match_dst, dst_line) - local count = math.min(#src_spans, #dst_spans) - for i = 1, count do - local src_range = make_range( - src_row, - src_base + src_spans[i].start_col - 1, - src_base + src_spans[i].end_col - ) - local dst_range = make_range( - dst_row, - dst_base + dst_spans[i].start_col - 1, - dst_base + dst_spans[i].end_col - ) - if src_range and dst_range then - table.insert(out, hunk_change(src_range, dst_range)) + if #src_spans > 0 or #dst_spans > 0 then + local emitted = false + local span_count = math.min(#src_spans, #dst_spans) + + for i = 1, span_count do + local src_range = make_range( + src_row, + src_base + src_spans[i].start_col - 1, + src_base + src_spans[i].end_col + ) + local dst_range = make_range( + dst_row, + dst_base + dst_spans[i].start_col - 1, + dst_base + dst_spans[i].end_col + ) + if src_range and dst_range then + table.insert(out, hunk_change(src_range, dst_range)) + emitted = true + end + end + + for i = span_count + 1, #src_spans do + local src_range = make_range( + src_row, + src_base + src_spans[i].start_col - 1, + src_base + src_spans[i].end_col + ) + if src_range then + table.insert(out, hunk_change(src_range, nil)) + emitted = true + end + end + + for i = span_count + 1, #dst_spans do + local dst_range = make_range( + dst_row, + dst_base + dst_spans[i].start_col - 1, + dst_base + dst_spans[i].end_col + ) + if dst_range then + table.insert(out, hunk_change(nil, dst_range)) + emitted = true + end + end + + if emitted then + return end end - return end local fragment = semantic.diff_fragment(src_line, dst_line) @@ -422,6 +560,7 @@ function M.enrich(actions, opts) local normalized_leaf = {} local hunks = {} local has_non_rename_change = false + local saw_precise_candidate = false for _, change in ipairs(raw_leaf_changes) do local src_range = range_metadata(change.src_node) @@ -435,6 +574,9 @@ function M.enrich(actions, opts) if change.src_text ~= change.dst_text and rename_pairs[change.src_text] ~= change.dst_text then has_non_rename_change = true + if src_range and dst_range then + saw_precise_candidate = true + end local hunk_src = src_range local hunk_dst = dst_range @@ -478,10 +620,11 @@ function M.enrich(actions, opts) local rename_only = (#normalized_leaf > 0) and (next(rename_pairs) ~= nil) and not has_non_rename_change - if #hunks == 0 and not rename_only then + if #hunks == 0 and not rename_only and not saw_precise_candidate then hunks = fallback_hunks_from_diff(action.src_node, action.dst_node, src_buf, dst_buf, rename_pairs) end - if #suppressed_pairs > 0 and #hunks > 0 then + + if #hunks > 0 and (#suppressed_pairs > 0 or next(declaration_pairs) ~= nil) then local filtered = {} for _, hunk in ipairs(hunks) do if @@ -501,6 +644,8 @@ function M.enrich(actions, opts) hunks = filtered end + hunks = merge_adjacent_hunks(hunks, src_buf, dst_buf) + action.analysis = { leaf_changes = normalized_leaf, rename_pairs = rename_pairs, From 1a9841d6b7e2a9a7575e61e5898627d3907a0b90 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Thu, 19 Feb 2026 10:44:36 +0530 Subject: [PATCH 35/48] refactor(core): key-aware literal overrides and temp-result refactor rendering --- lua/diffmantic/core/actions.lua | 277 +++++++++++++++++++++++++++++++- 1 file changed, 271 insertions(+), 6 deletions(-) diff --git a/lua/diffmantic/core/actions.lua b/lua/diffmantic/core/actions.lua index b9033f4..83a5715 100644 --- a/lua/diffmantic/core/actions.lua +++ b/lua/diffmantic/core/actions.lua @@ -3,6 +3,13 @@ local semantic = require("diffmantic.core.semantic") local roles = require("diffmantic.core.roles") local analysis = require("diffmantic.core.analysis") +local LITERAL_MEMBER_NODE_TYPES = { + pair = true, + keyed_element = true, + property_signature = true, + shorthand_property_identifier = true, +} + local function range_text(buf, range) if not buf or not range then return nil @@ -70,6 +77,239 @@ local function has_effective_update_hunks(action, src_buf, dst_buf) return false end +local function range_contains(outer, inner) + if not outer or not inner then + return false + end + if outer.start_row == nil or outer.end_row == nil or outer.start_col == nil or outer.end_col == nil then + return false + end + if inner.start_row == nil or inner.end_row == nil or inner.start_col == nil or inner.end_col == nil then + return false + end + if inner.start_row < outer.start_row or inner.end_row > outer.end_row then + return false + end + if inner.start_row == outer.start_row and inner.start_col < outer.start_col then + return false + end + if inner.end_row == outer.end_row and inner.end_col > outer.end_col then + return false + end + return true +end + +local function ranges_equal(a, b) + if not a or not b then + return false + end + return a.start_row == b.start_row + and a.start_col == b.start_col + and a.end_row == b.end_row + and a.end_col == b.end_col +end + +local function ranges_related(a, b) + return ranges_equal(a, b) or range_contains(a, b) or range_contains(b, a) +end + +local TEMP_RESULT_DECL_NODE_TYPES = { + lexical_declaration = true, + short_var_declaration = true, + init_declarator = true, + variable_declaration = true, + local_variable_declaration = true, +} + +local function line_text(buf, row) + if not buf or row == nil then + return nil + end + local lines = vim.api.nvim_buf_get_lines(buf, row, row + 1, false) + return lines and lines[1] or nil +end + +local function action_side_text(action, buf, side) + if not action or not buf then + return nil + end + local node = side == "src" and action.src_node or action.dst_node + if node then + return vim.treesitter.get_node_text(node, buf) + end + local range = side == "src" and action.src or action.dst + return range_text(buf, range) +end + +local function extract_assigned_identifier(text) + if not text or text == "" then + return nil + end + local id = text:match("^%s*[Cc]onst%s+([%a_][%w_]*)%s*=") + or text:match("^%s*[Ll]et%s+([%a_][%w_]*)%s*=") + or text:match("^%s*[Vv]ar%s+([%a_][%w_]*)%s*=") + or text:match("^%s*[Ll]ocal%s+([%a_][%w_]*)%s*=") + or text:match("^%s*([%a_][%w_]*)%s*:=") + if id then + return id + end + local lhs = text:match("^(.-)=") + if not lhs then + return nil + end + return lhs:match("([%a_][%w_]*)%s*$") +end + +local function extract_return_identifier(text) + if not text or text == "" then + return nil + end + local expr = text:match("^%s*return%s+(.+)$") + if not expr then + return nil + end + local trimmed = expr:gsub("%s*;%s*$", ""):match("^%s*(.-)%s*$") + if not trimmed then + return nil + end + return trimmed:match("^%(*%s*([%a_][%w_]*)%s*%)?$") +end + +local function mark_temp_result_refactor_overrides(actions_list, src_buf, dst_buf) + if not src_buf or not dst_buf then + return + end + + local function mark_insert_actions_for_rows(update_action, declared_name, decl_row, ret_row) + for _, action in ipairs(actions_list) do + if action.type == "insert" and action.dst and range_contains(update_action.dst, action.dst) then + local node_type = action.metadata and action.metadata.node_type or nil + if action.dst.start_row == decl_row and TEMP_RESULT_DECL_NODE_TYPES[node_type] then + local decl_text = action_side_text(action, dst_buf, "dst") or line_text(dst_buf, decl_row) + if extract_assigned_identifier(decl_text) == declared_name then + action.metadata = action.metadata or {} + action.metadata.render_as_change = true + end + elseif action.dst.start_row == ret_row and node_type == "return_statement" then + local ret_text = action_side_text(action, dst_buf, "dst") or line_text(dst_buf, ret_row) + if extract_return_identifier(ret_text) == declared_name then + action.metadata = action.metadata or {} + action.metadata.render_as_change = true + end + end + end + end + end + + for _, update_action in ipairs(actions_list) do + if update_action.type == "update" and update_action.dst then + local hunks = update_action.analysis and update_action.analysis.hunks or {} + for _, change_hunk in ipairs(hunks) do + if change_hunk.kind == "change" and change_hunk.src and change_hunk.dst then + local src_line = line_text(src_buf, change_hunk.src.start_row) + local dst_line = line_text(dst_buf, change_hunk.dst.start_row) + if src_line and dst_line and src_line:match("^%s*return%s+") then + local declared_name = extract_assigned_identifier(dst_line) + if declared_name then + local matched_insert = nil + for _, insert_hunk in ipairs(hunks) do + if + insert_hunk.kind == "insert" + and insert_hunk.dst + and insert_hunk.dst.start_row >= change_hunk.dst.start_row + and insert_hunk.dst.start_row <= change_hunk.dst.start_row + 2 + then + local inserted_line = line_text(dst_buf, insert_hunk.dst.start_row) + if extract_return_identifier(inserted_line) == declared_name then + matched_insert = insert_hunk + break + end + end + end + + if matched_insert then + matched_insert.render_as_change = true + mark_insert_actions_for_rows( + update_action, + declared_name, + change_hunk.dst.start_row, + matched_insert.dst.start_row + ) + end + end + end + end + end + end + end +end + +local function mark_literal_member_render_overrides(actions_list) + local update_contexts = {} + for _, action in ipairs(actions_list) do + if action.type == "update" then + local context = { + action = action, + src = action.src, + dst = action.dst, + src_change_hunks = {}, + dst_change_hunks = {}, + } + local hunks = action.analysis and action.analysis.hunks or {} + for _, hunk in ipairs(hunks) do + if hunk.kind == "change" then + if hunk.src then + table.insert(context.src_change_hunks, hunk.src) + end + if hunk.dst then + table.insert(context.dst_change_hunks, hunk.dst) + end + end + end + table.insert(update_contexts, context) + end + end + + local function overlaps_any(ranges, target) + if not target then + return false + end + for _, r in ipairs(ranges) do + if ranges_related(r, target) then + return true + end + end + return false + end + + for _, action in ipairs(actions_list) do + if action.type == "insert" or action.type == "delete" then + local metadata = action.metadata or {} + if LITERAL_MEMBER_NODE_TYPES[metadata.node_type] then + local target = action.type == "insert" and action.dst or action.src + for _, context in ipairs(update_contexts) do + local container = action.type == "insert" and context.dst or context.src + local change_hunks = action.type == "insert" and context.dst_change_hunks or context.src_change_hunks + -- Key-aware policy: only override inserts/deletes that overlap replacement hunks. + if range_contains(container, target) and overlaps_any(change_hunks, target) then + action.metadata = action.metadata or {} + action.metadata.render_as_change = true + local hunks = context.action.analysis and context.action.analysis.hunks or {} + for _, hunk in ipairs(hunks) do + if action.type == "insert" and hunk.kind == "insert" and ranges_related(hunk.dst, action.dst) then + hunk.render_as_change = true + elseif action.type == "delete" and hunk.kind == "delete" and ranges_related(hunk.src, action.src) then + hunk.render_as_change = true + end + end + break + end + end + end + end + end +end + local function range_metadata(node) if not node then return nil @@ -236,6 +476,9 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op local actions = {} local timings = nil local hrtime = nil + local src_has_parse_error = src_root and src_root.has_error and src_root:has_error() or false + local dst_has_parse_error = dst_root and dst_root.has_error and dst_root:has_error() or false + local has_parse_error = src_has_parse_error or dst_has_parse_error if opts and opts.timings then timings = {} if vim and vim.loop and vim.loop.hrtime then @@ -271,7 +514,7 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op local src_node = action.src_node local dst_node = action.dst_node if action.type == "update" and src_node and dst_node then - local leaf_changes = semantic.find_leaf_changes(src_node, dst_node, src_buf, dst_buf) + local leaf_changes = semantic.find_leaf_changes(src_node, dst_node, src_buf, dst_buf, src_role_index, dst_role_index) local rename_pairs = {} for _, change in ipairs(leaf_changes) do if @@ -845,10 +1088,25 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end -- Mark nodes NOT in LIS as moved (only if line difference is significant) + local function has_structural_move_evidence(index) + local dst_line = movable_pairs[index].dst_line + for j, other in ipairs(movable_pairs) do + if j ~= index then + local dst_inversion = (j < index and other.dst_line > dst_line) or (j > index and other.dst_line < dst_line) + if dst_inversion then + return true + end + end + end + return false + end + + -- Mark nodes NOT in LIS as moved only when there is real order inversion evidence. + -- If parse errors exist in either tree, keep move emission conservative. for i, pair in ipairs(movable_pairs) do if not in_lis[i] then local line_diff = math.abs(pair.dst_line - pair.src_line) - if line_diff > 3 then + if not has_parse_error and line_diff > 3 and has_structural_move_evidence(i) then local s = src_info[pair.src_id] local d = dst_info[pair.dst_id] if s and d then @@ -917,17 +1175,21 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op if action.type == "update" then local key = action_pair_key(action) local action_analysis = action.analysis or {} - local has_hunks = has_effective_update_hunks(action, src_buf, dst_buf) + local has_effective_hunks = has_effective_update_hunks(action, src_buf, dst_buf) + local has_any_hunks = action_analysis.hunks and #action_analysis.hunks > 0 local has_rename_pairs = action_analysis.rename_pairs and next(action_analysis.rename_pairs) ~= nil local overlaps_decl_rename = key and declaration_rename_pairs[key] or false local overlaps_move = key and moved_pairs[key] or false - if action_analysis.rename_only and has_rename_pairs and not has_hunks then + if action_analysis.rename_only and has_rename_pairs and not has_effective_hunks then goto continue end - if overlaps_decl_rename and not has_hunks then + if overlaps_decl_rename and not has_effective_hunks then goto continue end - if overlaps_move and not has_hunks then + if overlaps_move and not has_effective_hunks then + goto continue + end + if not has_effective_hunks and not has_any_hunks then goto continue end end @@ -937,6 +1199,9 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op actions = filtered_actions stop_timer(update_suppress_start, "update_suppress") + mark_temp_result_refactor_overrides(actions, src_buf, dst_buf) + mark_literal_member_render_overrides(actions) + local summary_start = start_timer() local summary = build_summary(actions) stop_timer(summary_start, "summary") From 7a35ccab2bf459e181da1f96590b8c7b89d26b42 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Thu, 19 Feb 2026 18:31:12 +0530 Subject: [PATCH 36/48] refactor(ui): emphasize update-over-move with accent layer and highlight tuning --- lua/diffmantic/init.lua | 9 +++- lua/diffmantic/ui/renderer.lua | 97 ++++++++++++++++++++++++++++------ 2 files changed, 87 insertions(+), 19 deletions(-) diff --git a/lua/diffmantic/init.lua b/lua/diffmantic/init.lua index 2397ee2..dc6f43e 100644 --- a/lua/diffmantic/init.lua +++ b/lua/diffmantic/init.lua @@ -35,7 +35,11 @@ local function setup_highlights() local add_bg = pick_bg({ "DiffAdd", "DiffText" }) local delete_bg = pick_bg({ "DiffDelete", "DiffText" }) local change_bg = pick_bg({ "DiffText", "DiffChange" }) - local move_bg = pick_bg({ "DiffText", "DiffChange" }) + local move_bg = pick_bg({ "DiffChange", "DiffText" }) + if move_bg == change_bg then + -- If move and change resolve to the same background, de-emphasize move fill so updates remain visible. + move_bg = nil + end local add_sign_fg = pick_fg({ "DiffAdd" }, 0x49D17D) local delete_sign_fg = pick_fg({ "DiffDelete" }, 0xFF6B6B) @@ -45,7 +49,8 @@ local function setup_highlights() vim.api.nvim_set_hl(0, "DiffmanticAdd", { fg = add_sign_fg, bg = add_bg }) vim.api.nvim_set_hl(0, "DiffmanticDelete", { fg = delete_sign_fg, bg = delete_bg }) vim.api.nvim_set_hl(0, "DiffmanticChange", { fg = change_sign_fg, bg = change_bg }) - vim.api.nvim_set_hl(0, "DiffmanticMove", { fg = move_sign_fg, bg = move_bg }) + vim.api.nvim_set_hl(0, "DiffmanticChangeAccent", { fg = change_sign_fg, bg = "NONE", underline = true, bold = true }) + vim.api.nvim_set_hl(0, "DiffmanticMove", { fg = move_sign_fg, bg = move_bg or "NONE" }) vim.api.nvim_set_hl(0, "DiffmanticRename", { fg = change_sign_fg, underline = true, bold = true, italic = true }) vim.api.nvim_set_hl(0, "DiffmanticAddSign", { fg = add_sign_fg, bg = "NONE" }) diff --git a/lua/diffmantic/ui/renderer.lua b/lua/diffmantic/ui/renderer.lua index e7f283e..94de382 100644 --- a/lua/diffmantic/ui/renderer.lua +++ b/lua/diffmantic/ui/renderer.lua @@ -7,6 +7,7 @@ local HL_PRIORITY = { DiffmanticAdd = 20, DiffmanticDelete = 20, DiffmanticChange = 30, + DiffmanticChangeAccent = 35, DiffmanticRename = 40, } @@ -45,6 +46,40 @@ local function apply_sign(buf, ns, row, text, hl_group, seen_rows) signs.mark(buf, ns, row, 0, text, hl_group, seen_rows) end +local function ranges_overlap(a, b) + if not a or not b then + return false + end + if a.start_row == nil or a.end_row == nil or a.start_col == nil or a.end_col == nil then + return false + end + if b.start_row == nil or b.end_row == nil or b.start_col == nil or b.end_col == nil then + return false + end + if a.end_row < b.start_row or b.end_row < a.start_row then + return false + end + if a.start_row == b.end_row and a.start_col >= b.end_col then + return false + end + if b.start_row == a.end_row and b.start_col >= a.end_col then + return false + end + return true +end + +local function overlaps_any(range, ranges) + if not range or not ranges then + return false + end + for _, candidate in ipairs(ranges) do + if ranges_overlap(range, candidate) then + return true + end + end + return false +end + local function apply_virt(buf, ns, row, col, text, hl_group, pos) if row == nil or not text then return @@ -158,37 +193,65 @@ end function M.render(src_buf, dst_buf, actions, ns) local src_sign_rows = {} local dst_sign_rows = {} + local src_move_ranges = {} + local dst_move_ranges = {} + + for _, action in ipairs(actions) do + if action.type == "move" then + if action.src then + table.insert(src_move_ranges, action.src) + end + if action.dst then + table.insert(dst_move_ranges, action.dst) + end + end + end for _, action in ipairs(actions) do - local style = TYPE_STYLE[action.type] - if style then + local base_style = TYPE_STYLE[action.type] + if base_style then local src = action.src local dst = action.dst local meta = action.metadata or {} + local style = base_style + if (action.type == "insert" or action.type == "delete") and meta.render_as_change then + style = TYPE_STYLE.update + end if action.type == "update" then local effective_hunks = effective_update_hunks(action, src_buf, dst_buf) if #effective_hunks == 0 then goto continue end - local rendered_hunk = false - for _, hunk in ipairs(effective_hunks) do - local hstyle = HUNK_STYLE[hunk.kind] or HUNK_STYLE.change - if hunk.src and hstyle.src_hl then - apply_span(src_buf, ns, hunk.src, hstyle.src_hl) - apply_sign(src_buf, ns, hunk.src.start_row, hstyle.src_sign, hstyle.src_hl, src_sign_rows) - rendered_hunk = true + local rendered_hunk = false + for _, hunk in ipairs(effective_hunks) do + local hstyle = HUNK_STYLE[hunk.kind] or HUNK_STYLE.change + if hunk.render_as_change then + hstyle = HUNK_STYLE.change + end + if hunk.src and hstyle.src_hl then + apply_span(src_buf, ns, hunk.src, hstyle.src_hl) + if hstyle.src_hl == "DiffmanticChange" and overlaps_any(hunk.src, src_move_ranges) then + -- Add a foreground/underline accent so updates stay visible over moved regions. + apply_span(src_buf, ns, hunk.src, "DiffmanticChangeAccent") + end + apply_sign(src_buf, ns, hunk.src.start_row, hstyle.src_sign, hstyle.src_hl, src_sign_rows) + rendered_hunk = true + end + if hunk.dst and hstyle.dst_hl then + apply_span(dst_buf, ns, hunk.dst, hstyle.dst_hl) + if hstyle.dst_hl == "DiffmanticChange" and overlaps_any(hunk.dst, dst_move_ranges) then + -- Add a foreground/underline accent so updates stay visible over moved regions. + apply_span(dst_buf, ns, hunk.dst, "DiffmanticChangeAccent") + end + apply_sign(dst_buf, ns, hunk.dst.start_row, hstyle.dst_sign, hstyle.dst_hl, dst_sign_rows) + rendered_hunk = true + end end - if hunk.dst and hstyle.dst_hl then - apply_span(dst_buf, ns, hunk.dst, hstyle.dst_hl) - apply_sign(dst_buf, ns, hunk.dst.start_row, hstyle.dst_sign, hstyle.dst_hl, dst_sign_rows) - rendered_hunk = true + if not rendered_hunk then + goto continue end - end - if not rendered_hunk then - goto continue - end else if src then apply_span(src_buf, ns, src, style.hl) From f1376dee40976db3cfaa86ea9e1c5819e2fc719c Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Fri, 20 Feb 2026 17:52:41 +0530 Subject: [PATCH 37/48] test(comparison): add cross-language before/after fixtures and sample update --- test/comparison/after.c | 62 +++++++++++++++++++++++++++++++++++ test/comparison/after.cpp | 62 +++++++++++++++++++++++++++++++++++ test/comparison/after.go | 57 ++++++++++++++++++++++++++++++++ test/comparison/after.js | 54 +++++++++++++++++++++++++++++++ test/comparison/after.lua | 66 ++++++++++++++++++++++++++++++++++++++ test/comparison/after.py | 54 +++++++++++++++++++++++++++++++ test/comparison/after.ts | 61 +++++++++++++++++++++++++++++++++++ test/comparison/before.c | 58 +++++++++++++++++++++++++++++++++ test/comparison/before.cpp | 57 ++++++++++++++++++++++++++++++++ test/comparison/before.go | 52 ++++++++++++++++++++++++++++++ test/comparison/before.js | 49 ++++++++++++++++++++++++++++ test/comparison/before.lua | 58 +++++++++++++++++++++++++++++++++ test/comparison/before.py | 48 +++++++++++++++++++++++++++ test/comparison/before.ts | 55 +++++++++++++++++++++++++++++++ test/test2.py | 10 +++--- 15 files changed, 798 insertions(+), 5 deletions(-) create mode 100644 test/comparison/after.c create mode 100644 test/comparison/after.cpp create mode 100644 test/comparison/after.go create mode 100644 test/comparison/after.js create mode 100644 test/comparison/after.lua create mode 100644 test/comparison/after.py create mode 100644 test/comparison/after.ts create mode 100644 test/comparison/before.c create mode 100644 test/comparison/before.cpp create mode 100644 test/comparison/before.go create mode 100644 test/comparison/before.js create mode 100644 test/comparison/before.lua create mode 100644 test/comparison/before.py create mode 100644 test/comparison/before.ts diff --git a/test/comparison/after.c b/test/comparison/after.c new file mode 100644 index 0000000..0ca8a1a --- /dev/null +++ b/test/comparison/after.c @@ -0,0 +1,62 @@ +#include +#include + +#define MAX_USERS 500 +const char *ROLE = "viewer"; + +struct User { + char name[64]; + char email[64]; + int active; + const char *created_at; +}; + +void format_user_display(const struct User *user, char *out, size_t out_size) { + snprintf(out, out_size, "%s <%s>", user->name, user->email); +} + +int validate_email_address(const char *email_str) { + const char *at = strchr(email_str, '@'); + if (!at) { + return 0; + } + if (!strchr(at, '.')) { + return 0; + } + return 1; +} + +struct User create_user(const char *username, const char *email, const char *role) { + if (!validate_email_address(email)) { + fprintf(stderr, "Invalid email format\n"); + } + + struct User user; + memset(&user, 0, sizeof(user)); + strncpy(user.name, username, sizeof(user.name) - 1); + strncpy(user.email, email, sizeof(user.email) - 1); + user.active = 1; + user.created_at = NULL; + (void)role; + return user; +} + +const char *get_user_permissions(const char *role) { + if (strcmp(role, "member") == 0) { + return "read"; + } + if (strcmp(role, "editor") == 0) { + return "read,write"; + } + if (strcmp(role, "admin") == 0) { + return "read,write,delete,manage"; + } + if (strcmp(role, "superadmin") == 0) { + return "read,write,delete,manage,configure"; + } + return ""; +} + +void deactivate_user(int user_id) { + printf("Deactivating user %d\n", user_id); +} diff --git a/test/comparison/after.cpp b/test/comparison/after.cpp new file mode 100644 index 0000000..7edb81c --- /dev/null +++ b/test/comparison/after.cpp @@ -0,0 +1,62 @@ +#include +#include +#include + +const int MAX_USERS = 500; +const std::string ROLE = "viewer"; + +struct User { + std::string name; + std::string email; + bool active; + std::string created_at; +}; + +std::string format_user_display(const User &user) { + std::string result = user.name + " <" + user.email + ">"; + return result; +} + +bool validate_email_address(const std::string &email_str) { + if (email_str.find('@') == std::string::npos) { + return false; + } + if (email_str.find('.') == std::string::npos) { + return false; + } + return true; +} + +User create_user(const std::string &username, const std::string &email, const std::string &role = ROLE) { + if (!validate_email_address(email)) { + throw std::runtime_error("Invalid email format"); + } + + User user; + user.name = username; + user.email = email; + user.active = true; + user.created_at = ""; + (void)role; + return user; +} + +std::string get_user_permissions(const std::string &role) { + if (role == "member") { + return "read"; + } + if (role == "editor") { + return "read,write"; + } + if (role == "admin") { + return "read,write,delete,manage"; + } + if (role == "superadmin") { + return "read,write,delete,manage,configure"; + } + return ""; +} + +void deactivate_user(int user_id) { + std::printf("Deactivating user %d\n", user_id); +} diff --git a/test/comparison/after.go b/test/comparison/after.go new file mode 100644 index 0000000..1b530aa --- /dev/null +++ b/test/comparison/after.go @@ -0,0 +1,57 @@ +package main + +import "fmt" + +type User struct { + Name string + Email string + Role string + Active bool + CreatedAt *string +} + +const MAX_USERS = 500 +const ROLE = "viewer" + +func formatUserDisplay(user User) string { + result := fmt.Sprintf("%s <%s>", user.Name, user.Email) + return result +} + +func validateEmailAddress(emailStr string) bool { + if len(emailStr) == 0 { + return false + } + return true +} + +func createUser(username string, email string, role string) User { + if role == "" { + role = ROLE + } + if !validateEmailAddress(email) { + panic("invalid email") + } + return User{Name: username, Email: email, Role: role, Active: true, CreatedAt: nil} +} + +func getUserPermissions(role string) []string { + if role == "member" { + return []string{"read"} + } + if role == "editor" { + return []string{"read", "write"} + } + if role == "admin" { + return []string{"read", "write", "delete", "manage"} + } + if role == "superadmin" { + return []string{"read", "write", "delete", "manage", "configure"} + } + return []string{} +} + +func deactivateUser(userID int) bool { + fmt.Printf("Deactivating user %d\n", userID) + return true +} diff --git a/test/comparison/after.js b/test/comparison/after.js new file mode 100644 index 0000000..dde230c --- /dev/null +++ b/test/comparison/after.js @@ -0,0 +1,54 @@ +// User management module. + +const MAX_USERS = 500; +const ROLE = "viewer"; + +function formatUserDisplay(user) { + const result = `${user.name} <${user.email}>`; + return result; +} + +function validateEmailAddress(emailStr) { + if (!emailStr.includes("@")) { + return false; + } + if (!emailStr.split("@")[1].includes(".")) { + return false; + } + return true; +} + +function createUser(username, email, role = ROLE) { + if (!validateEmailAddress(email)) { + throw new Error("Invalid email format"); + } + + return { + name: username, + email, + role, + active: true, + createdAt: null, + }; +} + +function getUserPermissions(role) { + if (role === "member") { + return ["read"]; + } + if (role === "editor") { + return ["read", "write"]; + } + if (role === "admin") { + return ["read", "write", "delete", "manage"]; + } + if (role === "superadmin") { + return ["read", "write", "delete", "manage", "configure"]; + } + return []; +} + +function deactivateUser(userId) { + console.log(`Deactivating user ${userId}`); + return true; +} diff --git a/test/comparison/after.lua b/test/comparison/after.lua new file mode 100644 index 0000000..2c288dd --- /dev/null +++ b/test/comparison/after.lua @@ -0,0 +1,66 @@ +-- User management module. + +local MAX_USERS = 500 +local ROLE = "viewer" + +local function format_user_display(username) + local x = 10 + return username.name .. " <" .. username.email .. ">" .. x +end + +local function validate_email_address(email_str) + if not string.find(email_str, "@") then + return false + end + local at = string.find(email_str, "@") + if not string.find(string.sub(email_str, at), ".") then + return false + end + return true +end + +local function create_user(username, email, role) + role = role or ROLE + if not validate_email_address(email) then + error("Invalid email format") + end + + local user = { + name = username, + email = email, + active = true, + created_at = nil, + } + return user +end + +local function get_user_permissions(user) + local permissions = { + member = { "read" }, + editor = { "read", "write" }, + admin = { "read", "write", "delete", "manage" }, + superadmin = { + "read", + "write", + "delete", + "manage", + "configure", + }, + } + return permissions[user.role] or {} +end + +local function deactivate_user(user_id) + print("Deactivating user " .. user_id) + return true +end + +return { + MAX_USERS = MAX_USERS, + ROLE = ROLE, + format_user_display = format_user_display, + validate_email_address = validate_email_address, + create_user = create_user, + get_user_permissions = get_user_permissions, + deactivate_user = deactivate_user, +} diff --git a/test/comparison/after.py b/test/comparison/after.py new file mode 100644 index 0000000..04c9fb0 --- /dev/null +++ b/test/comparison/after.py @@ -0,0 +1,54 @@ +"""User management module.""" + +MAX_USERS = 500 +ROLE = "viewer" + + +def format_user_display(user): + return f"{user['name']} <{user['email']}>" + + +def validate_email_address(email_str): + """Check if email format is valid.""" + if "@" not in email_str: + return False + if "." not in email_str.split("@")[1]: + return False + return True + + +def create_user(username, email, role=ROLE): + """Create a new user with the given details.""" + if not validate_email_address(email): + raise ValueError("Invalid email format") + + user = { + "name": username, + "email": email, + "role": role, + "active": True, + "created_at": None, + } + return user + + +def get_user_permissions(user): + """Get permissions based on user role.""" + permissions = { + "member": ["read"], + "admin": ["read", "write", "delete", "manage"], + "superadmin": [ + "read", + "write", + "delete", + "manage", + "configure", + ], + } + return permissions.get(user["role"], []) + + +def deactivate_user(user_id): + """Deactivate a user by ID instead of deleting.""" + print(f"Deactivating user {user_id}") + return True diff --git a/test/comparison/after.ts b/test/comparison/after.ts new file mode 100644 index 0000000..7846f05 --- /dev/null +++ b/test/comparison/after.ts @@ -0,0 +1,61 @@ +// User management module. + +type Role = "viewer" | "editor" | "admin" | "superadmin"; + +type User = { + name: string; + email: string; + role: Role; + active: boolean; + createdAt: string | null; +}; + +const MAX_USERS: number = 500; +const ROLE: Role = "viewer"; + +function formatUserDisplay(user: User): string { + const result = `${user.name} <${user.email}>`; + return result; +} + +function validateEmailAddress(emailStr: string): boolean { + if (!emailStr.includes("@")) { + return false; + } + if (!emailStr.split("@")[1].includes(".")) { + return false; + } + return true; +} + +function createUser(username: string, email: string, role: Role = ROLE): User { + if (!validateEmailAddress(email)) { + throw new Error("Invalid email format"); + } + + return { + name: username, + email, + role, + active: true, + createdAt: null, + }; +} + +function getUserPermissions(role: Role): string[] { + if (role === "member") { + return ["read"]; + } + if (role === "editor") { + return ["read", "write"]; + } + if (role === "admin") { + return ["read", "write", "delete", "manage"]; + } + return ["read", "write", "delete", "manage", "configure"]; +} + +function deactivateUser(userId: number): boolean { + console.log(`Deactivating user ${userId}`); + return true; +} diff --git a/test/comparison/before.c b/test/comparison/before.c new file mode 100644 index 0000000..f08f15a --- /dev/null +++ b/test/comparison/before.c @@ -0,0 +1,58 @@ +#include +#include + +#define MAX_USERS 100 +const char *DEFAULT_ROLE = "viewer"; + +int validate_email(const char *email) { + const char *at = strchr(email, '@'); + if (!at) { + return 0; + } + if (!strchr(at, '.')) { + return 0; + } + return 1; +} + +struct User { + char name[64]; + char email[64]; + char role[32]; + int active; +}; + +struct User create_user(const char *name, const char *email, const char *role) { + if (!validate_email(email)) { + fprintf(stderr, "Invalid email format\n"); + } + + struct User user; + memset(&user, 0, sizeof(user)); + strncpy(user.name, name, sizeof(user.name) - 1); + strncpy(user.email, email, sizeof(user.email) - 1); + strncpy(user.role, role, sizeof(user.role) - 1); + user.active = 1; + return user; +} + +const char *get_user_permissions(const char *role) { + if (strcmp(role, "viewer") == 0) { + return "read"; + } + if (strcmp(role, "editor") == 0) { + return "read,write"; + } + if (strcmp(role, "admin") == 0) { + return "read,write,delete,manage"; + } + return ""; +} + +void delete_user(int user_id) { + printf("Deleting user %d\n", user_id); +} + +void format_user_display(const struct User *user, char *out, size_t out_size) { + snprintf(out, out_size, "%s <%s>", user->name, user->email); +} diff --git a/test/comparison/before.cpp b/test/comparison/before.cpp new file mode 100644 index 0000000..32cb591 --- /dev/null +++ b/test/comparison/before.cpp @@ -0,0 +1,57 @@ +#include +#include +#include + +const int MAX_USERS = 100; +const std::string DEFAULT_ROLE = "viewer"; + +bool validate_email(const std::string &email) { + if (email.find('@') == std::string::npos) { + return false; + } + if (email.find('.') == std::string::npos) { + return false; + } + return true; +} + +struct User { + std::string name; + std::string email; + std::string role; + bool active; +}; + +User create_user(const std::string &name, const std::string &email, const std::string &role = DEFAULT_ROLE) { + if (!validate_email(email)) { + throw std::runtime_error("Invalid email format"); + } + + User user; + user.name = name; + user.email = email; + user.role = role; + user.active = true; + return user; +} + +std::string get_user_permissions(const std::string &role) { + if (role == "viewer") { + return "read"; + } + if (role == "editor") { + return "read,write"; + } + if (role == "admin") { + return "read,write,delete,manage"; + } + return ""; +} + +void delete_user(int user_id) { + std::printf("Deleting user %d\n", user_id); +} + +std::string format_user_display(const User &user) { + return user.name + " <" + user.email + ">"; +} diff --git a/test/comparison/before.go b/test/comparison/before.go new file mode 100644 index 0000000..3a20275 --- /dev/null +++ b/test/comparison/before.go @@ -0,0 +1,52 @@ +package main + +import "fmt" + +type User struct { + Name string + Email string + Role string + Active bool +} + +const MAX_USERS = 100 +const DEFAULT_ROLE = "viewer" + +func validateEmail(email string) bool { + if len(email) == 0 { + return false + } + return true +} + +func createUser(name string, email string, role string) User { + if role == "" { + role = DEFAULT_ROLE + } + if !validateEmail(email) { + panic("invalid email") + } + return User{Name: name, Email: email, Role: role, Active: true} +} + +func getUserPermissions(role string) []string { + if role == "viewer" { + return []string{"read"} + } + if role == "editor" { + return []string{"read", "write"} + } + if role == "admin" { + return []string{"read", "write", "delete", "manage"} + } + return []string{} +} + +func deleteUser(userID int) bool { + fmt.Printf("Deleting user %d\n", userID) + return true +} + +func formatUserDisplay(user User) string { + return fmt.Sprintf("%s <%s>", user.Name, user.Email) +} diff --git a/test/comparison/before.js b/test/comparison/before.js new file mode 100644 index 0000000..cb18a04 --- /dev/null +++ b/test/comparison/before.js @@ -0,0 +1,49 @@ +// User management module. + +const MAX_USERS = 100; +const DEFAULT_ROLE = "viewer"; + +function validateEmail(email) { + if (!email.includes("@")) { + return false; + } + if (!email.split("@")[1].includes(".")) { + return false; + } + return true; +} + +function createUser(name, email, role = DEFAULT_ROLE) { + if (!validateEmail(email)) { + throw new Error("Invalid email format"); + } + + return { + name, + email, + role, + active: true, + }; +} + +function getUserPermissions(role) { + if (role === "viewer") { + return ["read"]; + } + if (role === "editor") { + return ["read", "write"]; + } + if (role === "admin") { + return ["read", "write", "delete", "manage"]; + } + return []; +} + +function deleteUser(userId) { + console.log(`Deleting user ${userId}`); + return true; +} + +function formatUserDisplay(user) { + return `${user.name} <${user.email}>`; +} diff --git a/test/comparison/before.lua b/test/comparison/before.lua new file mode 100644 index 0000000..6b1324c --- /dev/null +++ b/test/comparison/before.lua @@ -0,0 +1,58 @@ +-- User management module. + +local MAX_USERS = 100 +local DEFAULT_ROLE = "viewer" + +local function validate_email(email) + if not string.find(email, "@") then + return false + end + local at = string.find(email, "@") + if not string.find(string.sub(email, at), ".") then + return false + end + return true +end + +local function create_user(name, email, role) + role = role or DEFAULT_ROLE + if not validate_email(email) then + error("Invalid email format") + end + + local user = { + name = name, + email = email, + role = role, + active = true, + } + return user +end + +local function get_user_permissions(user) + local permissions = { + viewer = { "read" }, + editor = { "read", "write" }, + admin = { "read", "write", "delete", "manage" }, + } + return permissions[user.role] or {} +end + +local function delete_user(user_id) + print("Deleting user " .. user_id) + return true +end + +local function format_user_display(user) + return user.name .. " <" .. user.email .. ">" +end + +return { + MAX_USERS = MAX_USERS, + DEFAULT_ROLE = DEFAULT_ROLE, + validate_email = validate_email, + create_user = create_user, + get_user_permissions = get_user_permissions, + delete_user = delete_user, + format_user_display = format_user_display, +} diff --git a/test/comparison/before.py b/test/comparison/before.py new file mode 100644 index 0000000..9c469dd --- /dev/null +++ b/test/comparison/before.py @@ -0,0 +1,48 @@ +"""User management module.""" + +MAX_USERS = 100 +DEFAULT_ROLE = "viewer" + + +def validate_email(email): + """Check if email format is valid.""" + if "@" not in email: + return False + if "." not in email.split("@")[1]: + return False + return True + + +def create_user(name, email, role=DEFAULT_ROLE): + """Create a new user with the given details.""" + if not validate_email(email): + raise ValueError("Invalid email format") + + user = { + "name": name, + "email": email, + "role": role, + "active": True, + } + return user + + +def get_user_permissions(user): + """Get permissions based on user role.""" + permissions = { + "viewer": ["read"], + "editor": ["read", "write"], + "admin": ["read", "write", "delete", "manage"], + } + return permissions.get(user["role"], []) + + +def format_user_display(user): + """Format user for display.""" + return f"{user['name']} <{user['email']}>" + + +def delete_user(user_id): + """Delete a user by ID.""" + print(f"Deleting user {user_id}") + return True diff --git a/test/comparison/before.ts b/test/comparison/before.ts new file mode 100644 index 0000000..e3e10d4 --- /dev/null +++ b/test/comparison/before.ts @@ -0,0 +1,55 @@ +// User management module. + +type Role = "viewer" | "editor" | "admin"; + +type User = { + name: string; + email: string; + role: Role; + active: boolean; +}; + +const MAX_USERS: number = 100; +const DEFAULT_ROLE: Role = "viewer"; + +function validateEmail(email: string): boolean { + if (!email.includes("@")) { + return false; + } + if (!email.split("@")[1].includes(".")) { + return false; + } + return true; +} + +function createUser(name: string, email: string, role: Role = DEFAULT_ROLE): User { + if (!validateEmail(email)) { + throw new Error("Invalid email format"); + } + + return { + name, + email, + role, + active: true, + }; +} + +function getUserPermissions(role: Role): string[] { + if (role === "viewer") { + return ["read"]; + } + if (role === "editor") { + return ["read", "write"]; + } + return ["read", "write", "delete", "manage"]; +} + +function deleteUser(userId: number): boolean { + console.log(`Deleting user ${userId}`); + return true; +} + +function formatUserDisplay(user: User): string { + return `${user.name} <${user.email}>`; +} diff --git a/test/test2.py b/test/test2.py index 1654080..94786f9 100644 --- a/test/test2.py +++ b/test/test2.py @@ -2,11 +2,6 @@ # Tests: Move, Update, Insert, Rename -def calculate_difference(a, b): - """Subtract two numbers.""" - return a - b - - def calculate_sum(a, b): """Add two numbers.""" return a + b @@ -35,6 +30,11 @@ def fetch_data(self, path): return self.base_url + path +def calculate_difference(x, y): + """Subtract two numbers.""" + return x - y + + # Configuration API_URL = "https://api.example.com/v2" # Updated URL CACHE_DIR = "/tmp/app/cache-v2" From 21be528b4ffe99eded1f9e3b03cfcd4024dff62b Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sat, 21 Feb 2026 10:17:43 +0530 Subject: [PATCH 38/48] perf(core): optimize preprocess hashing and parent-bucket top-down matching --- lua/diffmantic/core/top_down.lua | 95 +++++++++++++++++++------------- lua/diffmantic/treesitter.lua | 45 ++++++++++++--- 2 files changed, 94 insertions(+), 46 deletions(-) diff --git a/lua/diffmantic/core/top_down.lua b/lua/diffmantic/core/top_down.lua index c9cee4e..ab33094 100644 --- a/lua/diffmantic/core/top_down.lua +++ b/lua/diffmantic/core/top_down.lua @@ -2,14 +2,13 @@ local ts_utils = require("diffmantic.treesitter") local M = {} -local function node_key(node) - local sr, sc, er, ec = node:range() - return sr, sc, er, ec +local function node_key(info) + return info.start_row, info.start_col, info.end_row, info.end_col end local function compare_node_order(a, b) - local asr, asc, aer, aec = node_key(a.node) - local bsr, bsc, ber, bec = node_key(b.node) + local asr, asc, aer, aec = node_key(a) + local bsr, bsc, ber, bec = node_key(b) if asr ~= bsr then return asr < bsr end @@ -27,14 +26,17 @@ end -- Top-down matching: match nodes from the top of the tree downwards -- Matches nodes with the same hash at each height level -function M.top_down_match(src_root, dst_root, src_buf, dst_buf) +function M.top_down_match(src_root, dst_root, src_buf, dst_buf, opts) + opts = opts or {} local mappings = {} - local src_info = ts_utils.preprocess_tree(src_root, src_buf) - local dst_info = ts_utils.preprocess_tree(dst_root, dst_buf) + local src_info = opts.src_info or ts_utils.preprocess_tree(src_root, src_buf, opts) + local dst_info = opts.dst_info or ts_utils.preprocess_tree(dst_root, dst_buf, opts) local src_mapped = {} local dst_mapped = {} local src_to_dst = {} + local src_root_id = src_root:id() + local dst_root_id = dst_root:id() -- Group nodes by their height in the tree local function get_nodes_by_height(info) @@ -55,24 +57,26 @@ function M.top_down_match(src_root, dst_root, src_buf, dst_buf) local src_by_height = get_nodes_by_height(src_info) local dst_by_height = get_nodes_by_height(dst_info) - local function parents_are_compatible(src_data, dst_data) - local src_parent = src_data.parent - local dst_parent = dst_data.parent - - if not src_parent and not dst_parent then - return true + local function dst_parent_key(info) + local parent_id = info.parent_id + if not parent_id then + return 0 end - if not src_parent or not dst_parent then - return false + if parent_id == dst_root_id then + return dst_root_id end + return parent_id + end - local src_parent_id = src_parent:id() - local dst_parent_id = dst_parent:id() - if src_parent_id == src_root:id() and dst_parent_id == dst_root:id() then - return true + local function src_parent_key(info) + local parent_id = info.parent_id + if not parent_id then + return 0 end - - return src_to_dst[src_parent_id] == dst_parent_id + if parent_id == src_root_id then + return dst_root_id + end + return src_to_dst[parent_id] or -1 end -- Find the maximum height in both trees @@ -88,36 +92,51 @@ function M.top_down_match(src_root, dst_root, src_buf, dst_buf) end end - -- For each height, match nodes with the same hash using hash indexing + -- For each height, match nodes with the same hash and compatible mapped parent. + -- Parent buckets avoid O(k) scans through all same-hash candidates. for h = max_h, 1, -1 do local s_nodes = src_by_height[h] or {} local d_nodes = dst_by_height[h] or {} - local dst_by_hash = {} + local dst_by_hash_parent = {} for _, d in ipairs(d_nodes) do if not dst_mapped[d.id] then - if not dst_by_hash[d.hash] then - dst_by_hash[d.hash] = {} + local hash_buckets = dst_by_hash_parent[d.hash] + if not hash_buckets then + hash_buckets = {} + dst_by_hash_parent[d.hash] = hash_buckets end - table.insert(dst_by_hash[d.hash], d) + + local pkey = dst_parent_key(d) + local queue = hash_buckets[pkey] + if not queue then + queue = { head = 1, items = {} } + hash_buckets[pkey] = queue + end + local items = queue.items + items[#items + 1] = d end end for _, s in ipairs(s_nodes) do if not src_mapped[s.id] then - local candidates = dst_by_hash[s.hash] - if candidates then - for i, d in ipairs(candidates) do - if not dst_mapped[d.id] and parents_are_compatible(s, d) then - table.insert(mappings, { src = s.id, dst = d.id }) - src_mapped[s.id] = true - dst_mapped[d.id] = true - src_to_dst[s.id] = d.id - table.remove(candidates, i) - break - end + local pkey = src_parent_key(s) + if pkey ~= -1 then + local hash_buckets = dst_by_hash_parent[s.hash] + local queue = hash_buckets and hash_buckets[pkey] or nil + if queue then + local head = queue.head + local items = queue.items + local d = items[head] + if d then + queue.head = head + 1 + table.insert(mappings, { src = s.id, dst = d.id }) + src_mapped[s.id] = true + dst_mapped[d.id] = true + src_to_dst[s.id] = d.id end end + end end end end diff --git a/lua/diffmantic/treesitter.lua b/lua/diffmantic/treesitter.lua index 88b6e68..10bbb2b 100644 --- a/lua/diffmantic/treesitter.lua +++ b/lua/diffmantic/treesitter.lua @@ -10,6 +10,10 @@ local function string_hash(str) return h end +local function hash_combine(acc, value) + return ((acc * 33) + value + 97) % 4294967296 +end + -- A leaf node has no children (e.g., a variable name, number, string literal) local function is_leaf(node) return node:named_child_count() == 0 @@ -27,34 +31,54 @@ end -- Walk through the entire syntax tree and compute metadata for each node -- Returns a table mapping node IDs to their computed info -function M.preprocess_tree(root, bufnr) +function M.preprocess_tree(root, bufnr, opts) + opts = opts or {} local info = {} + local label_hash_cache = {} + local type_hash_cache = {} + + local function cached_string_hash(cache, text) + local value = cache[text] + if value == nil then + value = string_hash(text) + cache[text] = value + end + return value + end local function visit(node) local id = node:id() local type = node:type() local label = get_label(node, bufnr) + local sr, sc, er, ec = node:range() local height = 1 local size = 1 - local child_hashes = "" - local child_structure_hashes = "" + local hash_acc = cached_string_hash(type_hash_cache, type) + local structure_hash_acc = hash_acc -- Recursively process all children first (post-order traversal) for child in node:iter_children() do local child_info = visit(child) height = math.max(height, child_info.height + 1) size = size + child_info.size - child_hashes = child_hashes .. tostring(child_info.hash) - child_structure_hashes = child_structure_hashes .. tostring(child_info.structure_hash) - info[child:id()].parent = node + hash_acc = hash_combine(hash_acc, child_info.hash) + structure_hash_acc = hash_combine(structure_hash_acc, child_info.structure_hash) + child_info.parent = node + child_info.parent_id = id + end + + if label ~= "" then + hash_acc = hash_combine(hash_acc, cached_string_hash(label_hash_cache, label)) + else + hash_acc = hash_combine(hash_acc, 0) end -- hash: unique if type + label + children all match (exact match) - local hash = string_hash(type .. label .. child_hashes) + local hash = hash_acc -- structure_hash: unique if type + children structure match (ignores labels) -- useful for detecting moved/renamed code - local structure_hash = string_hash(type .. child_structure_hashes) + local structure_hash = structure_hash_acc info[id] = { node = node, @@ -65,6 +89,11 @@ function M.preprocess_tree(root, bufnr) type = type, label = label, id = id, + start_row = sr, + start_col = sc, + end_row = er, + end_col = ec, + parent_id = nil, } return info[id] end From a942f32d2fc6722984bc13a334ee84bb568671d7 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sat, 21 Feb 2026 14:46:12 +0530 Subject: [PATCH 39/48] perf(core): cache role/name resolution and speed bottom-up matching --- lua/diffmantic/core/bottom_up.lua | 221 +++++++++++++++++++++--------- lua/diffmantic/core/roles.lua | 51 ++++++- 2 files changed, 204 insertions(+), 68 deletions(-) diff --git a/lua/diffmantic/core/bottom_up.lua b/lua/diffmantic/core/bottom_up.lua index bfff02d..fd7ca2e 100644 --- a/lua/diffmantic/core/bottom_up.lua +++ b/lua/diffmantic/core/bottom_up.lua @@ -1,14 +1,17 @@ local M = {} local roles = require("diffmantic.core.roles") -local function node_key(node) - local sr, sc, er, ec = node:range() +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.node) - local bsr, bsc, ber, bec = node_key(b_info.node) + 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 @@ -26,9 +29,23 @@ end -- Bottom-up matching: match nodes from leaves up, using parent mappings -- Tries to match nodes with the same type and label, and optionally name -function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src_buf, dst_buf) - local src_role_index = roles.build_index(src_root, src_buf) - local dst_role_index = roles.build_index(dst_root, dst_buf) +function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src_buf, dst_buf, opts) + opts = opts or {} + local src_role_index = opts.src_role_index or roles.build_index(src_root, src_buf) + local dst_role_index = opts.dst_role_index or roles.build_index(dst_root, dst_buf) + local src_root_id = src_root:id() + + local node_text_cache = {} + local function node_text(node, bufnr) + local key = tostring(bufnr) .. ":" .. tostring(node:id()) + local cached = node_text_cache[key] + if cached ~= nil then + return cached + end + local text = vim.treesitter.get_node_text(node, bufnr) + node_text_cache[key] = text + return text + end -- Build O(1) lookup tables local src_to_dst = {} @@ -39,7 +56,18 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src end -- Get the name of a declaration node (function or variable) - local function get_declaration_name(node, bufnr, role_index) + local function get_declaration_name(node, bufnr, role_index, name_cache) + local node_id = node:id() + local cached = name_cache[node_id] + if cached ~= nil then + return cached or nil + end + + local function cache_and_return(value) + name_cache[node_id] = value or false + return value + end + local function find_first_identifier(n) if not n then return nil @@ -62,17 +90,17 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src local function_name = roles.get_kind_name_text(node, role_index, bufnr, "function") if function_name and #function_name > 0 then - return function_name + return cache_and_return(function_name) end local class_name = roles.get_kind_name_text(node, role_index, bufnr, "class") if class_name and #class_name > 0 then - return class_name + return cache_and_return(class_name) end local variable_name = roles.get_kind_name_text(node, role_index, bufnr, "variable") if variable_name and #variable_name > 0 then - return variable_name + return cache_and_return(variable_name) end if @@ -83,7 +111,7 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src then local name_node = node:field("name")[1] or node:field("tag")[1] if name_node then - return vim.treesitter.get_node_text(name_node, bufnr) + return cache_and_return(node_text(name_node, bufnr)) end end @@ -94,7 +122,7 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src end local ntype = name_node:type() if ntype == "identifier" then - return vim.treesitter.get_node_text(name_node, bufnr) + return node_text(name_node, bufnr) end if ntype == "dot_index_expression" then local tbl = name_node:field("table")[1] @@ -114,21 +142,21 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src return left .. ":" .. right end end - return vim.treesitter.get_node_text(name_node, bufnr) + return node_text(name_node, bufnr) end local name_nodes = node:field("name") if name_nodes and name_nodes[1] then local full_name = lua_name_from_node(name_nodes[1]) if full_name and #full_name > 0 then - return full_name + return cache_and_return(full_name) end end end for child in node:iter_children() do if child:type() == "identifier" then - return vim.treesitter.get_node_text(child, bufnr) + return cache_and_return(node_text(child, bufnr)) end end @@ -140,7 +168,7 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src if subchild:type() == "variable_list" then for id_node in subchild:iter_children() do if id_node:type() == "identifier" then - return vim.treesitter.get_node_text(id_node, bufnr) + return cache_and_return(node_text(id_node, bufnr)) end end end @@ -155,7 +183,7 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src if child:type() == "function_declarator" then local found = find_first_identifier(child) if found then - return vim.treesitter.get_node_text(found, bufnr) + return cache_and_return(node_text(found, bufnr)) end end end @@ -169,7 +197,7 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src if decl then for subchild in decl:iter_children() do if subchild:type() == "identifier" or subchild:type() == "field_identifier" then - return vim.treesitter.get_node_text(subchild, bufnr) + return cache_and_return(node_text(subchild, bufnr)) end end end @@ -183,7 +211,7 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src if decl then for subchild in decl:iter_children() do if subchild:type() == "identifier" or subchild:type() == "field_identifier" then - return vim.treesitter.get_node_text(subchild, bufnr) + return cache_and_return(node_text(subchild, bufnr)) end end end @@ -195,21 +223,26 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src if child:type() == "assignment" then for subchild in child:iter_children() do if subchild:type() == "identifier" then - return vim.treesitter.get_node_text(subchild, bufnr) + return cache_and_return(node_text(subchild, bufnr)) end end end end end - return nil + return cache_and_return(nil) end -- Try to extract a stable "value hash" for assignments to disambiguate renames. - local function get_assignment_value_hash(node, info) + local function get_assignment_value_hash(node, info, cache) if not node then return nil end + local node_id = node:id() + local cached = cache[node_id] + if cached ~= nil then + return cached or nil + end -- Python: expression_statement (assignment left: ..., right: ...) if node:type() == "expression_statement" then for child in node:iter_children() do @@ -223,11 +256,13 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src right = last end if right and info[right:id()] then - return info[right:id()].hash + cache[node_id] = info[right:id()].hash + return cache[node_id] end end end end + cache[node_id] = false return nil end @@ -341,20 +376,89 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src return ah < bh end) + local src_decl_name_cache = {} + local dst_decl_name_cache = {} + local src_value_hash_cache = {} + local dst_value_hash_cache = {} + local parent_candidates = {} + + local function candidate_signature(info) + return info.type .. "\x1f" .. info.label + end + + local function build_parent_candidates(dest_parent_id) + local state = { by_sig = {} } + local function push_child(child) + local child_id = child:id() + if dst_to_src[child_id] then + return + end + local d_info = dst_info[child_id] + if not d_info then + return + end + local sig = candidate_signature(d_info) + local queue = state.by_sig[sig] + if not queue then + queue = { head = 1, items = {} } + state.by_sig[sig] = queue + end + local items = queue.items + items[#items + 1] = child_id + end + + if dest_parent_id then + local d_parent = dst_info[dest_parent_id] and dst_info[dest_parent_id].node or nil + if d_parent then + for child in d_parent:iter_children() do + push_child(child) + end + end + else + for child in dst_root:iter_children() do + push_child(child) + end + end + return state + end + + local function queue_for_parent_sig(dest_parent_id, sig) + local key = dest_parent_id or 0 + local state = parent_candidates[key] + if not state then + state = build_parent_candidates(dest_parent_id) + parent_candidates[key] = state + end + return state.by_sig[sig] + end + + local function first_unmapped_candidate_id(queue) + if not queue then + return nil + end + local items = queue.items + local head = queue.head + while head <= #items and dst_to_src[items[head]] do + head = head + 1 + end + queue.head = head + return items[head] + end + -- Try to match unmapped nodes whose parent is mapped for _, id in ipairs(src_ids) do local info = src_info[id] if not src_to_dst[id] then - local parent = info.parent + local parent_id = info.parent_id local parent_mapped = false local dest_parent_id = nil - if not parent then + if not parent_id then parent_mapped = true - elseif parent:id() == src_root:id() then + elseif parent_id == src_root_id then parent_mapped = true else - local dst_id = src_to_dst[parent:id()] + local dst_id = src_to_dst[parent_id] if dst_id then parent_mapped = true dest_parent_id = dst_id @@ -362,70 +466,61 @@ function M.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src end if parent_mapped then - local candidates = {} - if dest_parent_id then - local d_parent = dst_info[dest_parent_id].node - for child in d_parent:iter_children() do - if not dst_to_src[child:id()] then - table.insert(candidates, child) - end - end - else - for child in dst_root:iter_children() do - if not dst_to_src[child:id()] then - table.insert(candidates, child) - end - end - end + local queue = queue_for_parent_sig(dest_parent_id, candidate_signature(info)) + local candidates = queue and queue.items or nil local src_name = nil if is_identifier_type(info, src_role_index) then - src_name = get_declaration_name(info.node, src_buf, src_role_index) + src_name = get_declaration_name(info.node, src_buf, src_role_index, src_decl_name_cache) end - local src_value_hash = get_assignment_value_hash(info.node, src_info) + local src_value_hash = get_assignment_value_hash(info.node, src_info, src_value_hash_cache) local rename_candidate = nil local structure_candidates = {} local rename_score = -1 local rename_tie = false - for _, cand in ipairs(candidates) do - local d_info = dst_info[cand:id()] - if d_info.type == info.type and d_info.label == info.label then - if src_name then - local dst_name = get_declaration_name(cand, dst_buf, dst_role_index) + if not src_name then + local candidate_id = first_unmapped_candidate_id(queue) + if candidate_id then + table.insert(mappings, { src = id, dst = candidate_id }) + src_to_dst[id] = candidate_id + dst_to_src[candidate_id] = id + end + elseif candidates then + local start_idx = queue and queue.head or 1 + for i = start_idx, #candidates do + local candidate_id = candidates[i] + if not dst_to_src[candidate_id] then + local cand = dst_info[candidate_id].node + local d_info = dst_info[candidate_id] + local dst_name = get_declaration_name(cand, dst_buf, dst_role_index, dst_decl_name_cache) if src_name == dst_name then - table.insert(mappings, { src = id, dst = cand:id() }) - src_to_dst[id] = cand:id() - dst_to_src[cand:id()] = id + table.insert(mappings, { src = id, dst = candidate_id }) + src_to_dst[id] = candidate_id + dst_to_src[candidate_id] = id rename_candidate = nil break elseif dst_name and src_info[id].structure_hash == d_info.structure_hash then - local dst_value_hash = get_assignment_value_hash(cand, dst_info) + local dst_value_hash = get_assignment_value_hash(cand, dst_info, dst_value_hash_cache) if src_value_hash and dst_value_hash and src_value_hash ~= dst_value_hash then goto continue_candidate end - table.insert(structure_candidates, cand:id()) + table.insert(structure_candidates, candidate_id) local score = name_similarity(src_name, dst_name) if score < 0.8 then goto continue_candidate end if score > rename_score then - rename_candidate = cand:id() + rename_candidate = candidate_id rename_score = score rename_tie = false elseif score == rename_score and score > 0 then rename_tie = true end end - else - table.insert(mappings, { src = id, dst = cand:id() }) - src_to_dst[id] = cand:id() - dst_to_src[cand:id()] = id - rename_candidate = nil - break end + ::continue_candidate:: end - ::continue_candidate:: end if not src_to_dst[id] and rename_candidate and not rename_tie and rename_score > 0 then diff --git a/lua/diffmantic/core/roles.lua b/lua/diffmantic/core/roles.lua index d3e82c5..75d5ea9 100644 --- a/lua/diffmantic/core/roles.lua +++ b/lua/diffmantic/core/roles.lua @@ -44,7 +44,11 @@ local function add_capture(index, capture, node) index.by_node[id][capture] = true index.by_capture[capture] = index.by_capture[capture] or {} - index.by_capture[capture][id] = node + if not index.by_capture[capture][id] then + index.by_capture[capture][id] = node + index.by_capture_list[capture] = index.by_capture_list[capture] or {} + table.insert(index.by_capture_list[capture], node) + end end local function resolve_lang(bufnr) @@ -84,6 +88,10 @@ function M.build_index(root, bufnr) lang = lang, by_node = {}, by_capture = {}, + by_capture_list = {}, + _descendant_capture_cache = {}, + _kind_name_node_cache = {}, + _kind_name_text_cache = {}, } for id, node in query:iter_captures(root, bufnr, 0, -1) do @@ -109,17 +117,27 @@ function M.find_descendant_with_capture(node, index, capture) return nil end - local by_capture = index.by_capture[capture] + local cache_key = capture .. ":" .. tostring(node:id()) + local cache = index._descendant_capture_cache + local cached = cache[cache_key] + if cached ~= nil then + return cached or nil + end + + local by_capture = index.by_capture_list[capture] if not by_capture then + cache[cache_key] = false return nil end - for _, captured in pairs(by_capture) do + for _, captured in ipairs(by_capture) do if node:equal(captured) or node:child_with_descendant(captured) then + cache[cache_key] = captured return captured end end + cache[cache_key] = false return nil end @@ -154,16 +172,39 @@ function M.has_structural_kind(node, index, kind) end function M.get_kind_name_node(node, index, kind) + if not node or not index then + return nil + end + local key = kind .. ":" .. tostring(node:id()) + local cache = index._kind_name_node_cache + local cached = cache[key] + if cached ~= nil then + return cached or nil + end local capture = string.format("diff.%s.name", kind) - return M.find_descendant_with_capture(node, index, capture) + local found = M.find_descendant_with_capture(node, index, capture) + cache[key] = found or false + return found end function M.get_kind_name_text(node, index, bufnr, kind) + if not node or not index then + return nil + end + local key = kind .. ":" .. tostring(node:id()) + local cache = index._kind_name_text_cache + local cached = cache[key] + if cached ~= nil then + return cached or nil + end local name_node = M.get_kind_name_node(node, index, kind) if not name_node then + cache[key] = false return nil end - return vim.treesitter.get_node_text(name_node, bufnr) + local text = vim.treesitter.get_node_text(name_node, bufnr) + cache[key] = text or false + return text end return M From 0b653e9c88199d1b1a0db67e6afebce518bd5c1e Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sat, 21 Feb 2026 18:08:57 +0530 Subject: [PATCH 40/48] perf(core): switch recovery to sparse worklist with bounded LCS --- lua/diffmantic/core/recovery.lua | 103 +++++++++++++++++++++++++------ 1 file changed, 84 insertions(+), 19 deletions(-) diff --git a/lua/diffmantic/core/recovery.lua b/lua/diffmantic/core/recovery.lua index 738a86a..4a40094 100644 --- a/lua/diffmantic/core/recovery.lua +++ b/lua/diffmantic/core/recovery.lua @@ -1,13 +1,16 @@ local M = {} -local function node_key(node) - local sr, sc, er, ec = node:range() +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.node) - local bsr, bsc, ber, bec = node_key(b_info.node) + 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 @@ -24,7 +27,9 @@ local function compare_info_order(a_info, b_info) 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) +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, } @@ -46,6 +51,23 @@ function M.recovery_match(src_root, dst_root, mappings, src_info, dst_info, src_ 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) @@ -53,6 +75,9 @@ function M.recovery_match(src_root, dst_root, mappings, src_info, dst_info, src_ 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 = 1, m + 1 do @@ -96,6 +121,41 @@ function M.recovery_match(src_root, dst_root, mappings, src_info, dst_info, src_ return result end + 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 + 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 + return true + end + end + 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 + -- 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 @@ -104,6 +164,11 @@ function M.recovery_match(src_root, dst_root, mappings, src_info, dst_info, src_ 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 @@ -133,7 +198,6 @@ function M.recovery_match(src_root, dst_root, mappings, src_info, dst_info, src_ -- 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 - -- Recurse immediately so children of newly matched nodes are not skipped by outer ordering. simple_recovery(match.src, match.dst) end end @@ -142,7 +206,6 @@ function M.recovery_match(src_root, dst_root, mappings, src_info, dst_info, src_ 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 - -- Recurse immediately for the same reason as Step 1. simple_recovery(match.src, match.dst) end end @@ -173,20 +236,22 @@ function M.recovery_match(src_root, dst_root, mappings, src_info, dst_info, src_ end end - -- Apply recovery to all mapped nodes. - local src_ids = {} - for id in pairs(src_info) do - table.insert(src_ids, id) + -- 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 - table.sort(src_ids, function(a, b) - return compare_info_order(src_info[a], src_info[b]) - end) - for _, id in ipairs(src_ids) do - local info = src_info[id] - local dst_id = src_to_dst[id] - if dst_id then - simple_recovery(info.node, dst_info[dst_id].node) + 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 From 47bb9c562dff24d44c16a4c245773fbd6255af35 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sun, 22 Feb 2026 10:23:19 +0530 Subject: [PATCH 41/48] perf(core): reuse role indexes and memoize action precompute ancestry --- lua/diffmantic/core/actions.lua | 147 +++++++++++++++++--------------- lua/diffmantic/init.lua | 30 ++++++- 2 files changed, 106 insertions(+), 71 deletions(-) diff --git a/lua/diffmantic/core/actions.lua b/lua/diffmantic/core/actions.lua index 83a5715..1f5c05d 100644 --- a/lua/diffmantic/core/actions.lua +++ b/lua/diffmantic/core/actions.lua @@ -473,21 +473,24 @@ end -- Generate edit actions from node mappings -- Actions describe what changed: insert, delete, update, move, rename function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, opts) + opts = opts or {} local actions = {} local timings = nil local hrtime = nil local src_has_parse_error = src_root and src_root.has_error and src_root:has_error() or false local dst_has_parse_error = dst_root and dst_root.has_error and dst_root:has_error() or false local has_parse_error = src_has_parse_error or dst_has_parse_error - if opts and opts.timings then + if opts.timings then timings = {} if vim and vim.loop and vim.loop.hrtime then hrtime = vim.loop.hrtime end end - local src_role_index = nil - local dst_role_index = nil + local src_role_index = opts.src_role_index or nil + local dst_role_index = opts.dst_role_index or nil + local src_buf = opts.src_buf or nil + local dst_buf = opts.dst_buf or nil local function start_timer() if not hrtime then @@ -504,8 +507,6 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end local function enrich_update_actions_with_semantics(actions_list) - local src_buf = opts and opts.src_buf or nil - local dst_buf = opts and opts.dst_buf or nil if not src_buf or not dst_buf then return end @@ -536,8 +537,6 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op local function emit_rename_actions(actions_list) local renames = {} local seen = {} - local src_buf = opts and opts.src_buf or nil - local dst_buf = opts and opts.dst_buf or nil local is_buf_available = src_buf and dst_buf local function pair_key(from_text, to_text) @@ -797,11 +796,13 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end local roles_start = start_timer() - local src_buf = opts and opts.src_buf or nil - local dst_buf = opts and opts.dst_buf or nil if src_buf and dst_buf then - src_role_index = roles.build_index(src_root, src_buf) - dst_role_index = roles.build_index(dst_root, dst_buf) + if not src_role_index then + src_role_index = roles.build_index(src_root, src_buf) + end + if not dst_role_index then + dst_role_index = roles.build_index(dst_root, dst_buf) + end end stop_timer(roles_start, "roles") @@ -939,66 +940,68 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op end end - -- Precompute ancestry flags for source nodes (unmapped significant ancestors) - local src_has_unmapped_sig_ancestor = {} - for id, info in pairs(src_info) do - local current = info.parent - while current do - local p_id = current:id() - local p_info = src_info[p_id] - if p_info then - if not src_to_dst[p_id] and is_significant(p_info, src_role_index) then - src_has_unmapped_sig_ancestor[id] = true - break - end - current = p_info.parent - else - break - end + local function parent_id_of(info) + if not info then + return nil end + if info.parent_id ~= nil then + return info.parent_id + end + return info.parent and info.parent:id() or nil end - -- Precompute ancestry flags for destination nodes (unmapped significant ancestors) - local dst_has_unmapped_sig_ancestor = {} - for id, info in pairs(dst_info) do - local current = info.parent - while current do - local p_id = current:id() - local p_info = dst_info[p_id] - if p_info then - if not dst_to_src[p_id] and is_significant(p_info, dst_role_index) then - dst_has_unmapped_sig_ancestor[id] = true - break - end - current = p_info.parent - else - break + local function memoized_ancestor_flags(info_map, predicate) + local memo = {} + local function has_match(id) + local cached = memo[id] + if cached ~= nil then + return cached + end + local info = info_map[id] + if not info then + memo[id] = false + return false + end + local parent_id = parent_id_of(info) + if not parent_id then + memo[id] = false + return false + end + local parent_info = info_map[parent_id] + if not parent_info then + memo[id] = false + return false end + if predicate(parent_id, parent_info) then + memo[id] = true + return true + end + local result = has_match(parent_id) + memo[id] = result + return result + end + for id in pairs(info_map) do + has_match(id) end + return memo end + -- Precompute ancestry flags for source nodes (unmapped significant ancestors) + local src_has_unmapped_sig_ancestor = memoized_ancestor_flags(src_info, function(parent_id, parent_info) + return not src_to_dst[parent_id] and is_significant(parent_info, src_role_index) + end) + + -- Precompute ancestry flags for destination nodes (unmapped significant ancestors) + local dst_has_unmapped_sig_ancestor = memoized_ancestor_flags(dst_info, function(parent_id, parent_info) + return not dst_to_src[parent_id] and is_significant(parent_info, dst_role_index) + end) + -- Precompute ancestry flags for updated significant ancestors - local src_has_updated_sig_ancestor = {} - for id, info in pairs(src_info) do - local current = info.parent - while current do - local p_id = current:id() - local p_info = src_info[p_id] - if p_info then - if - nodes_with_changes[p_id] - and is_significant(p_info, src_role_index) - and not is_transparent_update_ancestor(p_info, src_role_index) - then - src_has_updated_sig_ancestor[id] = true - break - end - current = p_info.parent - else - break - end - end - end + local src_has_updated_sig_ancestor = memoized_ancestor_flags(src_info, function(parent_id, parent_info) + return nodes_with_changes[parent_id] + and is_significant(parent_info, src_role_index) + and not is_transparent_update_ancestor(parent_info, src_role_index) + end) -- UPDATES: mapped nodes with different content, but only significant types without updated ancestors stop_timer(precompute_start, "precompute") @@ -1017,16 +1020,24 @@ function M.generate_actions(src_root, dst_root, mappings, src_info, dst_info, op -- MOVES: use LCS to find which top-level mapped functions changed relative order -- Only functions not in the LCS are considered moved. local moves_start = start_timer() - local movable_pairs = {} + local movable_pairs = {} + local src_root_id = src_root:id() + local dst_root_id = dst_root:id() for _, m in ipairs(mappings) do local s = src_info[m.src] if s and is_movable(s, src_role_index) then - local src_parent_is_root = s.parent and s.parent:id() == src_root:id() + local src_parent_is_root = (s.parent_id or (s.parent and s.parent:id()) or nil) == src_root_id local d = dst_info[m.dst] - local dst_parent_is_root = d and d.parent and d.parent:id() == dst_root:id() + local dst_parent_is_root = d and ((d.parent_id or (d.parent and d.parent:id()) or nil) == dst_root_id) if src_parent_is_root and dst_parent_is_root then - local src_line = s.node:range() - local dst_line = d.node:range() + local src_line = s.start_row + if src_line == nil then + src_line = s.node:range() + end + local dst_line = d.start_row + if dst_line == nil then + dst_line = d.node:range() + end table.insert(movable_pairs, { src_id = m.src, dst_id = m.dst, diff --git a/lua/diffmantic/init.lua b/lua/diffmantic/init.lua index dc6f43e..75bafcb 100644 --- a/lua/diffmantic/init.lua +++ b/lua/diffmantic/init.lua @@ -2,6 +2,7 @@ local M = {} local core = require("diffmantic.core") local ui = require("diffmantic.ui") local debug_utils = require("diffmantic.debug_utils") +local roles = require("diffmantic.core.roles") local function hl(name) local ok, value = pcall(vim.api.nvim_get_hl, 0, { name = name, link = false }) @@ -122,21 +123,44 @@ function M.diff(args) end local root1 = parser1:parse()[1]:root() local root2 = parser2:parse()[1]:root() + local src_role_index = roles.build_index(root1, buf1) + local dst_role_index = roles.build_index(root2, buf2) - local mappings, src_info, dst_info = core.top_down_match(root1, root2, buf1, buf2) + local mappings, src_info, dst_info = core.top_down_match(root1, root2, buf1, buf2, { + adaptive_mode = true, + }) -- print("Top-down mappings: " .. #mappings) -- local before_bottom_up = #mappings - mappings = core.bottom_up_match(mappings, src_info, dst_info, root1, root2, buf1, buf2) + mappings = core.bottom_up_match(mappings, src_info, dst_info, root1, root2, buf1, buf2, { + src_role_index = src_role_index, + dst_role_index = dst_role_index, + adaptive_mode = true, + }) -- print("Mappings after Bottom-up: " .. #mappings .. " (+" .. (#mappings - before_bottom_up) .. " new)") -- local before_recovery = #mappings - mappings = core.recovery_match(root1, root2, mappings, src_info, dst_info, buf1, buf2) + local src_count = 0 + local dst_count = 0 + for _ in pairs(src_info) do + src_count = src_count + 1 + end + for _ in pairs(dst_info) do + dst_count = dst_count + 1 + end + local max_nodes = math.max(src_count, dst_count) + mappings = core.recovery_match(root1, root2, mappings, src_info, dst_info, buf1, buf2, { + recovery_lcs_cell_limit = max_nodes >= 25000 and 1500 or 6000, + adaptive_mode = true, + }) -- debug_utils.print_recovery_mappings(mappings, before_recovery, src_info, dst_info, buf1, buf2) local actions = core.generate_actions(root1, root2, mappings, src_info, dst_info, { src_buf = buf1, dst_buf = buf2, + src_role_index = src_role_index, + dst_role_index = dst_role_index, + adaptive_mode = true, }) -- debug_utils.print_actions(actions, buf1, buf2) From 988bad28d72ab44b6b5f69979b97fb912526410c Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Sun, 22 Feb 2026 13:58:41 +0530 Subject: [PATCH 42/48] test(perf): benchmark timing breakdown --- test/benchmark.lua | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/test/benchmark.lua b/test/benchmark.lua index 1e77053..c3d269f 100644 --- a/test/benchmark.lua +++ b/test/benchmark.lua @@ -4,6 +4,7 @@ local core = require("diffmantic.core") local ts = require("diffmantic.treesitter") +local roles = require("diffmantic.core.roles") -- Redirect output to file for headless mode local output_file = io.open("/tmp/gumtree_benchmark.txt", "w") @@ -121,23 +122,44 @@ local function run_benchmark(name, num_functions, vars_per_function, changes) local dst_tree = dst_parser:parse()[1] local src_root = src_tree:root() local dst_root = dst_tree:root() + local src_role_index = roles.build_index(src_root, src_buf) + local dst_role_index = roles.build_index(dst_root, dst_buf) -- Benchmark the full matching pipeline local start_total = vim.loop.hrtime() -- Top-down match (includes preprocessing internally) local start_topdown = vim.loop.hrtime() - local mappings, src_info, dst_info = core.top_down_match(src_root, dst_root, src_buf, dst_buf) + local mappings, src_info, dst_info = core.top_down_match(src_root, dst_root, src_buf, dst_buf, { + adaptive_mode = true, + }) local topdown_time = (vim.loop.hrtime() - start_topdown) / 1e6 + local src_node_count = 0 + local dst_node_count = 0 + for _ in pairs(src_info) do + src_node_count = src_node_count + 1 + end + for _ in pairs(dst_info) do + dst_node_count = dst_node_count + 1 + end + local max_nodes = math.max(src_node_count, dst_node_count) + -- Bottom-up match local start_bottomup = vim.loop.hrtime() - mappings = core.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src_buf, dst_buf) + mappings = core.bottom_up_match(mappings, src_info, dst_info, src_root, dst_root, src_buf, dst_buf, { + src_role_index = src_role_index, + dst_role_index = dst_role_index, + adaptive_mode = true, + }) local bottomup_time = (vim.loop.hrtime() - start_bottomup) / 1e6 -- Recovery match (simple recovery) local start_recovery = vim.loop.hrtime() - mappings = core.recovery_match(src_root, dst_root, mappings, src_info, dst_info, src_buf, dst_buf) + mappings = core.recovery_match(src_root, dst_root, mappings, src_info, dst_info, src_buf, dst_buf, { + recovery_lcs_cell_limit = max_nodes >= 25000 and 1500 or 6000, + adaptive_mode = true, + }) local recovery_time = (vim.loop.hrtime() - start_recovery) / 1e6 -- Generate actions @@ -146,6 +168,9 @@ local function run_benchmark(name, num_functions, vars_per_function, changes) timings = true, src_buf = src_buf, dst_buf = dst_buf, + src_role_index = src_role_index, + dst_role_index = dst_role_index, + adaptive_mode = true, }) local actions_time = (vim.loop.hrtime() - start_actions) / 1e6 @@ -174,11 +199,15 @@ local function run_benchmark(name, num_functions, vars_per_function, changes) log(string.format(" Actions: %8.2f ms", actions_time)) if action_timings then log(string.format(" Prep: %8.2f ms", action_timings.precompute or 0)) + log(string.format(" Roles: %8.2f ms", action_timings.roles or 0)) log(string.format(" Updates: %8.2f ms", action_timings.updates or 0)) log(string.format(" Moves: %8.2f ms", action_timings.moves or 0)) log(string.format(" Renames: %8.2f ms", action_timings.renames or 0)) log(string.format(" Deletes: %8.2f ms", action_timings.deletes or 0)) log(string.format(" Inserts: %8.2f ms", action_timings.inserts or 0)) + log(string.format(" Semantic: %8.2f ms", action_timings.semantic or 0)) + log(string.format(" Analysis: %8.2f ms", action_timings.analysis or 0)) + log(string.format(" Suppress: %8.2f ms", action_timings.update_suppress or 0)) end log(string.format(" TOTAL: %8.2f ms", total_time)) From 3c9bac398796fdb36625128fc293934ef7a640b5 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Mon, 23 Feb 2026 10:20:14 +0530 Subject: [PATCH 43/48] feat(ui/filler): introduce semantic filler pipeline and move-aware placement scaffolding --- lua/diffmantic/ui/filler.lua | 1013 ++++++++++++++++++++++++++++++++++ 1 file changed, 1013 insertions(+) create mode 100644 lua/diffmantic/ui/filler.lua diff --git a/lua/diffmantic/ui/filler.lua b/lua/diffmantic/ui/filler.lua new file mode 100644 index 0000000..ed9c039 --- /dev/null +++ b/lua/diffmantic/ui/filler.lua @@ -0,0 +1,1013 @@ +local M = {} +local VIRT_LINE_LEN = 300 +local HL_ADD = "DiffmanticAddFiller" +local HL_DELETE = "DiffmanticDeleteFiller" +local HL_MOVE = "DiffmanticMoveFiller" + +local function make_virt_line(hl_group) + return { { string.rep("╱", VIRT_LINE_LEN), hl_group } } +end + +--- Return the number of lines a range spans (1-indexed count). +local function line_span(range) + if not range then + return 0 + end + local sr = range.start_row + local er = range.end_row + local ec = range.end_col + if sr == nil or er == nil or ec == nil then + return 0 + end + local count = er - sr + if ec > 0 then + count = count + 1 + end + if count <= 0 then + count = 1 + end + return count +end + +local function range_contains(outer, inner) + if not outer or not inner then + return false + end + if outer.start_row == nil or outer.end_row == nil or outer.start_col == nil or outer.end_col == nil then + return false + end + if inner.start_row == nil or inner.end_row == nil or inner.start_col == nil or inner.end_col == nil then + return false + end + if inner.start_row < outer.start_row or inner.end_row > outer.end_row then + return false + end + if inner.start_row == outer.start_row and inner.start_col < outer.start_col then + return false + end + if inner.end_row == outer.end_row and inner.end_col > outer.end_col then + return false + end + return true +end + +local function ranges_equal(a, b) + if not a or not b then + return false + end + return a.start_row == b.start_row + and a.start_col == b.start_col + and a.end_row == b.end_row + and a.end_col == b.end_col +end + +local function ranges_related(a, b) + return ranges_equal(a, b) or range_contains(a, b) or range_contains(b, a) +end + +local function hl_for_type(action_type) + if action_type == "insert" then + return HL_ADD + elseif action_type == "delete" then + return HL_DELETE + end + return HL_MOVE +end + +local function clamp_row(row, line_count) + row = tonumber(row) or 0 + if row < 0 then + return 0 + end + if row > line_count then + return line_count + end + return row +end + +local function count_trailing_blank_lines(buf, range) + if not buf or not range or range.end_row == nil then + return 0 + end + local line_count = vim.api.nvim_buf_line_count(buf) + if line_count <= 0 then + return 0 + end + local row = range.end_row + 1 + if row < 0 then + row = 0 + end + if row >= line_count then + return 0 + end + local lines = vim.api.nvim_buf_get_lines(buf, row, line_count, false) + local count = 0 + for _, line in ipairs(lines) do + if line:match("^%s*$") then + count = count + 1 + else + break + end + end + return count +end + +local function count_leading_blank_lines(buf, range) + if not buf or not range or range.start_row == nil then + return 0 + end + local row = range.start_row - 1 + if row < 0 then + return 0 + end + local count = 0 + while row >= 0 do + local line = vim.api.nvim_buf_get_lines(buf, row, row + 1, false)[1] or "" + if line:match("^%s*$") then + count = count + 1 + row = row - 1 + else + break + end + end + return count +end + +local function padding_mode_for_buf(buf) + if not buf then + return "default" + end + local ft = vim.bo[buf] and vim.bo[buf].filetype or "" + if ft == "python" then + return "python_bottom" + end + return "default" +end + +local function normalize_move_padding(leading, trailing, mode) + leading = math.max(0, tonumber(leading) or 0) + trailing = math.max(0, tonumber(trailing) or 0) + local gap = math.max(leading, trailing) + if gap <= 0 then + return 0, 0 + end + if leading > 0 and trailing == 0 then + local top = math.floor((gap + 1) / 2) + local bottom = gap - top + return top, bottom + end + if mode == "python_bottom" then + return 0, gap + end + local top = math.floor(gap / 2) + local bottom = gap - top + return top, bottom +end + +local function style_priority(style) + if not style then + return -1 + end + if style.transparent then + return 0 + end + local hl = style.hl_group + if hl == HL_ADD or hl == HL_DELETE then + return 2 + end + if hl == HL_MOVE then + return 1 + end + return 1 +end + +local function merge_style(existing, candidate) + if not existing then + return { + hl_group = candidate.hl_group, + transparent = candidate.transparent or false, + } + end + if candidate.transparent then + return existing + end + if existing.transparent then + return { + hl_group = candidate.hl_group, + transparent = false, + } + end + if style_priority(candidate) >= style_priority(existing) then + return { + hl_group = candidate.hl_group, + transparent = false, + } + end + return existing +end + +local function ensure_entry(side_rows, row) + local entry = side_rows[row] + if entry then + return entry + end + entry = { + row = row, + line_styles = {}, + } + side_rows[row] = entry + return entry +end + +local function find_first_group(line_styles, group) + for idx, style in ipairs(line_styles or {}) do + if style and not style.transparent and style.hl_group == group then + return idx + end + end + return nil +end + +local function find_last_group(line_styles, group) + local last = nil + for idx, style in ipairs(line_styles or {}) do + if style and not style.transparent and style.hl_group == group then + last = idx + end + end + return last +end + +local function add_filler_lines(side_rows, row, count, opts) + count = tonumber(count) or 0 + if count <= 0 then + return + end + opts = opts or {} + local entry = ensure_entry(side_rows, row) + local line_styles = entry.line_styles + local candidate + if opts.transparent then + candidate = { transparent = true } + else + candidate = { + hl_group = opts.hl_group or HL_MOVE, + transparent = false, + } + end + local offset = opts.offset + if offset == nil then + if opts.append_if_existing and not candidate.transparent then + local insert_at = #line_styles + 1 + if candidate.hl_group == HL_ADD then + local first_delete = find_first_group(line_styles, HL_DELETE) + if first_delete then + insert_at = first_delete + end + elseif candidate.hl_group == HL_DELETE then + local last_add = find_last_group(line_styles, HL_ADD) + if last_add then + insert_at = last_add + 1 + end + end + for i = 1, count do + table.insert(line_styles, insert_at + (i - 1), { + hl_group = candidate.hl_group, + transparent = false, + }) + end + return + end + offset = opts.append_if_existing and #line_styles or 0 + end + if offset < 0 then + offset = 0 + end + for i = 1, count do + local idx = offset + i + local last = #line_styles + if idx > last + 1 then + for gap = last + 1, idx - 1 do + line_styles[gap] = { transparent = true } + end + end + line_styles[idx] = merge_style(line_styles[idx], candidate) + end +end + +local function find_containing_move(moves, target, side_key) + for _, move in ipairs(moves) do + local container = move[side_key] + if range_contains(container, target) then + return move + end + end + return nil +end + +local function summed_contained_span(actions, action_type, side_key, container_range) + if not container_range then + return 0 + end + local total = 0 + for _, action in ipairs(actions or {}) do + local meta = action.metadata or {} + if action.type == action_type then + local target = action[side_key] + if range_contains(container_range, target) then + total = total + line_span(target) + end + end + end + return total +end + +local function uncovered_update_hunk_ranges(update_action, actions, kind, side_key) + local analysis = update_action and update_action.analysis or nil + local hunks = analysis and analysis.hunks or nil + if not hunks or #hunks == 0 then + return {} + end + local ranges = {} + for _, hunk in ipairs(hunks) do + if hunk.kind == kind then + local target = hunk[side_key] + if target then + local covered = false + for _, action in ipairs(actions or {}) do + local meta = action.metadata or {} + if action.type == kind and action[side_key] then + if ranges_related(action[side_key], target) then + covered = true + break + end + end + end + if not covered then + table.insert(ranges, target) + end + end + end + end + table.sort(ranges, function(a, b) + if a.start_row ~= b.start_row then + return a.start_row < b.start_row + end + return (a.start_col or 0) < (b.start_col or 0) + end) + return ranges +end + +local function merge_adjacent_ranges(ranges) + if not ranges or #ranges == 0 then + return {} + end + table.sort(ranges, function(a, b) + if a.start_row ~= b.start_row then + return a.start_row < b.start_row + end + return (a.start_col or 0) < (b.start_col or 0) + end) + local out = {} + local current = nil + for _, r in ipairs(ranges) do + if not current then + current = { + start_row = r.start_row, + start_col = r.start_col, + end_row = r.end_row, + end_col = r.end_col, + } + else + local touch_or_overlap = r.start_row <= (current.end_row + 1) + if touch_or_overlap then + if + r.end_row > current.end_row + or (r.end_row == current.end_row and (r.end_col or 0) > (current.end_col or 0)) + then + current.end_row = r.end_row + current.end_col = r.end_col + end + else + table.insert(out, current) + current = { + start_row = r.start_row, + start_col = r.start_col, + end_row = r.end_row, + end_col = r.end_col, + } + end + end + end + if current then + table.insert(out, current) + end + return out +end + +local function is_projection_context(action) + if not action or not action.src or not action.dst then + return false + end + local atype = action.type + if atype == "insert" or atype == "delete" or atype == "rename" or atype == "move" then + return false + end + return true +end + +local function is_valid_range(range) + if not range then + return false + end + if range.start_row == nil or range.start_col == nil then + return false + end + if range.end_row == nil or range.end_col == nil then + return false + end + return true +end + +local function to_range(info) + if not info then + return nil + end + local range = { + start_row = info.start_row, + start_col = info.start_col, + end_row = info.end_row, + end_col = info.end_col, + } + if not is_valid_range(range) then + return nil + end + return range +end + +local function build_projection_contexts_from_actions(actions) + local contexts = {} + for _, action in ipairs(actions or {}) do + if is_projection_context(action) then + table.insert(contexts, { + src = action.src, + dst = action.dst, + }) + end + end + return contexts +end + +local function build_projection_contexts_from_mappings(mappings, src_info, dst_info) + local contexts = {} + local seen = {} + for _, mapping in ipairs(mappings or {}) do + local src = src_info and src_info[mapping.src] or nil + local dst = dst_info and dst_info[mapping.dst] or nil + if src and dst and src.parent_id ~= nil and dst.parent_id ~= nil then + local src_node = src.node + local dst_node = dst.node + if src_node and dst_node and src_node:named() and dst_node:named() then + local src_range = to_range(src) + local dst_range = to_range(dst) + if src_range and dst_range then + local key = table.concat({ + src_range.start_row, + src_range.start_col, + src_range.end_row, + src_range.end_col, + "|", + dst_range.start_row, + dst_range.start_col, + dst_range.end_row, + dst_range.end_col, + }, ":") + if not seen[key] then + seen[key] = true + table.insert(contexts, { + src = src_range, + dst = dst_range, + }) + end + end + end + end + end + return contexts +end + +local function sort_projection_contexts(contexts) + table.sort(contexts, function(a, b) + local as = (a.src and a.src.start_row) or 0 + local bs = (b.src and b.src.start_row) or 0 + if as ~= bs then + return as < bs + end + local ae = (a.src and a.src.end_row) or as + local be = (b.src and b.src.end_row) or bs + return ae < be + end) + return contexts +end + +local function build_projection_contexts(actions, mappings, src_info, dst_info) + local contexts = build_projection_contexts_from_mappings(mappings, src_info, dst_info) + if #contexts == 0 then + contexts = build_projection_contexts_from_actions(actions) + else + local action_contexts = build_projection_contexts_from_actions(actions) + for _, ctx in ipairs(action_contexts) do + table.insert(contexts, ctx) + end + end + return sort_projection_contexts(contexts) +end + +local function contains_row(range, row) + if not range or range.start_row == nil or range.end_row == nil then + return false + end + return row >= range.start_row and row <= range.end_row +end + +local function best_containing_context(contexts, from_key, row) + local best = nil + local best_span = math.huge + for _, ctx in ipairs(contexts or {}) do + local r = ctx[from_key] + if contains_row(r, row) then + local span = line_span(r) + if span < best_span then + best = ctx + best_span = span + end + end + end + return best +end + +local function project_within_range(from_range, to_range, row) + if not from_range or not to_range then + return row + end + local from_start = from_range.start_row or row + local from_end = from_range.end_row or from_start + local to_start = to_range.start_row or row + local to_end = to_range.end_row or to_start + local from_span = math.max(1, (from_end - from_start) + 1) + local to_span = math.max(1, (to_end - to_start) + 1) + if from_span == 1 or to_span == 1 then + local rel = row - from_start + if rel < 0 then + rel = 0 + end + if rel > to_span - 1 then + rel = to_span - 1 + end + return to_start + rel + end + local rel = row - from_start + if rel < 0 then + rel = 0 + elseif rel > from_span - 1 then + rel = from_span - 1 + end + local ratio = rel / (from_span - 1) + return math.floor(to_start + (ratio * (to_span - 1)) + 0.5) +end + +local function neighbor_projected_row(row, contexts, from_key, to_key, opts) + opts = opts or {} + local preserve_gap = opts.preserve_gap or false + local to_buf = opts.to_buf + + local function prev_anchor(prev_to) + if not prev_to or prev_to.end_row == nil then + return nil + end + local anchor = prev_to.end_row + 1 + if preserve_gap and to_buf then + anchor = anchor + count_trailing_blank_lines(to_buf, prev_to) + end + return anchor + end + + local function next_anchor(next_to) + if not next_to or next_to.start_row == nil then + return nil + end + local anchor = next_to.start_row + if preserve_gap and to_buf then + anchor = anchor - count_leading_blank_lines(to_buf, next_to) + end + return anchor + end + + local prev_ctx = nil + local next_ctx = nil + for _, ctx in ipairs(contexts) do + local from = ctx[from_key] + if from then + if from.end_row and from.end_row < row then + if not prev_ctx or from.end_row > ((prev_ctx[from_key] and prev_ctx[from_key].end_row) or -1) then + prev_ctx = ctx + end + end + if from.start_row and from.start_row > row then + if + not next_ctx + or from.start_row < ((next_ctx[from_key] and next_ctx[from_key].start_row) or math.huge) + then + next_ctx = ctx + end + end + end + end + + local raw_row + if prev_ctx and next_ctx then + local prev_from = prev_ctx[from_key] + local next_from = next_ctx[from_key] + local prev_to = prev_ctx[to_key] + local next_to = next_ctx[to_key] + local prev_distance = row - (prev_from and prev_from.end_row or row) + local next_distance = (next_from and next_from.start_row or row) - row + if next_distance < prev_distance and next_to and next_to.start_row ~= nil then + raw_row = next_anchor(next_to) + elseif prev_to and prev_to.end_row ~= nil then + raw_row = prev_anchor(prev_to) + end + end + if raw_row == nil and prev_ctx and prev_ctx[to_key] and prev_ctx[to_key].end_row ~= nil then + raw_row = prev_anchor(prev_ctx[to_key]) + elseif raw_row == nil and next_ctx and next_ctx[to_key] and next_ctx[to_key].start_row ~= nil then + raw_row = next_anchor(next_ctx[to_key]) + elseif raw_row == nil then + raw_row = row + end + return raw_row +end + +local function project_row_from_contexts(row, contexts, from_key, to_key, line_count, opts) + opts = opts or {} + if row == nil then + return 0 + end + if not contexts or #contexts == 0 then + return clamp_row(row, line_count) + end + local containing = nil + if not opts.prefer_neighbors then + containing = best_containing_context(contexts, from_key, row) + end + local raw_row + if containing then + raw_row = project_within_range(containing[from_key], containing[to_key], row) + else + raw_row = neighbor_projected_row(row, contexts, from_key, to_key, opts) + end + return clamp_row(raw_row, line_count) +end + +local function move_overlaps_context(action, contexts) + local src = action and action.src + local dst = action and action.dst + if not src or not dst then + return false + end + for _, ctx in ipairs(contexts or {}) do + if ranges_related(ctx.src, src) and ranges_related(ctx.dst, dst) then + return true + end + end + return false +end + +local function filter_contexts_for_move_anchor(contexts, move_action) + if not move_action then + return contexts or {} + end + local out = {} + for _, ctx in ipairs(contexts or {}) do + local same_pair = ranges_related(ctx.src, move_action.src) and ranges_related(ctx.dst, move_action.dst) + if not same_pair then + table.insert(out, ctx) + end + end + return out +end + +local function rows_to_fillers(side_rows) + local out = {} + for _, entry in pairs(side_rows) do + table.insert(out, { + row = entry.row, + line_styles = entry.line_styles, + }) + end + table.sort(out, function(a, b) + return a.row < b.row + end) + return out +end + +local function total_virtual_lines(side_rows) + local total = 0 + for _, entry in pairs(side_rows) do + total = total + #(entry.line_styles or {}) + end + return total +end + +function M.compute(actions, src_buf, dst_buf, opts) + opts = opts or {} + local src_line_count = src_buf and vim.api.nvim_buf_line_count(src_buf) or 0 + local dst_line_count = dst_buf and vim.api.nvim_buf_line_count(dst_buf) or 0 + local src_rows = {} + local dst_rows = {} + local moves = {} + local updates = {} + local move_layout = {} + local src_move_events = {} + local dst_move_events = {} + local projection_contexts = build_projection_contexts(actions, opts.mappings, opts.src_info, opts.dst_info) + + for _, action in ipairs(actions or {}) do + if action.type == "move" and action.src and action.dst then + table.insert(moves, action) + table.insert(src_move_events, action) + table.insert(dst_move_events, action) + elseif (action.type == "update" or action.type == "rename") and action.src and action.dst then + table.insert(updates, action) + end + end + + table.sort(src_move_events, function(a, b) + return a.dst.start_row < b.dst.start_row + end) + table.sort(dst_move_events, function(a, b) + return a.src.start_row < b.src.start_row + end) + + local src_shift = 0 + local single_src_move = #src_move_events == 1 + local src_move_padding_mode = padding_mode_for_buf(dst_buf) + for _, action in ipairs(src_move_events) do + local leading = count_leading_blank_lines(dst_buf, action.dst) + local trailing = count_trailing_blank_lines(dst_buf, action.dst) + local raw_row + if single_src_move then + local move_anchor_contexts = filter_contexts_for_move_anchor(projection_contexts, action) + raw_row = project_row_from_contexts( + action.dst.start_row, + move_anchor_contexts, + "dst", + "src", + src_line_count, + { preserve_gap = false, to_buf = src_buf, prefer_neighbors = true } + ) + if leading > 0 and trailing > 0 then + raw_row = clamp_row(raw_row + math.min(leading, trailing), src_line_count) + end + else + raw_row = clamp_row(action.dst.start_row, src_line_count) + end + local row = raw_row - src_shift + if row < 0 then + row = 0 + end + local entry = src_rows[row] + local base_offset = entry and #(entry.line_styles or {}) or 0 + local span = line_span(action.dst) + leading, trailing = normalize_move_padding(leading, trailing, src_move_padding_mode) + add_filler_lines(src_rows, row, leading, { offset = base_offset, transparent = true }) + add_filler_lines(src_rows, row, span, { offset = base_offset + leading, hl_group = HL_MOVE }) + add_filler_lines(src_rows, row, trailing, { offset = base_offset + leading + span, transparent = true }) + src_shift = src_shift + leading + span + trailing + move_layout[action] = move_layout[action] or {} + move_layout[action].src_row = row + move_layout[action].src_base_offset = base_offset + leading + end + + local dst_move_padding_mode = padding_mode_for_buf(src_buf) + for _, action in ipairs(dst_move_events) do + local move_anchor_contexts = filter_contexts_for_move_anchor(projection_contexts, action) + local row = project_row_from_contexts( + action.src.start_row, + move_anchor_contexts, + "src", + "dst", + dst_line_count, + { preserve_gap = true, to_buf = dst_buf } + ) + local entry = dst_rows[row] + local base_offset = entry and #(entry.line_styles or {}) or 0 + local span = line_span(action.src) + local leading = count_leading_blank_lines(src_buf, action.src) + local trailing = count_trailing_blank_lines(src_buf, action.src) + leading, trailing = normalize_move_padding(leading, trailing, dst_move_padding_mode) + add_filler_lines(dst_rows, row, leading, { offset = base_offset, transparent = true }) + add_filler_lines(dst_rows, row, span, { offset = base_offset + leading, hl_group = HL_MOVE }) + add_filler_lines(dst_rows, row, trailing, { offset = base_offset + leading + span, transparent = true }) + move_layout[action] = move_layout[action] or {} + move_layout[action].dst_row = row + move_layout[action].dst_base_offset = base_offset + leading + end + + for _, action in ipairs(actions or {}) do + local meta = action.metadata or {} + if meta.render_as_change then + goto continue + end + local atype = action.type + local src = action.src + local dst = action.dst + + if atype == "insert" and dst then + local span = line_span(dst) + local nested_move = find_containing_move(moves, dst, "dst") + if nested_move then + local layout = move_layout[nested_move] or {} + local row = layout.src_row or clamp_row(nested_move.dst.start_row, src_line_count) + local base_offset = layout.src_base_offset or 0 + local offset = base_offset + (dst.start_row - nested_move.dst.start_row) + add_filler_lines(src_rows, row, span, { hl_group = HL_ADD, offset = offset }) + else + local row = project_row_from_contexts( + dst.start_row, + projection_contexts, + "dst", + "src", + src_line_count, + { prefer_neighbors = true } + ) + add_filler_lines(src_rows, row, span, { hl_group = HL_ADD, append_if_existing = true }) + end + elseif atype == "delete" and src then + local span = line_span(src) + local nested_move = find_containing_move(moves, src, "src") + if nested_move then + local layout = move_layout[nested_move] or {} + local row = layout.dst_row or clamp_row(nested_move.src.start_row, dst_line_count) + local base_offset = layout.dst_base_offset or 0 + local offset = base_offset + (src.start_row - nested_move.src.start_row) + add_filler_lines(dst_rows, row, span, { hl_group = HL_DELETE, offset = offset }) + else + local row = project_row_from_contexts( + src.start_row, + projection_contexts, + "src", + "dst", + dst_line_count, + { prefer_neighbors = true } + ) + add_filler_lines(dst_rows, row, span, { hl_group = HL_DELETE, append_if_existing = true }) + end + elseif (atype == "update" or atype == "rename") and src and dst then + local overlaps_move = move_overlaps_context(action, moves) + local src_lines = line_span(src) + local dst_lines = line_span(dst) + local uncovered_delete = + merge_adjacent_ranges(uncovered_update_hunk_ranges(action, actions, "delete", "src")) + local uncovered_insert = + merge_adjacent_ranges(uncovered_update_hunk_ranges(action, actions, "insert", "dst")) + local uncovered_delete_span = 0 + local uncovered_insert_span = 0 + + for _, hunk_src in ipairs(uncovered_delete) do + local span = line_span(hunk_src) + local nested_move = overlaps_move and find_containing_move(moves, hunk_src, "src") or nil + if nested_move then + local layout = move_layout[nested_move] or {} + local row = layout.dst_row or clamp_row(nested_move.src.start_row, dst_line_count) + local base_offset = layout.dst_base_offset or 0 + local offset = base_offset + (hunk_src.start_row - nested_move.src.start_row) + add_filler_lines(dst_rows, row, span, { hl_group = HL_DELETE, offset = offset }) + else + local row = project_row_from_contexts( + hunk_src.start_row, + projection_contexts, + "src", + "dst", + dst_line_count, + { prefer_neighbors = true } + ) + add_filler_lines(dst_rows, row, span, { hl_group = HL_DELETE, append_if_existing = true }) + end + uncovered_delete_span = uncovered_delete_span + span + end + + for _, hunk_dst in ipairs(uncovered_insert) do + local span = line_span(hunk_dst) + local nested_move = overlaps_move and find_containing_move(moves, hunk_dst, "dst") or nil + if nested_move then + local layout = move_layout[nested_move] or {} + local row = layout.src_row or clamp_row(nested_move.dst.start_row, src_line_count) + local base_offset = layout.src_base_offset or 0 + local offset = base_offset + (hunk_dst.start_row - nested_move.dst.start_row) + add_filler_lines(src_rows, row, span, { hl_group = HL_ADD, offset = offset }) + else + local row = project_row_from_contexts( + hunk_dst.start_row, + projection_contexts, + "dst", + "src", + src_line_count, + { prefer_neighbors = true } + ) + add_filler_lines(src_rows, row, span, { hl_group = HL_ADD, append_if_existing = true }) + end + uncovered_insert_span = uncovered_insert_span + span + end + + if overlaps_move then + goto continue + end + + if src_lines > dst_lines then + local delete_inside = summed_contained_span(actions, "delete", "src", src) + local remaining = (src_lines - dst_lines) - delete_inside - uncovered_delete_span + if remaining > 0 then + local row = clamp_row(dst.start_row + dst_lines, dst_line_count) + add_filler_lines(dst_rows, row, remaining, { hl_group = HL_DELETE, append_if_existing = true }) + end + elseif dst_lines > src_lines then + local insert_inside = summed_contained_span(actions, "insert", "dst", dst) + local remaining = (dst_lines - src_lines) - insert_inside - uncovered_insert_span + if remaining > 0 then + local row = clamp_row(src.start_row + src_lines, src_line_count) + add_filler_lines(src_rows, row, remaining, { hl_group = HL_ADD, append_if_existing = true }) + end + end + end + ::continue:: + end + + local src_total = total_virtual_lines(src_rows) + local dst_total = total_virtual_lines(dst_rows) + local src_visual = src_line_count + src_total + local dst_visual = dst_line_count + dst_total + if src_visual > dst_visual then + add_filler_lines(dst_rows, dst_line_count, src_visual - dst_visual, { + transparent = true, + append_if_existing = true, + }) + elseif dst_visual > src_visual then + add_filler_lines(src_rows, src_line_count, dst_visual - src_visual, { + transparent = true, + append_if_existing = true, + }) + end + + return rows_to_fillers(src_rows), rows_to_fillers(dst_rows) +end + +function M.apply(buf, ns, fillers) + if not buf or not ns or not fillers or #fillers == 0 then + return + end + local line_count = vim.api.nvim_buf_line_count(buf) + if line_count <= 0 then + return + end + for _, filler in ipairs(fillers) do + local row = clamp_row(filler.row or 0, line_count) + local virt_lines = {} + local line_styles = filler.line_styles + if line_styles and #line_styles > 0 then + for _, style in ipairs(line_styles) do + if style and style.transparent then + table.insert(virt_lines, {}) + else + local hl = (style and style.hl_group) or filler.hl_group or HL_MOVE + table.insert(virt_lines, make_virt_line(hl)) + end + end + else + local count = filler.count or 0 + local hl = filler.hl_group or HL_MOVE + for _ = 1, count do + table.insert(virt_lines, make_virt_line(hl)) + end + end + if #virt_lines > 0 then + pcall(vim.api.nvim_buf_set_extmark, buf, ns, row, 0, { + virt_lines = virt_lines, + virt_lines_above = true, + }) + end + end +end + +M._private = { + line_span = line_span, + range_contains = range_contains, + clamp_row = clamp_row, + count_trailing_blank_lines = count_trailing_blank_lines, + count_leading_blank_lines = count_leading_blank_lines, + summed_contained_span = summed_contained_span, + add_filler_lines = add_filler_lines, + total_virtual_lines = total_virtual_lines, +} + +return M From 3caecedf8c4f8e40bcc41cb005efd4eb54c44c47 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Mon, 23 Feb 2026 14:45:27 +0530 Subject: [PATCH 44/48] fix(ui/filler): ignore render_as_change update hunks when deriving uncovered insert/delete fillers --- lua/diffmantic/ui/filler.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/diffmantic/ui/filler.lua b/lua/diffmantic/ui/filler.lua index ed9c039..4a956f7 100644 --- a/lua/diffmantic/ui/filler.lua +++ b/lua/diffmantic/ui/filler.lua @@ -312,7 +312,7 @@ local function summed_contained_span(actions, action_type, side_key, container_r local total = 0 for _, action in ipairs(actions or {}) do local meta = action.metadata or {} - if action.type == action_type then + if action.type == action_type and not meta.render_as_change then local target = action[side_key] if range_contains(container_range, target) then total = total + line_span(target) @@ -330,13 +330,13 @@ local function uncovered_update_hunk_ranges(update_action, actions, kind, side_k end local ranges = {} for _, hunk in ipairs(hunks) do - if hunk.kind == kind then + if hunk.kind == kind and not hunk.render_as_change then local target = hunk[side_key] if target then local covered = false for _, action in ipairs(actions or {}) do local meta = action.metadata or {} - if action.type == kind and action[side_key] then + if action.type == kind and not meta.render_as_change and action[side_key] then if ranges_related(action[side_key], target) then covered = true break From 7657c42b41685c473e89ceb524297cd53ec36a3a Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Mon, 23 Feb 2026 18:10:52 +0530 Subject: [PATCH 45/48] fix(ui/renderer): render insert/delete as change only via update hunks (skip action-level duplicates) --- lua/diffmantic/ui/renderer.lua | 110 +++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 34 deletions(-) diff --git a/lua/diffmantic/ui/renderer.lua b/lua/diffmantic/ui/renderer.lua index 94de382..aaa65eb 100644 --- a/lua/diffmantic/ui/renderer.lua +++ b/lua/diffmantic/ui/renderer.lua @@ -1,4 +1,5 @@ local signs = require("diffmantic.ui.signs") +local filler = require("diffmantic.ui.filler") local M = {} @@ -190,11 +191,15 @@ local function move_to_arrow(from_line, to_line) return "⤴" end -function M.render(src_buf, dst_buf, actions, ns) +function M.render(src_buf, dst_buf, actions, ns, opts) local src_sign_rows = {} local dst_sign_rows = {} local src_move_ranges = {} local dst_move_ranges = {} + local src_fillers, dst_fillers = filler.compute(actions, src_buf, dst_buf, opts) + + filler.apply(src_buf, ns, src_fillers) + filler.apply(dst_buf, ns, dst_fillers) for _, action in ipairs(actions) do if action.type == "move" then @@ -207,7 +212,6 @@ function M.render(src_buf, dst_buf, actions, ns) end end - for _, action in ipairs(actions) do local base_style = TYPE_STYLE[action.type] if base_style then @@ -215,44 +219,45 @@ function M.render(src_buf, dst_buf, actions, ns) local dst = action.dst local meta = action.metadata or {} local style = base_style - if (action.type == "insert" or action.type == "delete") and meta.render_as_change then - style = TYPE_STYLE.update - end if action.type == "update" then local effective_hunks = effective_update_hunks(action, src_buf, dst_buf) if #effective_hunks == 0 then goto continue end - local rendered_hunk = false - for _, hunk in ipairs(effective_hunks) do - local hstyle = HUNK_STYLE[hunk.kind] or HUNK_STYLE.change - if hunk.render_as_change then - hstyle = HUNK_STYLE.change + local rendered_hunk = false + for _, hunk in ipairs(effective_hunks) do + local hstyle = HUNK_STYLE[hunk.kind] or HUNK_STYLE.change + if hunk.render_as_change then + hstyle = HUNK_STYLE.change + end + if hunk.src and hstyle.src_hl then + apply_span(src_buf, ns, hunk.src, hstyle.src_hl) + if hstyle.src_hl == "DiffmanticChange" and overlaps_any(hunk.src, src_move_ranges) then + -- Add a foreground/underline accent so updates stay visible over moved regions. + apply_span(src_buf, ns, hunk.src, "DiffmanticChangeAccent") end - if hunk.src and hstyle.src_hl then - apply_span(src_buf, ns, hunk.src, hstyle.src_hl) - if hstyle.src_hl == "DiffmanticChange" and overlaps_any(hunk.src, src_move_ranges) then - -- Add a foreground/underline accent so updates stay visible over moved regions. - apply_span(src_buf, ns, hunk.src, "DiffmanticChangeAccent") - end - apply_sign(src_buf, ns, hunk.src.start_row, hstyle.src_sign, hstyle.src_hl, src_sign_rows) - rendered_hunk = true - end - if hunk.dst and hstyle.dst_hl then - apply_span(dst_buf, ns, hunk.dst, hstyle.dst_hl) - if hstyle.dst_hl == "DiffmanticChange" and overlaps_any(hunk.dst, dst_move_ranges) then - -- Add a foreground/underline accent so updates stay visible over moved regions. - apply_span(dst_buf, ns, hunk.dst, "DiffmanticChangeAccent") - end - apply_sign(dst_buf, ns, hunk.dst.start_row, hstyle.dst_sign, hstyle.dst_hl, dst_sign_rows) - rendered_hunk = true - end + apply_sign(src_buf, ns, hunk.src.start_row, hstyle.src_sign, hstyle.src_hl, src_sign_rows) + rendered_hunk = true end - if not rendered_hunk then - goto continue + if hunk.dst and hstyle.dst_hl then + apply_span(dst_buf, ns, hunk.dst, hstyle.dst_hl) + if hstyle.dst_hl == "DiffmanticChange" and overlaps_any(hunk.dst, dst_move_ranges) then + -- Add a foreground/underline accent so updates stay visible over moved regions. + apply_span(dst_buf, ns, hunk.dst, "DiffmanticChangeAccent") + end + apply_sign(dst_buf, ns, hunk.dst.start_row, hstyle.dst_sign, hstyle.dst_hl, dst_sign_rows) + rendered_hunk = true end + end + if not rendered_hunk then + goto continue + end else + if (action.type == "insert" or action.type == "delete") and meta.render_as_change then + -- render_as_change inserts/deletes are represented by update hunks only. + goto continue + end if src then apply_span(src_buf, ns, src, style.hl) apply_sign(src_buf, ns, src.start_row, style.sign, style.hl, src_sign_rows) @@ -265,23 +270,60 @@ function M.render(src_buf, dst_buf, actions, ns) if action.type == "move" then if src and meta.to_line then local arrow = move_to_arrow(meta.from_line, meta.to_line) - apply_virt(src_buf, ns, src.start_row, src.end_col or 0, string.format(" %s moved to L%d", arrow, meta.to_line), "Comment", "eol") + apply_virt( + src_buf, + ns, + src.start_row, + src.end_col or 0, + string.format(" %s moved to L%d", arrow, meta.to_line), + "Comment", + "eol" + ) end if dst and meta.from_line then - apply_virt(dst_buf, ns, dst.start_row, dst.end_col or 0, string.format(" ⤶ from L%d", meta.from_line), "Comment", "eol") + apply_virt( + dst_buf, + ns, + dst.start_row, + dst.end_col or 0, + string.format(" ⤶ from L%d", meta.from_line), + "Comment", + "eol" + ) end elseif action.type == "rename" then if src and meta.new_name then - apply_virt(src_buf, ns, src.start_row, src.end_col or 0, " -> " .. meta.new_name, "Comment", "inline") + apply_virt( + src_buf, + ns, + src.start_row, + src.end_col or 0, + " -> " .. meta.new_name, + "Comment", + "inline" + ) end if dst and meta.old_name then - apply_virt(dst_buf, ns, dst.start_row, dst.end_col or 0, string.format(" (was %s)", meta.old_name), "Comment", "inline") + apply_virt( + dst_buf, + ns, + dst.start_row, + dst.end_col or 0, + string.format(" (was %s)", meta.old_name), + "Comment", + "inline" + ) end end end ::continue:: end end + + return { + src_fillers = src_fillers, + dst_fillers = dst_fillers, + } end return M From 219f92ab714adc514752153bee1743358c6b17e1 Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Tue, 24 Feb 2026 10:35:09 +0530 Subject: [PATCH 46/48] refactor(core/analysis): refine single-line change hunks to minimal changed spans --- lua/diffmantic/core/analysis.lua | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/lua/diffmantic/core/analysis.lua b/lua/diffmantic/core/analysis.lua index 894eb73..e751e96 100644 --- a/lua/diffmantic/core/analysis.lua +++ b/lua/diffmantic/core/analysis.lua @@ -400,6 +400,44 @@ local function merge_adjacent_hunks(hunks, src_buf, dst_buf) return merged end +local function refine_single_line_change_hunks(hunks, src_buf, dst_buf) + if not hunks or #hunks == 0 then + return hunks or {} + end + for _, hunk in ipairs(hunks) do + if + hunk.kind == "change" + and hunk.src + and hunk.dst + and hunk.src.start_row == hunk.src.end_row + and hunk.dst.start_row == hunk.dst.end_row + then + local src_text = text_for_range(src_buf, hunk.src) + local dst_text = text_for_range(dst_buf, hunk.dst) + if src_text and dst_text and src_text ~= dst_text then + local fragment = semantic.diff_fragment(src_text, dst_text) + if fragment then + local refined_src = make_range( + hunk.src.start_row, + hunk.src.start_col + math.max(fragment.old_start - 1, 0), + hunk.src.start_col + math.max(fragment.old_end, 0) + ) + local refined_dst = make_range( + hunk.dst.start_row, + hunk.dst.start_col + math.max(fragment.new_start - 1, 0), + hunk.dst.start_col + math.max(fragment.new_end, 0) + ) + if refined_src and refined_dst then + hunk.src = refined_src + hunk.dst = refined_dst + end + end + end + end + end + return hunks +end + local function fallback_hunks_from_diff(src_node, dst_node, src_buf, dst_buf, rename_pairs) local src_text = vim.treesitter.get_node_text(src_node, src_buf) local dst_text = vim.treesitter.get_node_text(dst_node, dst_buf) @@ -645,6 +683,8 @@ function M.enrich(actions, opts) end hunks = merge_adjacent_hunks(hunks, src_buf, dst_buf) + hunks = refine_single_line_change_hunks(hunks, src_buf, dst_buf) + hunks = collapse_identical_insert_delete_hunks(hunks, src_buf, dst_buf) action.analysis = { leaf_changes = normalized_leaf, From 96e4e3bafe9f72c4c3f70b3ea7e85abb1320298d Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Tue, 24 Feb 2026 17:50:31 +0530 Subject: [PATCH 47/48] fix(core/analysis): collapse identical insert/delete hunk pairs to remove false core diff noise --- lua/diffmantic/core/analysis.lua | 48 ++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/lua/diffmantic/core/analysis.lua b/lua/diffmantic/core/analysis.lua index e751e96..10bfb6e 100644 --- a/lua/diffmantic/core/analysis.lua +++ b/lua/diffmantic/core/analysis.lua @@ -438,6 +438,54 @@ local function refine_single_line_change_hunks(hunks, src_buf, dst_buf) return hunks end +local function collapse_identical_insert_delete_hunks(hunks, src_buf, dst_buf) + if not hunks or #hunks == 0 then + return hunks or {} + end + + local deletes_by_text = {} + local inserts_by_text = {} + for idx, hunk in ipairs(hunks) do + if hunk.kind == "delete" and hunk.src and hunk.src.start_row == hunk.src.end_row then + local text = text_for_range(src_buf, hunk.src) + if text and text ~= "" then + deletes_by_text[text] = deletes_by_text[text] or {} + table.insert(deletes_by_text[text], idx) + end + elseif hunk.kind == "insert" and hunk.dst and hunk.dst.start_row == hunk.dst.end_row then + local text = text_for_range(dst_buf, hunk.dst) + if text and text ~= "" then + inserts_by_text[text] = inserts_by_text[text] or {} + table.insert(inserts_by_text[text], idx) + end + end + end + + local drop = {} + for text, delete_idxs in pairs(deletes_by_text) do + local insert_idxs = inserts_by_text[text] + if insert_idxs and #insert_idxs > 0 then + local pair_count = math.min(#delete_idxs, #insert_idxs) + for i = 1, pair_count do + drop[delete_idxs[i]] = true + drop[insert_idxs[i]] = true + end + end + end + + if next(drop) == nil then + return hunks + end + + local filtered = {} + for idx, hunk in ipairs(hunks) do + if not drop[idx] then + table.insert(filtered, hunk) + end + end + return filtered +end + local function fallback_hunks_from_diff(src_node, dst_node, src_buf, dst_buf, rename_pairs) local src_text = vim.treesitter.get_node_text(src_node, src_buf) local dst_text = vim.treesitter.get_node_text(dst_node, dst_buf) From 74e103864174a2ae978e953e7c74ecc9b2deb08a Mon Sep 17 00:00:00 2001 From: HarshK97 Date: Tue, 24 Feb 2026 19:05:43 +0530 Subject: [PATCH 48/48] feat(ui): pass mappings to renderer and disable diagnostics in diff buffers --- lua/diffmantic/init.lua | 11 ++++++++--- lua/diffmantic/ui.lua | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lua/diffmantic/init.lua b/lua/diffmantic/init.lua index 75bafcb..27f26d5 100644 --- a/lua/diffmantic/init.lua +++ b/lua/diffmantic/init.lua @@ -50,7 +50,11 @@ local function setup_highlights() vim.api.nvim_set_hl(0, "DiffmanticAdd", { fg = add_sign_fg, bg = add_bg }) vim.api.nvim_set_hl(0, "DiffmanticDelete", { fg = delete_sign_fg, bg = delete_bg }) vim.api.nvim_set_hl(0, "DiffmanticChange", { fg = change_sign_fg, bg = change_bg }) - vim.api.nvim_set_hl(0, "DiffmanticChangeAccent", { fg = change_sign_fg, bg = "NONE", underline = true, bold = true }) + vim.api.nvim_set_hl( + 0, + "DiffmanticChangeAccent", + { fg = change_sign_fg, bg = "NONE", underline = true, bold = true } + ) vim.api.nvim_set_hl(0, "DiffmanticMove", { fg = move_sign_fg, bg = move_bg or "NONE" }) vim.api.nvim_set_hl(0, "DiffmanticRename", { fg = change_sign_fg, underline = true, bold = true, italic = true }) @@ -63,7 +67,6 @@ local function setup_highlights() vim.api.nvim_set_hl(0, "DiffmanticAddFiller", { fg = add_sign_fg, bg = add_bg }) vim.api.nvim_set_hl(0, "DiffmanticDeleteFiller", { fg = delete_sign_fg, bg = delete_bg }) vim.api.nvim_set_hl(0, "DiffmanticMoveFiller", { fg = move_sign_fg, bg = move_bg }) - end function M.setup(opts) @@ -162,10 +165,12 @@ function M.diff(args) dst_role_index = dst_role_index, adaptive_mode = true, }) + vim.diagnostic.enable(false, { bufnr = buf1 }) + vim.diagnostic.enable(false, { bufnr = buf2 }) -- debug_utils.print_actions(actions, buf1, buf2) -- debug_utils.print_mappings(mappings, src_info, dst_info, buf1, buf2) - ui.apply_highlights(buf1, buf2, actions) + ui.apply_highlights(buf1, buf2, actions, { mappings = mappings, src_info = src_info, dst_info = dst_info }) vim.api.nvim_win_set_cursor(win1, { 1, 0 }) vim.api.nvim_win_set_cursor(win2, { 1, 0 }) diff --git a/lua/diffmantic/ui.lua b/lua/diffmantic/ui.lua index a155533..c72b786 100644 --- a/lua/diffmantic/ui.lua +++ b/lua/diffmantic/ui.lua @@ -10,10 +10,10 @@ function M.clear_highlights(bufnr) end end -function M.apply_highlights(src_buf, dst_buf, actions) +function M.apply_highlights(src_buf, dst_buf, actions, opts) M.clear_highlights(src_buf) M.clear_highlights(dst_buf) - renderer.render(src_buf, dst_buf, actions, ns) + return renderer.render(src_buf, dst_buf, actions, ns, opts) end return M

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%0Acc|ca$pGGSB0BnKk zA}?*5fhywL6jFm=l*BKE0bICN^iAC_{UImw?RU4GUbsQns%d$TEF3eq`ggh zlslujUbEsV!w4+v3nnds4-$%xqXrPO>_Anvo4K)@|KB=lwhYWSKbK=G3z@bS{XY4I zlT%A(TfNzLYc$0yE0@$1z&__)Ok zJZEYqLjE?^NVpyp2S@$RLi@Y(A9GGYd&oi6{<-Kk_7Y#1V(}DqWWkHw9n^nIfh5D( znP~G?F6@1!7~?}C0SdhXp*gbPs0^-M@{QN=nDfZ+^1(k+F$}va_FD3T z#9&qH+yh=UuETx~*u7hE?FShGGPvxCk(4n3dTE`%5##k$?gHK0E5IFVBd`m?x7x`- z^z_@^py2pEPbfHKgvXfCmLbgnMeG}5ddKKI3jBigt$bOP**7>hE1oB*{&+y?v8%mObBc^`@kbleJD6L)D!ib#q%M_gGd5aoB76wXr|Q)<={ zz{WH1BTO2T0KU_JgjP~eSv6QE+L{8n^^AttKnTJ zvT<<^)16o48+|?~ZzJ5nI5!9SgPF8jG2K-6(UfsCE@(@lMu9@7F&w+FN9K-_4Z=9^Btxq6-q)Eo2&@3QHy$~<&!y?3ag%xF- ze=ZdT{#}ixSzcv*9gh&~HR(~@*C*H=Rfv#Opn|`#31FDUg3uKvjWvCgb2tUH16Yiv zwjVpeo$*ck5Rp={nV!2p7^%@QjgV*3EE?;1Hqk>#+O}YZ6q9BuikY;9UpYw_L4>RM zRk5HqXFinEyeICip5cUrT$xb#=9S#U$d5bCCBZ5^8MGW%jk0|QR?Xt#;J~@+N;sGT zH8QJWDR-YrL)Yifx0RDcm#KL4RsfUk{h!Q(Hy+vz|Cf0H3fCj)fO+tV>x%QzPrt$Z z7AeG(MmQ4Pg?B+K((VVz{mH-pz8WHrdUh{+E%Jg#Q^nw=ITva@a>vyXt70}0Z zM982rEx&!;y1Km(YPVt5Q{1TK&lhH}wyHzABeAq2&oIlT6p5<`2K`^YLA34Pe1na5 zjqMw=s=jEM(%v)0Wl#399UR=NnE-tK)98OaPEE~B4&!4#_0|7)5iW>w;a3_U_GAW< z3X&dgi*4cqKfrnxLngb#Q^IeNpVjk(pghp=k%~-ojJs7rB0jg!^m%&gFm0>_t#s*1 zmkh0UtwF^9uU4JX`3{i-x$t2I9cQIt2)bw~Z2HoB7{0@#^SxDZD!#o)$IYR~(; ztBkjIBK(LAi2ksjBbJ#S!v`JgbqeP+=W%8_!5qo@O3sBr>F47gR+ljkKP>62Uw7|^ zAQT?`dFTT241i(mLBDkTrLv>dm9IO=+IeVjiVhd)jvax|=AxQ5TomEQ?^7 zPPK=QHfv~!vP{ZUhe_gL!~Xk&Guo_Xn{LX76mit!DNm$uxz0o#1hF5}CY^%Q98Iwv zW-HDcSWH)mCLf|2GQ&;*v8cT$Y;CE$Jp}mYu9V*CAG_mG+i=q+voo0iY|A~pD`zg4 z0_?=-0&?!l@9|(k@GL1TvZMmN#c;tQK3sqn3%?RJ}`=Y{aHa;!!p( zLcP#0!JV-Hjo-Tq3J-rVTe_Oesz?}H<-aYFpaN&H;RfYAHALHXGW1ZC3 zQ%Cy({`0J=nY&iIoxW`X@8~QA(>-C2hLMUoL~*vsL9TwpPZqLY-XH_^Xb63z)|O33 z$j4+Ibe#`T=1v+XgGu96UJq;mB}uP~IzQJmP*-Ub4uICzj>I2VtNc5tVd3L*IX5j5 z@-*lEgcbm(JTr>)>^`l6B?F#EBFq9t*2h2Z=h?5rs;a+Pew;)|V0l~cw0i%I^epGv z5gJc28PiUQFGOVs=h;hCGHE6CNG6CQtxrJ=BU!Q8!@R6sGv|;*Jd`R6d3%ws^)TI4 z#P_bfT?{{@5RDoL6VLh1?a^{g`j59v-Ld7)Dt$j7+oe!bQ4{{+OjJT?Z|1YeC9R@laStoA8Eu^3haz6xCgiXmbI)~(CItEu)>I=|6{$&=| zs0{VJ^KFeRa30GRgdmxR)VpsEh6(0@mH9b1Ffbd=+Qy=B<39& C`m?G7XhwK3(u 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 NK!z3uHF|E3{|3f&V$T2o literal 123424 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