Skip to content

fix: handle line removal diff #1744

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,22 @@
// Avoid timing out when stopping on breakpoints during debugging in VSCode
"MOCHA_TIMEOUT": "0"
}
},
{
"name": "[Core] Jest Test Debugger, Current Open File",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--inspect-brk",
"${workspaceRoot}/core/node_modules/jest/bin/jest.js",
"--runInBand",
"--config",
"${workspaceRoot}/core/jest.config.js",
"${relativeFile}"
],
"cwd": "${workspaceRoot}/core",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
55 changes: 39 additions & 16 deletions core/diff/streamDiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,46 +13,69 @@ export async function* streamDiff(
oldLines: string[],
newLines: LineStream,
): AsyncGenerator<DiffLine> {
const mutatedOldLines = [...oldLines]; // be careful

// If one indentation mistake is made, others are likely. So we are more permissive about matching
let seenIndentationMistake = false;

let newLineResult = await newLines.next();

while (oldLines.length > 0 && !newLineResult.done) {
const [matchIndex, isPerfectMatch, newLine] = matchLine(
const { matchIndex, isPerfectMatch, newLine } = matchLine(
newLineResult.value,
oldLines,
seenIndentationMistake,
);

if (!seenIndentationMistake && newLineResult.value !== newLine) {
seenIndentationMistake = true;
}

if (matchIndex < 0) {
// Insert new line
yield { type: "new", line: newLine };
let type: DiffLine["type"];

let isLineRemoval = false;
const isNewLine = matchIndex === -1;

if (isNewLine) {
type = "new";
} else {
// Insert all deleted lines before match
for (let i = 0; i < matchIndex; i++) {
yield { type: "old", line: oldLines.shift()! };
}

if (isPerfectMatch) {
// Same
yield { type: "same", line: oldLines.shift()! };
} else {
// Delete old line and insert the new
yield { type: "old", line: oldLines.shift()! };
yield { type: "new", line: newLine };
}
type = isPerfectMatch ? "same" : "old";
}

newLineResult = await newLines.next();
switch (type) {
case "new":
yield { type, line: newLine };
break;

case "same":
yield { type, line: oldLines.shift()! };
break;

case "old":
yield { type, line: oldLines.shift()! };

if (oldLines[0] !== newLine) {
yield { type: "new", line: newLine };
} else {
isLineRemoval = true;
}

break;

default:
console.error(`Error streaming diff, unrecognized diff type: ${type}`);
}

if (!isLineRemoval) {
newLineResult = await newLines.next();
}
}

// Once at the edge, only one choice
if (newLineResult.done === true && oldLines.length > 0) {
if (newLineResult.done && oldLines.length > 0) {
for (const oldLine of oldLines) {
yield { type: "old", line: oldLine };
}
Expand Down
34 changes: 27 additions & 7 deletions core/diff/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ import { stripImages } from "../llm/countTokens.js";

export type LineStream = AsyncGenerator<string>;

export type MatchLineResult = {
/**
* -1 if it's a new line, otherwise the index of the first match
* in the old lines.
*/
matchIndex: number;
isPerfectMatch: boolean;
newLine: string;
};

function linesMatchPerfectly(lineA: string, lineB: string): boolean {
return lineA === lineB && lineA !== "";
}
Expand All @@ -18,6 +28,7 @@ function linesMatch(lineA: string, lineB: string, linesBetween = 0): boolean {
}

const d = distance(lineA, lineB);

return (
// Should be more unlikely for lines to fuzzy match if they are further away
(d / Math.max(lineA.length, lineB.length) < 0.5 - linesBetween * 0.05 ||
Expand All @@ -34,35 +45,44 @@ export function matchLine(
newLine: string,
oldLines: string[],
permissiveAboutIndentation = false,
): [number, boolean, string] {
): MatchLineResult {
// Only match empty lines if it's the next one:
if (newLine.trim() === "" && oldLines[0]?.trim() === "") {
return [0, true, newLine.trim()];
return {
matchIndex: 0,
isPerfectMatch: true,
newLine: newLine.trim(),
};
}

const isEndBracket = END_BRACKETS.includes(newLine.trim());

for (let i = 0; i < oldLines.length; i++) {
// Don't match end bracket lines if too far away
if (i > 4 && isEndBracket) {
return [-1, false, newLine];
return { matchIndex: -1, isPerfectMatch: false, newLine };
}

if (linesMatchPerfectly(newLine, oldLines[i])) {
return [i, true, newLine];
return { matchIndex: i, isPerfectMatch: true, newLine };
}
if (linesMatch(newLine, oldLines[i], i)) {
// This is a way to fix indentation, but only for sufficiently long lines to avoid matching whitespace or short lines
if (
newLine.trimStart() === oldLines[i].trimStart() &&
(permissiveAboutIndentation || newLine.trim().length > 8)
) {
return [i, true, oldLines[i]];
return {
matchIndex: i,
isPerfectMatch: true,
newLine: oldLines[i],
};
}
return [i, false, newLine];
return { matchIndex: i, isPerfectMatch: false, newLine };
}
}

return [-1, false, newLine];
return { matchIndex: -1, isPerfectMatch: false, newLine };
}

/**
Expand Down
150 changes: 0 additions & 150 deletions core/test/diff.test.ts

This file was deleted.

Loading
Loading