diff --git a/src/core/tools/fileEditTool.ts b/src/core/tools/fileEditTool.ts index 7128528a0..7507b27c6 100644 --- a/src/core/tools/fileEditTool.ts +++ b/src/core/tools/fileEditTool.ts @@ -494,312 +494,3 @@ function* escapeNormalizedReplacer(content: string, find: string): Generator { - // 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 { - 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 { - 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 { - 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 { - 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, -]