constructNewFileContent: aplicador de bloques SEARCH/REPLACE
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=falseentrega lo antes posible un partial para que la vista diff se actualice en vivo (partial mode:434); conisFinal=truereconstruye 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
indexOfexacto falla → match por línea con trim → match por ancla de primera/última línea →indexOfdesde 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 marcapendingOutOfOrderReplacement = true(out of order:384), no se emite de inmediato, sino que se acumula en el arregloreplacementsy, al llegarisFinal, 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----- SEAhaga que las líneas siguientes se confundan con contenido SEARCH. - Doble implementación v1 / v2:
constructNewFileContentVersionMappingregistra v1 y v2 (version mapping:258); v2 usa la claseNewFileContentConstructorcon 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únversionelige v1 o v2.version mapping:258—constructNewFileContentVersionMapping, 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:16—SEARCH_BLOCK_CHAR = "-",REPLACE_BLOCK_CHAR = "+",LEGACY_SEARCH_BLOCK_CHAR = "<",LEGACY_REPLACE_BLOCK_CHAR = ">".block regex:22—SEARCH_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 claseNewFileContentConstructorpara contenido non-standard y lógica de reparación más compleja.internalProcessLine:600— procesamiento de línea núcleo de v2, contryFixSearchReplaceBlock,pendingNonStandardLines, etc.caller:502— punto de llamada; el handler pasa!block.partialcomoisFinal.
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 (=======):
// 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
------- SEARCHseguido 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, lanzaEmpty 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.
------ SEAa 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,
pendingOutOfOrderReplacementse hace true y la salida partial se pausa (out of order:384), esperando aisFinal=truepara 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
<en vez de<en el diff; el handler llamaapplyModelContentFixesantes deconstructNewFileContent(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.originalContentestá vacío. El handler verificaisEditingprimero; si no está abierto, llamadiffViewProvider.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.