Skip to content

constructNewFileContent:SEARCH/REPLACE 块应用器

源码版本v4.0.10

职责

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 双实现:constructNewFileContentVersionMappingv1v2 都注册 (version mapping:258),v2 用 NewFileContentConstructor 类做更精细的非标内容处理 (pendingNonStandardLines、tryFixSearchReplaceBlock 等),v1 是直接的函数式实现。调用方默认传 v1。

关键文件

数据流

v1 算法逐行扫 diff,关键路径是「遇到 SEARCH 开始 → 累积 search 内容 → 遇到 ======= 切换到 replace 累积 → 遇到 +++++++ REPLACE 把这次替换存进 replacements」。下面这段是 search 块结束 (=======) 时,跑四级 fallback 找匹配位置的逻辑:

typescript
// 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++:516consecutiveMistakeCount 自增,并 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 里用 &lt; 而不是 <,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

对照官方资料:Cline 文档 · README