constructNewFileContent:SEARCH/REPLACE 块应用器
职责
constructNewFileContent 是 Cline 把 replace_in_file 工具的 diff 内容应用到原文件、生成新文件内容的核心算法,写在 apps/vscode/src/core/assistant-message/diff.ts。它的输入是三段:模型流式吐出的 diff 字符串 (用 ------- SEARCH / ======= / +++++++ REPLACE 三个标记分段)、原文件内容、以及 isFinal 标志;输出是 { newContent, matchIndices },newContent 是应用后的完整文件内容,matchIndices 是每个 SEARCH 块在原文件里的起始字符位置 (用于 UI 上高亮行号)。
它在 agent loop 里的位置是:parseAssistantMessageV2 切出一个 replace_in_file 的 ToolUse block 后,presentAssistantMessage 把它交给 WriteToFileToolHandler.executeTool,handler 在 constructNewFileContent call:502 调它。注意它会被流式调用很多次:每次模型多吐一段 diff,handler 就用 block.partial=true 调一次,算法要在不完整输入下也能给出合理的 partial 结果,让 diff view 能实时预览。等 block.partial=false 时调最后一次 isFinal=true 出最终结果。
设计动机
- 三段标记协议:
------- SEARCH开,=======分隔,+++++++ REPLACE闭 (block regex:22)。比 unified diff 更适合 LLM:每个块自包含,模型不需要算行号,只需要复制要改的原文 + 写新内容。Cline 还兼容旧版<<<>>>标记。 - 流式 + 最终双模式:
isFinal=false时尽量给 partial 结果让 diff view 实时更新 (partial mode:434),isFinal=true时才真正按 replacement 列表重建完整文件 (isFinal mode:441)。partial 模式下没看到的 REPLACE 标记会被当成还没结束,只输出已知的 search/replace 切片。 - 多级 fallback 匹配:精确
indexOf失败 → 逐行 trim 匹配 → 首尾行 anchor 匹配 → 全文从 0 重新indexOf(fallback chain:349)。模型经常多输出空格、tab、缩进不一致,逐行 trim 能救大部分。最后兜底的全文搜索处理「模型把后面的 SEARCH 块写在前面」这种乱序情况。 - out-of-order 支持:如果 SEARCH 块在
lastProcessedIndex之前找到,标记pendingOutOfOrderReplacement = true(out of order:384),不再立刻输出,而是攒到replacements数组里,等isFinal时按 start 位置排序统一应用 (sort and apply:469)。 - partial marker 截尾保护:最后一行如果以
-/</=/+/>开头但又不是已知 marker,说明是流到一半的 marker,直接 pop 掉 (pop partial marker:290),防止半个----- SEA把后续行误识别成 search 内容。 - v1 / v2 双实现:
constructNewFileContentVersionMapping把v1和v2都注册 (version mapping:258),v2 用NewFileContentConstructor类做更精细的非标内容处理 (pendingNonStandardLines、tryFixSearchReplaceBlock 等),v1 是直接的函数式实现。调用方默认传 v1。
关键文件
constructNewFileContent:245— 函数入口,按version选 v1 或 v2 实现。version mapping:258—constructNewFileContentVersionMapping,把"v1" | "v2"映射到具体实现。constructNewFileContentV1:266— v1 主循环,逐行扫,识别 SEARCH/REPLACE 标记,匹配 search 内容,应用 replace。block chars:16—SEARCH_BLOCK_CHAR = "-"、REPLACE_BLOCK_CHAR = "+"、LEGACY_SEARCH_BLOCK_CHAR = "<"、LEGACY_REPLACE_BLOCK_CHAR = ">"。block regex:22—SEARCH_BLOCK_START_REGEX = /^[-]{3,} SEARCH>?$/等,匹配 3+ 个 dash 加 SEARCH 的灵活格式。isSearchBlockStart:31— 判断一行是不是 SEARCH 开始标记,兼容新旧前缀。lineTrimmedFallbackMatch:51— 逐行 trim 后匹配,救模型多输出空格/tab 的情况。blockAnchorFallbackMatch:132— 首尾行 anchor 匹配,3+ 行的大块用,允许中间行不一致。fallback chain:349— 精确匹配 → 逐行 trim → 首尾 anchor → 全文从 0 搜的四层兜底。isFinal rebuild:472— isFinal=true 时按 replacement 列表排序重建完整文件,清空 result 后逐个应用 replacement。getLineNumberFromCharIndex:11— 把字符位置转行号,给 UI 高亮用。constructNewFileContentV2:823— v2 实现,用NewFileContentConstructor类处理非标内容和更复杂的修复逻辑。internalProcessLine:600— v2 的核心行处理,带tryFixSearchReplaceBlock、pendingNonStandardLines等机制。caller:502— 调用点,handler 用block.partial取反作为isFinal参数。
数据流
v1 算法逐行扫 diff,关键路径是「遇到 SEARCH 开始 → 累积 search 内容 → 遇到 ======= 切换到 replace 累积 → 遇到 +++++++ REPLACE 把这次替换存进 replacements」。下面这段是 search 块结束 (=======) 时,跑四级 fallback 找匹配位置的逻辑:
// apps/vscode/src/core/assistant-message/diff.ts
// Exact search match scenario
const exactIndex = originalContent.indexOf(currentSearchContent, lastProcessedIndex)
if (exactIndex !== -1) {
searchMatchIndex = exactIndex
searchEndIndex = exactIndex + currentSearchContent.length
} else {
// Attempt fallback line-trimmed matching
const lineMatch = lineTrimmedFallbackMatch(originalContent, currentSearchContent, lastProcessedIndex)
if (lineMatch) {
;[searchMatchIndex, searchEndIndex] = lineMatch
} else {
// Try block anchor fallback for larger blocks
const blockMatch = blockAnchorFallbackMatch(originalContent, currentSearchContent, lastProcessedIndex)
if (blockMatch) {
;[searchMatchIndex, searchEndIndex] = blockMatch
} else {
// Last resort: search the entire file from the beginning
const fullFileIndex = originalContent.indexOf(currentSearchContent, 0)
if (fullFileIndex !== -1) {
searchMatchIndex = fullFileIndex
searchEndIndex = fullFileIndex + currentSearchContent.length
if (searchMatchIndex < lastProcessedIndex) {
pendingOutOfOrderReplacement = true
}
} else {
throw new Error(
`The SEARCH block:\n${currentSearchContent.trimEnd()}\n...does not match anything in the file.`,
)
}
}
}
}这段在 fallback chain:349 附近。匹配到位置后,如果是 in-order (searchMatchIndex >= lastProcessedIndex),立刻把 lastProcessedIndex 之前的原文 + match 位置之前的原文拼到 result 里,这样 partial 模式下 diff view 能看到「改之前」+「改之后」的渐进内容 (partial output:389)。等所有 SEARCH/REPLACE 块处理完且 isFinal=true,进入 isFinal rebuild:441 这段:对 replacements 按 start 排序,从原文 currentPos=0 开始,逐个把 [currentPos, replacement.start) 的原文 + replacement.content 拼到 result,最后补上 [currentPos, end) 的剩余原文。这种「先攒后用」让 out-of-order 块也能正确应用。失败时 handler 在 consecutiveMistakeCount++:516 给 consecutiveMistakeCount 自增,并 say 一条 diff_error 让 UI 提示用户。
边界与失败
- search 块在文件里找不到:四级 fallback 全失败时抛
The SEARCH block ... does not match anything in the file.(not found error:374)。handler 在 partial 模式下吞掉错误不弹 UI (skip partial error:512),只让 final 模式的失败计入 mistake。 - 空 SEARCH 块:模型发了
------- SEARCH紧接=======,空 search 内容。如果原文件也空,当成「新建文件」从位置 0 插入;否则抛Empty SEARCH block detected with non-empty file(empty search error:332),提示模型修正格式。 - partial marker 残留:最后一行像 marker 但不完整 (比如流到一半的
------ SEA),pop 掉防止下一轮重解析时被当成 search 起点 (pop partial marker:290)。 - out-of-order 块:模型把后面的 SEARCH 块写在前面时,
pendingOutOfOrderReplacement置真,partial 输出暂停 (out of order:384),等isFinal=true时统一排序应用。这样 partial 阶段 diff view 可能看不到完整结果,但最终结果是正确的。 - isFinal 时还在 replace 状态:流结束时如果还没看到
+++++++ REPLACE闭合 (missing replace close:444),假设 replace 内容到字符串末尾就结束,把这次替换也存进 replacements,避免丢最后一次编辑。 - deepseek 模型 unescaped HTML:模型在 diff 里用
<而不是<,handler 在调constructNewFileContent前先applyModelContentFixes(applyModelContentFixes:493) 把常见 HTML 实体转回来,再交给 diff 算法。 - 文件没在 diff view 打开:模型给的内容正确但 Cline 报错,因为
diffViewProvider.originalContent是空。handler 先检查isEditing,没开就先diffViewProvider.open(ensure open:497) 再调 diff。
小结
constructNewFileContent 是 Cline 把「LLM 流式吐出的 SEARCH/REPLACE diff」变成「可写回磁盘的新文件内容」的算法核心。它用四级 fallback 匹配救模型不一致的输出,partial/isFinal 双模式让 diff view 能实时预览,out-of-order 排序重建保证最终结果正确。要看这个算法的调用方怎么把它的结果写到磁盘、怎么更新 diff view,转 /edit-tools/write-to-file;要看上游 ToolUse block 怎么被推进来,转 /agent-loop/present-assistant-message。