Skip to content
Closed
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
309 changes: 0 additions & 309 deletions src/core/tools/fileEditTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,312 +494,3 @@ function* escapeNormalizedReplacer(content: string, find: string): Generator<str
case "\\":
return "\\"
case "\n":
return "\n"
case "$":
return "$"
default:
return match
}
})

const unescapedFind = unescapeString(find)

if (content.includes(unescapedFind)) {
yield unescapedFind
}

const lines = content.split("\n")
const findLines = unescapedFind.split("\n")

for (let i = 0; i <= lines.length - findLines.length; i++) {
const block = lines.slice(i, i + findLines.length).join("\n")
const unescapedBlock = unescapeString(block)
if (unescapedBlock === unescapedFind) {
yield block
}
}
}

/**
* Handles the case where old_string contains actual newlines/tabs/quotes but the file
* contains their escape sequence representations (e.g., source code with string literals).
* This is common when editing string literals in source files.
*
* Strategy: Find string quote positions and convert actual special characters
* within quoted portions to their escape sequence representations.
*/
function* sourceCodeEscapeReplacer(content: string, find: string): Generator<string, void, undefined> {
// Find the first quote character (", ', or `) that starts string content
const quoteMatch = find.match(/["'`]/)
if (!quoteMatch || quoteMatch.index === undefined) {
// No quotes found, try simple full escape as fallback
const withEscapedNewlines = find.replace(/\n/g, "\\n")
if (withEscapedNewlines !== find && content.includes(withEscapedNewlines)) {
yield withEscapedNewlines
}
return
}

const quoteIndex = quoteMatch.index
const quoteChar = quoteMatch[0]

// Split into structural part (before quote) and content part (from quote onwards)
const structuralPart = find.substring(0, quoteIndex + 1) // Include the opening quote
const contentPart = find.substring(quoteIndex + 1)

// Escape special characters in the content part for string literals
// Order matters: escape backslashes first, then other characters
const escapeForStringLiteral = (str: string): string => {
return str
.replace(/\\/g, "\\\\") // Backslashes first
.replace(/\n/g, "\\n") // Newlines
.replace(/\t/g, "\\t") // Tabs
.replace(/\r/g, "\\r") // Carriage returns
.replace(/"/g, '\\"') // Double quotes (common in JSON/JS strings)
}

// Try full escape (all special chars)
const fullyEscaped = escapeForStringLiteral(contentPart)
if (fullyEscaped !== contentPart) {
const hybrid = structuralPart + fullyEscaped
if (content.includes(hybrid)) {
yield hybrid
}
}

// Try escaping just newlines and quotes (most common case for string literals)
const escapedNewlinesAndQuotes = contentPart.replace(/\n/g, "\\n").replace(/"/g, '\\"')
if (escapedNewlinesAndQuotes !== contentPart && escapedNewlinesAndQuotes !== fullyEscaped) {
const hybrid = structuralPart + escapedNewlinesAndQuotes
if (content.includes(hybrid)) {
yield hybrid
}
}

// Try escaping just newlines (simpler case)
const escapedNewlinesOnly = contentPart.replace(/\n/g, "\\n")
if (
escapedNewlinesOnly !== contentPart &&
escapedNewlinesOnly !== fullyEscaped &&
escapedNewlinesOnly !== escapedNewlinesAndQuotes
) {
const hybrid = structuralPart + escapedNewlinesOnly
if (content.includes(hybrid)) {
yield hybrid
}
}
}

/**
* Flexible substring replacer that handles cases where old_string is a true substring
* of the content (e.g., missing trailing characters like quotes or commas).
* This normalizes line endings and tries multiple matching strategies.
*/
function* flexibleSubstringReplacer(content: string, find: string): Generator<string, void, undefined> {
if (find.length === 0) return

// Normalize line endings for comparison
const normalizeLineEndings = (str: string) => str.replace(/\r\n/g, "\n").replace(/\r/g, "\n")

const normalizedContent = normalizeLineEndings(content)
const normalizedFind = normalizeLineEndings(find)

// Direct substring match with normalized line endings
if (normalizedContent.includes(normalizedFind)) {
// Find the position in normalized content and extract from original
const normalizedIndex = normalizedContent.indexOf(normalizedFind)
if (normalizedIndex !== -1) {
// Map back to original content position
let originalIndex = 0
let normalizedPos = 0
while (normalizedPos < normalizedIndex && originalIndex < content.length) {
if (content[originalIndex] === "\r" && content[originalIndex + 1] === "\n") {
originalIndex += 2
normalizedPos += 1
} else {
originalIndex += 1
normalizedPos += 1
}
}
// Calculate the length in original content
let endOriginalIndex = originalIndex
let endNormalizedPos = normalizedPos
while (endNormalizedPos < normalizedIndex + normalizedFind.length && endOriginalIndex < content.length) {
if (content[endOriginalIndex] === "\r" && content[endOriginalIndex + 1] === "\n") {
endOriginalIndex += 2
endNormalizedPos += 1
} else {
endOriginalIndex += 1
endNormalizedPos += 1
}
}
yield content.substring(originalIndex, endOriginalIndex)
}
}

// Try with trimmed find (handles trailing/leading whitespace differences)
const trimmedFind = normalizedFind.trim()
if (trimmedFind !== normalizedFind && trimmedFind.length > 0) {
const trimmedIndex = normalizedContent.indexOf(trimmedFind)
if (trimmedIndex !== -1) {
// Find original position and extract
let originalIndex = 0
let normalizedPos = 0
while (normalizedPos < trimmedIndex && originalIndex < content.length) {
if (content[originalIndex] === "\r" && content[originalIndex + 1] === "\n") {
originalIndex += 2
normalizedPos += 1
} else {
originalIndex += 1
normalizedPos += 1
}
}
let endOriginalIndex = originalIndex
let endNormalizedPos = normalizedPos
while (endNormalizedPos < trimmedIndex + trimmedFind.length && endOriginalIndex < content.length) {
if (content[endOriginalIndex] === "\r" && content[endOriginalIndex + 1] === "\n") {
endOriginalIndex += 2
endNormalizedPos += 1
} else {
endOriginalIndex += 1
endNormalizedPos += 1
}
}
yield content.substring(originalIndex, endOriginalIndex)
}
}
}

function* multiOccurrenceReplacer(content: string, find: string): Generator<string, void, undefined> {
if (find.length === 0) return
let startIndex = 0
while (true) {
const index = content.indexOf(find, startIndex)
if (index === -1) break
yield find
startIndex = index + find.length
}
}

function* trimmedBoundaryReplacer(content: string, find: string): Generator<string, void, undefined> {
const trimmed = find.trim()
if (trimmed === find) return

if (content.includes(trimmed)) {
yield trimmed
}

const lines = content.split("\n")
const findLines = find.split("\n")

for (let i = 0; i <= lines.length - findLines.length; i++) {
const block = lines.slice(i, i + findLines.length).join("\n")
if (block.trim() === trimmed) {
yield block
}
}
}

function* contextAwareReplacer(content: string, find: string): Generator<string, void, undefined> {
const findLines = find.split("\n")
if (findLines.length < 3) return
if (findLines[findLines.length - 1] === "") {
findLines.pop()
}

const contentLines = content.split("\n")
const firstLine = findLines[0].trim()
const lastLine = findLines[findLines.length - 1].trim()

for (let i = 0; i < contentLines.length; i++) {
if (contentLines[i].trim() !== firstLine) continue

for (let j = i + 2; j < contentLines.length; j++) {
if (contentLines[j].trim() !== lastLine) continue
const blockLines = contentLines.slice(i, j + 1)
if (blockLines.length !== findLines.length) continue

let matchingLines = 0
let totalNonEmpty = 0
for (let k = 1; k < blockLines.length - 1; k++) {
const blockLine = blockLines[k].trim()
const findLine = findLines[k].trim()
if (blockLine.length > 0 || findLine.length > 0) {
totalNonEmpty++
if (blockLine === findLine) {
matchingLines++
}
}
}

if (totalNonEmpty === 0 || matchingLines / totalNonEmpty >= 0.5) {
yield blockLines.join("\n")
break
}
}
}
}

function extractBlock(content: string, lines: string[], start: number, end: number): string {
let startIndex = 0
for (let i = 0; i < start; i++) {
startIndex += lines[i].length + 1
}

let endIndex = startIndex
for (let i = start; i <= end; i++) {
endIndex += lines[i].length
if (i < end) {
endIndex += 1
}
}

return content.substring(startIndex, endIndex)
}

function truncatePreview(value: string, limit: number): string {
if (value.length <= limit) {
return value
}
return value.slice(0, limit) + "\n...(truncated)"
}

function levenshtein(a: string, b: string): number {
if (a === "" || b === "") {
return Math.max(a.length, b.length)
}

const matrix = Array.from({ length: a.length + 1 }, (_, i) =>
Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
)

for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost)
}
}

return matrix[a.length][b.length]
}

function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
}

const SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0
const MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.3

const REPLACERS: Replacer[] = [
simpleReplacer,
sourceCodeEscapeReplacer,
flexibleSubstringReplacer,
lineTrimmedReplacer,
blockAnchorReplacer,
whitespaceNormalizedReplacer,
indentationFlexibleReplacer,
escapeNormalizedReplacer,
trimmedBoundaryReplacer,
contextAwareReplacer,
multiOccurrenceReplacer,
]