opencode-gbk-tools 0.1.26 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16585,45 +16585,103 @@ function assertInsertArguments(input) {
16585
16585
  throw createGbkError("GBK_INVALID_ARGUMENT", "content \u4E0D\u80FD\u4E3A\u7A7A");
16586
16586
  }
16587
16587
  if (input.replaceAll !== void 0) {
16588
- throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\uFFFD? replaceAll");
16588
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\u6301 replaceAll");
16589
16589
  }
16590
16590
  if (input.startLine !== void 0 || input.endLine !== void 0 || input.startAnchor !== void 0 || input.endAnchor !== void 0) {
16591
- throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\uFFFD? startLine/endLine/startAnchor/endAnchor");
16591
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u6A21\u5F0F\u4E0D\u652F\u6301 startLine/endLine/startAnchor/endAnchor");
16592
16592
  }
16593
16593
  }
16594
- function findOccurrenceIndex(text, token, occurrence) {
16595
- assertStringArgument(text, "text");
16596
- assertStringArgument(token, "anchor");
16597
- assertPositiveInteger(occurrence, "occurrence");
16594
+ function collectOccurrencePositions(text, token) {
16598
16595
  if (token.length === 0) {
16599
- throw createGbkError("GBK_INVALID_ARGUMENT", "anchor \u4E0D\u80FD\u4E3A\u7A7A");
16596
+ return [];
16600
16597
  }
16601
- let count = 0;
16598
+ const positions = [];
16602
16599
  let searchFrom = 0;
16603
16600
  while (true) {
16604
16601
  const index = text.indexOf(token, searchFrom);
16605
16602
  if (index === -1) {
16606
- break;
16607
- }
16608
- count += 1;
16609
- if (count === occurrence) {
16610
- return { index, total: countOccurrences(text, token) };
16603
+ return positions;
16611
16604
  }
16605
+ positions.push(index);
16612
16606
  searchFrom = index + token.length;
16613
16607
  }
16614
- if (count === 0) {
16615
- throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\uFFFD?: ${token}`);
16608
+ }
16609
+ function buildFlexibleSearchVariants(token, newlineStyle) {
16610
+ const variants = /* @__PURE__ */ new Set();
16611
+ const pushVariant = (value) => {
16612
+ if (typeof value !== "string" || value.length === 0) {
16613
+ return;
16614
+ }
16615
+ variants.add(value);
16616
+ };
16617
+ pushVariant(token);
16618
+ pushVariant(alignTextToNewlineStyle(token, newlineStyle));
16619
+ const prefixedBlock = parseLineNumberPrefixedBlock(token);
16620
+ if (prefixedBlock) {
16621
+ pushVariant(prefixedBlock.strippedText);
16622
+ pushVariant(alignTextToNewlineStyle(prefixedBlock.strippedText, newlineStyle));
16623
+ }
16624
+ const trimmedLines = trimTrailingEmptyLines(splitNormalizedLines(token));
16625
+ if (trimmedLines.length > 0) {
16626
+ const trimmedToken = trimmedLines.join("\n");
16627
+ pushVariant(trimmedToken);
16628
+ pushVariant(alignTextToNewlineStyle(trimmedToken, newlineStyle));
16629
+ const trimmedPrefixedBlock = parseLineNumberPrefixedBlock(trimmedToken);
16630
+ if (trimmedPrefixedBlock) {
16631
+ pushVariant(trimmedPrefixedBlock.strippedText);
16632
+ pushVariant(alignTextToNewlineStyle(trimmedPrefixedBlock.strippedText, newlineStyle));
16633
+ }
16634
+ }
16635
+ return [...variants];
16636
+ }
16637
+ function findNextFlexibleMatch(text, token, searchFrom, newlineStyle) {
16638
+ let bestMatch = null;
16639
+ for (const candidate of buildFlexibleSearchVariants(token, newlineStyle)) {
16640
+ const index = text.indexOf(candidate, searchFrom);
16641
+ if (index === -1) {
16642
+ continue;
16643
+ }
16644
+ if (bestMatch === null || index < bestMatch.index || index === bestMatch.index && candidate.length > bestMatch.matchedToken.length) {
16645
+ bestMatch = { index, matchedToken: candidate };
16646
+ }
16647
+ }
16648
+ return bestMatch;
16649
+ }
16650
+ function findFlexibleOccurrenceIndex(text, token, occurrence, newlineStyle) {
16651
+ assertStringArgument(text, "text");
16652
+ assertStringArgument(token, "anchor");
16653
+ assertPositiveInteger(occurrence, "occurrence");
16654
+ if (token.length === 0) {
16655
+ throw createGbkError("GBK_INVALID_ARGUMENT", "anchor \u4E0D\u80FD\u4E3A\u7A7A");
16656
+ }
16657
+ let maxMatches = 0;
16658
+ for (const candidate of buildFlexibleSearchVariants(token, newlineStyle)) {
16659
+ const positions = collectOccurrencePositions(text, candidate);
16660
+ if (positions.length === 0) {
16661
+ continue;
16662
+ }
16663
+ maxMatches = Math.max(maxMatches, positions.length);
16664
+ if (positions.length >= occurrence) {
16665
+ return {
16666
+ index: positions[occurrence - 1],
16667
+ total: positions.length,
16668
+ matchedToken: candidate
16669
+ };
16670
+ }
16671
+ }
16672
+ if (maxMatches === 0) {
16673
+ throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\u70B9: ${token}`);
16616
16674
  }
16617
- throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${token} \u53EA\u627E\uFFFD? ${count} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\uFFFD? ${occurrence} \u5904`);
16675
+ throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${token} \u53EA\u627E\u5230 ${maxMatches} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7B2C ${occurrence} \u5904`);
16618
16676
  }
