apply-multi-diff
Version:
A zero-dependency library to apply unified diffs and search-and-replace patches, with support for fuzzy matching.
1 lines • 22.7 kB
Source Map (JSON)
{"version":3,"sources":["../../src/strategies/search-replace.ts"],"names":["getToolDescription","cwd","stripLineNumbers","text","lines","line","cleanBlock","block","_parseDiff_for_debug","diffContent","blocks","searchMarker","replaceMarker","content","firstLineEnd","searchStart","replaceEndMatch","replaceEnd","parts","_findBestMatch_for_debug","sourceLines","searchLines","startLine","endLine","bestMatchIndex","minDistance","searchText","dedentedSearchText","dedent","searchEnd","i","sliceText","dedentedSliceText","distance","levenshtein","bestMatchSliceText","dedentedBestMatchSliceText","maxDistanceThreshold","searchHasString","sliceHasString","searchStringMatch","sliceStringMatch","searchString","sliceString","applyDiff","original_content","diff_content","options","createErrorResult","ERROR_CODES","currentContent","insertionIndex","indent","currentLine","currentLineIndent","prevLine","prevLineIndent","prevLineTrimmed","replaceLines","replaceBaseIndent","getCommonIndent","reindentedReplaceLines","dedentedLine","match","matchStartIndex","matchEndIndex","sourceMatchBlock","sourceMatchIndent","originalLine","replaceText","nonSearchParts","commentMatch","trailingComment"],"mappings":"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","file":"search-replace.cjs","sourcesContent":["import { ERROR_CODES } from \"../constants\";\nimport type { ApplyDiffResult } from \"../types\";\nimport { createErrorResult } from \"../utils/error\";\nimport { getCommonIndent, levenshtein, dedent } from \"../utils/string\";\n\nexport const getToolDescription = (cwd: string): string => {\n return `apply_diff Tool: Search and Replace\n\nTargeted code changes using search/replace blocks. Supports fuzzy matching.\n\nParameters:\n :file_path: Path to file relative to ${cwd}\n :diff_content: Search/replace blocks\n :start_line: (optional) Line to start search (required for insertions)\n :end_line: (optional) Line to end search\nFormat:\n<<<<< SEARCH\ncontent to find\n=======\nreplacement content\n>>>>> REPLACE\n\nSpecial cases:\n- INSERT Insertion (note the empty SEARCH block and \\`start_line\\`):\n<apply_diff file_path=\"src/app.ts\" start_line=\"5\">\n src/app.ts\n <<<<<<< SEARCH\n =======\n // Add a new configuration setting\n const newConfig = initializeNewDependency();\n >>>>>>> REPLACE\n</apply_diff>`;\n};\n\nconst stripLineNumbers = (text: string): string => {\n const lines = text.split(\"\\n\");\n const allLinesNumbered = lines\n .filter((line) => line.trim() !== \"\")\n .every((line) => /^\\s*\\d+\\s*\\|/.test(line));\n if (!allLinesNumbered) return text;\n return lines.map((line) => line.replace(/^\\s*\\d+\\s*\\|\\s?/, \"\")).join(\"\\n\");\n};\n\nconst cleanBlock = (block: string) =>\n // Be less greedy with the trailing newline, to distinguish\n // a search for a blank line from an empty search block.\n // \\n\\n (search for blank line) -> \\n\n // \\n (empty search block) -> ''\n block.replace(/^\\r?\\n/, \"\").replace(/\\r?\\n$/, \"\");\n\ntype SearchReplaceBlock = { search: string; replace: string };\n\nexport const _parseDiff_for_debug = (diffContent: string): SearchReplaceBlock[] | null => {\n const blocks: SearchReplaceBlock[] = [];\n const searchMarker = /^\\s*<<<<<<< SEARCH\\s*$/m;\n const replaceMarker = /^\\s*>>>>>>> REPLACE\\s*$/m;\n\n let content = diffContent;\n const firstLineEnd = content.indexOf(\"\\n\");\n if (firstLineEnd !== -1 && !content.substring(0, firstLineEnd).includes(\"<<<<<<<\")) {\n content = content.substring(firstLineEnd + 1);\n }\n\n while (searchMarker.test(content)) {\n const searchStart = content.search(searchMarker);\n const replaceEndMatch = content.match(replaceMarker);\n if (!replaceEndMatch || typeof replaceEndMatch.index === \"undefined\") break;\n \n const replaceEnd = replaceEndMatch.index + replaceEndMatch[0].length;\n const blockContent = content.substring(searchStart, replaceEnd);\n \n const parts = blockContent.split(\n /^\\s*<<<<<<< SEARCH\\s*$|^\\s*=======*\\s*$|^\\s*>>>>>>> REPLACE\\s*$/m\n );\n \n if (parts.length >= 4) {\n blocks.push({\n search: stripLineNumbers(cleanBlock(parts[1] ?? '')),\n replace: stripLineNumbers(cleanBlock(parts[2] ?? '')),\n });\n }\n content = content.substring(replaceEnd);\n }\n\n return blocks.length > 0 ? blocks : null;\n};\n\nexport const _findBestMatch_for_debug = (\n sourceLines: readonly string[],\n searchLines: readonly string[],\n startLine: number,\n endLine: number\n): { index: number; distance: number } | null => {\n if (searchLines.length === 0) return null;\n\n let bestMatchIndex = -1;\n let minDistance = Infinity;\n const searchText = searchLines.join(\"\\n\");\n const dedentedSearchText = dedent(searchText);\n\n const searchStart = startLine - 1;\n const searchEnd = endLine ?? sourceLines.length;\n\n for (let i = searchStart; i <= searchEnd - searchLines.length; i++) {\n const slice = sourceLines.slice(i, i + searchLines.length);\n const sliceText = slice.join(\"\\n\");\n const dedentedSliceText = dedent(sliceText);\n const distance = levenshtein(dedentedSearchText, dedentedSliceText);\n if (distance < minDistance) {\n minDistance = distance;\n bestMatchIndex = i;\n }\n if (distance === 0) break;\n }\n if (bestMatchIndex === -1) {\n return null;\n }\n \n const bestMatchSliceText = sourceLines.slice(bestMatchIndex, bestMatchIndex + searchLines.length).join('\\n');\n const dedentedBestMatchSliceText = dedent(bestMatchSliceText);\n // Threshold is based on the shorter of the search/slice text to be more robust against large length differences.\n const maxDistanceThreshold = Math.max(20, Math.floor(Math.min(dedentedSearchText.length, dedentedBestMatchSliceText.length) * 0.7));\n if (minDistance > maxDistanceThreshold) {\n return null;\n }\n if (minDistance > 0) {\n const slice = sourceLines.slice(bestMatchIndex, bestMatchIndex + searchLines.length);\n const sliceText = slice.join(\"\\n\");\n const dedentedSliceText = dedent(sliceText);\n \n // Check if both contain string literals and they're different\n const searchHasString = /[\"'].*[\"']/.test(dedentedSearchText);\n const sliceHasString = /[\"'].*[\"']/.test(dedentedSliceText);\n \n if (searchHasString && sliceHasString) {\n // Extract the string content to see if it's a semantic change\n const searchStringMatch = dedentedSearchText.match(/[\"'](.*?)[\"']/);\n const sliceStringMatch = dedentedSliceText.match(/[\"'](.*?)[\"']/);\n \n if (searchStringMatch && sliceStringMatch && typeof searchStringMatch[1] === 'string' && typeof sliceStringMatch[1] === 'string') {\n const searchString = searchStringMatch[1];\n const sliceString = sliceStringMatch[1];\n\n if (levenshtein(searchString, sliceString) > (searchString.length * 0.5)) {\n return null;\n }\n }\n }\n }\n return { index: bestMatchIndex, distance: minDistance };\n};\n\nexport const applyDiff = (\n original_content: string,\n diff_content: string,\n options: { start_line?: number; end_line?: number } = {}\n): ApplyDiffResult => {\n const blocks = _parseDiff_for_debug(diff_content);\n if (!blocks) {\n return createErrorResult(\n ERROR_CODES.INVALID_DIFF_FORMAT,\n \"Invalid diff format. Could not parse any '<<<<<<< SEARCH'...'>>>>>>> REPLACE' blocks.\"\n );\n }\n\n let currentContent = original_content;\n\n for (const block of blocks) {\n if (block.search === \"\") {\n // Pure insertion\n if (typeof options.start_line !== \"number\") {\n return createErrorResult(\n ERROR_CODES.INSERTION_REQUIRES_LINE_NUMBER,\n \"Insertion requires a start_line. A SEARCH block was empty, but no start_line was provided.\"\n );\n }\n // Special case for inserting into an empty file\n if (currentContent === \"\") {\n currentContent = block.replace;\n continue;\n }\n\n const lines = currentContent.split(\"\\n\");\n const insertionIndex = Math.max(0, options.start_line - 1);\n\n // Infer indentation from the insertion line or surrounding lines\n let indent = \"\";\n if (insertionIndex < lines.length) {\n const currentLine = lines[insertionIndex];\n const currentLineIndent = currentLine?.match(/^[ \\t]*/)?.[0] || \"\";\n if (insertionIndex > 0) {\n const prevLine = lines[insertionIndex - 1];\n const prevLineIndent = prevLine?.match(/^[ \\t]*/)?.[0] || \"\";\n const prevLineTrimmed = prevLine?.trim() ?? '';\n // If current line is an outdent (like a closing brace), use previous line's indent\n if (prevLineIndent.length > currentLineIndent.length && (currentLine?.trim()?.length ?? 0) > 0) {\n indent = prevLineIndent;\n } else if (prevLineTrimmed.endsWith('{') || prevLineTrimmed.endsWith('[') || prevLineTrimmed.endsWith('(')) {\n // If previous line opens a block, indent by 4 spaces (common practice)\n indent = prevLineIndent + ' ';\n } else {\n indent = currentLineIndent;\n }\n } else {\n indent = currentLineIndent;\n }\n } else if (lines.length > 0) {\n // If inserting at the very end, use indent of last line\n const lastLine = lines[lines.length - 1];\n indent = lastLine?.match(/^[ \\t]*/)?.[0] || \"\";\n }\n\n const replaceLines = block.replace.split('\\n');\n const replaceBaseIndent = getCommonIndent(block.replace);\n \n const reindentedReplaceLines = replaceLines.map(line => {\n if (line.trim() === \"\") return line;\n const dedentedLine = line.startsWith(replaceBaseIndent)\n ? line.substring(replaceBaseIndent.length)\n : line;\n return indent + dedentedLine;\n });\n\n lines.splice(insertionIndex, 0, ...reindentedReplaceLines);\n currentContent = lines.join(\"\\n\");\n continue;\n }\n\n const sourceLines = currentContent.split(\"\\n\");\n // JS `split` behavior with trailing newlines is tricky.\n // A search for a single blank line (`block.search`=\"\\n\") becomes `['', '']`,\n // which is interpreted as two lines. We want `['']`.\n const searchLines = block.search === '\\n' ? [''] : block.search.split(\"\\n\");\n\n const match = _findBestMatch_for_debug(sourceLines, searchLines, options.start_line ?? 1, options.end_line ?? sourceLines.length);\n\n if (match === null) {\n return createErrorResult(\n ERROR_CODES.SEARCH_BLOCK_NOT_FOUND,\n \"Search block not found in the original content. The content to be replaced could not be located in the file, even with fuzzy matching.\"\n );\n }\n \n const { index: matchStartIndex } = match;\n const matchEndIndex = matchStartIndex + searchLines.length;\n \n const sourceMatchBlock = sourceLines.slice(matchStartIndex, matchEndIndex).join('\\n');\n const sourceMatchIndent = getCommonIndent(sourceMatchBlock);\n\n const replaceLines = block.replace ? block.replace.split('\\n') : [];\n const replaceBaseIndent = getCommonIndent(block.replace);\n \n // Check if this is a substring replacement case\n let reindentedReplaceLines: string[];\n if (searchLines.length === 1 && replaceLines.length === 1 && match.distance > 0) {\n const originalLine = sourceLines[matchStartIndex];\n const searchText = searchLines[0] ?? '';\n const replaceText = replaceLines[0] ?? '';\n \n // If the search text is contained in the original line, do substring replacement\n if (originalLine?.includes(searchText)) {\n // Check if the replacement text looks like a complete line by checking if it contains\n // the non-search parts of the original line\n const nonSearchParts = originalLine.replace(searchText, '').trim();\n if (nonSearchParts.length > 0 && replaceText.includes(nonSearchParts)) {\n // The replace text is a complete new line, use it directly\n reindentedReplaceLines = [replaceText];\n } else {\n // Do substring replacement\n const newLine = originalLine.replace(searchText, replaceText);\n reindentedReplaceLines = [newLine];\n }\n } else if (match.distance > 0) {\n // Fuzzy match case - try to preserve trailing comments\n const originalTrimmed = originalLine?.trim() ?? '';\n \n // Look for trailing comments after semicolon\n const commentMatch = originalTrimmed.match(/;\\s*(\\/\\/.*|\\/\\*.*\\*\\/)$/);\n \n if (commentMatch) {\n const trailingComment = commentMatch[1] ?? '';\n const indent = originalLine?.match(/^[ \\t]*/)?.[0] || \"\";\n const newLine = indent + replaceText.trim() + ' ' + trailingComment;\n reindentedReplaceLines = [newLine];\n } else {\n // Standard replacement with indentation\n reindentedReplaceLines = replaceLines.map(line => {\n if (line.trim() === \"\") return \"\";\n const dedentedLine = line.startsWith(replaceBaseIndent)\n ? line.substring(replaceBaseIndent.length)\n : line;\n return sourceMatchIndent + dedentedLine;\n });\n }\n } else {\n // Standard replacement with indentation\n reindentedReplaceLines = replaceLines.map(line => {\n if (line.trim() === \"\") return \"\";\n const dedentedLine = line.startsWith(replaceBaseIndent)\n ? line.substring(replaceBaseIndent.length)\n : line;\n return sourceMatchIndent + dedentedLine;\n });\n }\n } else {\n // Standard replacement with indentation\n reindentedReplaceLines = replaceLines.map(line => {\n if (line.trim() === \"\") return \"\";\n const dedentedLine = line.startsWith(replaceBaseIndent)\n ? line.substring(replaceBaseIndent.length)\n : line;\n return sourceMatchIndent + dedentedLine;\n });\n }\n\n const newSourceLines = [\n ...sourceLines.slice(0, matchStartIndex),\n ...reindentedReplaceLines,\n ...sourceLines.slice(matchEndIndex)\n ];\n\n currentContent = newSourceLines.join(\"\\n\");\n }\n\n return { success: true, content: currentContent };\n};"]}