forked from CopilotC-Nvim/CopilotChat.nvim
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdiff.lua
More file actions
177 lines (163 loc) · 5.66 KB
/
diff.lua
File metadata and controls
177 lines (163 loc) · 5.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
local log = require('plenary.log')
local M = {}
--- Parse unified diff hunks from diff text
---@param diff_text string
---@return table hunks
local function parse_hunks(diff_text)
local hunks = {}
local current_hunk = nil
for _, line in ipairs(vim.split(diff_text, '\n')) do
if line:match('^@@') then
if current_hunk then
table.insert(hunks, current_hunk)
end
local start_old, len_old, start_new, len_new = line:match('@@%s%-(%d+),?(%d*)%s%+(%d+),?(%d*)%s@@')
current_hunk = {
start_old = tonumber(start_old),
len_old = tonumber(len_old) or 1,
start_new = tonumber(start_new),
len_new = tonumber(len_new) or 1,
old_snippet = {},
new_snippet = {},
}
elseif current_hunk then
local prefix, rest = line:sub(1, 1), tostring(line:sub(2))
if prefix == '-' then
table.insert(current_hunk.old_snippet, rest)
elseif prefix == '+' then
table.insert(current_hunk.new_snippet, rest)
elseif prefix == ' ' then
table.insert(current_hunk.old_snippet, rest)
table.insert(current_hunk.new_snippet, rest)
end
end
end
if current_hunk then
table.insert(hunks, current_hunk)
end
return hunks
end
--- Apply a single hunk to content, with fallback/context logic
---@param hunk table
---@param content string
---@return string patched_content, boolean applied_cleanly
local function apply_hunk(hunk, content)
local dmp = require('CopilotChat.vendor.diff_match_patch')
local patch = dmp.patch_make(table.concat(hunk.old_snippet, '\n'), table.concat(hunk.new_snippet, '\n'))
-- First try: direct application
local patched, results = dmp.patch_apply(patch, content)
if not vim.tbl_contains(results, false) then
return patched, true
end
-- Fallback: direct replacement
local lines = vim.split(content, '\n')
local insert_idx = hunk.start_old or 1
if not hunk.start_old then
-- No starting point, try to find best match
local match_idx, best_score = nil, -1
local context_lines = vim.tbl_filter(function(line)
return line and line ~= ''
end, hunk.old_snippet)
local context_len = #context_lines
if context_len > 0 then
for i = 1, #lines - context_len + 1 do
local score = 0
for j = 1, context_len do
if vim.trim(lines[i + j - 1] or '') == vim.trim(context_lines[j] or '') then
score = score + 1
end
end
if score > best_score then
best_score = score
match_idx = i
end
end
end
if best_score > 0 and match_idx then
insert_idx = match_idx
end
end
local start_idx = insert_idx
local end_idx = insert_idx + #hunk.old_snippet
local new_lines = vim.list_slice(lines, 1, start_idx - 1)
vim.list_extend(new_lines, hunk.new_snippet)
vim.list_extend(new_lines, lines, end_idx + 1, #lines)
return table.concat(new_lines, '\n'), false
end
--- Apply unified diff to a table of lines and return new lines
---@param diff_text string
---@param original_content string
---@return table<string>, boolean, integer, integer
function M.apply_unified_diff(diff_text, original_content)
local hunks = parse_hunks(diff_text)
local new_content = original_content
local applied = false
for _, hunk in ipairs(hunks) do
local patched, ok = apply_hunk(hunk, new_content)
new_content = patched
applied = applied or ok
end
local original_lines = vim.split(original_content, '\n', { trimempty = true })
local new_lines = vim.split(new_content, '\n', { trimempty = true })
local first, last
local max_len = math.max(#original_lines, #new_lines)
for i = 1, max_len do
if original_lines[i] ~= new_lines[i] then
if not first then
first = i
end
last = i
end
end
return new_lines, applied, first, last
end
--- Get diff from block content and buffer lines
---@param block CopilotChat.ui.chat.Block Block containing diff info
---@param lines table table of lines
---@return string diff, string content
function M.get_diff(block, lines)
local content = table.concat(lines, '\n')
if block.header.filetype == 'diff' then
return block.content, content
end
local patched_lines = vim.split(block.content, '\n')
local start_idx = block.header.start_line
local end_idx = block.header.end_line
local original_lines = lines
if start_idx and end_idx then
local new_lines = vim.list_slice(original_lines, 1, start_idx - 1)
vim.list_extend(new_lines, patched_lines)
vim.list_extend(new_lines, original_lines, end_idx + 1, #original_lines)
patched_lines = new_lines
end
return tostring(
vim.diff(
table.concat(original_lines, '\n'),
table.concat(patched_lines, '\n'),
{ algorithm = 'myers', ctxlen = 10, interhunkctxlen = 10, ignore_whitespace_change = true }
)
),
content
end
--- Apply a diff (unified or indices) to buffer lines
---@param block CopilotChat.ui.chat.Block Block containing diff info
---@param lines table table of lines
---@return table new_lines
function M.apply_diff(block, lines)
local diff, content = M.get_diff(block, lines)
local new_lines, applied, _, _ = M.apply_unified_diff(diff, content)
if not applied then
log.debug('Diff for ' .. block.header.filename .. ' failed to apply cleanly for:\n' .. diff)
end
return new_lines
end
--- Get changed region for diff (unified or indices)
---@param block CopilotChat.ui.chat.Block Block containing diff info
---@param lines table table of lines
---@return number? first, number? last
function M.get_diff_region(block, lines)
local diff, content = M.get_diff(block, lines)
local _, _, first, last = M.apply_unified_diff(diff, content)
return first, last
end
return M