constructNewFileContent: SEARCH/REPLACE-Block-Anwender
Verantwortung
constructNewFileContent ist Clines Kern-Algorithmus, der den Diff-Inhalt des Werkzeugs replace_in_file auf die Originaldatei anwendet und den neuen Dateiinhalt erzeugt, geschrieben in apps/vscode/src/core/assistant-message/diff.ts. Eingabe sind drei Teile: der Diff-String, den das Modell streamt (mit den drei Markierungen ------- SEARCH / ======= / +++++++ REPLACE segmentiert), der ursprüngliche Dateiinhalt sowie ein isFinal-Flag; die Ausgabe ist { newContent, matchIndices }, wobei newContent der angewandte vollständige Dateiinhalt ist und matchIndices die Startzeichenposition jedes SEARCH-Blocks in der Originaldatei (für UI-Hervorhebungen von Zeilennummern).
Seine Position in der Agent-Loop ist: nachdem parseAssistantMessageV2 einen replace_in_file-ToolUse-Block ausgeschnitten hat, übergibt presentAssistantMessage ihn an WriteToFileToolHandler.executeTool, der ihn in constructNewFileContent call:502 aufruft. Beachte, dass er mehrfach streamend aufgerufen wird: Jedes Mal, wenn das Modell einen weiteren Diff-Teil ausgibt, ruft der Handler ihn einmal mit block.partial=true auf; der Algorithmus muss auch bei unvollständiger Eingabe sinnvolle Partial-Ergebnisse liefern, damit die Diff-Ansicht in Echtzeit vorschauen kann. Wenn block.partial=false, wird das letzte Mal mit isFinal=true aufgerufen und liefert das Endergebnis.
Entwurfsmotivation
- Drei-Markierungen-Protokoll:
------- SEARCHöffnet,=======trennt,+++++++ REPLACEschließt (block regex:22). Besser für LLM geeignet als Unified-Diff: Jeder Block ist in sich geschlossen, das Modell muss keine Zeilennummern berechnen, sondern nur den zu ändernden Originaltext kopieren + neuen Inhalt schreiben. Cline unterstützt auch die älteren Markierungen<<<>>>. - Streaming + Finaler Dual-Modus: Bei
isFinal=falsewerden so gut wie möglich Partial-Ergebnisse geliefert, damit die Diff-Ansicht in Echtzeit aktualisiert wird (partial mode:434); erst beiisFinal=truewird die vollständige Datei anhand der replacement-Liste wirklich neu aufgebaut (isFinal mode:441). Im Partial-Modus gelten noch nicht gesehene REPLACE-Markierungen als «noch nicht beendet», es werden nur die bekannten search/replace-Slices ausgegeben. - Mehrstufige Fallback-Suche: Exaktes
indexOfschlägt fehl → zeilenweises trim-Match → first/last-line-anchor-Match → komplette Datei ab 0 mitindexOf(fallback chain:349). Das Modell gibt häufig zusätzliche Leerzeichen, Tabs oder inkonsistente Einrückungen aus; zeilenweises trim rettet die meisten. Die letzte Rückfall-Suche in der gesamten Datei behandelt den Fall, dass das Modell später liegende SEARCH-Blöcke vorne schreibt (ungeordnet). - out-of-order-Unterstützung: Wenn ein SEARCH-Block vor
lastProcessedIndexgefunden wird, wirdpendingOutOfOrderReplacement = truegesetzt (out of order:384), nicht sofort ausgegeben, sondern imreplacements-Array gesammelt; beiisFinalwird es sortiert nach Startposition einheitlich angewendet (sort and apply:469). - Partial-Marker-Ende-Schutz: Wenn die letzte Zeile mit
-/</=/+/>beginnt, aber kein bekannter Marker ist, ist das ein halber Marker aus dem Stream; er wird direkt gepoppt (pop partial marker:290), um zu verhindern, dass ein halbes----- SEAdie folgenden Zeilen fälschlich als search-Inhalt erkennt. - v1 / v2-Dual-Implementierung:
constructNewFileContentVersionMappingregistriert sowohlv1als auchv2(version mapping:258); v2 nutzt dieNewFileContentConstructor-Klasse für feinere Nicht-Standard-Inhaltsbehandlung (pendingNonStandardLines, tryFixSearchReplaceBlock etc.), v1 ist eine direkte funktionale Implementierung. Aufrufer übergeben standardmäßig v1.
Schlüsseldateien
constructNewFileContent:245— Funktionseinstieg, wählt je nachversionv1 oder v2.version mapping:258—constructNewFileContentVersionMapping, mappt"v1" | "v2"auf konkrete Implementierung.constructNewFileContentV1:266— v1-Hauptschleife: zeilenweises Scannen, Erkennen von SEARCH/REPLACE-Markierungen, Matchen des search-Inhalts, Anwenden von 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>?$/usw., matcht flexible Formate mit 3+ Dashs plus SEARCH.isSearchBlockStart:31— beurteilt, ob eine Zeile ein SEARCH-Start-Marker ist, kompatibel mit alten und neuen Präfixen.lineTrimmedFallbackMatch:51— zeilenweises trim-Match, rettet Fälle mit zusätzlichen Leerzeichen/Tab des Modells.blockAnchorFallbackMatch:132— first/last-line-anchor-Match, für 3+ Zeilen große Blöcke, erlaubt inkonsistente mittlere Zeilen.fallback chain:349— vierstufiger Rückfall: exakt → zeilenweises trim → first/last-anchor → gesamte Datei ab 0.isFinal rebuild:472— bei isFinal=true wird die vollständige Datei sortiert nach replacement-Liste neu aufgebaut; zuerst result geleert, dann replacements einzeln angewendet.getLineNumberFromCharIndex:11— konvertiert Zeichenposition in Zeilennummer, für UI-Hervorhebungen.constructNewFileContentV2:823— v2-Implementierung, nutzt dieNewFileContentConstructor-Klasse für Nicht-Standard-Inhalt und komplexere Fix-Logik.internalProcessLine:600— v2-Kern-Zeilenverarbeitung, mittryFixSearchReplaceBlock,pendingNonStandardLinesetc.caller:502— Aufrufstelle, Handler nutzt die Negation vonblock.partialalsisFinal-Parameter.
Datenfluss
Der v1-Algorithmus scannt den Diff zeilenweise; der kritische Pfad ist «SEARCH-Start begegnen → search-Inhalt akkumulieren → bei ======= zum replace-Akkumulieren wechseln → bei +++++++ REPLACE diese Ersetzung in replacements speichern». Das folgende Stück ist die Logik, die beim Enden des search-Blocks (=======) die vierstufige Fallback-Suche ausführt, um die Match-Position zu finden:
// 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.`,
)
}
}
}
}Dieses Stück liegt nahe fallback chain:349. Nachdem die Position gematcht wurde, wird bei in-order (searchMatchIndex >= lastProcessedIndex) sofort der Originaltext vor lastProcessedIndex + der Originaltext vor der Match-Position an result angehängt, sodass im Partial-Modus die Diff-Ansicht den fortschreitenden Inhalt «vor der Änderung» + «nach der Änderung» sieht (partial output:389). Wenn alle SEARCH/REPLACE-Blöcke verarbeitet sind und isFinal=true gilt, wird der Abschnitt isFinal rebuild:441 betreten: replacements werden nach start sortiert, ab original currentPos=0 wird pro replacement [currentPos, replacement.start) des Originals + replacement.content an result angehängt, zuletzt der verbleibende Originalteil [currentPos, end) ergänzt. Dieses «erst sammeln, dann anwenden» ermöglicht auch die korrekte Anwendung von out-of-order-Blöcken. Bei Fehlern erhöht der Handler in consecutiveMistakeCount++:516 den consecutiveMistakeCount und gibt eine diff_error-say-Nachricht aus, damit die UI den Benutzer benachrichtigt.
Grenzen und Fehler
- search-Block in der Datei nicht gefunden: Wenn alle vier Fallbacks fehlschlagen, wird
The SEARCH block ... does not match anything in the file.geworfen (not found error:374). Der Handler schluckt den Fehler im Partial-Modus, ohne UI zu öffnen (skip partial error:512); nur Fehler im Final-Modus zählen als mistake. - Leerer SEARCH-Block: Das Modell sendet
------- SEARCHdirekt gefolgt von=======, leerer search-Inhalt. Wenn die Originaldatei ebenfalls leer ist, wird es als «neue Datei» an Position 0 eingefügt; sonst wirdEmpty SEARCH block detected with non-empty filegeworfen (empty search error:332), um das Modell zur Formatkorrektur aufzufordern. - Partial-Marker-Rest: Eine letzte Zeile, die wie ein Marker aussieht, aber unvollständig ist (z. B. ein halber
------ SEAaus dem Stream), wird gepoppt, um zu verhindern, dass sie bei der erneuten Analyse in der nächsten Runde als search-Start fehlinterpretiert wird (pop partial marker:290). - out-of-order-Blöcke: Wenn das Modell später liegende SEARCH-Blöcke vorne schreibt, wird
pendingOutOfOrderReplacementauf true gesetzt und die Partial-Ausgabe pausiert (out of order:384); beiisFinal=truewird einheitlich sortiert und angewendet. So sieht die Diff-Ansicht im Partial-Stadium möglicherweise kein vollständiges Ergebnis, aber das Endergebnis ist korrekt. - isFinal noch im replace-Zustand: Wenn der Stream endet, ohne dass
+++++++ REPLACEgeschlossen wurde (missing replace close:444), wird angenommen, dass der replace-Inhalt am Ende des Strings endet; diese Ersetzung wird ebenfalls in replacements gespeichert, um die letzte Bearbeitung nicht zu verlieren. - deepseek-Modell mit unescaped HTML: Das Modell verwendet im Diff
<statt<; der Handler führt vor dem Aufruf vonconstructNewFileContentzunächstapplyModelContentFixesaus (applyModelContentFixes:493), um häufige HTML-Entities zurückzuwandeln, bevor der Diff-Algorithmus sie erhält. - Datei nicht in Diff-Ansicht geöffnet: Das Modell liefert korrekte Inhalte, aber Cline meldet einen Fehler, weil
diffViewProvider.originalContentleer ist. Der Handler prüft zuerstisEditing; wenn nicht geöffnet, wird zunächstdiffViewProvider.openaufgerufen (ensure open:497), bevor diff aufgerufen wird.
Zusammenfassung
constructNewFileContent ist Clines Algorithmus-Kern, um «den vom LLM gestreamten SEARCH/REPLACE-Diff» in «den auf die Festplatte schreibbaren neuen Dateiinhalt» zu verwandeln. Es nutzt eine vierstufige Fallback-Suche, um inkonsistente Modellausgaben zu retten; der Partial/isFinal-Dual-Modus ermöglicht der Diff-Ansicht eine Echtzeit-Vorschau; die out-of-order-Sortierung und der Neuaufbau garantieren ein korrektes Endergebnis. Um zu sehen, wie der Aufrufer dieses Algorithmus das Ergebnis auf die Festplatte schreibt und die Diff-Ansicht aktualisiert, gehe zu /edit-tools/write-to-file; um zu sehen, wie der vorgelagerte ToolUse-Block hereingepusht wird, gehe zu /agent-loop/present-assistant-message.
Siehe offizielle Dokumentation: Cline-Dokumentation · README