Skip to content

ApplyPatchHandler : patch multi-fichiers et SEARCH/REPLACE ​

源码版本v4.0.10

Responsabilités ​

ApplyPatchHandler est le processeur de patch multi-fichiers introduit par Cline v4, correspondant au nom d'outil ClineDefaultTool.APPLY_PATCH. Il prend le texte *** Begin Patch / *** End Patch produit par le LLM et, dans un seul bloc tool_use, modifie, ajoute, supprime et déplace plusieurs fichiers simultanément, compressant en un seul appel d'outil ce qui aurait autrefois demandé N write_to_file/replace_in_file successifs (class declaration:45-46).

Il n'emprunte pas le même chemin diff que WriteToFileToolHandler. replace_in_file utilise des blocs SEARCH/REPLACE à délimiteurs 7 caractères (parsés par constructNewFileContent, voir SEARCH block markers:1-3), un seul fichier à la fois ; ApplyPatchHandler utilise un autre format : *** Begin Patch en en-tête, puis *** Add File: path / *** Update File: path / *** Delete File: path pour sectionner par fichier, et à l'intérieur de chaque section des lignes +/- pour ajouter/supprimer (PATCH_MARKERS:4-13). Ce format est issu du protocole apply_patch d'OpenAI, adapté à l'expression d'un diff multi-fichiers en un seul tenant.

Dans l'architecture de Cline, il implémente lui aussi IFullyManagedTool, mais avec un état plus complexe que WriteToFileToolHandler : il détient deux assistants, PathResolver et FileProviderOperations, et execute enchaîne « prétraitement → chargement de tous les fichiers → parsing PatchParser → conversion en commit → prepare par fichier → approbation → save → nettoyage ». Chaque fichier passe individuellement par prepare + approbation + save, et non en un seul lot.

Motivation de conception ​

  • Multi-fichiers en un seul appel : le modèle peut émettre un groupe de changements logiquement liés (modifier une interface et tous ses appelants, par exemple), évitant les allers-retours de N write_to_file successifs (execute entry:210).
  • Réutiliser la vue diff : chaque modification de fichier continue de transiter par DiffViewProvider, donc le patch multi-fichiers se présente à l'approbation comme un diff par fichier, pas comme une boîte noire globale (prepareFileChange:632).
  • Repli sur wrapper bash : le modèle emballe souvent le patch dans ```bash/apply_patch/EOF ; stripBashWrapper débarrasse ces enveloppes avant le parsing (stripBashWrapper:433).
  • Erreur sur sentinelles incomplètes : si seul *** Begin Patch ou *** End Patch est présent, on lève DiffError pour que le modèle réessaie en plus petit, sans tenter de deviner (preprocessLines:416-431).
  • MOVE géré en create + delete : sur Update + *** Move to:, le prepare traite le nouveau chemin comme un create, et après save réussie on supprime le chemin d'origine (move handling:316-323).
  • Fuzz en transparence : PatchParser tolère le fuzz matching ; si fuzz > 0, on ajoute une ligne au résultat pour informer le modèle que la correspondance n'est pas exacte (fuzz note:403-405).

Fichiers clés ​

  • class declaration:45 — export class ApplyPatchHandler implements IFullyManagedTool, name = ClineDefaultTool.APPLY_PATCH.
  • handlePartialBlock:66 — aperçu streaming du patch du premier fichier : extractAllFiles récupère le path, puis previewPatchStream ouvre la vue diff.
  • execute:210 — flux principal : preprocess → loadFiles → PatchParser.parse → patchToCommit → prepare/approve/save par fichier.
  • preprocessLines:416-431 — complète les sentinelles BEGIN/END, lève DiffError("incomplete sentinels") s'il en manque une.
  • stripBashWrapper:433 — retire les enveloppes %%bash / apply_patch / EOF / ``` et conserve le corps du patch.
  • loadFiles:514 — charge d'un coup tous les chemins UPDATE/DELETE ; une correspondance clineignore (hit) ou un fichier absent lève DiffError.
  • patchToCommit:539 — convertit le Patch en Commit : DELETE stocke oldContent, ADD stocke newContent, UPDATE appelle applyChunks.
  • applyChunks:584 — découpe le fichier original selon l'origIndex de chaque chunk, copie les segments non modifiés, insère insLines, saute delLines.
  • prepareFileChange:632 — utilise FileProviderOperations pour ouvrir la vue diff et update, sans save.
  • handleApproval:718 — un ask par fichier, avec télémétrie fileOps (filesCreated/Deleted/Moved).
  • constructNewFileContent:245 — parseur SEARCH/REPLACE utilisé par replace_in_file, chemin indépendant d'apply_patch.
  • PATCH_MARKERS:4 — constantes de toutes les sentinelles de patch.

