UNPKG

t-comm

Version:

专业、稳定、纯粹的工具库

189 lines (186 loc) 7.3 kB
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 };