Skip to content

constructNewFileContent: SEARCH/REPLACE-Block-Anwender

源码版本v4.0.10

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, +++++++ REPLACE schließ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=false werden so gut wie möglich Partial-Ergebnisse geliefert, damit die Diff-Ansicht in Echtzeit aktualisiert wird (partial mode:434); erst bei isFinal=true wird 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 indexOf schlägt fehl → zeilenweises trim-Match → first/last-line-anchor-Match → komplette Datei ab 0 mit indexOf (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 lastProcessedIndex gefunden wird, wird pendingOutOfOrderReplacement = true gesetzt (out of order:384), nicht sofort ausgegeben, sondern im replacements-Array gesammelt; bei isFinal wird 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 ----- SEA die folgenden Zeilen fälschlich als search-Inhalt erkennt.
  • v1 / v2-Dual-Implementierung: constructNewFileContentVersionMapping registriert sowohl v1 als auch v2 (version mapping:258); v2 nutzt die NewFileContentConstructor-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 nach version v1 oder v2.
  • version mapping:258constructNewFileContentVersionMapping, 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:16SEARCH_BLOCK_CHAR = "-", REPLACE_BLOCK_CHAR = "+", LEGACY_SEARCH_BLOCK_CHAR = "<", LEGACY_REPLACE_BLOCK_CHAR = ">".
  • block regex:22SEARCH_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 die NewFileContentConstructor-Klasse für Nicht-Standard-Inhalt und komplexere Fix-Logik.
  • internalProcessLine:600 — v2-Kern-Zeilenverarbeitung, mit tryFixSearchReplaceBlock, pendingNonStandardLines etc.
  • caller:502 — Aufrufstelle, Handler nutzt die Negation von block.partial als isFinal-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:

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

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 ------- SEARCH direkt gefolgt von =======, leerer search-Inhalt. Wenn die Originaldatei ebenfalls leer ist, wird es als «neue Datei» an Position 0 eingefügt; sonst wird Empty SEARCH block detected with non-empty file geworfen (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 ------ SEA aus 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 pendingOutOfOrderReplacement auf true gesetzt und die Partial-Ausgabe pausiert (out of order:384); bei isFinal=true wird 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 +++++++ REPLACE geschlossen 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 &lt; statt <; der Handler führt vor dem Aufruf von constructNewFileContent zunächst applyModelContentFixes aus (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.originalContent leer ist. Der Handler prüft zuerst isEditing; wenn nicht geöffnet, wird zunächst diffViewProvider.open aufgerufen (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