Skip to content

constructNewFileContent : applicateur de blocs SEARCH/REPLACE

源码版本v4.0.10

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 : ------- SEARCH ouvre, ======= sépare, +++++++ REPLACE ferme (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=false donne autant que possible un résultat partiel pour rafraîchir la diff view en temps réel (partial mode:434) ; isFinal=true reconstruit 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 : indexOf exact échoue → match ligne par ligne après trim → match par ancrage première/dernière ligne → indexOf depuis 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 pose pendingOutOfOrderReplacement = true (out of order:384), on ne sort pas tout de suite, on l'accumule dans le tableau replacements, puis à isFinal on 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 ----- SEA ne fasse interpréter les lignes suivantes comme du contenu search.
  • Double implémentation v1 / v2 : constructNewFileContentVersionMapping enregistre à la fois v1 et v2 (version mapping:258), v2 utilise la classe NewFileContentConstructor pour 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 selon version.
  • version mapping:258constructNewFileContentVersionMapping, 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:16SEARCH_BLOCK_CHAR = "-", REPLACE_BLOCK_CHAR = "+", LEGACY_SEARCH_BLOCK_CHAR = "<", LEGACY_REPLACE_BLOCK_CHAR = ">".
  • block regex:22SEARCH_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 classe NewFileContentConstructor pour gérer le contenu non standard et des logiques de réparation plus complexes.
  • internalProcessLine:600 — traitement de ligne central de v2, avec tryFixSearchReplaceBlock, pendingNonStandardLines, etc.
  • caller:502 — point d'appel, le handler passe block.partial inversé comme paramètre isFinal.

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 :

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

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 ------- SEARCH immé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ève Empty 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, pendingOutOfOrderReplacement passe à 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 +++++++ REPLACE fermant (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 &lt; au lieu de < dans le diff ; le handler appelle d'abord applyModelContentFixes avant constructNewFileContent (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.originalContent est vide. Le handler vérifie d'abord isEditing, et si non, il ouvre via diffViewProvider.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.