Flux de données ​

execute démarre par prétraitement et chargement, parse le texte du patch en un commit structuré, puis boucle par fichier sur approbation puis écriture. Le cœur est la conversion 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 complète ou rejette les entrées sans sentinelles (preprocessLines:416) ; loadFiles lit en mémoire les fichiers originaux visés par UPDATE/DELETE ; PatchParser parse le tableau de lignes en Patch (actions + chunks + facteur fuzz) ; patchToCommit convertit chaque action en FileChange. La branche UPDATE appelle applyChunks, qui découpe et réassemble le tableau de lignes du fichier original selon origIndex :

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))

Une fois le commit construit, generateChangeSummary produit un message ClineSayTool par fichier, puis on enchaîne par fichier prepareFileChange (ouvre la vue diff, update, sans save) → handleApproval (auto ou ask) → saveFileChange. L'opération MOVE déclenche deleteFile(originalPath) après save réussie du nouveau fichier. Quand tous les fichiers sont passés, on appelle uniformément markFileAsEditedByCline + trackFileContext("cline_edited") + invalidation du fileReadCache sur chaque changedFilePath.

Limites et échecs ​

  • Sentinelles incomplètes : si seul BEGIN ou seul END est présent, on lève DiffError et on invite le modèle à « découper en plus petits patchs » (incomplete sentinels:429-431).
  • clineignore touché : une correspondance (hit) à l'étape loadFiles lève DiffError, le patch entier est abandonné, aucune application partielle (clineignore throw:522-526).
  • fichier absent : si la cible d'un UPDATE/DELETE n'est pas sur le disque, on lève File not found DiffError (file not found:528-530).
  • un refus utilisateur, tout revient en arrière : si handleApproval renvoie false, on appelle immédiatement revertChanges + reset et on renvoie un message de refus ; les fichiers suivants ne sont pas traités (reject abort:304-310).
  • invalidation du cache MOVE : pour un MOVE, on invalide le fileReadCache à la fois sur le nouveau chemin et sur l'ancien, afin que la prochaine lecture de l'ancien fichier ne récupère pas un contenu déjà supprimé depuis le cache (move cache invalidate:343-345).
  • partial block silencieux : toute erreur de parsing dans previewPatchStream est catchée silencieusement, en attendant davantage de données pour réessayer (partial catch:83-85).
  • chunk désordonné : si currentIndex dépasse origIndex (les chunks ne correspondent plus au fichier original), on lève DiffError (chunk order check:597-599).

Résumé ​

ApplyPatchHandler est la version multi-fichiers de WriteToFileToolHandler : il exprime des changements inter-fichiers dans un seul tool_use via le protocole *** Begin Patch / *** End Patch. Il et replace_in_file suivent deux chemins de parsing indépendants : le premier découpe par lignes avec PatchParser, le second localise via constructNewFileContent et ses blocs SEARCH/REPLACE. Les deux aboutissent in fine au DiffViewProvider pour présentation à l'utilisateur.

Pour aller plus loin :

  • Override de fichier unique / SEARCH/REPLACE : /edit-tools/write-to-file
  • Sous-couche de la vue diff : /edit-tools/diff-view-provider
  • Comment Task distribue les outils : /agent-loop/task-class

Voir la documentation officielle : documentation Cline · README.