Regex Search/Replace for Smart Edit Tool (#10178)

This commit is contained in:
Victor May
2025-09-30 12:06:03 -04:00
committed by GitHub
parent 42436d2ed4
commit ec08129fba
2 changed files with 98 additions and 0 deletions

View File

@@ -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', () => {

View File

@@ -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<ReplacementResult | null> {
@@ -146,6 +155,68 @@ async function calculateFlexibleReplacement(
return null;
}
async function calculateRegexReplacement(
context: ReplacementContext,
): Promise<ReplacementResult | null> {
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,