diff --git a/packages/core/src/tools/smart-edit.test.ts b/packages/core/src/tools/smart-edit.test.ts index a14dd8add1..9462ded913 100644 --- a/packages/core/src/tools/smart-edit.test.ts +++ b/packages/core/src/tools/smart-edit.test.ts @@ -240,6 +240,28 @@ describe('SmartEditTool', () => { expect(result.newContent).toBe(content); expect(result.occurrences).toBe(0); }); + + it('should perform a regex-based replacement for flexible intra-line whitespace', async () => { + // This case would fail with the previous exact and line-trimming flexible logic + // because the whitespace *within* the line is different. + const content = ' function myFunc( a, b ) {\n return a + b;\n }'; + const result = await calculateReplacement({ + params: { + file_path: 'test.js', + instruction: 'test', + old_string: 'function myFunc(a, b) {', // Note the normalized whitespace + new_string: 'const yourFunc = (a, b) => {', + }, + currentContent: content, + abortSignal, + }); + + // The indentation from the original line should be preserved and applied to the new string. + const expectedContent = + ' const yourFunc = (a, b) => {\n return a + b;\n }'; + expect(result.newContent).toBe(expectedContent); + expect(result.occurrences).toBe(1); + }); }); describe('correctPath', () => { it('should correct a relative path if it is unambiguous', () => { diff --git a/packages/core/src/tools/smart-edit.ts b/packages/core/src/tools/smart-edit.ts index 6abd318c10..ea93e9023b 100644 --- a/packages/core/src/tools/smart-edit.ts +++ b/packages/core/src/tools/smart-edit.ts @@ -59,6 +59,15 @@ function restoreTrailingNewline( return modifiedContent; } +/** + * Escapes characters with special meaning in regular expressions. + * @param str The string to escape. + * @returns The escaped string. + */ +function escapeRegex(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + async function calculateExactReplacement( context: ReplacementContext, ): Promise { @@ -146,6 +155,68 @@ async function calculateFlexibleReplacement( return null; } +async function calculateRegexReplacement( + context: ReplacementContext, +): Promise { + const { currentContent, params } = context; + const { old_string, new_string } = params; + + // Normalize line endings for consistent processing. + const normalizedSearch = old_string.replace(/\r\n/g, '\n'); + const normalizedReplace = new_string.replace(/\r\n/g, '\n'); + + // This logic is ported from your Python implementation. + // It builds a flexible, multi-line regex from a search string. + const delimiters = ['(', ')', ':', '[', ']', '{', '}', '>', '<', '=']; + + let processedString = normalizedSearch; + for (const delim of delimiters) { + processedString = processedString.split(delim).join(` ${delim} `); + } + + // Split by any whitespace and remove empty strings. + const tokens = processedString.split(/\s+/).filter(Boolean); + + if (tokens.length === 0) { + return null; + } + + const escapedTokens = tokens.map(escapeRegex); + // Join tokens with `\s*` to allow for flexible whitespace between them. + const pattern = escapedTokens.join('\\s*'); + + // The final pattern captures leading whitespace (indentation) and then matches the token pattern. + // 'm' flag enables multi-line mode, so '^' matches the start of any line. + const finalPattern = `^(\\s*)${pattern}`; + const flexibleRegex = new RegExp(finalPattern, 'm'); + + const match = flexibleRegex.exec(currentContent); + + if (!match) { + return null; + } + + const indentation = match[1] || ''; + const newLines = normalizedReplace.split('\n'); + const newBlockWithIndent = newLines + .map((line) => `${indentation}${line}`) + .join('\n'); + + // Use replace with the regex to substitute the matched content. + // Since the regex doesn't have the 'g' flag, it will only replace the first occurrence. + const modifiedCode = currentContent.replace( + flexibleRegex, + newBlockWithIndent, + ); + + return { + newContent: restoreTrailingNewline(currentContent, modifiedCode), + occurrences: 1, // This method is designed to find and replace only the first occurrence. + finalOldString: normalizedSearch, + finalNewString: normalizedReplace, + }; +} + /** * Detects the line ending style of a string. * @param content The string content to analyze. @@ -184,6 +255,11 @@ export async function calculateReplacement( return flexibleResult; } + const regexResult = await calculateRegexReplacement(context); + if (regexResult) { + return regexResult; + } + return { newContent: currentContent, occurrences: 0,