Skip to content

ApplyPatchHandler: Multi-File-Patches und SEARCH/REPLACE

源码版本v4.0.10

Verantwortung

ApplyPatchHandler ist der in Cline v4 eingeführte Multi-File-Patch-Prozessor und entspricht dem Werkzeugnamen ClineDefaultTool.APPLY_PATCH. Er nimmt einen kompletten, vom LLM ausgegebenen *** Begin Patch / *** End Patch-Textblock entgegen und kann in einem einzigen tool_use-Block mehrere Dateien gleichzeitig ändern, hinzufügen, löschen und verschieben, sodass Mehrfachdatei-Änderungen, die früher in N aufeinanderfolgende write_to_file/replace_in_file-Aufrufe aufgespalten werden mussten, in einem einzigen Werkzeugaufruf zusammenlaufen (class declaration:45-46).

Er läuft nicht über denselben Diff-Pfad wie WriteToFileToolHandler. replace_in_file verwendet SEARCH/REPLACE-Blocks mit 7-Zeichen-Begrenzern (geparst von constructNewFileContent, siehe SEARCH block markers:1-3) und kann nur eine Datei pro Aufruf ändern; ApplyPatchHandler nutzt ein anderes Format: Eingeleitet mit *** Begin Patch, dann *** Add File: path / *** Update File: path / *** Delete File: path als Datei-Blöcke, innerhalb derer +/--Zeilen Hinzufügungen und Löschungen markieren (PATCH_MARKERS:4-13). Dieses Format stammt aus dem apply_patch-Protokoll von OpenAI und eignet sich, um Änderungen über mehrere Dateien hinweg in einem Schritt auszudrücken.

In der Cline-Architektur implementiert er ebenfalls IFullyManagedTool, ist aber zustandskomplexer als WriteToFileToolHandler: Er hält zwei Helfer, PathResolver und FileProviderOperations, und führt in execute die Pipeline „Präprozessierung → Laden aller zu ändernden Dateien → PatchParser-Parsing → Umwandlung in Commit → Datei für Datei prepare → Approval → save → Aufräumen" aus. Jede Datei durchläuft einzeln prepare + Approval + save, nicht alles in einem Rutsch.

Entwurfsmotivation

  • Multi-File, einzelner Aufruf: Das Modell kann eine Gruppe logisch zusammenhängender Änderungen (z. B. Interface ändern und gleichzeitig alle Aufrufer anpassen) in einem Schritt ausgeben, um den Overhead von N aufeinanderfolgenden WriteToFileToolHandler-Aufrufen zu vermeiden (execute entry:210).
  • Diff-Ansicht wiederverwenden: Die Änderung jeder Datei wird weiterhin über den DiffViewProvider dem Nutzer präsentiert, sodass der Approval-Schritt für einen Multi-File-Patch dateiweise als Diff erfolgt statt als einmaliger Blackbox (prepareFileChange:632).
  • bash-Wrapper-Fallback: Modelle packen den Patch oft in ```bash/apply_patch/EOF ein; stripBashWrapper entfernt diese Hüllen vor dem Parsen (stripBashWrapper:433).
  • Fehler bei unvollständigen Sentinels: Wenn nur *** Begin Patch oder nur *** End Patch vorhanden ist, wird ein DiffError geworfen, damit das Modell in kleineren Schritten neu versucht, ohne zu raten (preprocessLines:416-431).
  • MOVE als eigener create+delete-Pfad: Bei Update + *** Move to: behandelt die prepare-Phase den neuen Pfad als create; nach erfolgreichem save wird der Originalpfad gelöscht (move handling:316-323).
  • fuzz durchreichen: PatchParser erlaubt fuzz-Matching beim Parsen; bei fuzz > 0 wird im Ergebnis ein Hinweis angefügt, damit das Modell erfährt, dass die Übereinstimmung ungenau war (fuzz note:403-405).

Schlüsseldateien

  • class declaration:45export class ApplyPatchHandler implements IFullyManagedTool,name = ClineDefaultTool.APPLY_PATCH.
  • handlePartialBlock:66 — Streamt eine Vorschau des Patches der ersten Datei; zuerst extractAllFiles für den Pfad, dann previewPatchStream öffnet die Diff-Ansicht.
  • execute:210 — Hauptfluss: preprocess → loadFiles → PatchParser.parse → patchToCommit → dateiweises prepare/approve/save.
  • preprocessLines:416-431 — Fehlende BEGIN/END-Sentinels ergänzen; fehlt genau eines, wird DiffError("incomplete sentinels") geworfen.
  • stripBashWrapper:433 — Entfernt %%bash / apply_patch / EOF / ```-Wrapper und behält den Patch-Körper.
  • loadFiles:514 — Liest alle UPDATE/DELETE-Pfade auf einmal ein; clineignore-Treffer oder nicht existierende Datei werfen DiffError.
  • patchToCommit:539 — Wandelt einen Patch in eine Commit-Struktur um: DELETE speichert oldContent, ADD speichert newContent, UPDATE ruft applyChunks auf.
  • applyChunks:584 — Schneidet die Originaldatei an chunk.origIndex, kopiert unveränderte Segmente, fügt insLines ein und überspringt delLines.
  • prepareFileChange:632 — Öffnet mit FileProviderOperations die Diff-Ansicht und update, ohne zu save.
  • handleApproval:718 — Fragt für jede Datei einzeln ab; mit fileOps (filesCreated/Deleted/Moved) wird Telemetrie gemeldet.
  • constructNewFileContent:245 — Der SEARCH/REPLACE-Parser, über den replace_in_file läuft; er und apply_patch sind zwei unabhängige Pfade.
  • PATCH_MARKERS:4 — Konstanten für alle Patch-Sentinels.

