|
@@ -29,6 +29,48 @@ function getSimilarity(original: string, search: string): number {
|
|
|
return 1 - dist / maxLength
|
|
return 1 - dist / maxLength
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Performs a "middle-out" search of `lines` (between [startIndex, endIndex]) to find
|
|
|
|
|
+ * the slice that is most similar to `searchChunk`. Returns the best score, index, and matched text.
|
|
|
|
|
+ */
|
|
|
|
|
+function fuzzySearch(lines: string[], searchChunk: string, startIndex: number, endIndex: number) {
|
|
|
|
|
+ let bestScore = 0
|
|
|
|
|
+ let bestMatchIndex = -1
|
|
|
|
|
+ let bestMatchContent = ""
|
|
|
|
|
+ const searchLen = searchChunk.split(/\r?\n/).length
|
|
|
|
|
+
|
|
|
|
|
+ // Middle-out from the midpoint
|
|
|
|
|
+ const midPoint = Math.floor((startIndex + endIndex) / 2)
|
|
|
|
|
+ let leftIndex = midPoint
|
|
|
|
|
+ let rightIndex = midPoint + 1
|
|
|
|
|
+
|
|
|
|
|
+ while (leftIndex >= startIndex || rightIndex <= endIndex - searchLen) {
|
|
|
|
|
+ if (leftIndex >= startIndex) {
|
|
|
|
|
+ const originalChunk = lines.slice(leftIndex, leftIndex + searchLen).join("\n")
|
|
|
|
|
+ const similarity = getSimilarity(originalChunk, searchChunk)
|
|
|
|
|
+ if (similarity > bestScore) {
|
|
|
|
|
+ bestScore = similarity
|
|
|
|
|
+ bestMatchIndex = leftIndex
|
|
|
|
|
+ bestMatchContent = originalChunk
|
|
|
|
|
+ }
|
|
|
|
|
+ leftIndex--
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (rightIndex <= endIndex - searchLen) {
|
|
|
|
|
+ const originalChunk = lines.slice(rightIndex, rightIndex + searchLen).join("\n")
|
|
|
|
|
+ const similarity = getSimilarity(originalChunk, searchChunk)
|
|
|
|
|
+ if (similarity > bestScore) {
|
|
|
|
|
+ bestScore = similarity
|
|
|
|
|
+ bestMatchIndex = rightIndex
|
|
|
|
|
+ bestMatchContent = originalChunk
|
|
|
|
|
+ }
|
|
|
|
|
+ rightIndex++
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return { bestScore, bestMatchIndex, bestMatchContent }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
export class MultiSearchReplaceDiffStrategy implements DiffStrategy {
|
|
export class MultiSearchReplaceDiffStrategy implements DiffStrategy {
|
|
|
private fuzzyThreshold: number
|
|
private fuzzyThreshold: number
|
|
|
private bufferLines: number
|
|
private bufferLines: number
|
|
@@ -253,7 +295,9 @@ Only use a single line of '=======' between search and replacement content, beca
|
|
|
? { success: true }
|
|
? { success: true }
|
|
|
: {
|
|
: {
|
|
|
success: false,
|
|
success: false,
|
|
|
- error: `ERROR: Unexpected end of sequence: Expected '${state.current === State.AFTER_SEARCH ? SEP : REPLACE}' was not found.`,
|
|
|
|
|
|
|
+ error: `ERROR: Unexpected end of sequence: Expected '${
|
|
|
|
|
+ state.current === State.AFTER_SEARCH ? "=======" : ">>>>>>> REPLACE"
|
|
|
|
|
+ }' was not found.`,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -329,19 +373,21 @@ Only use a single line of '=======' between search and replacement content, beca
|
|
|
}))
|
|
}))
|
|
|
.sort((a, b) => a.startLine - b.startLine)
|
|
.sort((a, b) => a.startLine - b.startLine)
|
|
|
|
|
|
|
|
- for (let { searchContent, replaceContent, startLine, endLine } of replacements) {
|
|
|
|
|
- startLine += startLine === 0 ? 0 : delta
|
|
|
|
|
- endLine += delta
|
|
|
|
|
|
|
+ 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
|
|
// First unescape any escaped markers in the content
|
|
|
searchContent = this.unescapeMarkers(searchContent)
|
|
searchContent = this.unescapeMarkers(searchContent)
|
|
|
replaceContent = this.unescapeMarkers(replaceContent)
|
|
replaceContent = this.unescapeMarkers(replaceContent)
|
|
|
|
|
|
|
|
// Strip line numbers from search and replace content if every line starts with a line number
|
|
// Strip line numbers from search and replace content if every line starts with a line number
|
|
|
- if (
|
|
|
|
|
|
|
+ const hasAllLineNumbers =
|
|
|
(everyLineHasLineNumbers(searchContent) && everyLineHasLineNumbers(replaceContent)) ||
|
|
(everyLineHasLineNumbers(searchContent) && everyLineHasLineNumbers(replaceContent)) ||
|
|
|
(everyLineHasLineNumbers(searchContent) && replaceContent.trim() === "")
|
|
(everyLineHasLineNumbers(searchContent) && replaceContent.trim() === "")
|
|
|
- ) {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (hasAllLineNumbers) {
|
|
|
searchContent = stripLineNumbers(searchContent)
|
|
searchContent = stripLineNumbers(searchContent)
|
|
|
replaceContent = stripLineNumbers(replaceContent)
|
|
replaceContent = stripLineNumbers(replaceContent)
|
|
|
}
|
|
}
|
|
@@ -360,8 +406,8 @@ Only use a single line of '=======' between search and replacement content, beca
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Split content into lines, handling both \n and \r\n
|
|
// Split content into lines, handling both \n and \r\n
|
|
|
- const searchLines = searchContent === "" ? [] : searchContent.split(/\r?\n/)
|
|
|
|
|
- const replaceLines = replaceContent === "" ? [] : replaceContent.split(/\r?\n/)
|
|
|
|
|
|
|
+ let searchLines = searchContent === "" ? [] : searchContent.split(/\r?\n/)
|
|
|
|
|
+ let replaceLines = replaceContent === "" ? [] : replaceContent.split(/\r?\n/)
|
|
|
|
|
|
|
|
// Validate that empty search requires start line
|
|
// Validate that empty search requires start line
|
|
|
if (searchLines.length === 0 && !startLine) {
|
|
if (searchLines.length === 0 && !startLine) {
|
|
@@ -385,7 +431,7 @@ Only use a single line of '=======' between search and replacement content, beca
|
|
|
let matchIndex = -1
|
|
let matchIndex = -1
|
|
|
let bestMatchScore = 0
|
|
let bestMatchScore = 0
|
|
|
let bestMatchContent = ""
|
|
let bestMatchContent = ""
|
|
|
- const searchChunk = searchLines.join("\n")
|
|
|
|
|
|
|
+ let searchChunk = searchLines.join("\n")
|
|
|
|
|
|
|
|
// Determine search bounds
|
|
// Determine search bounds
|
|
|
let searchStartIndex = 0
|
|
let searchStartIndex = 0
|
|
@@ -421,68 +467,70 @@ Only use a single line of '=======' between search and replacement content, beca
|
|
|
|
|
|
|
|
// If no match found yet, try middle-out search within bounds
|
|
// If no match found yet, try middle-out search within bounds
|
|
|
if (matchIndex === -1) {
|
|
if (matchIndex === -1) {
|
|
|
- const midPoint = Math.floor((searchStartIndex + searchEndIndex) / 2)
|
|
|
|
|
- let leftIndex = midPoint
|
|
|
|
|
- let rightIndex = midPoint + 1
|
|
|
|
|
-
|
|
|
|
|
- // Search outward from the middle within bounds
|
|
|
|
|
- while (leftIndex >= searchStartIndex || rightIndex <= searchEndIndex - searchLines.length) {
|
|
|
|
|
- // Check left side if still in range
|
|
|
|
|
- if (leftIndex >= searchStartIndex) {
|
|
|
|
|
- const originalChunk = resultLines.slice(leftIndex, leftIndex + searchLines.length).join("\n")
|
|
|
|
|
- const similarity = getSimilarity(originalChunk, searchChunk)
|
|
|
|
|
- if (similarity > bestMatchScore) {
|
|
|
|
|
- bestMatchScore = similarity
|
|
|
|
|
- matchIndex = leftIndex
|
|
|
|
|
- bestMatchContent = originalChunk
|
|
|
|
|
- }
|
|
|
|
|
- leftIndex--
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Check right side if still in range
|
|
|
|
|
- if (rightIndex <= searchEndIndex - searchLines.length) {
|
|
|
|
|
- const originalChunk = resultLines.slice(rightIndex, rightIndex + searchLines.length).join("\n")
|
|
|
|
|
- const similarity = getSimilarity(originalChunk, searchChunk)
|
|
|
|
|
- if (similarity > bestMatchScore) {
|
|
|
|
|
- bestMatchScore = similarity
|
|
|
|
|
- matchIndex = rightIndex
|
|
|
|
|
- bestMatchContent = originalChunk
|
|
|
|
|
- }
|
|
|
|
|
- rightIndex++
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const {
|
|
|
|
|
+ bestScore,
|
|
|
|
|
+ bestMatchIndex,
|
|
|
|
|
+ bestMatchContent: midContent,
|
|
|
|
|
+ } = fuzzySearch(resultLines, searchChunk, searchStartIndex, searchEndIndex)
|
|
|
|
|
+ matchIndex = bestMatchIndex
|
|
|
|
|
+ bestMatchScore = bestScore
|
|
|
|
|
+ bestMatchContent = midContent
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Require similarity to meet threshold
|
|
|
|
|
|
|
+ // Try aggressive line number stripping as a fallback if regular matching fails
|
|
|
if (matchIndex === -1 || bestMatchScore < this.fuzzyThreshold) {
|
|
if (matchIndex === -1 || bestMatchScore < this.fuzzyThreshold) {
|
|
|
- const searchChunk = searchLines.join("\n")
|
|
|
|
|
- const originalContentSection =
|
|
|
|
|
- startLine !== undefined && endLine !== undefined
|
|
|
|
|
- ? `\n\nOriginal Content:\n${addLineNumbers(
|
|
|
|
|
- resultLines
|
|
|
|
|
- .slice(
|
|
|
|
|
- Math.max(0, startLine - 1 - this.bufferLines),
|
|
|
|
|
- Math.min(resultLines.length, endLine + this.bufferLines),
|
|
|
|
|
- )
|
|
|
|
|
- .join("\n"),
|
|
|
|
|
- Math.max(1, startLine - this.bufferLines),
|
|
|
|
|
- )}`
|
|
|
|
|
- : `\n\nOriginal Content:\n${addLineNumbers(resultLines.join("\n"))}`
|
|
|
|
|
-
|
|
|
|
|
- const bestMatchSection = bestMatchContent
|
|
|
|
|
- ? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}`
|
|
|
|
|
- : `\n\nBest Match Found:\n(no match)`
|
|
|
|
|
-
|
|
|
|
|
- const lineRange =
|
|
|
|
|
- startLine || endLine
|
|
|
|
|
- ? ` at ${startLine ? `start: ${startLine}` : "start"} to ${endLine ? `end: ${endLine}` : "end"}`
|
|
|
|
|
- : ""
|
|
|
|
|
|
|
+ // Strip both search and replace content once (simultaneously)
|
|
|
|
|
+ const aggressiveSearchContent = stripLineNumbers(searchContent, true)
|
|
|
|
|
+ const aggressiveReplaceContent = stripLineNumbers(replaceContent, true)
|
|
|
|
|
+
|
|
|
|
|
+ const aggressiveSearchLines = aggressiveSearchContent ? aggressiveSearchContent.split(/\r?\n/) : []
|
|
|
|
|
+ const aggressiveSearchChunk = aggressiveSearchLines.join("\n")
|
|
|
|
|
+
|
|
|
|
|
+ // Try middle-out search again with aggressive stripped content (respecting the same search bounds)
|
|
|
|
|
+ const {
|
|
|
|
|
+ bestScore,
|
|
|
|
|
+ bestMatchIndex,
|
|
|
|
|
+ bestMatchContent: aggContent,
|
|
|
|
|
+ } = fuzzySearch(resultLines, aggressiveSearchChunk, searchStartIndex, searchEndIndex)
|
|
|
|
|
+ if (bestMatchIndex !== -1 && bestScore >= this.fuzzyThreshold) {
|
|
|
|
|
+ matchIndex = bestMatchIndex
|
|
|
|
|
+ bestMatchScore = bestScore
|
|
|
|
|
+ bestMatchContent = aggContent
|
|
|
|
|
+ // Replace the original search/replace with their stripped versions
|
|
|
|
|
+ searchContent = aggressiveSearchContent
|
|
|
|
|
+ replaceContent = aggressiveReplaceContent
|
|
|
|
|
+ searchLines = aggressiveSearchLines
|
|
|
|
|
+ replaceLines = replaceContent ? replaceContent.split(/\r?\n/) : []
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // No match found with either method
|
|
|
|
|
+ const originalContentSection =
|
|
|
|
|
+ startLine !== undefined && endLine !== undefined
|
|
|
|
|
+ ? `\n\nOriginal Content:\n${addLineNumbers(
|
|
|
|
|
+ resultLines
|
|
|
|
|
+ .slice(
|
|
|
|
|
+ Math.max(0, startLine - 1 - this.bufferLines),
|
|
|
|
|
+ Math.min(resultLines.length, endLine + this.bufferLines),
|
|
|
|
|
+ )
|
|
|
|
|
+ .join("\n"),
|
|
|
|
|
+ Math.max(1, startLine - this.bufferLines),
|
|
|
|
|
+ )}`
|
|
|
|
|
+ : `\n\nOriginal Content:\n${addLineNumbers(resultLines.join("\n"))}`
|
|
|
|
|
+
|
|
|
|
|
+ const bestMatchSection = bestMatchContent
|
|
|
|
|
+ ? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}`
|
|
|
|
|
+ : `\n\nBest Match Found:\n(no match)`
|
|
|
|
|
+
|
|
|
|
|
+ const lineRange =
|
|
|
|
|
+ startLine || endLine
|
|
|
|
|
+ ? ` at ${startLine ? `start: ${startLine}` : "start"} to ${endLine ? `end: ${endLine}` : "end"}`
|
|
|
|
|
+ : ""
|
|
|
|
|
|
|
|
- diffResults.push({
|
|
|
|
|
- success: false,
|
|
|
|
|
- error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine && endLine ? `lines ${startLine}-${endLine}` : "start to end"}\n- Tip: Use the read_file tool to get the latest content of the file before attempting to use the apply_diff tool again, as the file content may have changed\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`,
|
|
|
|
|
- })
|
|
|
|
|
- continue
|
|
|
|
|
|
|
+ diffResults.push({
|
|
|
|
|
+ success: false,
|
|
|
|
|
+ error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine && endLine ? `lines ${startLine}-${endLine}` : "start to end"}\n- Tried both standard and aggressive line number stripping\n- Tip: Use the read_file tool to get the latest content of the file before attempting to use the apply_diff tool again, as the file content may have changed\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`,
|
|
|
|
|
+ })
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Get the matched lines from the original content
|
|
// Get the matched lines from the original content
|