Skip to content

constructNewFileContent: aplicador de bloques SEARCH/REPLACE

源码版本v4.0.10

Responsabilidades

constructNewFileContent es el algoritmo núcleo de Cline que aplica el contenido diff de la herramienta replace_in_file al archivo original y genera el contenido nuevo. Vive en apps/vscode/src/core/assistant-message/diff.ts. Su entrada son tres partes: la cadena diff que el modelo emite en streaming (con tres marcadores ------- SEARCH / ======= / +++++++ REPLACE), el contenido original del archivo y un flag isFinal; la salida es { newContent, matchIndices }, donde newContent es el contenido completo tras aplicar el diff y matchIndices es la posición inicial (en caracteres) de cada bloque SEARCH en el archivo original (usado por la UI para resaltar números de línea).

Dentro del agent loop (bucle del agente): parseAssistantMessageV2 recorta un ToolUse block de replace_in_file, presentAssistantMessage lo entrega a WriteToFileToolHandler.executeTool, y el handler lo llama en constructNewFileContent call:502. Ojo: se invoca muchas veces en streaming; cada vez que el modelo emite más diff, el handler lo llama con block.partial=true, y el algoritmo debe producir un partial razonable con input incompleto para que la vista diff pueda previsualizar en vivo. Cuando llega block.partial=false, se llama una última vez con isFinal=true para el resultado final.

Motivación de diseño

  • Protocolo de tres marcadores: abre con ------- SEARCH, separa con ======= y cierra con +++++++ REPLACE (block regex:22). Es más adecuado para LLM que el unified diff: cada bloque es autocontenido, el modelo no calcula números de línea, solo copia el original a cambiar y escribe el nuevo. Cline también soporta los marcadores legacy <<< >>>.
  • Modo dual streaming + final: con isFinal=false entrega lo antes posible un partial para que la vista diff se actualice en vivo (partial mode:434); con isFinal=true reconstruye el archivo completo según la lista de replacements (isFinal mode:441). En partial, un marcador REPLACE aún no visto se trata como «todavía no terminado» y solo se emiten los slice search/replace ya conocidos.
  • Cadena de fallback multinivel: si el indexOf exacto falla → match por línea con trim → match por ancla de primera/última línea → indexOf desde 0 en todo el archivo (fallback chain:349). El modelo suele añadir espacios, tabs e indentación inconsistente; el trim por líneas rescata la mayoría. El último fallback de búsqueda full-text cubre el caso en que el modelo escribió primero un bloque SEARCH que debería ir después (out-of-order).
  • Soporte out-of-order: si el bloque SEARCH se encuentra antes de lastProcessedIndex, se marca pendingOutOfOrderReplacement = true (out of order:384), no se emite de inmediato, sino que se acumula en el arreglo replacements y, al llegar isFinal, se ordena por posición inicial y se aplica de forma unificada (sort and apply:469).
  • Protección de cola con marcador parcial: si la última línea empieza con -/</=/+/> pero no coincide con un marcador conocido, es un marcador a medio streaming; se hace pop (pop partial marker:290), evitando que medio ----- SEA haga que las líneas siguientes se confundan con contenido SEARCH.
  • Doble implementación v1 / v2: constructNewFileContentVersionMapping registra v1 y v2 (version mapping:258); v2 usa la clase NewFileContentConstructor con manejo más fino de contenido non-standard (pendingNonStandardLines, tryFixSearchReplaceBlock, etc.); v1 es una implementación funcional directa. El llamador por defecto pasa v1.

Archivos clave

  • constructNewFileContent:245 — entrada de la función; según version elige v1 o v2.
  • version mapping:258constructNewFileContentVersionMapping, mapea "v1" | "v2" a la implementación concreta.
  • constructNewFileContentV1:266 — bucle principal de v1: escanea línea por línea, detecta marcadores SEARCH/REPLACE, matchea el contenido search y aplica 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>?$/ y similares; matchean formatos flexibles de 3+ guiones seguidos de SEARCH.
  • isSearchBlockStart:31 — decide si una línea es inicio SEARCH; compatible con prefijos nuevos y legacy.
  • lineTrimmedFallbackMatch:51 — match tras trim por líneas, rescata espacios/tabs extras del modelo.
  • blockAnchorFallbackMatch:132 — match por anclas primera/última línea, para bloques grandes de 3+ líneas, tolera discrepancias en líneas intermedias.
  • fallback chain:349 — cadena de 4 niveles: exacto → trim por líneas → anclas → buscar desde 0 full-text.
  • isFinal rebuild:472 — con isFinal=true, ordena la lista de replacements y reconstruye el archivo completo, vacía result y aplica cada replacement.
  • getLineNumberFromCharIndex:11 — convierte posición de caracteres en número de línea, para highlight en UI.
  • constructNewFileContentV2:823 — implementación v2, con la clase NewFileContentConstructor para contenido non-standard y lógica de reparación más compleja.
  • internalProcessLine:600 — procesamiento de línea núcleo de v2, con tryFixSearchReplaceBlock, pendingNonStandardLines, etc.
  • caller:502 — punto de llamada; el handler pasa !block.partial como isFinal.

