diff --git a/src/utils/diff-utils.ts b/src/utils/diff-utils.ts index 0ff2a406..da92c5a3 100644 --- a/src/utils/diff-utils.ts +++ b/src/utils/diff-utils.ts @@ -39,63 +39,127 @@ export function parsePatch(patch: string): Hunk[] { * from the original content that are changed. * @param diff Diff expressed in GNU diff format. * @returns Map + * ToDO: Need to Handle Distant Changes with some proximity threshold number */ export function parseAllHunks(diff: string): Map { const hunksByFile: Map = new Map(); parseDiff(diff).forEach(file => { const filename = file.to ? file.to : file.from!; - const chunks = file.chunks.map(chunk => { - let oldStart = chunk.oldStart; - let newStart = chunk.newStart; - let normalLines = 0; - let changeSeen = false; - const newLines: string[] = []; - let previousLine: string | null = null; - let nextLine: string | null = null; - + const hunks: Hunk[] = []; + file.chunks.forEach(chunk => { + // Track different types of lines + const allAddedLines: {ln: number; content: string}[] = []; + const allDeletedLines: {ln: number; content: string}[] = []; + const allNormalLines: {ln: number; lnNew: number; content: string}[] = []; + // First pass: collect all changes by type chunk.changes.forEach(change => { - // strip off leading '+', '-', or ' ' and trailing carriage return + if (change.content.includes('No newline at end of file')) { + return; + } const content = change.content.substring(1).replace(/[\n\r]+$/g, ''); - if (change.type === 'normal') { - normalLines++; - if (changeSeen) { - if (nextLine === null) { - nextLine = content; - } - } else { - previousLine = content; - } - } else { - if (change.type === 'add') { - // strip off leading '+' and trailing carriage return - newLines.push(content); - } - if (!changeSeen) { - oldStart += normalLines; - newStart += normalLines; - changeSeen = true; - } + + if (change.type === 'add') { + allAddedLines.push({ + ln: (change as any).ln || 0, + content: content, + }); + } else if (change.type === 'del') { + allDeletedLines.push({ + ln: (change as any).ln || 0, + content: content, + }); + } else if (change.type === 'normal') { + allNormalLines.push({ + ln: (change as any).ln1 || 0, + lnNew: (change as any).ln2 || 0, // New file line number + content: content, + }); } }); - const newEnd = newStart + chunk.newLines - normalLines - 1; - const oldEnd = oldStart + chunk.oldLines - normalLines - 1; - let hunk: Hunk = { - oldStart: oldStart, - oldEnd: oldEnd, - newStart: newStart, - newEnd: newEnd, - newContent: newLines, - }; - if (previousLine) { - hunk = {...hunk, previousLine: previousLine}; - } - if (nextLine) { - hunk = {...hunk, nextLine: nextLine}; + // If no modifications, skip + if (allAddedLines.length === 0 && allDeletedLines.length === 0) return; + + // Sort lines by line number as ParseDiff does not guarantee order + allAddedLines.sort((a, b) => a.ln - b.ln); + allDeletedLines.sort((a, b) => a.ln - b.ln); + allNormalLines.sort((a, b) => a.ln - b.ln); + + // Identify the range to replace + let startLineToReplace: number; + let endLineToReplace: number; + if (allDeletedLines.length > 0) { + // If there are deletions, start with their range + const lastDelLine = allDeletedLines[allDeletedLines.length - 1].ln; + const lastAddedLine = + allAddedLines.length > 0 + ? Math.max(...allAddedLines.map(a => a.ln)) + : -1; + + // Find neutral lines between additions and deletions + // Find the full change range + const allChangeLines = [...allAddedLines, ...allDeletedLines]; + const earliestChangeLine = Math.min( + ...allChangeLines.map(line => line.ln) + ); + + // Include all normal lines that fall within this range + const relevantNormalLines = allNormalLines.filter( + normal => + normal.ln >= earliestChangeLine && + (normal.ln < lastDelLine || normal.lnNew < lastAddedLine) + ); + // Calculate the full replacement range including relevant normal lines + const allRelevantLines = [...allDeletedLines, ...relevantNormalLines]; + startLineToReplace = Math.min(...allRelevantLines.map(line => line.ln)); + endLineToReplace = Math.max(...allRelevantLines.map(line => line.ln)); + } else { + // Pure additions (no deletions) + // Use the first added line as the insertion point + startLineToReplace = allAddedLines[0].ln; + endLineToReplace = startLineToReplace; } - return hunk; + // Now build the new content + const newContent: string[] = []; + + // Normal processing: include additions and normal lines in the right order + const linesToInclude: {ln: number; content: string}[] = []; + + // Add all the additions to our map + allAddedLines.forEach(line => { + linesToInclude.push({ln: line.ln, content: line.content}); + }); + // Add relevant normal lines that should be preserved + allNormalLines.forEach(line => { + // Only include normal lines within our replacement range if they haven't been replaced by additions + if (line.ln >= startLineToReplace && line.ln <= endLineToReplace) { + linesToInclude.push({ln: line.lnNew, content: line.content}); + } + }); + + // Order the lines and build the final content + linesToInclude + .sort((a, b) => a.ln - b.ln) + .forEach(line => { + newContent.push(line.content); + }); + + // Create the hunk with the replacement range + const hunk: Hunk = { + oldStart: startLineToReplace, + oldEnd: endLineToReplace, + newStart: startLineToReplace, + newEnd: startLineToReplace + newContent.length - 1, + newContent, + }; + + hunks.push(hunk); }); - hunksByFile.set(filename, chunks); + + if (hunks.length > 0) { + hunksByFile.set(filename, hunks); + } }); + return hunksByFile; }