constructNewFileContent: the SEARCH/REPLACE block applier
Responsibilities
constructNewFileContent is Cline's core algorithm for applying the diff from the replace_in_file tool to the original file and producing the new file content. It lives in apps/vscode/src/core/assistant-message/diff.ts. Its inputs are three pieces: the diff string the model streamed out (segmented by the three markers ------- SEARCH / ======= / +++++++ REPLACE), the original file content, and an isFinal flag. Its output is { newContent, matchIndices }, where newContent is the full file content after applying the diff and matchIndices is the starting character position of each SEARCH block in the original file (used for UI line-number highlighting).
Its position in the agent loop is this: after parseAssistantMessageV2 slices out a replace_in_file ToolUse block, presentAssistantMessage hands it to WriteToFileToolHandler.executeTool, which calls it at constructNewFileContent call:502. Note that it gets called many times in a streaming fashion: each time the model emits another piece of the diff, the handler invokes it once with block.partial=true, and the algorithm must produce a reasonable partial result from incomplete input so the diff view can preview in real time. Only when block.partial=false does the final call with isFinal=true produce the final result.
Design motivation
- Three-marker protocol:
------- SEARCHopens,=======separates,+++++++ REPLACEcloses (block regex:22). This is better suited to LLMs than unified diff: each block is self-contained, the model does not need to compute line numbers, it just copies the original text to change and writes the new content. Cline also accepts the legacy<<<>>>markers. - Streaming + final dual mode: when
isFinal=false, the algorithm produces a best-effort partial result so the diff view updates in real time (partial mode:434); only whenisFinal=truedoes it actually rebuild the full file from the replacement list (isFinal mode:441). In partial mode, a REPLACE marker that has not yet been seen is treated as "not finished yet" — only the known search/replace slices are emitted. - Multi-layer fallback matching: exact
indexOffails → line-by-line trimmed match → first/last line anchor match → full-fileindexOfagain from 0 (fallback chain:349). Models routinely emit extra spaces, tabs, or inconsistent indentation; the line-by-line trim rescues most of those. The final full-file search handles "the model wrote a later SEARCH block before an earlier one" out-of-order cases. - Out-of-order support: if a SEARCH block matches before
lastProcessedIndex, the code setspendingOutOfOrderReplacement = true(out of order:384), does not emit immediately, and instead accumulates it into thereplacementsarray; atisFinalit sorts by start position and applies them all together (sort and apply:469). - Partial-marker trailing protection: if the last line begins with
-/</=/+/>but is not a known marker, the line is a marker streamed halfway through and is popped off (pop partial marker:290), preventing a half-marker like----- SEAfrom being misread as search content on the next pass. - v1 / v2 dual implementation:
constructNewFileContentVersionMappingregisters bothv1andv2(version mapping:258). v2 uses theNewFileContentConstructorclass for finer handling of non-standard content (pendingNonStandardLines, tryFixSearchReplaceBlock, etc.); v1 is a direct functional implementation. Callers default to v1.
Key files
constructNewFileContent:245— function entry; picks v1 or v2 byversion.version mapping:258—constructNewFileContentVersionMapping, maps"v1" | "v2"to a concrete implementation.constructNewFileContentV1:266— v1 main loop; scans line by line, recognizes SEARCH/REPLACE markers, matches search content, applies 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>?$/etc., matching a flexible format of 3+ dashes followed by SEARCH.isSearchBlockStart:31— decides whether a line is a SEARCH start marker, accepting both new and legacy prefixes.lineTrimmedFallbackMatch:51— matches after trimming each line, rescuing cases where the model emits extra spaces/tabs.blockAnchorFallbackMatch:132— first/last line anchor match, used for large blocks of 3+ lines, allowing middle lines to differ.fallback chain:349— the four-layer fallback: exact match → line-by-line trim → first/last anchor → full-file search from 0.isFinal rebuild:472— when isFinal=true, sorts by the replacement list and rebuilds the full file, clearing result and applying each replacement in turn.getLineNumberFromCharIndex:11— converts character position to line number for UI highlighting.constructNewFileContentV2:823— v2 implementation, using theNewFileContentConstructorclass for non-standard content and more complex fix-up logic.internalProcessLine:600— v2's core line processing, with mechanisms liketryFixSearchReplaceBlockandpendingNonStandardLines.caller:502— call site; the handler passes!block.partialas theisFinalargument.
Data flow
The v1 algorithm scans the diff line by line. The critical path is "see SEARCH start → accumulate search content → see ======= switch to accumulating replace → see +++++++ REPLACE store this replacement into replacements." Below is the logic that runs the four-layer fallback to find the match position when a search block ends (=======):
// 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.`,
)
}
}
}
}This sits near fallback chain:349. After a position is matched, if it is in order (searchMatchIndex >= lastProcessedIndex), the original text up to lastProcessedIndex and the original text up to the match position is appended to result immediately, so in partial mode the diff view sees progressive "before" + "after" content (partial output:389). Once all SEARCH/REPLACE blocks are processed and isFinal=true, the code enters isFinal rebuild:441: it sorts replacements by start, walks the original from currentPos=0, and for each replacement appends [currentPos, replacement.start) of the original plus replacement.content to result, finally appending the remaining [currentPos, end) of the original. This "accumulate first, apply later" approach is what lets out-of-order blocks be applied correctly. On failure, the handler increments consecutiveMistakeCount at consecutiveMistakeCount++:516 and emits a diff_error say so the UI can prompt the user.
Boundaries and failure modes
- search block not found in the file: when all four fallbacks fail, it throws
The SEARCH block ... does not match anything in the file.(not found error:374). The handler swallows this error in partial mode and does not surface UI (skip partial error:512); only a final-mode failure counts as a mistake. - Empty SEARCH block: the model emits
------- SEARCHimmediately followed by=======, with empty search content. If the original file is also empty, it is treated as "create file" and inserted at position 0; otherwise it throwsEmpty SEARCH block detected with non-empty file(empty search error:332), prompting the model to fix the format. - Partial marker residue: the last line looks like a marker but is incomplete (e.g. a half-streamed
------ SEA); it is popped off so the next re-parse does not treat it as a search start (pop partial marker:290). - Out-of-order blocks: when the model writes a later SEARCH block before an earlier one,
pendingOutOfOrderReplacementis set to true and partial output pauses (out of order:384); atisFinal=truethey are sorted and applied together. This means the diff view may not show a complete result during the partial phase, but the final result is correct. - Still in replace state at isFinal: if the stream ends before a closing
+++++++ REPLACEis seen (missing replace close:444), the code assumes the replace content runs to the end of the string and still stores this replacement into replacements, so the last edit is not lost. - deepseek model unescaped HTML: the model writes
<instead of<in the diff; before callingconstructNewFileContent, the handler runsapplyModelContentFixes(applyModelContentFixes:493) to convert common HTML entities back, then hands the result to the diff algorithm. - File not open in diff view: the model's content is correct but Cline reports an error because
diffViewProvider.originalContentis empty. The handler checksisEditingfirst; if not open, it callsdiffViewProvider.open(ensure open:497) before invoking the diff.
Summary
constructNewFileContent is Cline's algorithmic core for turning "SEARCH/REPLACE diffs streamed by the LLM" into "new file content that can be written back to disk." It uses a four-layer fallback chain to rescue inconsistent model output, a partial/isFinal dual mode so the diff view can preview in real time, and an out-of-order sort-and-rebuild to guarantee a correct final result. To see how the caller writes the algorithm's result to disk and updates the diff view, go to /edit-tools/write-to-file; to see how the upstream ToolUse block is pushed in, go to /agent-loop/present-assistant-message.