Skip to content

constructNewFileContent: the SEARCH/REPLACE block applier

源码版本v4.0.10

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: ------- SEARCH opens, ======= separates, +++++++ REPLACE closes (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 when isFinal=true does 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 indexOf fails → line-by-line trimmed match → first/last line anchor match → full-file indexOf again 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 sets pendingOutOfOrderReplacement = true (out of order:384), does not emit immediately, and instead accumulates it into the replacements array; at isFinal it 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 ----- SEA from being misread as search content on the next pass.
  • v1 / v2 dual implementation: constructNewFileContentVersionMapping registers both v1 and v2 (version mapping:258). v2 uses the NewFileContentConstructor class 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 by version.
  • version mapping:258constructNewFileContentVersionMapping, 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:16SEARCH_BLOCK_CHAR = "-", REPLACE_BLOCK_CHAR = "+", LEGACY_SEARCH_BLOCK_CHAR = "<", LEGACY_REPLACE_BLOCK_CHAR = ">".
  • block regex:22SEARCH_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 the NewFileContentConstructor class for non-standard content and more complex fix-up logic.
  • internalProcessLine:600 — v2's core line processing, with mechanisms like tryFixSearchReplaceBlock and pendingNonStandardLines.
  • caller:502 — call site; the handler passes !block.partial as the isFinal argument.

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 (=======):

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.`,
                )
            }
        }
    }
}

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 ------- SEARCH immediately 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 throws Empty 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, pendingOutOfOrderReplacement is set to true and partial output pauses (out of order:384); at isFinal=true they 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 +++++++ REPLACE is 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 &lt; instead of < in the diff; before calling constructNewFileContent, the handler runs applyModelContentFixes (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.originalContent is empty. The handler checks isEditing first; if not open, it calls diffViewProvider.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.

See official docs:Cline 文档 · README