Skip to content

Commit a416ed2

Browse files
committed
refactor(diff): remove diff-match-patch dependency
Remove the bundled diff-match-patch Lua port and replace its usage in the diff utility with a simpler, line-based matching and replacement algorithm. This reduces code complexity and removes a large vendored file. The new approach uses context-aware line matching to apply hunks, which should be sufficient for the plugin's needs. Closes #1490 Signed-off-by: Tomas Slusny <[email protected]>
1 parent ce48533 commit a416ed2

File tree

4 files changed

+139
-2131
lines changed

4 files changed

+139
-2131
lines changed

README.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -523,14 +523,6 @@ make test
523523

524524
See [CONTRIBUTING.md](/CONTRIBUTING.md) for detailed guidelines.
525525

526-
# Acknowledgments
527-
528-
## diff-match-patch
529-
530-
CopilotChat.nvim includes [diff-match-patch (Lua port)](https://github.com/google/diff-match-patch) for diffing and patching functionality.
531-
Copyright 2018 The diff-match-patch Authors.
532-
Licensed under the Apache License 2.0.
533-
534526
# Contributors
535527

536528
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):

lua/CopilotChat/utils/diff.lua

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -40,55 +40,81 @@ local function parse_hunks(diff_text)
4040
return hunks
4141
end
4242

43-
--- Apply a single hunk to content, with fallback/context logic
43+
--- Try to match old_snippet in lines starting at approximate start_line
44+
---@param lines table
45+
---@param old_snippet table
46+
---@param approx_start number
47+
---@param search_range number
48+
---@return number? matched_start
49+
local function find_best_match(lines, old_snippet, approx_start, search_range)
50+
local best_idx, best_score = nil, -1
51+
local old_len = #old_snippet
52+
53+
if old_len == 0 then
54+
return approx_start
55+
end
56+
57+
local min_start = math.max(1, approx_start - search_range)
58+
local max_start = math.min(#lines - old_len + 1, approx_start + search_range)
59+
60+
for start_idx = min_start, max_start do
61+
local score = 0
62+
for i = 1, old_len do
63+
if vim.trim(lines[start_idx + i - 1] or '') == vim.trim(old_snippet[i] or '') then
64+
score = score + 1
65+
end
66+
end
67+
68+
if score > best_score then
69+
best_score = score
70+
best_idx = start_idx
71+
end
72+
73+
if score == old_len then
74+
return best_idx
75+
end
76+
end
77+
78+
if best_score >= math.ceil(old_len * 0.8) then
79+
return best_idx
80+
end
81+
82+
return nil
83+
end
84+
85+
--- Apply a single hunk to content
4486
---@param hunk table
4587
---@param content string
4688
---@return string patched_content, boolean applied_cleanly
4789
local function apply_hunk(hunk, content)
48-
local dmp = require('CopilotChat.vendor.diff_match_patch')
49-
local patch = dmp.patch_make(table.concat(hunk.old_snippet, '\n'), table.concat(hunk.new_snippet, '\n'))
50-
51-
-- First try: direct application
52-
local patched, results = dmp.patch_apply(patch, content)
53-
if not vim.tbl_contains(results, false) then
54-
return patched, true
55-
end
56-
57-
-- Fallback: direct replacement
5890
local lines = vim.split(content, '\n')
59-
local insert_idx = hunk.start_old or 1
60-
if not hunk.start_old then
61-
-- No starting point, try to find best match
62-
local match_idx, best_score = nil, -1
63-
local context_lines = vim.tbl_filter(function(line)
64-
return line and line ~= ''
65-
end, hunk.old_snippet)
66-
local context_len = #context_lines
67-
if context_len > 0 then
68-
for i = 1, #lines - context_len + 1 do
69-
local score = 0
70-
for j = 1, context_len do
71-
if vim.trim(lines[i + j - 1] or '') == vim.trim(context_lines[j] or '') then
72-
score = score + 1
73-
end
74-
end
75-
if score > best_score then
76-
best_score = score
77-
match_idx = i
78-
end
79-
end
91+
local start_idx = hunk.start_old
92+
93+
-- If we have a start line hint, try to find best match within +/- 2 lines
94+
if start_idx and start_idx > 0 and start_idx <= #lines then
95+
local match_idx = find_best_match(lines, hunk.old_snippet, start_idx, 2)
96+
if match_idx then
97+
start_idx = match_idx
8098
end
81-
if best_score > 0 and match_idx then
82-
insert_idx = match_idx
99+
else
100+
-- No valid start line, search for best match in whole content
101+
local match_idx = find_best_match(lines, hunk.old_snippet, 1, #lines)
102+
if match_idx then
103+
start_idx = match_idx
104+
else
105+
start_idx = 1
83106
end
84107
end
85108

86-
local start_idx = insert_idx
87-
local end_idx = insert_idx + #hunk.old_snippet
109+
-- Replace old lines with new lines
110+
local end_idx = start_idx + #hunk.old_snippet - 1
88111
local new_lines = vim.list_slice(lines, 1, start_idx - 1)
89112
vim.list_extend(new_lines, hunk.new_snippet)
90113
vim.list_extend(new_lines, lines, end_idx + 1, #lines)
91-
return table.concat(new_lines, '\n'), false
114+
115+
-- Check if we matched exactly at the hinted position
116+
local applied_cleanly = find_best_match(lines, hunk.old_snippet, hunk.start_old or start_idx, 0) == start_idx
117+
return table.concat(new_lines, '\n'), applied_cleanly
92118
end
93119

94120
--- Apply unified diff to a table of lines and return new lines
@@ -129,7 +155,7 @@ function M.get_diff(block, lines)
129155
return block.content, content
130156
end
131157

132-
local patched_lines = vim.split(block.content, '\n')
158+
local patched_lines = vim.split(block.content, '\n', { trimempty = true })
133159
local start_idx = block.header.start_line
134160
local end_idx = block.header.end_line
135161
local original_lines = lines

0 commit comments

Comments
 (0)