Skip to content

Commit b18830f

Browse files
committed
Add ApplyHunkLoose method with comprehensive test coverage
Implemented a new loose hunk application algorithm for patch files that tolerates minor formatting variations and provides more flexible matching. The implementation includes: * Added EditableMockTextArea test fixture to support in-memory text modifications during testing * Implemented ApplyHunkLoose() method that searches for matching context/deletion lines as anchors and applies hunks flexibly without strict line number validation * Enhanced patch parsing to find the first non-addition line as an anchor point for matching * Added comprehensive test suite covering simple matches, multiple context lines, additions/deletions, error cases, edge cases (empty lines, end-of-file), and complex real-world scenarios * Updated ParseUnifiedPatchLoose documentation with detailed behavior description * Modified ApplyPatchLoose to use the new ApplyHunkLoose method instead of strict ApplyHunk * Fixed MockTextArea::PositionFromLine to return non-zero values for proper position calculation Affected areas: * Tests/CodeliteTest/test_patch_applier.cpp * Plugin/Diff/clPatchApplier.hpp * Plugin/Diff/clPatchApplier.cpp **Generated by CodeLite** Signed-off-by: Eran Ifrah <eran@codelite.org>
1 parent c6cd55f commit b18830f

File tree

3 files changed

+642
-3
lines changed

3 files changed

+642
-3
lines changed

Plugin/Diff/clPatchApplier.cpp

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,157 @@ PatchApplier::ApplyPatchStrict(const wxString& filePath, const wxString& patchCo
245245
}
246246
}
247247

