Skip to content

Commit 2050f7e

Browse files
authored
Fix diff generation showing entire file as changed (fixes #83) (#98)
The diff preview was showing the entire file as changed when only a few lines were actually modified. This was caused by a broken change detection algorithm that couldn't properly handle insertions and deletions. Replaced the naive line-matching algorithm with a proper LCS (Longest Common Subsequence) based diff algorithm using dynamic programming. This produces minimal, accurate diffs that match what git diff shows. Changes: - Added computeLCS(): Computes longest common subsequence between old/new lines - Added extractChanges(): Extracts actual change regions from LCS table - Updated generateDiff(): Uses LCS-based change detection instead of broken algorithm Impact: - Diff previews now show only actual changes (e.g., 4 lines instead of 260) - Drastically reduces context pollution in conversation history - Matches git diff output accuracy 🤖 Contributed by ZDS-AI (https://zds.group)
1 parent debe6f9 commit 2050f7e

File tree

1 file changed

+94
-58
lines changed

1 file changed

+94
-58
lines changed

src/tools/text-editor.ts

Lines changed: 94 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -468,84 +468,120 @@ export class TextEditorTool {
468468
const tokens = str.match(/\b(function|console\.log|return|if|else|for|while)\b/g) || [];
469469
return tokens;
470470
};
471-
471+
472472
const searchTokens = extractTokens(search);
473473
const actualTokens = extractTokens(actual);
474-
474+
475475
if (searchTokens.length !== actualTokens.length) return false;
476-
476+
477477
for (let i = 0; i < searchTokens.length; i++) {
478478
if (searchTokens[i] !== actualTokens[i]) return false;
479479
}
480-
480+
481481
return true;
482482
}
483483

484-
private generateDiff(
484+
/**
485+
* Compute Longest Common Subsequence using dynamic programming
486+
* Returns array of indices in oldLines that are part of LCS
487+
*/
488+
private computeLCS(oldLines: string[], newLines: string[]): number[][] {
489+
const m = oldLines.length;
490+
const n = newLines.length;
491+
const dp: number[][] = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0));
492+
493+
// Build LCS length table
494+
for (let i = 1; i <= m; i++) {
495+
for (let j = 1; j <= n; j++) {
496+
if (oldLines[i - 1] === newLines[j - 1]) {
497+
dp[i][j] = dp[i - 1][j - 1] + 1;
498+
} else {
499+
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
500+
}
501+
}
502+
}
503+
504+
return dp;
505+
}
506+
507+
/**
508+
* Extract changes from LCS table
509+
* Returns array of change regions
510+
*/
511+
private extractChanges(
485512
oldLines: string[],
486513
newLines: string[],
487-
filePath: string
488-
): string {
489-
const CONTEXT_LINES = 3;
490-
514+
lcs: number[][]
515+
): Array<{ oldStart: number; oldEnd: number; newStart: number; newEnd: number }> {
491516
const changes: Array<{
492517
oldStart: number;
493518
oldEnd: number;
494519
newStart: number;
495520
newEnd: number;
496521
}> = [];
497-
498-
let i = 0, j = 0;
499-
500-
while (i < oldLines.length || j < newLines.length) {
501-
while (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {
502-
i++;
503-
j++;
504-
}
505-
506-
if (i < oldLines.length || j < newLines.length) {
507-
const changeStart = { old: i, new: j };
508-
509-
let oldEnd = i;
510-
let newEnd = j;
511-
512-
while (oldEnd < oldLines.length || newEnd < newLines.length) {
513-
let matchFound = false;
514-
let matchLength = 0;
515-
516-
for (let k = 0; k < Math.min(2, oldLines.length - oldEnd, newLines.length - newEnd); k++) {
517-
if (oldEnd + k < oldLines.length &&
518-
newEnd + k < newLines.length &&
519-
oldLines[oldEnd + k] === newLines[newEnd + k]) {
520-
matchLength++;
521-
} else {
522-
break;
523-
}
524-
}
525-
526-
if (matchLength >= 2 || (oldEnd >= oldLines.length && newEnd >= newLines.length)) {
527-
matchFound = true;
528-
}
529-
530-
if (matchFound) {
531-
break;
532-
}
533-
534-
if (oldEnd < oldLines.length) oldEnd++;
535-
if (newEnd < newLines.length) newEnd++;
522+
523+
let i = oldLines.length;
524+
let j = newLines.length;
525+
let oldEnd = i;
526+
let newEnd = j;
527+
let inChange = false;
528+
529+
while (i > 0 || j > 0) {
530+
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
531+
// Lines match - if we were in a change, close it
532+
if (inChange) {
533+
changes.unshift({
534+
oldStart: i,
535+
oldEnd: oldEnd,
536+
newStart: j,
537+
newEnd: newEnd
538+
});
539+
inChange = false;
536540
}
537-
538-
changes.push({
539-
oldStart: changeStart.old,
540-
oldEnd: oldEnd,
541-
newStart: changeStart.new,
542-
newEnd: newEnd
543-
});
544-
545-
i = oldEnd;
546-
j = newEnd;
541+
i--;
542+
j--;
543+
} else if (j > 0 && (i === 0 || lcs[i][j - 1] >= lcs[i - 1][j])) {
544+
// Insertion in new file
545+
if (!inChange) {
546+
oldEnd = i;
547+
newEnd = j;
548+
inChange = true;
549+
}
550+
j--;
551+
} else if (i > 0) {
552+
// Deletion from old file
553+
if (!inChange) {
554+
oldEnd = i;
555+
newEnd = j;
556+
inChange = true;
557+
}
558+
i--;
547559
}
548560
}
561+
562+
// Close any remaining change
563+
if (inChange) {
564+
changes.unshift({
565+
oldStart: 0,
566+
oldEnd: oldEnd,
567+
newStart: 0,
568+
newEnd: newEnd
569+
});
570+
}
571+
572+
return changes;
573+
}
574+
575+
private generateDiff(
576+
oldLines: string[],
577+
newLines: string[],
578+
filePath: string
579+
): string {
580+
const CONTEXT_LINES = 3;
581+
582+
// Use LCS-based diff algorithm to find actual changes
583+
const lcs = this.computeLCS(oldLines, newLines);
584+
const changes = this.extractChanges(oldLines, newLines, lcs);
549585

550586
const hunks: Array<{
551587
oldStart: number;

0 commit comments

Comments
 (0)