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。