248+
clStatusOr<int> PatchApplier::ApplyHunkLoose(ITextArea* ctrl, const wxArrayString& lines, int start_line)
249+
{
250+
if (!ctrl || lines.IsEmpty()) {
251+
return StatusInvalidArgument("Empty hunk");
252+
}
253+
254+
int totalLines = ctrl->GetLineCount();
255+
if (start_line < 0 || start_line > totalLines) {
256+
return StatusInvalidArgument("Not enough lines");
257+
}
258+
259+
// Find the first non-addition line (context or deletion) to use as an anchor
260+
wxString anchor_line;
261+
int anchor_index = -1;
262+
for (int i = 0; i < static_cast<int>(lines.GetCount()); ++i) {
263+
if (lines[i].empty()) {
264+
continue;
265+
}
266+
wxChar prefix = lines[i][0];
267+
if (prefix == ' ' || prefix == '-') {
268+
anchor_line = lines[i].Mid(1).Trim();
269+
anchor_index = i;
270+
break;
271+
}
272+
}
273+
274+
// If we only have additions, we can't find a match
275+
if (anchor_index == -1) {
276+
return StatusInvalidArgument("Hunk has no context or deletion lines to match");
277+
}
278+
279+
// Validate the hunk + prepare list of lines to search in the editor (removed or context lines).
280+
wxArrayString lines_to_find;
281+
for (auto l : lines) {
282+
if (l.empty()) {
283+
return StatusInvalidArgument("Invalid hunk");
284+
}
285+
286+
wxChar prefix = l[0];
287+
switch (prefix) {
288+
case ' ':
289+
case '-':
290+
lines_to_find.push_back(l.Mid(1).Trim());
291+
break;
292+
case '+':
293+
break;
294+
default:
295+
return StatusInvalidArgument("Hunk lines must start with ' ', '-' or '+'");
296+
}
297+
}
298+
299+
// Search for the anchor line starting from start_line
300+
int hunk_found_line = wxNOT_FOUND;
301+
302+
// Count how many context/deletion lines come before the anchor
303+
int context_lines_before_anchor = 0;
304+
for (int i = 0; i < anchor_index; ++i) {
305+
if (lines[i][0] == ' ' || lines[i][0] == '-') {
306+
context_lines_before_anchor++;
307+
}
308+
}
309+
310+
for (int current_editor_line_number = start_line; current_editor_line_number < totalLines;
311+
++current_editor_line_number) {
312+
wxString editor_line = ctrl->GetLine(current_editor_line_number);
313+
editor_line.Trim();
314+
315+
if (editor_line != anchor_line) {
316+
continue;
317+
}
318+
319+
// Found a potential match, now verify the rest of the hunk matches
320+
bool hunk_found = true;
321+
int hunk_line_index = 0; // Index into lines_to_find
322+
323+
for (int hunk_line_number = anchor_index; hunk_line_number < static_cast<int>(lines.GetCount());
324+
++hunk_line_number) {
325+
wxString hunk_line = lines[hunk_line_number];
326+
wxChar prefix = hunk_line[0];
327+
328+
// Skip addition lines when matching
329+
if (prefix == '+') {
330+
continue;
331+
}
332+
333+
// For context and deletion lines, verify they match the editor
334+
int editor_line_to_check = current_editor_line_number + (hunk_line_index - context_lines_before_anchor);
335+
336+
if (editor_line_to_check < 0 || editor_line_to_check >= totalLines) {
337+
hunk_found = false;
338+
break;
339+
}
340+
341+
wxString editor_content = ctrl->GetLine(editor_line_to_check);
342+
editor_content.Trim();
343+
wxString expected_content = lines_to_find[hunk_line_index];
344+
345+
if (editor_content != expected_content) {
346+
hunk_found = false;
347+
break;
348+
}
349+
350+
hunk_line_index++;
351+
}
352+
353+
if (hunk_found) {
354+
hunk_found_line = current_editor_line_number - context_lines_before_anchor;
355+
break;
356+
}
357+
}
358+
359+
if (hunk_found_line == wxNOT_FOUND) {
360+
return StatusNotFound("Could not find matching lines in editor");
361+
}
362+
363+
// Apply the hunk starting at the found location
364+
int currentLine = hunk_found_line;
365+
for (size_t i = 0; i < lines.GetCount(); ++i) {
366+
wxString line = lines[i];
367+
if (line.IsEmpty()) {
368+
continue;
369+
}
370+
371+
wxChar prefix = line[0];
372+
wxString content = line.Mid(1);
373+
374+
if (prefix == ' ') {
375+
// Context line - just move to next line
376+
currentLine++;
377+
} else if (prefix == '-') {
378+
// Delete line
379+
int lineStart = ctrl->PositionFromLine(currentLine);
380+
int lineEnd = ctrl->PositionFromLine(currentLine + 1);
381+
ctrl->DeleteRange(lineStart, lineEnd - lineStart);
382+
// Don't increment currentLine since we deleted the line
383+
} else if (prefix == '+') {
384+
// Add line
385+
int pos = ctrl->PositionFromLine(currentLine);
386+
// Ensure content has a line ending
387+
if (!content.EndsWith("\n") && !content.EndsWith("\r\n")) {
388+
content += "\n";
389+
}
390+
ctrl->InsertText(pos, content);
391+
currentLine++;
392+
}
393+
}
394+
395+
// Return the line where the next hunk can be applied
396+
return currentLine;
397+
}
398+
248399
clStatusOr<int> PatchApplier::ApplyHunk(ITextArea* ctrl, const wxArrayString& lines, int start_line)
249400
{
250401
if (!ctrl || lines.IsEmpty()) {
@@ -389,7 +540,7 @@ PatchResult PatchApplier::ApplyPatchLoose(const wxString& filePath, const wxStri
389540
stc->BeginUndoAction();
390541
StcViewArea ctrl{stc};
391542
for (const auto& hunk : patch.hunks) {
392-
auto res = ApplyHunk(&ctrl, hunk.lines, start_line);
543+
auto res = ApplyHunkLoose(&ctrl, hunk.lines, start_line);
393544
if (!res.ok()) {
394545
// Revert the changes
395546
stc->EndUndoAction();

Plugin/Diff/clPatchApplier.hpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,25 @@ class WXDLLIMPEXP_SDK PatchApplier
114114
*/
115115
static UnifiedPatch ParseUnifiedPatch(const wxString& patchContent);
116116

117+
/**
118+
* Parses a unified patch format string with lenient line handling.
119+
*
120+
* This function performs a loose parse of unified patch content, extracting hunks and their
121+
* constituent lines. It is tolerant of minor formatting variations and does not enforce strict
122+
* validation of line counts or patch structure. Hunk headers are recognized by the @@ marker
123+
* and may or may not include explicit line number information; missing line numbers default to
124+
* zero. Lines within hunks are classified by their leading character (space for context, plus
125+
* for additions, minus for deletions) and stored as-is, including the leading character. Empty
126+
* lines are skipped within hunks, and unrecognized line types are silently ignored.
127+
*
128+
* Parameters:
129+
* patchContent - A wxString containing the complete unified patch content to be parsed,
130+
* with lines separated by newline characters.
131+
*
132+
* Return value:
133+
* An UnifiedPatch structure containing the parsed hunks and their associated line data.
134+
* If no valid hunk headers are found, the patch will be empty.
135+
*/
117136
static UnifiedPatch ParseUnifiedPatchLoose(const wxString& patchContent);
118137

119138
/**
@@ -159,4 +178,5 @@ class WXDLLIMPEXP_SDK PatchApplier
159178
* @return The line number where the next hunk can be applied, or wxNOT_FOUND if the hunk failed to apply.
160179
*/
161180
static clStatusOr<int> ApplyHunk(ITextArea* ctrl, const wxArrayString& lines, int start_line);
181+
static clStatusOr<int> ApplyHunkLoose(ITextArea* ctrl, const wxArrayString& lines, int start_line);
162182
};

0 commit comments

Comments
 (0)