t-comm
Version:
专业、稳定、纯粹的工具库
189 lines (186 loc) • 7.3 kB
JavaScript
import _toConsumableArray from '@babel/runtime/helpers/toConsumableArray';
/**
* 通用「搜索-替换」工具
*
* 适用场景:AI Agent / Code Mod 需要根据"搜索-替换对"对源文件进行原地修改的场景。
*
* 提供两个函数:
* - cleanLineNumberPrefix:清理 AI 输出 search/replace 块时常带的行号前缀(如 "12| " "12 | ")
* - applySearchReplace:在源代码中查找 search 并替换为 replace,按 "精确 → 行级 → 子串" 三级策略匹配
*/
/**
* 清理 AI 输出 search/replace 块时常带的行号前缀
*
* AI 在生成搜索字符串时,有时会无意把行号也带上,导致与源文件无法精确匹配。
* 本函数支持 5 种常见格式:
* - "12| code" → "code"
* - "12 | code" → "code"
* - "12 code" → "code" (多空格分隔,仅当首行匹配且行号递增时才认为是行号)
* - "12: code" → "code"
* - "12. code" → "code"
*
* 为避免误删(如某些 markdown / 注释开头是数字),仅当:
* - 首行能匹配到行号格式
* - 后续行行号严格递增(差值为 1)
* 时才视为带行号前缀,全部清理。
*
* @param text 原始字符串(来自 AI 输出的 search/replace 块)
* @returns 清理后的字符串;不像行号前缀时原样返回
*/
function cleanLineNumberPrefix(text) {
if (!text) return text;
var lines = text.split('\n');
if (lines.length === 0) return text;
// 5 种常见行号格式
var patterns = [/^(\d+)\|\s?(.*)$/, /^(\d+)\s+\|\s?(.*)$/, /^(\d+)\s{2,}(.*)$/, /^(\d+):\s(.*)$/, /^(\d+)\.\s(.*)$/ // "12. code"
];
// 先用首行确定使用哪种 pattern
var detectedPattern = null;
var firstLineNum = -1;
for (var _i = 0, _patterns = patterns; _i < _patterns.length; _i++) {
var pat = _patterns[_i];
var m = lines[0].match(pat);
if (m) {
detectedPattern = pat;
firstLineNum = parseInt(m[1], 10);
break;
}
}
if (!detectedPattern) return text;
// 校验后续行行号严格递增
var cleanedLines = [];
for (var i = 0; i < lines.length; i += 1) {
var line = lines[i];
var _m = line.match(detectedPattern);
if (!_m) {
// 空行允许保留
if (line.trim() === '') {
cleanedLines.push(line);
continue;
}
// 行号校验失败,原样返回
return text;
}
var num = parseInt(_m[1], 10);
if (i > 0 && num !== firstLineNum + i) {
// 行号不连续,认为不是行号前缀
return text;
}
cleanedLines.push(_m[2]);
}
return cleanedLines.join('\n');
}
/**
* 在源代码中查找 search 并替换为 replace,按 3 级策略匹配
*
* 策略:
* 1. 精确匹配:原样查找 search
* 2. 行级匹配:去除每行首尾空白后逐行对比(应对 AI 输出与源文件缩进不一致)
* 3. 子串匹配:查找首行 trim 后的子串位置,从该位置开始截取与 search 等长的范围替换
*
* @param source 源代码字符串
* @param search 要查找的字符串(可能被 AI 带了行号前缀,函数会自动清理)
* @param replace 替换为的新字符串(同上,自动清理行号前缀)
* @returns 替换后的字符串;查找失败返回 null
*/
function applySearchReplace(fileContent, rawSearch, rawReplace) {
// Level 1
if (fileContent.includes(rawSearch)) {
return {
content: fileContent.replace(rawSearch, rawReplace),
applied: true,
matchLevel: 'exact'
};
}
var searchLines = rawSearch.split('\n');
var contentLines = fileContent.split('\n');
// Level 2
var trimEndMatch = findLineMatch(contentLines, searchLines, function (fileLine, searchLine) {
return fileLine.trimEnd() === searchLine.trimEnd();
});
if (trimEndMatch >= 0) {
return buildReplacedResult(contentLines, trimEndMatch, searchLines.length, rawReplace, 'trimEnd');
}
// Level 3
var indentMatch = findLineMatch(contentLines, searchLines, function (fileLine, searchLine) {
return fileLine.trimStart() === searchLine.trimStart();
});
if (indentMatch >= 0) {
var adjustedReplace = adjustReplaceIndent(rawReplace, searchLines, contentLines, indentMatch);
return buildReplacedResult(contentLines, indentMatch, searchLines.length, adjustedReplace, 'indentNormalize');
}
return {
content: fileContent,
applied: false,
matchLevel: 'none'
};
}
// ============ 内部工具函数 =====// ...省略其他代码...
// ==== 修改后代码 ====
function findLineMatch(contentLines, searchLines, compareFn) {
for (var i = 0; i <= contentLines.length - searchLines.length; i += 1) {
var matched = true;
for (var j = 0; j < searchLines.length; j += 1) {
if (!compareFn(contentLines[i + j], searchLines[j])) {
matched = false;
break;
}
}
if (matched) return i;
}
return -1;
}
function buildReplacedResult(contentLines, matchStart, matchLength, replaceText, matchLevel) {
var before = contentLines.slice(0, matchStart);
var after = contentLines.slice(matchStart + matchLength);
var replaceLines = replaceText.split('\n');
return {
content: [].concat(_toConsumableArray(before), _toConsumableArray(replaceLines), _toConsumableArray(after)).join('\n'),
applied: true,
matchLevel: matchLevel
};
}
function leadingWhitespaceCount(line) {
var m = line.match(/^(\s*)/);
return m ? m[1].length : 0;
}
function leadingWhitespace(line) {
var m = line.match(/^(\s*)/);
return m ? m[1] : '';
}
function adjustReplaceIndent(rawReplace, searchLines, contentLines, matchStart) {
var searchFirstNonEmpty = searchLines.find(function (l) {
return l.trim().length > 0;
});
var fileMatchLines = contentLines.slice(matchStart, matchStart + searchLines.length);
var fileFirstNonEmpty = fileMatchLines.find(function (l) {
return l.trim().length > 0;
});
if (!searchFirstNonEmpty || !fileFirstNonEmpty) return rawReplace;
var searchIndent = leadingWhitespaceCount(searchFirstNonEmpty);
var fileIndent = leadingWhitespaceCount(fileFirstNonEmpty);
var delta = fileIndent - searchIndent;
if (delta === 0) {
var fileWhitespaceChar = leadingWhitespace(fileFirstNonEmpty).includes('\t') ? '\t' : ' ';
var searchWhitespaceChar = leadingWhitespace(searchFirstNonEmpty).includes('\t') ? '\t' : ' ';
if (fileWhitespaceChar === searchWhitespaceChar) return rawReplace;
var _replaceLines = rawReplace.split('\n');
return _replaceLines.map(function (line) {
if (line.trim().length === 0) return line;
var lw = leadingWhitespace(line);
var rest = line.slice(lw.length);
var newLw = lw.replace(searchWhitespaceChar === '\t' ? /\t/g : / /g, fileWhitespaceChar);
return newLw + rest;
}).join('\n');
}
var fileWhitespaceUnit = leadingWhitespace(fileFirstNonEmpty).includes('\t') ? '\t' : ' ';
var replaceLines = rawReplace.split('\n');
return replaceLines.map(function (line) {
if (line.trim().length === 0) return line;
if (delta > 0) return fileWhitespaceUnit.repeat(delta) + line;
var lw = leadingWhitespace(line);
var removeCount = Math.min(Math.abs(delta), lw.length);
return line.slice(removeCount);
}).join('\n');
}
export { applySearchReplace, cleanLineNumberPrefix };