const LEFT_SINGLE_CURLY_QUOTE = "\u2018";
const RIGHT_SINGLE_CURLY_QUOTE = "\u2019";
const LEFT_DOUBLE_CURLY_QUOTE = "\u201C";
const RIGHT_DOUBLE_CURLY_QUOTE = "\u201D";
* Normalize curly/smart quotes to their ASCII counterparts.
*/
export function normalizeQuotes(str: string): string {
return str
.replaceAll(LEFT_SINGLE_CURLY_QUOTE, "'")
.replaceAll(RIGHT_SINGLE_CURLY_QUOTE, "'")
.replaceAll(LEFT_DOUBLE_CURLY_QUOTE, '"')
.replaceAll(RIGHT_DOUBLE_CURLY_QUOTE, '"');
}
* Strip trailing whitespace from every line, preserving line endings.
*/
export function stripTrailingWhitespace(str: string): string {
const parts = str.split(/(\r\n|\n|\r)/);
let result = "";
for (let i = 0; i < parts.length; i++) {
const part = parts[i]!;
result += i % 2 === 0 ? part.replace(/\s+$/, "") : part;
}
return result;
}
* Two-level string matching:
* 1. Exact match — returns `searchString` as-is.
* 2. Quote-normalized match — normalizes both sides and maps the index back
* to the original file content.
*
* Returns the *actual* substring from `fileContent` that corresponds to
* `searchString`, or `null` when nothing matches.
*/
export function findActualString(
fileContent: string,
searchString: string,
): string | null {
if (fileContent.includes(searchString)) {
return searchString;
}
const normalizedSearch = normalizeQuotes(searchString);
const normalizedFile = normalizeQuotes(fileContent);
const idx = normalizedFile.indexOf(normalizedSearch);
if (idx !== -1) {
return fileContent.substring(idx, idx + searchString.length);
}
return null;
}
* Pre-process old_string and new_string before applying an edit.
* - Strips trailing whitespace from new_string (except for markdown files).
*/
export function normalizeEditInput(
absolutePath: string,
oldString: string,
newString: string,
): { oldString: string; newString: string } {
const isMarkdown = /\.(md|mdx)$/i.test(absolutePath);
return {
oldString,
newString: isMarkdown ? newString : stripTrailingWhitespace(newString),
};
}