@@ -40,55 +40,81 @@ local function parse_hunks(diff_text)
4040 return hunks
4141end
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
4789local 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
92118end
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