constructNewFileContent : applicateur de blocs SEARCH/REPLACE
Responsabilités
constructNewFileContent est l'algorithme central par lequel Cline applique le diff de l'outil replace_in_file au fichier d'origine pour produire le nouveau contenu, écrit dans apps/vscode/src/core/assistant-message/diff.ts. Il prend trois entrées : la chaîne de diff produite en flux par le modèle (découpée par les trois marqueurs ------- SEARCH / ======= / +++++++ REPLACE), le contenu du fichier d'origine, et un flag isFinal ; il renvoie { newContent, matchIndices }, où newContent est le contenu complet du fichier après application, et matchIndices la position de départ (en caractères) de chaque bloc SEARCH dans le fichier d'origine, pour surligner les lignes côté UI.
Sa place dans la boucle agent : une fois que parseAssistantMessageV2 a extrait un ToolUse block replace_in_file, presentAssistantMessage le transmet à WriteToFileToolHandler.executeTool, qui l'invoque à constructNewFileContent call:502. À noter : il est appelé en flux de nombreuses fois — à chaque fois que le modèle produit un bout de diff supplémentaire, le handler l'invoque avec block.partial=true, et l'algorithme doit produire un résultat partiel raisonnable sur une entrée incomplète, pour que la diff view puisse prévisualiser en temps réel. Quand block.partial=false arrive, le dernier appel avec isFinal=true produit le résultat définitif.
Motivation de conception
- Protocole à trois marqueurs :
------- SEARCHouvre,=======sépare,+++++++ REPLACEferme (block regex:22). Plus adapté à un LLM que le unified diff : chaque bloc est autonome, le modèle n'a pas à calculer de numéros de ligne, il lui suffit de copier le texte d'origine à modifier et d'écrire la nouvelle version. Cline reste compatible avec les anciens marqueurs<<<>>>. - Double mode flux + final :
isFinal=falsedonne autant que possible un résultat partiel pour rafraîchir la diff view en temps réel (partial mode:434) ;isFinal=truereconstruit le fichier complet selon la liste de replacements (isFinal mode:441). En mode partial, un marqueur REPLACE pas encore vu est considéré comme non terminé, on ne sort que les tranches search/replace déjà connues. - Fallback à plusieurs étages :
indexOfexact échoue → match ligne par ligne après trim → match par ancrage première/dernière ligne →indexOfdepuis 0 sur tout le fichier (fallback chain:349). Le modèle produit souvent des espaces ou tabulations en trop, et le trim ligne par ligne sauve la plupart des cas. La recherche pleine-page en dernier recours rattrape les cas où « le modèle a écrit un bloc SEARCH suivant avant le précédent ». - Gestion out-of-order : si un bloc SEARCH est trouvé avant
lastProcessedIndex, on posependingOutOfOrderReplacement = true(out of order:384), on ne sort pas tout de suite, on l'accumule dans le tableaureplacements, puis àisFinalon applique tout d'un coup après tri par position de départ (sort and apply:469). - Protection contre les marqueurs partiels en queue : si la dernière ligne commence par
-/</=/+/>sans être un marqueur connu, c'est un marqueur en cours de streaming, on la pop (pop partial marker:290), pour éviter qu'un demi----- SEAne fasse interpréter les lignes suivantes comme du contenu search. - Double implémentation v1 / v2 :
constructNewFileContentVersionMappingenregistre à la foisv1etv2(version mapping:258), v2 utilise la classeNewFileContentConstructorpour un traitement plus fin du contenu non standard (pendingNonStandardLines, tryFixSearchReplaceBlock, etc.), v1 est une implémentation fonctionnelle directe. Les appelants passent v1 par défaut.
Fichiers clés
constructNewFileContent:245— point d'entrée, choisit l'implémentation v1 ou v2 selonversion.version mapping:258—constructNewFileContentVersionMapping, mappe"v1" | "v2"vers l'implémentation concrète.constructNewFileContentV1:266— boucle principale v1, scanne ligne par ligne, reconnaît les marqueurs SEARCH/REPLACE, matche le contenu search, applique 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>?$/etc., matche un format souple de 3+ tirets suivis de SEARCH.isSearchBlockStart:31— détermine si une ligne est un marqueur de début SEARCH, en restant compatible avec les anciens et nouveaux préfixes.lineTrimmedFallbackMatch:51— match après trim de chaque ligne, rattrape les espaces/tabulations en trop du modèle.blockAnchorFallbackMatch:132— match par ancrage première/dernière ligne, pour les gros blocs de 3+ lignes, tolère des divergences sur les lignes du milieu.fallback chain:349— quatre étages : exact → trim ligne par ligne → anchor première/dernière ligne → recherche pleine-page depuis 0.isFinal rebuild:472— quand isFinal=true, reconstruit le fichier complet à partir de la liste triée de replacements, en vidant result puis en appliquant chaque replacement.getLineNumberFromCharIndex:11— convertit position de caractère en numéro de ligne, pour le surlignage UI.constructNewFileContentV2:823— implémentation v2, utilise la classeNewFileContentConstructorpour gérer le contenu non standard et des logiques de réparation plus complexes.internalProcessLine:600— traitement de ligne central de v2, avectryFixSearchReplaceBlock,pendingNonStandardLines, etc.caller:502— point d'appel, le handler passeblock.partialinversé comme paramètreisFinal.
Flux de données
L'algorithme v1 scanne le diff ligne par ligne, le chemin clé étant « rencontrer un début SEARCH → accumuler le contenu search → rencontrer ======= et basculer sur l'accumulation replace → rencontrer +++++++ REPLACE et stocker ce remplacement dans replacements ». Voici la logique à la fin d'un bloc search (=======) qui enchaîne les quatre fallbacks pour trouver la position de match :
// 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.`,
)
}
}
}
}Ce bloc se trouve près de fallback chain:349. Une fois la position trouvée, si on est in-order (searchMatchIndex >= lastProcessedIndex), on colle tout de suite au result l'original avant lastProcessedIndex + l'original avant la position de match, de sorte qu'en mode partial la diff view voit une progression « avant » + « après » (partial output:389). Une fois tous les blocs SEARCH/REPLACE traités et isFinal=true, on entre à isFinal rebuild:441 : on trie les replacements par start, on part de l'original à currentPos=0, on colle au result le [currentPos, replacement.start) d'original + replacement.content pour chaque entrée, puis on ajoute à la fin l'original restant [currentPos, end). Ce « accumuler puis appliquer » permet aux blocs out-of-order d'être correctement appliqués. En cas d'échec, le handler incrémente consecutiveMistakeCount à consecutiveMistakeCount++:516 et dit un diff_error pour alerter l'utilisateur côté UI.
Limites et échecs
- Bloc search introuvable dans le fichier : quand les quatre fallbacks échouent, on lève
The SEARCH block ... does not match anything in the file.(not found error:374). Le handler avale l'erreur en mode partial sans toucher l'UI (skip partial error:512), seules les erreurs en mode final comptent comme mistake. - Bloc SEARCH vide : le modèle envoie
------- SEARCHimmédiatement suivi de=======, avec un contenu search vide. Si le fichier d'origine est également vide, on considère « nouveau fichier » et on insère à la position 0 ; sinon on lèveEmpty SEARCH block detected with non-empty file(empty search error:332), pour inviter le modèle à corriger le format. - Marqueur partiel résiduel : la dernière ligne ressemble à un marqueur mais est incomplète (par ex. un
------ SEAà moitié streamé), on la pop pour éviter qu'au prochain parsing elle soit interprétée comme un début de search (pop partial marker:290). - Bloc out-of-order : quand le modèle écrit un bloc SEARCH suivant avant le précédent,
pendingOutOfOrderReplacementpasse à vrai, la sortie partial est suspendue (out of order:384), et on applique tout d'un coup trié àisFinal=true. Résultat : la diff view peut ne pas afficher le résultat complet pendant la phase partial, mais le résultat final est correct. - État encore en replace à isFinal : si le flux se termine sans
+++++++ REPLACEfermant (missing replace close:444), on suppose que le contenu replace va jusqu'à la fin de la chaîne et on stocke quand même ce remplacement, pour ne pas perdre la dernière édition. - HTML non échappé pour le modèle deepseek : le modèle utilise
<au lieu de<dans le diff ; le handler appelle d'abordapplyModelContentFixesavantconstructNewFileContent(applyModelContentFixes:493) pour reconvertir les entités HTML courantes, puis passe à l'algorithme de diff. - Fichier non ouvert dans la diff view : le contenu fourni par le modèle est correct mais Cline signale une erreur, car
diffViewProvider.originalContentest vide. Le handler vérifie d'abordisEditing, et si non, il ouvre viadiffViewProvider.open(ensure open:497) avant d'appeler le diff.
Résumé
constructNewFileContent est le cœur algorithmique qui transforme « le diff SEARCH/REPLACE produit en flux par le LLM » en « un nouveau contenu de fichier à écrire sur le disque ». Il rattrape les incohérences du modèle via quatre étages de fallback, le double mode partial/isFinal permet à la diff view de prévisualiser en temps réel, et la reconstruction triée gère les blocs out-of-order pour un résultat final correct. Pour voir comment l'appelant écrit le résultat sur le disque et met à jour la diff view, aller à /edit-tools/write-to-file ; pour voir comment le ToolUse block en amont est poussé, aller à /agent-loop/present-assistant-message.
Voir la documentation officielle : Cline docs · README.