Skip to content

Commit d34aa9e

Browse files
authored
refactor(diff): remove diff-match-patch dependency (#1491)
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 d34aa9e

File tree

4 files changed

+156
-2139
lines changed

4 files changed

+156
-2139
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: 81 additions & 46 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
@@ -104,16 +130,25 @@ function M.apply_unified_diff(diff_text, original_content)
104130
new_content = patched
105131
applied = applied or ok
106132
end
107-
local original_lines = vim.split(original_content, '\n', { trimempty = true })
133+
108134
local new_lines = vim.split(new_content, '\n', { trimempty = true })
135+
local hunks = vim.diff(
136+
original_content,
137+
new_content,
138+
{ algorithm = 'myers', ctxlen = 10, interhunkctxlen = 10, ignore_whitespace_change = true, result_type = 'indices' }
139+
)
140+
if not hunks or #hunks == 0 then
141+
return new_lines, applied, nil, nil
142+
end
109143
local first, last
110-
local max_len = math.max(#original_lines, #new_lines)
111-
for i = 1, max_len do
112-
if original_lines[i] ~= new_lines[i] then
113-
if not first then
114-
first = i
115-
end
116-
last = i
144+
for _, hunk in ipairs(hunks) do
145+
local hunk_start = hunk[1]
146+
local hunk_end = hunk[1] + hunk[2] - 1
147+
if not first or hunk_start < first then
148+
first = hunk_start
149+
end
150+
if not last or hunk_end > last then
151+
last = hunk_end
117152
end
118153
end
119154
return new_lines, applied, first, last
@@ -129,7 +164,7 @@ function M.get_diff(block, lines)
129164
return block.content, content
130165
end
131166

132-
local patched_lines = vim.split(block.content, '\n')
167+
local patched_lines = vim.split(block.content, '\n', { trimempty = true })
133168
local start_idx = block.header.start_line
134169
local end_idx = block.header.end_line
135170
local original_lines = lines

0 commit comments

Comments
 (0)