mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
Regex Search/Replace for Smart Edit Tool (#10178)
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user