Datenfluss

execute beginnt mit Präprozessierung und Laden; der ganze Patch-Text wird in einen strukturierten Commit geparst und dann dateiweise durch den Approval-Speicher-Zyklus geführt. Im Zentrum steht die Umwandlung patch → commit:

typescript
// apps/vscode/src/core/task/tools/handlers/ApplyPatchHandler.ts
const lines = this.preprocessLines(rawInput)
const filesToLoad = this.extractFilesForOperations(rawInput, [PATCH_MARKERS.UPDATE, PATCH_MARKERS.DELETE])
const currentFiles = await this.loadFiles(config, filesToLoad)

const parser = new PatchParser(lines, currentFiles)
const { patch, fuzz } = parser.parse()

const commit = await this.patchToCommit(patch, currentFiles)

preprocessLines ergänzt Eingaben mit fehlenden Sentinels oder lehnt sie ab (preprocessLines:416); loadFiles liest die von UPDATE/DELETE betroffenen Originaldateien in den Speicher; PatchParser parst das Zeilen-Array in einen Patch (actions + chunks + fuzz-Faktor); patchToCommit wandelt jede action in eine FileChange um. Der UPDATE-Zweig ruft applyChunks auf, was im Wesentlichen das Zeilen-Array der Originaldatei anhand von origIndex neu zusammensetzt:

typescript
// apps/vscode/src/core/task/tools/handlers/ApplyPatchHandler.ts
for (const chunk of chunks) {
    if (chunk.origIndex > lines.length) {
        throw new DiffError(`${path}: chunk.origIndex ${chunk.origIndex} > lines.length ${lines.length}`)
    }
    // Copy lines before the chunk
    result.push(...lines.slice(currentIndex, chunk.origIndex))
    const originalLines = lines.slice(chunk.origIndex, chunk.origIndex + chunk.delLines.length)
    const insertedLines = chunk.insLines.map((line) => {
        if (tryPreserveEscaping && originalText) {
            return preserveEscaping(originalText, line)
        }
        return line
    })
    result.push(...insertedLines)
    currentIndex = chunk.origIndex + chunk.delLines.length
}
result.push(...lines.slice(currentIndex))

Nachdem der Commit konstruiert ist, erzeugt generateChangeSummary für jede Datei eine ClineSayTool-Nachricht; dann wird dateiweise prepareFileChange (Diff-Ansicht öffnen, update, nicht save) → handleApproval (auto oder ask) → saveFileChange ausgeführt. Bei einer MOVE-Operation wird nach erfolgreichem save der neuen Datei deleteFile(originalPath) aufgerufen. Nachdem alle Dateien durchlaufen sind, wird auf jedem changedFilePath einheitlich markFileAsEditedByCline + trackFileContext("cline_edited") ausgeführt und der fileReadCache invalidiert.

Grenzen und Fehler

  • Unvollständige Sentinels: Nur BEGIN oder nur END wirft sofort DiffError und weist das Modell an, „in kleinere Patches aufzuspalten und neu zu versuchen" (incomplete sentinels:429-431).
  • clineignore-Treffer: Bei Treffer in loadFiles wird DiffError geworfen, der gesamte Patch wird abgebrochen und nicht teilweise angewendet (clineignore throw:522-526).
  • Datei nicht vorhanden: Ist die von UPDATE/DELETE angesprochene Datei auf der Platte nicht zu finden, wird ein File not found DiffError geworfen (file not found:528-530).
  • Nutzer lehnt eine Datei ab, alle werden zurückgerollt: Gibt handleApproval false zurück, wird sofort revertChanges + reset aufgerufen und eine Ablehnungsnachricht zurückgegeben; die weiteren Dateien werden nicht mehr bearbeitet (reject abort:304-310).
  • MOVE: alter Pfad verliert Cache: Der fileReadCache einer MOVE muss gleichzeitig für den neuen und den alten Pfad invalidiert werden, damit ein künftiger Lesezugriff auf die alte Datei nicht aus dem Cache gelöschten Inhalt liefert (move cache invalidate:343-345).
  • partial block schweigt: Jeder Parse-Fehler in previewPatchStream wird im catch stillgeschwiegen; es wird auf weitere Daten gewartet und dann erneut versucht (partial catch:83-85).
  • chunk-Reihenfolge falsch: Wenn currentIndex größer als origIndex ist (die chunk-Reihenfolge passt nicht zur Originaldatei), wird DiffError geworfen (chunk order check:597-599).

Zusammenfassung

ApplyPatchHandler ist die Multi-File-Version des WriteToFileToolHandler: Sie verwendet das *** Begin Patch / *** End Patch-Protokoll, um dateiübergreifende Änderungen in einem einzelnen tool_use auszudrücken. Sie und replace_in_file laufen über jeweils einen unabhängigen Parse-Pfad: Ersterer nutzt PatchParser zum zeilenweisen Zerschneiden, Letzterer nutzt constructNewFileContent zur Lokalisierung über SEARCH/REPLACE-Blocks. Beide landen letztlich im DiffViewProvider, um dem Nutzer präsentiert zu werden.

Wer tiefer einsteigen will, kann weiterlesen bei:

  • Single-File-Überschreibung / SEARCH/REPLACE: /edit-tools/write-to-file
  • Diff-Ansicht-Grundlage: /edit-tools/diff-view-provider
  • Wie die Task Werkzeuge ansteuert: /agent-loop/task-class

Siehe offizielle Dokumentation: Cline-Dokumentation · README