diff-06-06-25.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729
  1. const SEARCH_BLOCK_START = "------- SEARCH"
  2. const SEARCH_BLOCK_END = "======="
  3. const REPLACE_BLOCK_END = "+++++++ REPLACE"
  4. const SEARCH_BLOCK_CHAR = "-"
  5. const REPLACE_BLOCK_CHAR = "+"
  6. /**
  7. * Attempts a line-trimmed fallback match for the given search content in the original content.
  8. * It tries to match `searchContent` lines against a block of lines in `originalContent` starting
  9. * from `lastProcessedIndex`. Lines are matched by trimming leading/trailing whitespace and ensuring
  10. * they are identical afterwards.
  11. *
  12. * Returns [matchIndexStart, matchIndexEnd] if found, or false if not found.
  13. */
  14. function lineTrimmedFallbackMatch(originalContent: string, searchContent: string, startIndex: number): [number, number] | false {
  15. // Split both contents into lines
  16. const originalLines = originalContent.split("\n")
  17. const searchLines = searchContent.split("\n")
  18. // Trim trailing empty line if exists (from the trailing \n in searchContent)
  19. if (searchLines[searchLines.length - 1] === "") {
  20. searchLines.pop()
  21. }
  22. // Find the line number where startIndex falls
  23. let startLineNum = 0
  24. let currentIndex = 0
  25. while (currentIndex < startIndex && startLineNum < originalLines.length) {
  26. currentIndex += originalLines[startLineNum].length + 1 // +1 for \n
  27. startLineNum++
  28. }
  29. // For each possible starting position in original content
  30. for (let i = startLineNum; i <= originalLines.length - searchLines.length; i++) {
  31. let matches = true
  32. // Try to match all search lines from this position
  33. for (let j = 0; j < searchLines.length; j++) {
  34. const originalTrimmed = originalLines[i + j].trim()
  35. const searchTrimmed = searchLines[j].trim()
  36. if (originalTrimmed !== searchTrimmed) {
  37. matches = false
  38. break
  39. }
  40. }
  41. // If we found a match, calculate the exact character positions
  42. if (matches) {
  43. // Find start character index
  44. let matchStartIndex = 0
  45. for (let k = 0; k < i; k++) {
  46. matchStartIndex += originalLines[k].length + 1 // +1 for \n
  47. }
  48. // Find end character index
  49. let matchEndIndex = matchStartIndex
  50. for (let k = 0; k < searchLines.length; k++) {
  51. matchEndIndex += originalLines[i + k].length + 1 // +1 for \n
  52. }
  53. return [matchStartIndex, matchEndIndex]
  54. }
  55. }
  56. return false
  57. }
  58. /**
  59. * Attempts to match blocks of code by using the first and last lines as anchors.
  60. * This is a third-tier fallback strategy that helps match blocks where we can identify
  61. * the correct location by matching the beginning and end, even if the exact content
  62. * differs slightly.
  63. *
  64. * The matching strategy:
  65. * 1. Only attempts to match blocks of 3 or more lines to avoid false positives
  66. * 2. Extracts from the search content:
  67. * - First line as the "start anchor"
  68. * - Last line as the "end anchor"
  69. * 3. For each position in the original content:
  70. * - Checks if the next line matches the start anchor
  71. * - If it does, jumps ahead by the search block size
  72. * - Checks if that line matches the end anchor
  73. * - All comparisons are done after trimming whitespace
  74. *
  75. * This approach is particularly useful for matching blocks of code where:
  76. * - The exact content might have minor differences
  77. * - The beginning and end of the block are distinctive enough to serve as anchors
  78. * - The overall structure (number of lines) remains the same
  79. *
  80. * @param originalContent - The full content of the original file
  81. * @param searchContent - The content we're trying to find in the original file
  82. * @param startIndex - The character index in originalContent where to start searching
  83. * @returns A tuple of [startIndex, endIndex] if a match is found, false otherwise
  84. */
  85. function blockAnchorFallbackMatch(originalContent: string, searchContent: string, startIndex: number): [number, number] | false {
  86. const originalLines = originalContent.split("\n")
  87. const searchLines = searchContent.split("\n")
  88. // Only use this approach for blocks of 3+ lines
  89. if (searchLines.length < 3) {
  90. return false
  91. }
  92. // Trim trailing empty line if exists
  93. if (searchLines[searchLines.length - 1] === "") {
  94. searchLines.pop()
  95. }
  96. const firstLineSearch = searchLines[0].trim()
  97. const lastLineSearch = searchLines[searchLines.length - 1].trim()
  98. const searchBlockSize = searchLines.length
  99. // Find the line number where startIndex falls
  100. let startLineNum = 0
  101. let currentIndex = 0
  102. while (currentIndex < startIndex && startLineNum < originalLines.length) {
  103. currentIndex += originalLines[startLineNum].length + 1
  104. startLineNum++
  105. }
  106. // Look for matching start and end anchors
  107. for (let i = startLineNum; i <= originalLines.length - searchBlockSize; i++) {
  108. // Check if first line matches
  109. if (originalLines[i].trim() !== firstLineSearch) {
  110. continue
  111. }
  112. // Check if last line matches at the expected position
  113. if (originalLines[i + searchBlockSize - 1].trim() !== lastLineSearch) {
  114. continue
  115. }
  116. // Calculate exact character positions
  117. let matchStartIndex = 0
  118. for (let k = 0; k < i; k++) {
  119. matchStartIndex += originalLines[k].length + 1
  120. }
  121. let matchEndIndex = matchStartIndex
  122. for (let k = 0; k < searchBlockSize; k++) {
  123. matchEndIndex += originalLines[i + k].length + 1
  124. }
  125. return [matchStartIndex, matchEndIndex]
  126. }
  127. return false
  128. }
  129. /**
  130. * This function reconstructs the file content by applying a streamed diff (in a
  131. * specialized SEARCH/REPLACE block format) to the original file content. It is designed
  132. * to handle both incremental updates and the final resulting file after all chunks have
  133. * been processed.
  134. *
  135. * The diff format is a custom structure that uses three markers to define changes:
  136. *
  137. * ------- SEARCH
  138. * [Exact content to find in the original file]
  139. * =======
  140. * [Content to replace with]
  141. * +++++++ REPLACE
  142. *
  143. * Behavior and Assumptions:
  144. * 1. The file is processed chunk-by-chunk. Each chunk of `diffContent` may contain
  145. * partial or complete SEARCH/REPLACE blocks. By calling this function with each
  146. * incremental chunk (with `isFinal` indicating the last chunk), the final reconstructed
  147. * file content is produced.
  148. *
  149. * 2. Matching Strategy (in order of attempt):
  150. * a. Exact Match: First attempts to find the exact SEARCH block text in the original file
  151. * b. Line-Trimmed Match: Falls back to line-by-line comparison ignoring leading/trailing whitespace
  152. * c. Block Anchor Match: For blocks of 3+ lines, tries to match using first/last lines as anchors
  153. * If all matching strategies fail, an error is thrown.
  154. *
  155. * 3. Empty SEARCH Section:
  156. * - If SEARCH is empty and the original file is empty, this indicates creating a new file
  157. * (pure insertion).
  158. * - If SEARCH is empty and the original file is not empty, this indicates a complete
  159. * file replacement (the entire original content is considered matched and replaced).
  160. *
  161. * 4. Applying Changes:
  162. * - Before encountering the "=======" marker, lines are accumulated as search content.
  163. * - After "=======" and before ">>>>>>> REPLACE", lines are accumulated as replacement content.
  164. * - Once the block is complete (">>>>>>> REPLACE"), the matched section in the original
  165. * file is replaced with the accumulated replacement lines, and the position in the original
  166. * file is advanced.
  167. *
  168. * 5. Incremental Output:
  169. * - As soon as the match location is found and we are in the REPLACE section, each new
  170. * replacement line is appended to the result so that partial updates can be viewed
  171. * incrementally.
  172. *
  173. * 6. Partial Markers:
  174. * - If the final line of the chunk looks like it might be part of a marker but is not one
  175. * of the known markers, it is removed. This prevents incomplete or partial markers
  176. * from corrupting the output.
  177. *
  178. * 7. Finalization:
  179. * - Once all chunks have been processed (when `isFinal` is true), any remaining original
  180. * content after the last replaced section is appended to the result.
  181. * - Trailing newlines are not forcibly added. The code tries to output exactly what is specified.
  182. *
  183. * Errors:
  184. * - If the search block cannot be matched using any of the available matching strategies,
  185. * an error is thrown.
  186. */
  187. export async function constructNewFileContent(
  188. diffContent: string,
  189. originalContent: string,
  190. isFinal: boolean,
  191. version: "v1" | "v2" = "v1",
  192. ): Promise<string> {
  193. const constructor = constructNewFileContentVersionMapping[version]
  194. if (!constructor) {
  195. throw new Error(`Invalid version '${version}' for file content constructor`)
  196. }
  197. return constructor(diffContent, originalContent, isFinal)
  198. }
  199. const constructNewFileContentVersionMapping: Record<
  200. string,
  201. (diffContent: string, originalContent: string, isFinal: boolean) => Promise<string>
  202. > = {
  203. v1: constructNewFileContentV1,
  204. v2: constructNewFileContentV2,
  205. } as const
  206. /**
  207. * @deprecated
  208. */
  209. async function constructNewFileContentV1(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
  210. let result = ""
  211. let lastProcessedIndex = 0
  212. let currentSearchContent = ""
  213. let currentReplaceContent = ""
  214. let inSearch = false
  215. let inReplace = false
  216. let searchMatchIndex = -1
  217. let searchEndIndex = -1
  218. let lines = diffContent.split("\n")
  219. // If the last line looks like a partial marker but isn't recognized,
  220. // remove it because it might be incomplete.
  221. const lastLine = lines[lines.length - 1]
  222. if (
  223. lines.length > 0 &&
  224. (lastLine.startsWith(SEARCH_BLOCK_CHAR) || lastLine.startsWith("=") || lastLine.startsWith(REPLACE_BLOCK_CHAR)) &&
  225. lastLine !== SEARCH_BLOCK_START &&
  226. lastLine !== SEARCH_BLOCK_END &&
  227. lastLine !== REPLACE_BLOCK_END
  228. ) {
  229. lines.pop()
  230. }
  231. for (const line of lines) {
  232. if (line === SEARCH_BLOCK_START) {
  233. inSearch = true
  234. currentSearchContent = ""
  235. currentReplaceContent = ""
  236. continue
  237. }
  238. if (line === SEARCH_BLOCK_END) {
  239. inSearch = false
  240. inReplace = true
  241. // Remove trailing linebreak for adding the === marker
  242. // if (currentSearchContent.endsWith("\r\n")) {
  243. // currentSearchContent = currentSearchContent.slice(0, -2)
  244. // } else if (currentSearchContent.endsWith("\n")) {
  245. // currentSearchContent = currentSearchContent.slice(0, -1)
  246. // }
  247. if (!currentSearchContent) {
  248. // Empty search block
  249. if (originalContent.length === 0) {
  250. // New file scenario: nothing to match, just start inserting
  251. searchMatchIndex = 0
  252. searchEndIndex = 0
  253. } else {
  254. // Complete file replacement scenario: treat the entire file as matched
  255. searchMatchIndex = 0
  256. searchEndIndex = originalContent.length
  257. }
  258. } else {
  259. // Add check for inefficient full-file search
  260. // if (currentSearchContent.trim() === originalContent.trim()) {
  261. // throw new Error(
  262. // "The SEARCH block contains the entire file content. Please either:\n" +
  263. // "1. Use an empty SEARCH block to replace the entire file, or\n" +
  264. // "2. Make focused changes to specific parts of the file that need modification.",
  265. // )
  266. // }
  267. // Exact search match scenario
  268. const exactIndex = originalContent.indexOf(currentSearchContent, lastProcessedIndex)
  269. if (exactIndex !== -1) {
  270. searchMatchIndex = exactIndex
  271. searchEndIndex = exactIndex + currentSearchContent.length
  272. } else {
  273. // Attempt fallback line-trimmed matching
  274. const lineMatch = lineTrimmedFallbackMatch(originalContent, currentSearchContent, lastProcessedIndex)
  275. if (lineMatch) {
  276. ;[searchMatchIndex, searchEndIndex] = lineMatch
  277. } else {
  278. // Try block anchor fallback for larger blocks
  279. const blockMatch = blockAnchorFallbackMatch(originalContent, currentSearchContent, lastProcessedIndex)
  280. if (blockMatch) {
  281. ;[searchMatchIndex, searchEndIndex] = blockMatch
  282. } else {
  283. throw new Error(
  284. `The SEARCH block:\n${currentSearchContent.trimEnd()}\n...does not match anything in the file or was searched out of order in the provided blocks.`,
  285. )
  286. }
  287. }
  288. }
  289. }
  290. // Output everything up to the match location
  291. result += originalContent.slice(lastProcessedIndex, searchMatchIndex)
  292. continue
  293. }
  294. if (line === REPLACE_BLOCK_END) {
  295. // Finished one replace block
  296. // // Remove the artificially added linebreak in the last line of the REPLACE block
  297. // if (result.endsWith("\r\n")) {
  298. // result = result.slice(0, -2)
  299. // } else if (result.endsWith("\n")) {
  300. // result = result.slice(0, -1)
  301. // }
  302. // Advance lastProcessedIndex to after the matched section
  303. lastProcessedIndex = searchEndIndex
  304. // Reset for next block
  305. inSearch = false
  306. inReplace = false
  307. currentSearchContent = ""
  308. currentReplaceContent = ""
  309. searchMatchIndex = -1
  310. searchEndIndex = -1
  311. continue
  312. }
  313. // Accumulate content for search or replace
  314. // (currentReplaceContent is not being used for anything right now since we directly append to result.)
  315. // (We artificially add a linebreak since we split on \n at the beginning. In order to not include a trailing linebreak in the final search/result blocks we need to remove it before using them. This allows for partial line matches to be correctly identified.)
  316. // NOTE: search/replace blocks must be arranged in the order they appear in the file due to how we build the content using lastProcessedIndex. We also cannot strip the trailing newline since for non-partial lines it would remove the linebreak from the original content. (If we remove end linebreak from search, then we'd also have to remove it from replace but we can't know if it's a partial line or not since the model may be using the line break to indicate the end of the block rather than as part of the search content.) We require the model to output full lines in order for our fallbacks to work as well.
  317. if (inSearch) {
  318. currentSearchContent += line + "\n"
  319. } else if (inReplace) {
  320. currentReplaceContent += line + "\n"
  321. // Output replacement lines immediately if we know the insertion point
  322. if (searchMatchIndex !== -1) {
  323. result += line + "\n"
  324. }
  325. }
  326. }
  327. // If this is the final chunk, append any remaining original content
  328. if (isFinal && lastProcessedIndex < originalContent.length) {
  329. result += originalContent.slice(lastProcessedIndex)
  330. }
  331. return result
  332. }
  333. enum ProcessingState {
  334. Idle = 0,
  335. StateSearch = 1 << 0,
  336. StateReplace = 1 << 1,
  337. }
  338. class NewFileContentConstructor {
  339. private originalContent: string
  340. private isFinal: boolean
  341. private state: number
  342. private pendingNonStandardLines: string[]
  343. private result: string
  344. private lastProcessedIndex: number
  345. private currentSearchContent: string
  346. private currentReplaceContent: string
  347. private searchMatchIndex: number
  348. private searchEndIndex: number
  349. constructor(originalContent: string, isFinal: boolean) {
  350. this.originalContent = originalContent
  351. this.isFinal = isFinal
  352. this.pendingNonStandardLines = []
  353. this.result = ""
  354. this.lastProcessedIndex = 0
  355. this.state = ProcessingState.Idle
  356. this.currentSearchContent = ""
  357. this.currentReplaceContent = ""
  358. this.searchMatchIndex = -1
  359. this.searchEndIndex = -1
  360. }
  361. private resetForNextBlock() {
  362. // Reset for next block
  363. this.state = ProcessingState.Idle
  364. this.currentSearchContent = ""
  365. this.currentReplaceContent = ""
  366. this.searchMatchIndex = -1
  367. this.searchEndIndex = -1
  368. }
  369. private findLastMatchingLineIndex(regx: RegExp, lineLimit: number) {
  370. for (let i = lineLimit; i > 0; ) {
  371. i--
  372. if (this.pendingNonStandardLines[i].match(regx)) {
  373. return i
  374. }
  375. }
  376. return -1
  377. }
  378. private updateProcessingState(newState: ProcessingState) {
  379. const isValidTransition =
  380. (this.state === ProcessingState.Idle && newState === ProcessingState.StateSearch) ||
  381. (this.state === ProcessingState.StateSearch && newState === ProcessingState.StateReplace)
  382. if (!isValidTransition) {
  383. throw new Error(
  384. `Invalid state transition.\n` +
  385. "Valid transitions are:\n" +
  386. "- Idle → StateSearch\n" +
  387. "- StateSearch → StateReplace",
  388. )
  389. }
  390. this.state |= newState
  391. }
  392. private isStateActive(state: ProcessingState): boolean {
  393. return (this.state & state) === state
  394. }
  395. private activateReplaceState() {
  396. this.updateProcessingState(ProcessingState.StateReplace)
  397. }
  398. private activateSearchState() {
  399. this.updateProcessingState(ProcessingState.StateSearch)
  400. this.currentSearchContent = ""
  401. this.currentReplaceContent = ""
  402. }
  403. private isSearchingActive(): boolean {
  404. return this.isStateActive(ProcessingState.StateSearch)
  405. }
  406. private isReplacingActive(): boolean {
  407. return this.isStateActive(ProcessingState.StateReplace)
  408. }
  409. private hasPendingNonStandardLines(pendingNonStandardLineLimit: number): boolean {
  410. return this.pendingNonStandardLines.length - pendingNonStandardLineLimit < this.pendingNonStandardLines.length
  411. }
  412. public processLine(line: string) {
  413. this.internalProcessLine(line, true, this.pendingNonStandardLines.length)
  414. }
  415. public getResult() {
  416. // If this is the final chunk, append any remaining original content
  417. if (this.isFinal && this.lastProcessedIndex < this.originalContent.length) {
  418. this.result += this.originalContent.slice(this.lastProcessedIndex)
  419. }
  420. if (this.isFinal && this.state !== ProcessingState.Idle) {
  421. throw new Error("File processing incomplete - SEARCH/REPLACE operations still active during finalization")
  422. }
  423. return this.result
  424. }
  425. private internalProcessLine(
  426. line: string,
  427. canWritependingNonStandardLines: boolean,
  428. pendingNonStandardLineLimit: number,
  429. ): number {
  430. let removeLineCount = 0
  431. if (line === SEARCH_BLOCK_START) {
  432. removeLineCount = this.trimPendingNonStandardTrailingEmptyLines(pendingNonStandardLineLimit)
  433. if (removeLineCount > 0) {
  434. pendingNonStandardLineLimit = pendingNonStandardLineLimit - removeLineCount
  435. }
  436. if (this.hasPendingNonStandardLines(pendingNonStandardLineLimit)) {
  437. this.tryFixSearchReplaceBlock(pendingNonStandardLineLimit)
  438. canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
  439. }
  440. this.activateSearchState()
  441. } else if (line === SEARCH_BLOCK_END) {
  442. // 校验非标内容
  443. if (!this.isSearchingActive()) {
  444. this.tryFixSearchBlock(pendingNonStandardLineLimit)
  445. canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
  446. }
  447. this.activateReplaceState()
  448. this.beforeReplace()
  449. } else if (line === REPLACE_BLOCK_END) {
  450. if (!this.isReplacingActive()) {
  451. this.tryFixReplaceBlock(pendingNonStandardLineLimit)
  452. canWritependingNonStandardLines && (this.pendingNonStandardLines.length = 0)
  453. }
  454. this.lastProcessedIndex = this.searchEndIndex
  455. this.resetForNextBlock()
  456. } else {
  457. // Accumulate content for search or replace
  458. // (currentReplaceContent is not being used for anything right now since we directly append to result.)
  459. // (We artificially add a linebreak since we split on \n at the beginning. In order to not include a trailing linebreak in the final search/result blocks we need to remove it before using them. This allows for partial line matches to be correctly identified.)
  460. // NOTE: search/replace blocks must be arranged in the order they appear in the file due to how we build the content using lastProcessedIndex. We also cannot strip the trailing newline since for non-partial lines it would remove the linebreak from the original content. (If we remove end linebreak from search, then we'd also have to remove it from replace but we can't know if it's a partial line or not since the model may be using the line break to indicate the end of the block rather than as part of the search content.) We require the model to output full lines in order for our fallbacks to work as well.
  461. if (this.isReplacingActive()) {
  462. this.currentReplaceContent += line + "\n"
  463. // Output replacement lines immediately if we know the insertion point
  464. if (this.searchMatchIndex !== -1) {
  465. this.result += line + "\n"
  466. }
  467. } else if (this.isSearchingActive()) {
  468. this.currentSearchContent += line + "\n"
  469. } else {
  470. let appendToPendingNonStandardLines = canWritependingNonStandardLines
  471. if (appendToPendingNonStandardLines) {
  472. // 处理非标内容
  473. this.pendingNonStandardLines.push(line)
  474. }
  475. }
  476. }
  477. return removeLineCount
  478. }
  479. private beforeReplace() {
  480. // Remove trailing linebreak for adding the === marker
  481. // if (currentSearchContent.endsWith("\r\n")) {
  482. // currentSearchContent = currentSearchContent.slice(0, -2)
  483. // } else if (currentSearchContent.endsWith("\n")) {
  484. // currentSearchContent = currentSearchContent.slice(0, -1)
  485. // }
  486. if (!this.currentSearchContent) {
  487. // Empty search block
  488. if (this.originalContent.length === 0) {
  489. // New file scenario: nothing to match, just start inserting
  490. this.searchMatchIndex = 0
  491. this.searchEndIndex = 0
  492. } else {
  493. // Complete file replacement scenario: treat the entire file as matched
  494. this.searchMatchIndex = 0
  495. this.searchEndIndex = this.originalContent.length
  496. }
  497. } else {
  498. // Add check for inefficient full-file search
  499. // if (currentSearchContent.trim() === originalContent.trim()) {
  500. // throw new Error(
  501. // "The SEARCH block contains the entire file content. Please either:\n" +
  502. // "1. Use an empty SEARCH block to replace the entire file, or\n" +
  503. // "2. Make focused changes to specific parts of the file that need modification.",
  504. // )
  505. // }
  506. // Exact search match scenario
  507. const exactIndex = this.originalContent.indexOf(this.currentSearchContent, this.lastProcessedIndex)
  508. if (exactIndex !== -1) {
  509. this.searchMatchIndex = exactIndex
  510. this.searchEndIndex = exactIndex + this.currentSearchContent.length
  511. } else {
  512. // Attempt fallback line-trimmed matching
  513. const lineMatch = lineTrimmedFallbackMatch(
  514. this.originalContent,
  515. this.currentSearchContent,
  516. this.lastProcessedIndex,
  517. )
  518. if (lineMatch) {
  519. ;[this.searchMatchIndex, this.searchEndIndex] = lineMatch
  520. } else {
  521. // Try block anchor fallback for larger blocks
  522. const blockMatch = blockAnchorFallbackMatch(
  523. this.originalContent,
  524. this.currentSearchContent,
  525. this.lastProcessedIndex,
  526. )
  527. if (blockMatch) {
  528. ;[this.searchMatchIndex, this.searchEndIndex] = blockMatch
  529. } else {
  530. throw new Error(
  531. `The SEARCH block:\n${this.currentSearchContent.trimEnd()}\n...does not match anything in the file.`,
  532. )
  533. }
  534. }
  535. }
  536. }
  537. if (this.searchMatchIndex < this.lastProcessedIndex) {
  538. throw new Error(
  539. `The SEARCH block:\n${this.currentSearchContent.trimEnd()}\n...matched an incorrect content in the file.`,
  540. )
  541. }
  542. // Output everything up to the match location
  543. this.result += this.originalContent.slice(this.lastProcessedIndex, this.searchMatchIndex)
  544. }
  545. private tryFixSearchBlock(lineLimit: number): number {
  546. let removeLineCount = 0
  547. if (lineLimit < 0) {
  548. lineLimit = this.pendingNonStandardLines.length
  549. }
  550. if (!lineLimit) {
  551. throw new Error("Invalid SEARCH/REPLACE block structure - no lines available to process")
  552. }
  553. let searchTagRegexp = /^[-]{3,} SEARCH$/
  554. const searchTagIndex = this.findLastMatchingLineIndex(searchTagRegexp, lineLimit)
  555. if (searchTagIndex !== -1) {
  556. let fixLines = this.pendingNonStandardLines.slice(searchTagIndex, lineLimit)
  557. fixLines[0] = SEARCH_BLOCK_START
  558. for (const line of fixLines) {
  559. removeLineCount += this.internalProcessLine(line, false, searchTagIndex)
  560. }
  561. } else {
  562. throw new Error(
  563. `Invalid REPLACE marker detected - could not find matching SEARCH block starting from line ${searchTagIndex + 1}`,
  564. )
  565. }
  566. return removeLineCount
  567. }
  568. private tryFixReplaceBlock(lineLimit: number): number {
  569. let removeLineCount = 0
  570. if (lineLimit < 0) {
  571. lineLimit = this.pendingNonStandardLines.length
  572. }
  573. if (!lineLimit) {
  574. throw new Error()
  575. }
  576. let replaceBeginTagRegexp = /^[=]{3,}$/
  577. const replaceBeginTagIndex = this.findLastMatchingLineIndex(replaceBeginTagRegexp, lineLimit)
  578. if (replaceBeginTagIndex !== -1) {
  579. // // 校验非标内容
  580. // if (!this.isSearchingActive()) {
  581. // removeLineCount += this.tryFixSearchBlock(replaceBeginTagIndex)
  582. // }
  583. let fixLines = this.pendingNonStandardLines.slice(replaceBeginTagIndex - removeLineCount, lineLimit - removeLineCount)
  584. fixLines[0] = SEARCH_BLOCK_END
  585. for (const line of fixLines) {
  586. removeLineCount += this.internalProcessLine(line, false, replaceBeginTagIndex - removeLineCount)
  587. }
  588. } else {
  589. throw new Error(`Malformed REPLACE block - missing valid separator after line ${replaceBeginTagIndex + 1}`)
  590. }
  591. return removeLineCount
  592. }
  593. private tryFixSearchReplaceBlock(lineLimit: number): number {
  594. let removeLineCount = 0
  595. if (lineLimit < 0) {
  596. lineLimit = this.pendingNonStandardLines.length
  597. }
  598. if (!lineLimit) {
  599. throw new Error()
  600. }
  601. let replaceEndTagRegexp = /^[+]{3,} REPLACE$/
  602. const replaceEndTagIndex = this.findLastMatchingLineIndex(replaceEndTagRegexp, lineLimit)
  603. const likeReplaceEndTag = replaceEndTagIndex === lineLimit - 1
  604. if (likeReplaceEndTag) {
  605. // // 校验非标内容
  606. // if (!this.isReplacingActive()) {
  607. // removeLineCount += this.tryFixReplaceBlock(replaceEndTagIndex)
  608. // }
  609. let fixLines = this.pendingNonStandardLines.slice(replaceEndTagIndex - removeLineCount, lineLimit - removeLineCount)
  610. fixLines[fixLines.length - 1] = REPLACE_BLOCK_END
  611. for (const line of fixLines) {
  612. removeLineCount += this.internalProcessLine(line, false, replaceEndTagIndex - removeLineCount)
  613. }
  614. } else {
  615. throw new Error("Malformed SEARCH/REPLACE block structure: Missing valid closing REPLACE marker")
  616. }
  617. return removeLineCount
  618. }
  619. /**
  620. * Removes trailing empty lines from the pendingNonStandardLines array
  621. * @param lineLimit - The index to start checking from (exclusive).
  622. * Removes empty lines from lineLimit-1 backwards.
  623. * @returns The number of empty lines removed
  624. */
  625. private trimPendingNonStandardTrailingEmptyLines(lineLimit: number): number {
  626. let removedCount = 0
  627. let i = Math.min(lineLimit, this.pendingNonStandardLines.length) - 1
  628. while (i >= 0 && this.pendingNonStandardLines[i].trim() === "") {
  629. this.pendingNonStandardLines.pop()
  630. removedCount++
  631. i--
  632. }
  633. return removedCount
  634. }
  635. }
  636. export async function constructNewFileContentV2(diffContent: string, originalContent: string, isFinal: boolean): Promise<string> {
  637. let newFileContentConstructor = new NewFileContentConstructor(originalContent, isFinal)
  638. let lines = diffContent.split("\n")
  639. // If the last line looks like a partial marker but isn't recognized,
  640. // remove it because it might be incomplete.
  641. const lastLine = lines[lines.length - 1]
  642. if (
  643. lines.length > 0 &&
  644. (lastLine.startsWith(SEARCH_BLOCK_CHAR) || lastLine.startsWith("=") || lastLine.startsWith(REPLACE_BLOCK_CHAR)) &&
  645. lastLine !== SEARCH_BLOCK_START &&
  646. lastLine !== SEARCH_BLOCK_END &&
  647. lastLine !== REPLACE_BLOCK_END
  648. ) {
  649. lines.pop()
  650. }
  651. for (const line of lines) {
  652. newFileContentConstructor.processLine(line)
  653. }
  654. let result = newFileContentConstructor.getResult()
  655. return result
  656. }