16619
16677
  function insertByAnchor(text, mode, anchor, content, occurrence, ifExists, newlineStyle) {
16620
16678
  const alignedContent = newlineStyle === "crlf" ? content.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n") : newlineStyle === "lf" ? content.replace(/\r\n/g, "\n") : content;
16621
- const located = findOccurrenceIndex(text, anchor, occurrence);
16622
- const insertionPoint = mode === "insertAfter" ? located.index + anchor.length : located.index;
16679
+ const located = findFlexibleOccurrenceIndex(text, anchor, occurrence, newlineStyle);
16680
+ const insertionPoint = mode === "insertAfter" ? located.index + located.matchedToken.length : located.index;
16623
16681
  const alreadyExists = mode === "insertAfter" ? text.slice(insertionPoint, insertionPoint + alignedContent.length) === alignedContent : text.slice(Math.max(0, insertionPoint - alignedContent.length), insertionPoint) === alignedContent;
16624
16682
  if (alreadyExists) {
16625
16683
  if (ifExists === "error") {
16626
- throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\uFFFD?");
16684
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
16627
16685
  }
16628
16686
  if (ifExists === "skip") {
16629
16687
  return {
@@ -16664,7 +16722,7 @@ function normalizeOptionalPositiveInteger(value, name) {
16664
16722
  }
16665
16723
  function assertNotBinary(buffer) {
16666
16724
  if (buffer.includes(0)) {
16667
- throw createGbkError("GBK_BINARY_FILE", "\u7591\u4F3C\u4E8C\u8FDB\u5236\u6587\u4EF6\uFF0C\u65E0\u6CD5\uFFFD? GBK \u6587\u672C\u5904\u7406");
16725
+ throw createGbkError("GBK_BINARY_FILE", "\u7591\u4F3C\u4E8C\u8FDB\u5236\u6587\u4EF6\uFF0C\u65E0\u6CD5\u6309 GBK \u6587\u672C\u5904\u7406");
16668
16726
  }
16669
16727
  }
16670
16728
  function splitLinesWithNumbers(text, offset = 1, limit = 2e3) {
@@ -16791,19 +16849,15 @@ function applyAnchors(text, startAnchor, endAnchor) {
16791
16849
  }
16792
16850
  let rangeStart = 0;
16793
16851
  let rangeEnd = text.length;
16852
+ const newlineStyle = detectNewlineStyle(text);
16794
16853
  if (startAnchor) {
16795
- const found = text.indexOf(startAnchor);
16796
- if (found === -1) {
16797
- throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8D77\u59CB\u951A\uFFFD?: ${startAnchor}`);
16798
- }
16799
- rangeStart = found + startAnchor.length;
16854
+ const found = findFlexibleOccurrenceIndex(text, startAnchor, 1, newlineStyle);
16855
+ rangeStart = found.index + found.matchedToken.length;
16800
16856
  }
16801
16857
  if (endAnchor) {
16802
- const found = text.indexOf(endAnchor, rangeStart);
16803
- if (found === -1) {
16804
- throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u7ED3\u675F\u951A\uFFFD?: ${endAnchor}`);
16805
- }
16806
- rangeEnd = found;
16858
+ const searchText = text.slice(rangeStart);
16859
+ const found = findFlexibleOccurrenceIndex(searchText, endAnchor, 1, newlineStyle);
16860
+ rangeEnd = rangeStart + found.index;
16807
16861
  }
16808
16862
  if (rangeEnd < rangeStart) {
16809
16863
  throw createGbkError("GBK_INVALID_ARGUMENT", "\u951A\u70B9\u8303\u56F4\u65E0\u6548");
@@ -16854,9 +16908,9 @@ function getNearestContext(content, oldString) {
16854
16908
  }
16855
16909
  function buildNoMatchMessage(content, oldString) {
16856
16910
  return [
16857
- "\u672A\u627E\u5230\u9700\u8981\u66FF\u6362\u7684\u6587\u672C\uFFFD?",
16858
- "oldString \u5FC5\u987B\u4E0E\u6587\u4EF6\u5B9E\u9645\u5185\u5BB9\u5B8C\u5168\u5BF9\u5E94\uFF0C\u6216\u4EC5\u5728\u6362\uFFFD?/\u5C3E\u968F\u7A7A\u884C\u4E0A\u5B58\u5728\u8F7B\u5FAE\u5DEE\u5F02\uFFFD?",
16859
- "\u6700\u63A5\u8FD1\u7684\u4E0A\u4E0B\u6587\uFFFD?",
16911
+ "\u672A\u627E\u5230\u9700\u8981\u66FF\u6362\u7684\u6587\u672C\u3002",
16912
+ "oldString \u5FC5\u987B\u4E0E\u6587\u4EF6\u5B9E\u9645\u5185\u5BB9\u5B8C\u5168\u5BF9\u5E94\uFF0C\u6216\u4EC5\u5728\u6362\u884C/\u5C3E\u968F\u7A7A\u884C\u4E0A\u5B58\u5728\u8F7B\u5FAE\u5DEE\u5F02\u3002",
16913
+ "\u6700\u63A5\u8FD1\u7684\u4E0A\u4E0B\u6587\uFF1A",
16860
16914
  getNearestContext(content, oldString)
16861
16915
  ].join("\n");
16862
16916
  }
@@ -16868,8 +16922,10 @@ function stripLineNumberPrefixes(lines) {
16868
16922
  return lines.map((l) => l.replace(/^\d+: /, ""));
16869
16923
  }
16870
16924
  function parseLineNumberPrefixedBlock(text) {
16871
- const lines = splitNormalizedLines(text);
16872
- if (!hasLineNumberPrefixes(lines)) {
16925
+ const normalizedText = normalizeNewlines(text);
16926
+ const hasTrailingNewline = normalizedText.endsWith("\n");
16927
+ const lines = trimTrailingEmptyLines(splitNormalizedLines(text));
16928
+ if (lines.length === 0 || !hasLineNumberPrefixes(lines)) {
16873
16929
  return null;
16874
16930
  }
16875
16931
  const lineNumbers = [];
@@ -16884,7 +16940,7 @@ function parseLineNumberPrefixedBlock(text) {
16884
16940
  }
16885
16941
  return {
16886
16942
  lineNumbers,
16887
- strippedText: strippedLines.join("\n"),
16943
+ strippedText: `${strippedLines.join("\n")}${hasTrailingNewline ? "\n" : ""}`,
16888
16944
  isContiguous: lineNumbers.every((lineNumber, index) => index === 0 || lineNumber === lineNumbers[index - 1] + 1),
16889
16945
  startLine: lineNumbers[0],
16890
16946
  endLine: lineNumbers[lineNumbers.length - 1]
@@ -16979,10 +17035,10 @@ async function resolveReadableGbkFile(input) {
16979
17035
  try {
16980
17036
  stat = await fs2.stat(candidatePath);
16981
17037
  } catch (error45) {
16982
- throw createGbkError("GBK_FILE_NOT_FOUND", `\u6587\u4EF6\u4E0D\u5B58\uFFFD?: ${candidatePath}`, error45);
17038
+ throw createGbkError("GBK_FILE_NOT_FOUND", `\u6587\u4EF6\u4E0D\u5B58\u5728: ${candidatePath}`, error45);
16983
17039
  }
16984
17040
  if (stat.isDirectory()) {
16985
- throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\uFFFD?: ${candidatePath}`);
17041
+ throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\u5F55: ${candidatePath}`);
16986
17042
  }
16987
17043
  return {
16988
17044
  filePath: candidatePath,
@@ -17187,7 +17243,7 @@ function replaceScopedTextContent(scopeText, oldString, newString, replaceAll, n
17187
17243
  } else if (occurrencesBefore === 0) {
17188
17244
  throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scopeText, oldString));
17189
17245
  } else if (occurrencesBefore > 1) {
17190
- throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\uFFFD?: ${oldString}`);
17246
+ throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${oldString}`);
17191
17247
  }
17192
17248
  const alignedNewString = alignTextToNewlineStyle(newString, newlineStyle);
17193
17249
  return {
@@ -17245,14 +17301,17 @@ async function replaceLargeGbkFileTextInLineRange(input) {
17245
17301
  }
17246
17302
  }
17247
17303
  async function replaceLargeGbkFileByAnchor(input) {
17304
+ const lineIndex = await getGbkLineIndex(input);
17305
+ const newlineStyle = lineIndex.newlineStyle;
17248
17306
  const tempPath = path2.join(
17249
17307
  path2.dirname(input.filePath),
17250
17308
  `${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
17251
17309
  );
17252
17310
  const handle = await fs2.open(tempPath, "w");
17253
- const alignedContent = input.content.replace(/\r\n/g, "\n");
17254
- const anchorLength = input.anchor.length;
17255
- const carryLength = Math.max(anchorLength + alignedContent.length, 1);
17311
+ const alignedContent = alignTextToNewlineStyle(input.content, newlineStyle);
17312
+ const anchorVariants = buildFlexibleSearchVariants(input.anchor, newlineStyle);
17313
+ const maxAnchorLength = anchorVariants.reduce((maxLength, candidate) => Math.max(maxLength, candidate.length), input.anchor.length);
17314
+ const carryLength = Math.max(maxAnchorLength + alignedContent.length, 1);
17256
17315
  let decoded = "";
17257
17316
  let scanFrom = 0;
17258
17317
  let totalMatches = 0;
@@ -17276,23 +17335,23 @@ async function replaceLargeGbkFileByAnchor(input) {
17276
17335
  await visitDecodedTextChunks(input, async (text) => {
17277
17336
  decoded += text;
17278
17337
  while (!inserted) {
17279
- const foundAt = decoded.indexOf(input.anchor, scanFrom);
17280
- if (foundAt === -1) {
17338
+ const located = findNextFlexibleMatch(decoded, input.anchor, scanFrom, newlineStyle);
17339
+ if (located === null) {
17281
17340
  break;
17282
17341
  }
17283
17342
  totalMatches += 1;
17284
- const afterAnchor = foundAt + anchorLength;
17343
+ const afterAnchor = located.index + located.matchedToken.length;
17285
17344
  if (totalMatches !== input.occurrence) {
17286
17345
  scanFrom = afterAnchor;
17287
17346
  continue;
17288
17347
  }
17289
- const before = decoded.slice(0, foundAt);
17348
+ const before = decoded.slice(0, located.index);
17290
17349
  const after = decoded.slice(afterAnchor);
17291
- const anchorAndAfter = decoded.slice(foundAt);
17350
+ const anchorAndAfter = decoded.slice(located.index);
17292
17351
  const alreadyExists = input.mode === "insertAfter" ? after.startsWith(alignedContent) : before.endsWith(alignedContent);
17293
17352
  if (alreadyExists) {
17294
17353
  if (input.ifExists === "error") {
17295
- throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\uFFFD?");
17354
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
17296
17355
  }
17297
17356
  if (input.ifExists === "skip") {
17298
17357
  skipped = true;
@@ -17304,7 +17363,7 @@ async function replaceLargeGbkFileByAnchor(input) {
17304
17363
  }
17305
17364
  if (input.mode === "insertAfter") {
17306
17365
  bytesWritten += await writeEncodedText(handle, input.encoding, before);
17307
- bytesWritten += await writeEncodedText(handle, input.encoding, input.anchor);
17366
+ bytesWritten += await writeEncodedText(handle, input.encoding, located.matchedToken);
17308
17367
  bytesWritten += await writeEncodedText(handle, input.encoding, alignedContent);
17309
17368
  bytesWritten += await writeEncodedText(handle, input.encoding, after);
17310
17369
  } else {
@@ -17323,23 +17382,23 @@ async function replaceLargeGbkFileByAnchor(input) {
17323
17382
  });
17324
17383
  if (!inserted) {
17325
17384
  while (true) {
17326
- const foundAt = decoded.indexOf(input.anchor, scanFrom);
17327
- if (foundAt === -1) {
17385
+ const located = findNextFlexibleMatch(decoded, input.anchor, scanFrom, newlineStyle);
17386
+ if (located === null) {
17328
17387
  break;
17329
17388
  }
17330
17389
  totalMatches += 1;
17331
- const afterAnchor = foundAt + anchorLength;
17390
+ const afterAnchor = located.index + located.matchedToken.length;
17332
17391
  if (totalMatches !== input.occurrence) {
17333
17392
  scanFrom = afterAnchor;
17334
17393
  continue;
17335
17394
  }
17336
- const before = decoded.slice(0, foundAt);
17395
+ const before = decoded.slice(0, located.index);
17337
17396
  const after = decoded.slice(afterAnchor);
17338
- const anchorAndAfter = decoded.slice(foundAt);
17397
+ const anchorAndAfter = decoded.slice(located.index);
17339
17398
  const alreadyExists = input.mode === "insertAfter" ? after.startsWith(alignedContent) : before.endsWith(alignedContent);
17340
17399
  if (alreadyExists) {
17341
17400
  if (input.ifExists === "error") {
17342
- throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\uFFFD?");
17401
+ throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
17343
17402
  }
17344
17403
  if (input.ifExists === "skip") {
17345
17404
  skipped = true;
@@ -17347,16 +17406,16 @@ async function replaceLargeGbkFileByAnchor(input) {
17347
17406
  break;
17348
17407
  }
17349
17408
  }
17350
- decoded = input.mode === "insertAfter" ? `${before}${input.anchor}${alignedContent}${after}` : `${before}${alignedContent}${anchorAndAfter}`;
17409
+ decoded = input.mode === "insertAfter" ? `${before}${located.matchedToken}${alignedContent}${after}` : `${before}${alignedContent}${anchorAndAfter}`;
17351
17410
  inserted = true;
17352
17411
  break;
17353
17412
  }
17354
17413
  }
17355
17414
  if (!inserted && totalMatches === 0) {
17356
- throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\uFFFD?: ${input.anchor}`);
17415
+ throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\u70B9: ${input.anchor}`);
17357
17416
  }
17358
17417
  if (!inserted && totalMatches > 0) {
17359
- throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${input.anchor} \u53EA\u627E\uFFFD? ${totalMatches} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\uFFFD? ${input.occurrence} \u5904`);
17418
+ throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${input.anchor} \u53EA\u627E\u5230 ${totalMatches} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7B2C ${input.occurrence} \u5904`);
17360
17419
  }
17361
17420
  await finalizeInserted();
17362
17421
  await handle.close();
@@ -17383,35 +17442,57 @@ async function replaceLargeGbkFileByAnchor(input) {
17383
17442
  }
17384
17443
  }
17385
17444
  async function replaceLargeGbkFileText(input) {
17445
+ const lineIndex = await getGbkLineIndex(input);
17446
+ const newlineStyle = lineIndex.newlineStyle;
17386
17447
  const tempPath = path2.join(
17387
17448
  path2.dirname(input.filePath),
17388
17449
  `${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
17389
17450
  );
17390
17451
  const handle = await fs2.open(tempPath, "w");
17391
17452
  const carryLength = Math.max(input.oldString.length - 1, 0);
17453
+ const alignedNewString = alignTextToNewlineStyle(input.newString, newlineStyle);
17392
17454
  let carry = "";
17393
17455
  let occurrencesBefore = 0;
17394
17456
  let bytesWritten = 0;
17457
+ let replacedFirstMatch = false;
17395
17458
  const flushText = async (text, flush = false) => {
17396
17459
  const combined = carry + text;
17397
- const splitAt = flush ? combined.length : Math.max(0, combined.length - carryLength);
17398
- const processable = combined.slice(0, splitAt);
17399
- carry = combined.slice(splitAt);
17400
- if (processable.length === 0) {
17460
+ if (combined.length === 0) {
17401
17461
  return;
17402
17462
  }
17403
- const matchCount = countOccurrences(processable, input.oldString);
17404
- const seenBefore = occurrencesBefore;
17405
- occurrencesBefore += matchCount;
17406
- let output = processable;
17407
- if (input.replaceAll) {
17408
- if (matchCount > 0) {
17409
- output = processable.split(input.oldString).join(input.newString);
17463
+ const safeEnd = flush ? combined.length : Math.max(0, combined.length - carryLength);
17464
+ const outputParts = [];
17465
+ let cursor = 0;
17466
+ let flushUpto = safeEnd;
17467
+ while (true) {
17468
+ const index = combined.indexOf(input.oldString, cursor);
17469
+ if (index === -1) {
17470
+ break;
17471
+ }
17472
+ const matchEnd = index + input.oldString.length;
17473
+ if (!flush && matchEnd > safeEnd) {
17474
+ flushUpto = index;
17475
+ break;
17410
17476
  }
17411
- } else if (seenBefore === 0 && matchCount > 0) {
17412
- output = processable.replace(input.oldString, input.newString);
17477
+ occurrencesBefore += 1;
17478
+ outputParts.push(combined.slice(cursor, index));
17479
+ if (input.replaceAll) {
17480
+ outputParts.push(alignedNewString);
17481
+ } else if (!replacedFirstMatch) {
17482
+ outputParts.push(alignedNewString);
17483
+ replacedFirstMatch = true;
17484
+ } else {
17485
+ outputParts.push(input.oldString);
17486
+ }
17487
+ cursor = matchEnd;
17488
+ }
17489
+ outputParts.push(combined.slice(cursor, flushUpto));
17490
+ carry = combined.slice(flushUpto);
17491
+ const processable = outputParts.join("");
17492
+ if (processable.length === 0) {
17493
+ return;
17413
17494
  }
17414
- bytesWritten += await writeEncodedText(handle, input.encoding, output);
17495
+ bytesWritten += await writeEncodedText(handle, input.encoding, processable);
17415
17496
  };
17416
17497
  try {
17417
17498
  await visitDecodedTextChunks(input, async (text) => {
@@ -17419,10 +17500,10 @@ async function replaceLargeGbkFileText(input) {
17419
17500
  });
17420
17501
  await flushText("", true);
17421
17502
  if (occurrencesBefore === 0) {
17422
- throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8981\u66FF\u6362\u7684\u5185\uFFFD?: ${input.oldString}`);
17503
+ throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8981\u66FF\u6362\u7684\u5185\u5BB9: ${input.oldString}`);
17423
17504
  }
17424
17505
  if (!input.replaceAll && occurrencesBefore > 1) {
17425
- throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\uFFFD?: ${input.oldString}`);
17506
+ throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${input.oldString}`);
17426
17507
  }
17427
17508
  await handle.close();
17428
17509
  const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
@@ -17648,7 +17729,7 @@ async function replaceGbkFileText(input) {
17648
17729
  } else if (occurrencesBefore === 0) {
17649
17730
  throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, effectiveOldString));
17650
17731
  } else if (occurrencesBefore > 1) {
17651
- throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\uFFFD?: ${effectiveOldString}`);
17732
+ throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${effectiveOldString}`);
17652
17733
  }
17653
17734
  const fileNewlineStyle = detectNewlineStyle(current.content);
17654
17735
  const alignedNewString = fileNewlineStyle === "crlf" ? input.newString.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n") : fileNewlineStyle === "lf" ? input.newString.replace(/\r\n/g, "\n") : input.newString;
@@ -17781,10 +17862,10 @@ async function writeGbkFile(input) {
17781
17862
  try {
17782
17863
  const stat = await fs2.stat(candidatePath);
17783
17864
  if (stat.isDirectory()) {
17784
- throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\uFFFD?: ${candidatePath}`);
17865
+ throw createGbkError("GBK_IS_DIRECTORY", `\u76EE\u6807\u8DEF\u5F84\u662F\u76EE\u5F55: ${candidatePath}`);
17785
17866
  }
17786
17867
  if (!overwrite) {
17787
- throw createGbkError("GBK_FILE_EXISTS", `\u76EE\u6807\u6587\u4EF6\u5DF2\u5B58\uFFFD?: ${candidatePath}`);
17868
+ throw createGbkError("GBK_FILE_EXISTS", `\u76EE\u6807\u6587\u4EF6\u5DF2\u5B58\u5728: ${candidatePath}`);
17788
17869
  }
17789
17870
  } catch (error45) {
17790
17871
  if (error45 instanceof Error && "code" in error45) {
@@ -18034,19 +18115,26 @@ var TEXT_TOOL_SYSTEM_MARKER = "[opencode-gbk-tools:text-rules]";
18034
18115
  var TEXT_TOOL_SYSTEM_PROMPT = [
18035
18116
  TEXT_TOOL_SYSTEM_MARKER,
18036
18117
  "\u6587\u672C\u6587\u4EF6\u5904\u7406\u89C4\u5219\uFF1A",
18037
- "- \u5904\u7406\u6587\u672C\u6587\u4EF6\u65F6\uFF0C\u4F18\u5148\u4F7F\u7528 text_read\u3001text_write\u3001text_edit\u3002",
18038
- "- text_* \u9ED8\u8BA4\u4F1A\u81EA\u52A8\u8BC6\u522B\u73B0\u6709\u6587\u4EF6\u7F16\u7801\uFF0C\u5E76\u5728\u4FEE\u6539\u65F6\u5C3D\u91CF\u4FDD\u6301\u539F\u7F16\u7801\u3001BOM \u548C\u6362\u884C\u98CE\u683C\u3002",
18118
+ "- \u5904\u7406\u6587\u672C\u6587\u4EF6\u65F6\uFF0C\u9ED8\u8BA4\u4F18\u5148\u4F7F\u7528 gbk_read\u3001gbk_write\u3001gbk_edit\u3001gbk_search\u3002",
18119
+ "- gbk_* \u9002\u5408\u5F53\u524D\u4ED3\u5E93\u7684\u81EA\u52A8\u7F16\u7801\u6587\u672C\u4E0E GBK/GB18030 \u6587\u4EF6\uFF1B\u5373\u4F7F\u4E0D\u662F\u660E\u786E\u7684 GBK \u6587\u4EF6\uFF0C\u4E5F\u4F18\u5148\u8D70\u8FD9\u7EC4\u5DE5\u5177\u3002",
18039
18120
  "- \u65B0\u5EFA .txt \u6587\u4EF6\u5728 encoding=auto \u4E0B\u9ED8\u8BA4\u4F7F\u7528 GBK\uFF1B\u5176\u4ED6\u65B0\u6587\u4EF6\u8BF7\u663E\u5F0F\u6307\u5B9A encoding\u3002",
18040
18121
  "- \u5982\u679C\u610F\u56FE\u662F\u2018\u5728\u67D0\u6807\u7B7E\u524D\u540E\u63D2\u5165\u5185\u5BB9\u2019\uFF0C\u4F18\u5148\u4F7F\u7528 mode=insertAfter \u6216 mode=insertBefore\uFF0C\u5E76\u4F20 anchor/content\u3002",
18041
18122
  "- \u53EA\u6709\u5728\u660E\u786E\u505A\u7CBE\u786E\u66FF\u6362\u65F6\uFF0C\u624D\u4F7F\u7528 oldString/newString\u3002",
18042
- "- \u82E5\u68C0\u6D4B\u7F6E\u4FE1\u5EA6\u4E0D\u8DB3\u6216\u51FA\u73B0 TEXT_UNKNOWN_ENCODING\uFF0C\u8BF7\u663E\u5F0F\u6307\u5B9A encoding \u540E\u91CD\u8BD5\u3002",
18043
- "- \u5904\u7406\u660E\u786E\u7684 GBK/GB18030 \u6587\u4EF6\u65F6\uFF0C\u4E5F\u53EF\u7EE7\u7EED\u4F7F\u7528 gbk_read\u3001gbk_write\u3001gbk_edit\u3001gbk_search\u3002"
18123
+ "- anchor\u3001startAnchor\u3001endAnchor\u3001oldString \u82E5\u76F4\u63A5\u590D\u5236\u81EA\u8BFB\u53D6\u7ED3\u679C\uFF0C\u53EF\u4FDD\u7559 LF \u6362\u884C\uFF1B\u5DE5\u5177\u4F1A\u5C3D\u91CF\u6309\u6587\u4EF6\u6362\u884C\u98CE\u683C\u81EA\u52A8\u5BF9\u9F50\u3002",
18124
+ '- \u82E5\u8BFB\u53D6\u7ED3\u679C\u5E26\u6709 "N: " \u884C\u53F7\u524D\u7F00\uFF0Cgbk_edit \u4F1A\u5C3D\u91CF\u81EA\u52A8\u5265\u79BB\u8FD9\u4E9B\u524D\u7F00\u540E\u518D\u5339\u914D\u3002',
18125
+ "- text_read\u3001text_write\u3001text_edit \u4EC5\u4F5C\u4E3A\u517C\u5BB9\u5DE5\u5177\uFF0C\u4E0D\u518D\u4F5C\u4E3A\u9ED8\u8BA4\u63A8\u8350\u8DEF\u5F84\u3002"
18044
18126
  ].join("\n");
18045
18127
  function appendTextToolSystemPrompt(system) {
18046
18128
  if (system.some((item) => item.includes(TEXT_TOOL_SYSTEM_MARKER))) {
18047
18129
  return;
18048
18130
  }
18049
- system.push(TEXT_TOOL_SYSTEM_PROMPT);
18131
+ if (system.length === 0) {
18132
+ system.push(TEXT_TOOL_SYSTEM_PROMPT);
18133
+ return;
18134
+ }
18135
+ system[0] = `${system[0]}
18136
+
18137
+ ${TEXT_TOOL_SYSTEM_PROMPT}`;
18050
18138
  }
18051
18139
 
18052
18140
  // src/lib/text-file.ts
@@ -18242,35 +18330,126 @@ function buildNoMatchMessage2(content, oldString) {
18242
18330
  assertStringArgument2(oldString, "oldString");
18243
18331
  return [
18244
18332
  "\u672A\u627E\u5230\u9700\u8981\u66FF\u6362\u7684\u6587\u672C\u3002",
18245
- "oldString \u5FC5\u987B\u4E0E\u6587\u4EF6\u5B9E\u9645\u5185\u5BB9\u5B8C\u5168\u5BF9\u5E94\u3002",
18333
+ "oldString \u5FC5\u987B\u4E0E\u6587\u4EF6\u5B9E\u9645\u5185\u5BB9\u5B8C\u5168\u5BF9\u5E94\uFF0C\u6216\u4EC5\u5728\u6362\u884C/\u5C3E\u968F\u7A7A\u884C\u4E0A\u5B58\u5728\u8F7B\u5FAE\u5DEE\u5F02\u3002",
18246
18334
  "\u6700\u63A5\u8FD1\u7684\u4E0A\u4E0B\u6587\uFF1A",
18247
18335
  getNearestContext2(content, oldString)
18248
18336
  ].join("\n");
18249
18337
  }
18250
- function findOccurrenceIndex2(text, token, occurrence) {
18338
+ function trimRightSpaces2(text) {
18251
18339
  assertStringArgument2(text, "text");
18252
- assertStringArgument2(token, "anchor");
18253
- assertPositiveInteger(occurrence, "occurrence");
18340
+ return text.replace(/[ \t]+$/g, "");
18341
+ }
18342
+ function splitNormalizedLines2(text) {
18343
+ return normalizeNewlines2(text).split("\n");
18344
+ }
18345
+ function trimTrailingEmptyLines2(lines) {
18346
+ const result = [...lines];
18347
+ while (result.length > 0 && trimRightSpaces2(result[result.length - 1]) === "") {
18348
+ result.pop();
18349
+ }
18350
+ return result;
18351
+ }
18352
+ function hasLineNumberPrefixes2(lines) {
18353
+ const nonEmpty = lines.filter((line) => line.trim().length > 0);
18354
+ return nonEmpty.length > 0 && nonEmpty.every((line) => /^\d+: /.test(line));
18355
+ }
18356
+ function stripLineNumberPrefixes2(lines) {
18357
+ return lines.map((line) => line.replace(/^\d+: /, ""));
18358
+ }
18359
+ function parseLineNumberPrefixedBlock2(text) {
18360
+ const normalizedText = normalizeNewlines2(text);
18361
+ const hasTrailingNewline = normalizedText.endsWith("\n");
18362
+ const lines = trimTrailingEmptyLines2(splitNormalizedLines2(text));
18363
+ if (lines.length === 0 || !hasLineNumberPrefixes2(lines)) {
18364
+ return null;
18365
+ }
18366
+ const lineNumbers = [];
18367
+ const strippedLines = [];
18368
+ for (const line of lines) {
18369
+ const match = /^(\d+): ?(.*)$/.exec(line);
18370
+ if (!match) {
18371
+ return null;
18372
+ }
18373
+ lineNumbers.push(Number(match[1]));
18374
+ strippedLines.push(match[2]);
18375
+ }
18376
+ return {
18377
+ lineNumbers,
18378
+ strippedText: `${strippedLines.join("\n")}${hasTrailingNewline ? "\n" : ""}`,
18379
+ isContiguous: lineNumbers.every((lineNumber, index) => index === 0 || lineNumber === lineNumbers[index - 1] + 1),
18380
+ startLine: lineNumbers[0],
18381
+ endLine: lineNumbers[lineNumbers.length - 1]
18382
+ };
18383
+ }
18384
+ function collectOccurrencePositions2(text, token) {
18254
18385
  if (token.length === 0) {
18255
- throw createTextError("GBK_INVALID_ARGUMENT", "anchor \u4E0D\u80FD\u4E3A\u7A7A");
18386
+ return [];
18256
18387
  }
18257
- let count = 0;
18388
+ const positions = [];
18258
18389
  let searchFrom = 0;
18259
18390
  while (true) {
18260
18391
  const index = text.indexOf(token, searchFrom);
18261
18392
  if (index === -1) {
18262
- break;
18263
- }
18264
- count += 1;
18265
- if (count === occurrence) {
18266
- return { index, total: countOccurrences(text, token) };
18393
+ return positions;
18267
18394
  }
18395
+ positions.push(index);
18268
18396
  searchFrom = index + token.length;
18269
18397
  }
18270
- if (count === 0) {
18398
+ }
18399
+ function buildFlexibleSearchVariants2(token, newlineStyle) {
18400
+ const variants = /* @__PURE__ */ new Set();
18401
+ const pushVariant = (value) => {
18402
+ if (typeof value !== "string" || value.length === 0) {
18403
+ return;
18404
+ }
18405
+ variants.add(value);
18406
+ };
18407
+ pushVariant(token);
18408
+ pushVariant(alignTextToNewlineStyle2(token, newlineStyle));
18409
+ const prefixedBlock = parseLineNumberPrefixedBlock2(token);
18410
+ if (prefixedBlock) {
18411
+ pushVariant(prefixedBlock.strippedText);
18412
+ pushVariant(alignTextToNewlineStyle2(prefixedBlock.strippedText, newlineStyle));
18413
+ }
18414
+ const trimmedLines = trimTrailingEmptyLines2(splitNormalizedLines2(token));
18415
+ if (trimmedLines.length > 0) {
18416
+ const trimmedToken = trimmedLines.join("\n");
18417
+ pushVariant(trimmedToken);
18418
+ pushVariant(alignTextToNewlineStyle2(trimmedToken, newlineStyle));
18419
+ const trimmedPrefixedBlock = parseLineNumberPrefixedBlock2(trimmedToken);
18420
+ if (trimmedPrefixedBlock) {
18421
+ pushVariant(trimmedPrefixedBlock.strippedText);
18422
+ pushVariant(alignTextToNewlineStyle2(trimmedPrefixedBlock.strippedText, newlineStyle));
18423
+ }
18424
+ }
18425
+ return [...variants];
18426
+ }
18427
+ function findOccurrenceIndex(text, token, occurrence, newlineStyle) {
18428
+ assertStringArgument2(text, "text");
18429
+ assertStringArgument2(token, "anchor");
18430
+ assertPositiveInteger(occurrence, "occurrence");
18431
+ if (token.length === 0) {
18432
+ throw createTextError("GBK_INVALID_ARGUMENT", "anchor \u4E0D\u80FD\u4E3A\u7A7A");
18433
+ }
18434
+ let maxMatches = 0;
18435
+ for (const candidate of buildFlexibleSearchVariants2(token, newlineStyle)) {
18436
+ const positions = collectOccurrencePositions2(text, candidate);
18437
+ if (positions.length === 0) {
18438
+ continue;
18439
+ }
18440
+ maxMatches = Math.max(maxMatches, positions.length);
18441
+ if (positions.length >= occurrence) {
18442
+ return {
18443
+ index: positions[occurrence - 1],
18444
+ total: positions.length,
18445
+ matchedToken: candidate
18446
+ };
18447
+ }
18448
+ }
18449
+ if (maxMatches === 0) {
18271
18450
  throw createTextError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\u70B9: ${token}`);
18272
18451
  }
18273
- throw createTextError("GBK_NO_MATCH", `\u951A\u70B9 ${token} \u53EA\u627E\u5230 ${count} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7B2C ${occurrence} \u5904`);
18452
+ throw createTextError("GBK_NO_MATCH", `\u951A\u70B9 ${token} \u53EA\u627E\u5230 ${maxMatches} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7B2C ${occurrence} \u5904`);
18274
18453
  }
18275
18454
  function assertInsertArguments2(input) {
18276
18455
  assertStringArgument2(input.anchor, "anchor");
@@ -18322,8 +18501,8 @@ function buildLineDiffPreview(filePath, encoding, beforeText, afterText) {
18322
18501
  }
18323
18502
  function buildInsertOutput(text, mode, anchor, content, occurrence, ifExists, newlineStyle) {
18324
18503
  const alignedContent = alignTextToNewlineStyle2(content, newlineStyle);
18325
- const located = findOccurrenceIndex2(text, anchor, occurrence);
18326
- const insertionPoint = mode === "insertAfter" ? located.index + anchor.length : located.index;
18504
+ const located = findOccurrenceIndex(text, anchor, occurrence, newlineStyle);
18505
+ const insertionPoint = mode === "insertAfter" ? located.index + located.matchedToken.length : located.index;
18327
18506
  const alreadyExists = mode === "insertAfter" ? text.slice(insertionPoint, insertionPoint + alignedContent.length) === alignedContent : text.slice(Math.max(0, insertionPoint - alignedContent.length), insertionPoint) === alignedContent;
18328
18507
  if (alreadyExists) {
18329
18508
  if (ifExists === "error") {
@@ -18337,8 +18516,8 @@ function buildInsertOutput(text, mode, anchor, content, occurrence, ifExists, ne
18337
18516
  anchorMatches: located.total,
18338
18517
  occurrence,
18339
18518
  anchor,
18340
- previewBefore: text.slice(Math.max(0, located.index - 80), Math.min(text.length, located.index + anchor.length + 80)),
18341
- previewAfter: text.slice(Math.max(0, located.index - 80), Math.min(text.length, located.index + anchor.length + 80))
18519
+ previewBefore: text.slice(Math.max(0, located.index - 80), Math.min(text.length, located.index + located.matchedToken.length + 80)),
18520
+ previewAfter: text.slice(Math.max(0, located.index - 80), Math.min(text.length, located.index + located.matchedToken.length + 80))
18342
18521
  };
18343
18522
  }
18344
18523
  }
@@ -18584,6 +18763,114 @@ async function visitDecodedTextChunks2(resolved, visitor) {
18584
18763
  stream.destroy();
18585
18764
  }
18586
18765
  }
18766
+ async function writeLargeTextFile(filePath, encoding, hasBom, producer) {
18767
+ const tempPath = path3.join(path3.dirname(filePath), `${path3.basename(filePath)}.opencode-text-${crypto2.randomUUID()}.tmp`);
18768
+ const handle = await fs3.open(tempPath, "w");
18769
+ try {
18770
+ if (hasBom) {
18771
+ const bom = getBomPrefix(encoding, hasBom);
18772
+ if (bom.length > 0) {
18773
+ await handle.writeFile(bom);
18774
+ }
18775
+ }
18776
+ const bytesWritten = await producer(handle);
18777
+ await handle.close();
18778
+ await fs3.rename(tempPath, filePath);
18779
+ return bytesWritten + getBomPrefix(encoding, hasBom).length;
18780
+ } catch (error45) {
18781
+ await handle.close().catch(() => void 0);
18782
+ await fs3.rm(tempPath, { force: true }).catch(() => void 0);
18783
+ throw error45;
18784
+ }
18785
+ }
18786
+ async function writeEncodedTextChunk(handle, encoding, text) {
18787
+ if (text.length === 0) {
18788
+ return 0;
18789
+ }
18790
+ const buffer = encodeTextBody(text, encoding);
18791
+ await handle.writeFile(buffer);
18792
+ return buffer.byteLength;
18793
+ }
18794
+ async function replaceLargeTextFileText(input) {
18795
+ const carryLength = Math.max(input.oldString.length - 1, 0);
18796
+ const fileNewlineStyle = input.loaded.newlineStyle;
18797
+ const alignedNewString = fileNewlineStyle === "crlf" ? input.newString.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n") : fileNewlineStyle === "lf" ? input.newString.replace(/\r\n/g, "\n") : input.newString;
18798
+ let carry = "";
18799
+ let occurrencesBefore = 0;
18800
+ let replacedFirstMatch = false;
18801
+ const bytesWritten = await writeLargeTextFile(input.loaded.filePath, input.loaded.encoding, input.loaded.hasBom, async (handle) => {
18802
+ let written = 0;
18803
+ const flushText = async (text, flush = false) => {
18804
+ const combined = carry + text;
18805
+ if (combined.length === 0) {
18806
+ return;
18807
+ }
18808
+ const safeEnd = flush ? combined.length : Math.max(0, combined.length - carryLength);
18809
+ const outputParts = [];
18810
+ let cursor = 0;
18811
+ let flushUpto = safeEnd;
18812
+ while (true) {
18813
+ const index = combined.indexOf(input.oldString, cursor);
18814
+ if (index === -1) {
18815
+ break;
18816
+ }
18817
+ const matchEnd = index + input.oldString.length;
18818
+ if (!flush && matchEnd > safeEnd) {
18819
+ flushUpto = index;
18820
+ break;
18821
+ }
18822
+ occurrencesBefore += 1;
18823
+ outputParts.push(combined.slice(cursor, index));
18824
+ if (input.replaceAll) {
18825
+ outputParts.push(alignedNewString);
18826
+ } else if (!replacedFirstMatch) {
18827
+ outputParts.push(alignedNewString);
18828
+ replacedFirstMatch = true;
18829
+ } else {
18830
+ outputParts.push(input.oldString);
18831
+ }
18832
+ cursor = matchEnd;
18833
+ }
18834
+ outputParts.push(combined.slice(cursor, flushUpto));
18835
+ carry = combined.slice(flushUpto);
18836
+ const processable = outputParts.join("");
18837
+ if (processable.length === 0) {
18838
+ return;
18839
+ }
18840
+ written += await writeEncodedTextChunk(handle, input.loaded.encoding, processable);
18841
+ };
18842
+ await visitDecodedTextChunks2({
18843
+ filePath: input.loaded.filePath,
18844
+ encoding: input.loaded.encoding,
18845
+ hasBom: input.loaded.hasBom
18846
+ }, async (text) => {
18847
+ await flushText(text);
18848
+ });
18849
+ await flushText("", true);
18850
+ if (occurrencesBefore === 0) {
18851
+ throw createTextError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8981\u66FF\u6362\u7684\u5185\u5BB9: ${input.oldString}`);
18852
+ }
18853
+ if (!input.replaceAll && occurrencesBefore > 1) {
18854
+ throw createTextError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${input.oldString}`);
18855
+ }
18856
+ return written;
18857
+ });
18858
+ return {
18859
+ mode: "replace",
18860
+ filePath: input.loaded.filePath,
18861
+ encoding: input.loaded.encoding,
18862
+ requestedEncoding: input.loaded.requestedEncoding,
18863
+ detectedEncoding: input.loaded.detectedEncoding,
18864
+ confidence: input.loaded.confidence,
18865
+ hasBom: input.loaded.hasBom,
18866
+ replacements: input.replaceAll ? occurrencesBefore : 1,
18867
+ occurrencesBefore,
18868
+ bytesRead: input.loaded.bytesRead,
18869
+ bytesWritten,
18870
+ newlineStyle: input.loaded.newlineStyle,
18871
+ diffPreview: void 0
18872
+ };
18873
+ }
18587
18874
  async function readWholeTextFile(input) {
18588
18875
  const resolved = await resolveReadableTextFile(input);
18589
18876
  try {
@@ -18677,19 +18964,14 @@ function applyAnchors2(text, startAnchor, endAnchor) {
18677
18964
  }
18678
18965
  let rangeStart = 0;
18679
18966
  let rangeEnd = text.length;
18967
+ const newlineStyle = detectNewlineStyle(text);
18680
18968
  if (startAnchor) {
18681
- const found = text.indexOf(startAnchor);
18682
- if (found === -1) {
18683
- throw createTextError("GBK_NO_MATCH", `\u672A\u627E\u5230\u8D77\u59CB\u951A\u70B9: ${startAnchor}`);
18684
- }
18685
- rangeStart = found + startAnchor.length;
18969
+ const found = findOccurrenceIndex(text, startAnchor, 1, newlineStyle);
18970
+ rangeStart = found.index + found.matchedToken.length;
18686
18971
  }
18687
18972
  if (endAnchor) {
18688
- const found = text.indexOf(endAnchor, rangeStart);
18689
- if (found === -1) {
18690
- throw createTextError("GBK_NO_MATCH", `\u672A\u627E\u5230\u7ED3\u675F\u951A\u70B9: ${endAnchor}`);
18691
- }
18692
- rangeEnd = found;
18973
+ const found = findOccurrenceIndex(text.slice(rangeStart), endAnchor, 1, newlineStyle);
18974
+ rangeEnd = rangeStart + found.index;
18693
18975
  }
18694
18976
  if (rangeEnd < rangeStart) {
18695
18977
  throw createTextError("GBK_INVALID_ARGUMENT", "\u951A\u70B9\u8303\u56F4\u65E0\u6548");
@@ -18720,6 +19002,64 @@ function alignTextToNewlineStyle2(text, newlineStyle) {
18720
19002
  }
18721
19003
  return text;
18722
19004
  }
19005
+ function matchLooseBlock2(contentLines, oldLines, newLines, newlineStyle) {
19006
+ for (let start = 0; start < contentLines.length; start += 1) {
19007
+ let contentIndex = start;
19008
+ let oldIndex = 0;
19009
+ while (oldIndex < oldLines.length && contentIndex < contentLines.length) {
19010
+ const expected = trimRightSpaces2(oldLines[oldIndex]);
19011
+ const actual = trimRightSpaces2(contentLines[contentIndex]);
19012
+ if (expected === actual) {
19013
+ oldIndex += 1;
19014
+ contentIndex += 1;
19015
+ continue;
19016
+ }
19017
+ if (actual === "") {
19018
+ contentIndex += 1;
19019
+ continue;
19020
+ }
19021
+ if (expected === "") {
19022
+ oldIndex += 1;
19023
+ continue;
19024
+ }
19025
+ break;
19026
+ }
19027
+ if (oldIndex === oldLines.length) {
19028
+ const matchedOriginal = contentLines.slice(start, contentIndex).join("\n");
19029
+ const replacedNormalized = [
19030
+ ...contentLines.slice(0, start),
19031
+ ...newLines,
19032
+ ...contentLines.slice(contentIndex)
19033
+ ].join("\n");
19034
+ return {
19035
+ occurrencesBefore: 1,
19036
+ matchedOriginal,
19037
+ content: newlineStyle === "crlf" ? replacedNormalized.replace(/\n/g, "\r\n") : replacedNormalized
19038
+ };
19039
+ }
19040
+ }
19041
+ return null;
19042
+ }
19043
+ function tryLooseBlockReplace2(content, oldString, newString) {
19044
+ const contentLines = splitNormalizedLines2(content);
19045
+ const oldLines = trimTrailingEmptyLines2(splitNormalizedLines2(oldString));
19046
+ const newLines = splitNormalizedLines2(newString);
19047
+ const newlineStyle = detectNewlineStyle(content);
19048
+ if (oldLines.length === 0) {
19049
+ return null;
19050
+ }
19051
+ const direct = matchLooseBlock2(contentLines, oldLines, newLines, newlineStyle);
19052
+ if (direct !== null) {
19053
+ return direct;
19054
+ }
19055
+ if (hasLineNumberPrefixes2(oldLines)) {
19056
+ const strippedOldLines = trimTrailingEmptyLines2(stripLineNumberPrefixes2(oldLines));
19057
+ if (strippedOldLines.length > 0) {
19058
+ return matchLooseBlock2(contentLines, strippedOldLines, newLines, newlineStyle);
19059
+ }
19060
+ }
19061
+ return null;
19062
+ }
18723
19063
  function ensureLossless(input, encoding, hasBom = encoding === "utf8-bom") {
18724
19064
  assertStringArgument2(input, "content");
18725
19065
  const buffer = encodeText(input, encoding, hasBom);
@@ -18919,37 +19259,89 @@ async function replaceTextFileText(input) {
18919
19259
  if (input.oldString.length === 0) {
18920
19260
  throw createTextError("GBK_EMPTY_OLD_STRING", "oldString \u4E0D\u80FD\u4E3A\u7A7A");
18921
19261
  }
19262
+ const prefixedBlock = parseLineNumberPrefixedBlock2(input.oldString);
19263
+ const derivedScopedRange = prefixedBlock?.isContiguous ? {
19264
+ startLine: normalizedInput.startLine ?? prefixedBlock.startLine,
19265
+ endLine: normalizedInput.endLine ?? prefixedBlock.endLine,
19266
+ oldString: prefixedBlock.strippedText
19267
+ } : null;
19268
+ const effectiveOldString = derivedScopedRange?.oldString ?? input.oldString;
18922
19269
  const resolved = await detectReadableTextFile({
18923
19270
  filePath: input.filePath,
18924
19271
  encoding: input.encoding ?? "auto",
18925
19272
  allowExternal: input.allowExternal,
18926
19273
  context: input.context
18927
19274
  });
19275
+ const hasScopedRange = normalizedInput.startLine !== void 0 || normalizedInput.endLine !== void 0 || normalizedInput.startAnchor !== void 0 || normalizedInput.endAnchor !== void 0 || derivedScopedRange !== null;
19276
+ const preserveEncoding = normalizedInput.preserveEncoding ?? true;
19277
+ const preserveNewlineStyle = normalizedInput.preserveNewlineStyle ?? true;
19278
+ const requestedEncoding = normalizedInput.encoding ?? "auto";
19279
+ if (resolved.stat.size >= TEXT_STREAMING_FILE_SIZE_THRESHOLD_BYTES && !hasScopedRange && preserveEncoding && preserveNewlineStyle && requestedEncoding === "auto") {
19280
+ const loadedForStreaming = await readWholeTextFile({
19281
+ filePath: input.filePath,
19282
+ encoding: requestedEncoding,
19283
+ allowExternal: input.allowExternal,
19284
+ context: input.context
19285
+ });
19286
+ return await replaceLargeTextFileText({
19287
+ loaded: loadedForStreaming,
19288
+ oldString: effectiveOldString,
19289
+ newString: input.newString,
19290
+ replaceAll: normalizedInput.replaceAll ?? false
19291
+ });
19292
+ }
18928
19293
  const loaded = await readWholeTextFile({
18929
19294
  filePath: input.filePath,
18930
19295
  encoding: input.encoding ?? "auto",
18931
19296
  allowExternal: input.allowExternal,
18932
19297
  context: input.context
18933
19298
  });
18934
- const scope = resolveEditScope2(loaded.content, normalizedInput);
19299
+ const scopedInput = derivedScopedRange === null ? normalizedInput : {
19300
+ ...normalizedInput,
19301
+ startLine: derivedScopedRange.startLine,
19302
+ endLine: derivedScopedRange.endLine
19303
+ };
19304
+ const scope = resolveEditScope2(loaded.content, scopedInput);
18935
19305
  const replaceAll = normalizedInput.replaceAll ?? false;
18936
- const preserveEncoding = normalizedInput.preserveEncoding ?? true;
18937
- const requestedEncoding = normalizedInput.encoding ?? "auto";
18938
- const occurrencesBefore = countOccurrences(scope.selectedText, input.oldString);
19306
+ const targetEncoding = preserveEncoding || requestedEncoding === "auto" ? loaded.encoding : resolveExplicitTextEncoding(requestedEncoding, loaded.encoding);
19307
+ const targetHasBom = preserveEncoding ? loaded.hasBom : targetEncoding === "utf8-bom" || targetEncoding === "utf16le" || targetEncoding === "utf16be";
19308
+ const occurrencesBefore = countOccurrences(scope.selectedText, effectiveOldString);
19309
+ if (!replaceAll && occurrencesBefore === 0) {
19310
+ const loose = tryLooseBlockReplace2(scope.selectedText, effectiveOldString, input.newString);
19311
+ if (loose !== null) {
19312
+ const outputText2 = `${loaded.content.slice(0, scope.rangeStart)}${loose.content}${loaded.content.slice(scope.rangeEnd)}`;
19313
+ ensureLossless(outputText2, targetEncoding, targetHasBom);
19314
+ const buffer2 = encodeText(outputText2, targetEncoding, targetHasBom);
19315
+ await fs3.writeFile(loaded.filePath, buffer2);
19316
+ return {
19317
+ mode: "replace",
19318
+ filePath: loaded.filePath,
19319
+ encoding: targetEncoding,
19320
+ requestedEncoding: loaded.requestedEncoding,
19321
+ detectedEncoding: loaded.detectedEncoding,
19322
+ confidence: loaded.confidence,
19323
+ hasBom: targetHasBom,
19324
+ replacements: 1,
19325
+ occurrencesBefore: loose.occurrencesBefore,
19326
+ bytesRead: loaded.bytesRead,
19327
+ bytesWritten: buffer2.byteLength,
19328
+ newlineStyle: detectNewlineStyle(outputText2),
19329
+ diffPreview: buildLineDiffPreview(loaded.filePath, targetEncoding, loose.matchedOriginal, input.newString)
19330
+ };
19331
+ }
19332
+ }
18939
19333
  if (replaceAll) {
18940
19334
  if (occurrencesBefore === 0) {
18941
- throw createTextError("GBK_NO_MATCH", buildNoMatchMessage2(scope.selectedText, input.oldString));
19335
+ throw createTextError("GBK_NO_MATCH", buildNoMatchMessage2(scope.selectedText, effectiveOldString));
18942
19336
  }
18943
19337
  } else if (occurrencesBefore === 0) {
18944
- throw createTextError("GBK_NO_MATCH", buildNoMatchMessage2(scope.selectedText, input.oldString));
19338
+ throw createTextError("GBK_NO_MATCH", buildNoMatchMessage2(scope.selectedText, effectiveOldString));
18945
19339
  } else if (occurrencesBefore > 1) {
18946
- throw createTextError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${input.oldString}`);
19340
+ throw createTextError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${effectiveOldString}`);
18947
19341
  }
18948
19342
  const alignedNewString = normalizedInput.preserveNewlineStyle === false ? input.newString : alignTextToNewlineStyle2(input.newString, loaded.newlineStyle);
18949
- const replaced = replaceAll ? scope.selectedText.split(input.oldString).join(alignedNewString) : scope.selectedText.replace(input.oldString, alignedNewString);
19343
+ const replaced = replaceAll ? scope.selectedText.split(effectiveOldString).join(alignedNewString) : scope.selectedText.replace(effectiveOldString, alignedNewString);
18950
19344
  const outputText = `${loaded.content.slice(0, scope.rangeStart)}${replaced}${loaded.content.slice(scope.rangeEnd)}`;
18951
- const targetEncoding = preserveEncoding || requestedEncoding === "auto" ? loaded.encoding : resolveExplicitTextEncoding(requestedEncoding, loaded.encoding);
18952
- const targetHasBom = preserveEncoding ? loaded.hasBom : targetEncoding === "utf8-bom" || targetEncoding === "utf16le" || targetEncoding === "utf16be";
18953
19345
  ensureLossless(outputText, targetEncoding, targetHasBom);
18954
19346
  const buffer = encodeText(outputText, targetEncoding, targetHasBom);
18955
19347
  await fs3.writeFile(loaded.filePath, buffer);
@@ -18966,7 +19358,7 @@ async function replaceTextFileText(input) {
18966
19358
  bytesRead: loaded.bytesRead,
18967
19359
  bytesWritten: buffer.byteLength,
18968
19360
  newlineStyle: detectNewlineStyle(outputText),
18969
- diffPreview: buildLineDiffPreview(loaded.filePath, targetEncoding, input.oldString, alignedNewString)
19361
+ diffPreview: buildLineDiffPreview(loaded.filePath, targetEncoding, effectiveOldString, alignedNewString)
18970
19362
  };
18971
19363
  }
18972
19364