Fix overdependence on start/end lines in diff strategy (#2567)

* Add test for issue #2556

* Fix overdependence on start/end lines in diff strategy
This commit is contained in:
Matt Rubens 2025-04-14 17:09:38 -04:00 committed by GitHub
parent 45fb958505
commit b196489b97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 99 additions and 135 deletions

View file

@ -1541,7 +1541,7 @@ function five() {
})
})
describe("insertion/deletion", () => {
describe("deletion", () => {
let strategy: MultiSearchReplaceDiffStrategy
beforeEach(() => {
@ -1646,126 +1646,6 @@ function five() {
}
})
})
describe("insertion", () => {
it("should insert code at specified line when search block is empty", async () => {
const originalContent = `function test() {
const x = 1;
return x;
}`
const diffContent = `test.ts
<<<<<<< SEARCH
:start_line:2
:end_line:2
-------
=======
console.log("Adding log");
>>>>>>> REPLACE`
const result = await strategy.applyDiff(originalContent, diffContent, 2, 2)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(`function test() {
console.log("Adding log");
const x = 1;
return x;
}`)
}
})
it("should preserve indentation when inserting at nested location", async () => {
const originalContent = `function test() {
if (true) {
const x = 1;
}
}`
const diffContent = `test.ts
<<<<<<< SEARCH
:start_line:3
:end_line:3
-------
=======
console.log("Before");
console.log("After");
>>>>>>> REPLACE`
const result = await strategy.applyDiff(originalContent, diffContent, 3, 3)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(`function test() {
if (true) {
console.log("Before");
console.log("After");
const x = 1;
}
}`)
}
})
it("should handle insertion at start of file", async () => {
const originalContent = `function test() {
return true;
}`
const diffContent = `test.ts
<<<<<<< SEARCH
:start_line:1
:end_line:1
-------
=======
// Copyright 2024
// License: MIT
>>>>>>> REPLACE`
const result = await strategy.applyDiff(originalContent, diffContent, 1, 1)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(`// Copyright 2024
// License: MIT
function test() {
return true;
}`)
}
})
it("should handle insertion at end of file", async () => {
const originalContent = `function test() {
return true;
}`
const diffContent = `test.ts
<<<<<<< SEARCH
:start_line:4
:end_line:4
-------
=======
// End of file
>>>>>>> REPLACE`
const result = await strategy.applyDiff(originalContent, diffContent, 4, 4)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(`function test() {
return true;
}
// End of file`)
}
})
it("should error if no start_line is provided for insertion", async () => {
const originalContent = `function test() {
return true;
}`
const diffContent = `test.ts
<<<<<<< SEARCH
=======
console.log("test");
>>>>>>> REPLACE`
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(false)
})
})
})
describe("fuzzy matching", () => {
@ -1949,6 +1829,98 @@ function three() {
}
})
it("should work correctly on this example with line numbers that are slightly off", async () => {
const originalContent = `.game-container {
display: flex;
flex-direction: column;
gap: 1rem;
}
.chess-board-container {
display: flex;
gap: 1rem;
align-items: center;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999; /* Ensure it's above the board but below the promotion dialog */
}
.game-container.promotion-active .chess-board,
.game-container.promotion-active .game-toolbar,
.game-container.promotion-active .game-info-container {
filter: blur(2px);
pointer-events: none; /* Disable clicks on these elements */
}
.game-container.promotion-active .promotion-dialog {
z-index: 1000; /* Ensure it's above the overlay */
pointer-events: auto; /* Enable clicks on the promotion dialog */
}`
const diffContent = `test.ts
<<<<<<< SEARCH
:start_line:12
:end_line:13
-------
.overlay {
=======
.piece {
will-change: transform;
}
.overlay {
>>>>>>> REPLACE
`
const result = await strategy.applyDiff(originalContent, diffContent)
expect(result.success).toBe(true)
if (result.success) {
expect(result.content).toBe(`.game-container {
display: flex;
flex-direction: column;
gap: 1rem;
}
.chess-board-container {
display: flex;
gap: 1rem;
align-items: center;
}
.piece {
will-change: transform;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999; /* Ensure it's above the board but below the promotion dialog */
}
.game-container.promotion-active .chess-board,
.game-container.promotion-active .game-toolbar,
.game-container.promotion-active .game-info-container {
filter: blur(2px);
pointer-events: none; /* Disable clicks on these elements */
}
.game-container.promotion-active .promotion-dialog {
z-index: 1000; /* Ensure it's above the overlay */
pointer-events: auto; /* Enable clicks on the promotion dialog */
}`)
}
})
it("should not find matches outside search range and buffer zone", async () => {
const originalContent = `
function one() {

View file

@ -7,8 +7,9 @@ import { ToolUse } from "../../assistant-message"
const BUFFER_LINES = 40 // Number of extra context lines to show before and after matches
function getSimilarity(original: string, search: string): number {
// Empty searches are no longer supported
if (search === "") {
return 1
return 0
}
// Normalize strings by removing extra whitespace but preserve case
@ -367,7 +368,6 @@ Only use a single line of '=======' between search and replacement content, beca
const replacements = matches
.map((match) => ({
startLine: Number(match[2] ?? 0),
endLine: Number(match[4] ?? resultLines.length),
searchContent: match[6],
replaceContent: match[7],
}))
@ -376,7 +376,6 @@ Only use a single line of '=======' between search and replacement content, beca
for (const replacement of replacements) {
let { searchContent, replaceContent } = replacement
let startLine = replacement.startLine + (replacement.startLine === 0 ? 0 : delta)
let endLine = replacement.endLine + delta
// First unescape any escaped markers in the content
searchContent = this.unescapeMarkers(searchContent)
@ -409,23 +408,16 @@ Only use a single line of '=======' between search and replacement content, beca
let searchLines = searchContent === "" ? [] : searchContent.split(/\r?\n/)
let replaceLines = replaceContent === "" ? [] : replaceContent.split(/\r?\n/)
// Validate that empty search requires start line
if (searchLines.length === 0 && !startLine) {
// Validate that search content is not empty
if (searchLines.length === 0) {
diffResults.push({
success: false,
error: `Empty search content requires start_line to be specified\n\nDebug Info:\n- Empty search content is only valid for insertions at a specific line\n- For insertions, specify the line number where content should be inserted`,
error: `Empty search content is not allowed\n\nDebug Info:\n- Search content cannot be empty\n- For insertions, provide a specific line using :start_line: and include content to search for\n- For example, match a single line to insert before/after it`,
})
continue
}
// Validate that empty search requires same start and end line
if (searchLines.length === 0 && startLine && endLine && startLine !== endLine) {
diffResults.push({
success: false,
error: `Empty search content requires start_line and end_line to be the same (got ${startLine}-${endLine})\n\nDebug Info:\n- Empty search content is only valid for insertions at a specific line\n- For insertions, use the same line number for both start_line and end_line`,
})
continue
}
let endLine = replacement.startLine + searchLines.length - 1
// Initialize search variables
let matchIndex = -1