Flujo de datos

El algoritmo v1 escanea diff línea por línea; la ruta clave es «encontrar inicio SEARCH → acumular contenido search → al encontrar ======= cambiar a acumular replace → al encontrar +++++++ REPLACE guardar el replacement en replacements». Este fragmento es la lógica de fallback de 4 niveles que corre al cerrar el bloque search (=======):

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

Esto está cerca de fallback chain:349. Tras localizar, si es in-order (searchMatchIndex >= lastProcessedIndex), se pega en result el original antes de lastProcessedIndex y el original antes de la posición de match, de modo que en partial mode la vista diff vea contenido progresivo «antes» + «después» (partial output:389). Al terminar todos los bloques SEARCH/REPLACE con isFinal=true, se entra en isFinal rebuild:441: se ordenan replacements por start, y desde currentPos=0 del original se pegan por pedazos [currentPos, replacement.start) + replacement.content en result, cerrando al final con el resto [currentPos, end). Este «acumular primero, aplicar después» permite que los bloques out-of-order se apliquen correctamente. En fallo, el handler incrementa consecutiveMistakeCount (consecutiveMistakeCount++:516) y hace say de un diff_error para que la UI avise al usuario.

Límites y fallos

  • Bloque search no encontrado: si los 4 fallbacks fallan, lanza The SEARCH block ... does not match anything in the file. (not found error:374). El handler lo atrapa en partial mode sin lanzar UI (skip partial error:512); solo los fallos en modo final cuentan como mistake.
  • Bloque SEARCH vacío: el modelo envía ------- SEARCH seguido de ======= con contenido vacío. Si el archivo original también está vacío, se trata como «crear archivo» y se inserta en posición 0; si no, lanza Empty SEARCH block detected with non-empty file (empty search error:332), sugiriendo al modelo corregir el formato.
  • Residuo de marcador parcial: la última línea se parece a un marcador pero está incompleta (p. ej. ------ SEA a medias); se hace pop para que en el siguiente ciclo no se la tome como inicio SEARCH (pop partial marker:290).
  • Bloque out-of-order: si el modelo coloca un bloque SEARCH posterior antes, pendingOutOfOrderReplacement se hace true y la salida partial se pausa (out of order:384), esperando a isFinal=true para ordenar y aplicar. En la fase partial, la vista diff puede no ver el resultado completo, pero el resultado final es correcto.
  • En isFinal aún en estado replace: si el stream termina sin haber visto +++++++ REPLACE (missing replace close:444), se asume que el contenido replace llega hasta el final de la cadena y se almacena igual en replacements, para no perder la última edición.
  • HTML sin escapar en modelos deepseek: el modelo usa &lt; en vez de < en el diff; el handler llama applyModelContentFixes antes de constructNewFileContent (applyModelContentFixes:493) para revertir entidades HTML comunes, y luego alimenta el algoritmo diff.
  • Archivo no abierto en diff view: el modelo da contenido correcto pero Cline reporta error porque diffViewProvider.originalContent está vacío. El handler verifica isEditing primero; si no está abierto, llama diffViewProvider.open (ensure open:497) antes de aplicar el diff.

Resumen

constructNewFileContent es el algoritmo núcleo que convierte «el diff SEARCH/REPLACE que el LLM emite en streaming» en «contenido nuevo escribible a disco». Usa una cadena de 4 niveles de fallback para rescatar la salida inconsistente del modelo; el dual partial/isFinal permite previsualizar en vivo; el ordenamiento de bloques out-of-order garantiza un resultado final correcto. Para ver cómo el llamador escribe el resultado a disco y actualiza la vista diff, ver /edit-tools/write-to-file; para ver cómo se empuja el ToolUse block上游, ver /agent-loop/present-assistant-message.

Véase la documentación oficial: Cline 文档 · README