opencode-gbk-tools 0.1.14 → 0.1.15
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.
- package/README.md +1 -0
- package/dist/opencode-tools/gbk_edit.js +419 -13
- package/dist/opencode-tools/gbk_read.js +123 -114
- package/dist/opencode-tools/gbk_search.js +52 -56
- package/dist/opencode-tools/gbk_write.js +19 -8
- package/dist/opencode-tools/text_edit.js +1 -0
- package/dist/opencode-tools/text_read.js +1 -0
- package/dist/opencode-tools/text_write.js +1 -0
- package/dist/plugin/index.js +522 -160
- package/dist/release-manifest.json +8 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -195,6 +195,7 @@ A:不要。现在优先用 `mode="insertAfter"` 或 `mode="insertBefore"`,
|
|
|
195
195
|
|
|
196
196
|
| 版本 | 说明 |
|
|
197
197
|
|------|------|
|
|
198
|
+
| 0.1.15 | 为 GBK 大文件统一引入行字节索引与流式读/搜/改路径,提升局部编辑、搜索与大块修改时的性能、稳定性与准确性 |
|
|
198
199
|
| 0.1.14 | 重新发布当前稳定产物,核对 `npm pack --dry-run` 输出包含完整 `dist/`,为下一次公开发布提供一致的打包基线 |
|
|
199
200
|
| 0.1.13 | `text_write` 在新文件 + `encoding=auto` 下改为显式报错,不再静默默认 `utf8`;同时优化 `text_read` 流式读取与 `text_edit` 预览生成,减少重复读取 |
|
|
200
201
|
| 0.1.12 | 仅提升发布版本号,用于重新发布当前 `0.1.11` 的稳定内容 |
|
|
@@ -16305,6 +16305,14 @@ async function assertPathAllowed(filePath, context, allowExternal = false) {
|
|
|
16305
16305
|
|
|
16306
16306
|
// src/lib/gbk-file.ts
|
|
16307
16307
|
var STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
|
|
16308
|
+
var STREAM_READ_CHUNK_SIZE_BYTES = 1024 * 1024;
|
|
16309
|
+
var gbkLineIndexCache = /* @__PURE__ */ new Map();
|
|
16310
|
+
function toSafeNumber(value) {
|
|
16311
|
+
return typeof value === "bigint" ? Number(value) : value;
|
|
16312
|
+
}
|
|
16313
|
+
function invalidateGbkLineIndex(filePath) {
|
|
16314
|
+
gbkLineIndexCache.delete(filePath);
|
|
16315
|
+
}
|
|
16308
16316
|
function assertStringArgument(value, name) {
|
|
16309
16317
|
if (typeof value !== "string") {
|
|
16310
16318
|
throw createGbkError("GBK_INVALID_ARGUMENT", `${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
|
|
@@ -16417,6 +16425,18 @@ function detectNewlineStyle(text) {
|
|
|
16417
16425
|
}
|
|
16418
16426
|
return "none";
|
|
16419
16427
|
}
|
|
16428
|
+
function finalizeNewlineStyle(crlfCount, lfCount) {
|
|
16429
|
+
if (crlfCount > 0 && lfCount === 0) {
|
|
16430
|
+
return "crlf";
|
|
16431
|
+
}
|
|
16432
|
+
if (lfCount > 0 && crlfCount === 0) {
|
|
16433
|
+
return "lf";
|
|
16434
|
+
}
|
|
16435
|
+
if (crlfCount > 0 && lfCount > 0) {
|
|
16436
|
+
return "mixed";
|
|
16437
|
+
}
|
|
16438
|
+
return "none";
|
|
16439
|
+
}
|
|
16420
16440
|
function applyLineRange(text, startLine, endLine) {
|
|
16421
16441
|
if (startLine === void 0 && endLine === void 0) {
|
|
16422
16442
|
return {
|
|
@@ -16542,6 +16562,29 @@ function hasLineNumberPrefixes(lines) {
|
|
|
16542
16562
|
function stripLineNumberPrefixes(lines) {
|
|
16543
16563
|
return lines.map((l) => l.replace(/^\d+: /, ""));
|
|
16544
16564
|
}
|
|
16565
|
+
function parseLineNumberPrefixedBlock(text) {
|
|
16566
|
+
const lines = splitNormalizedLines(text);
|
|
16567
|
+
if (!hasLineNumberPrefixes(lines)) {
|
|
16568
|
+
return null;
|
|
16569
|
+
}
|
|
16570
|
+
const lineNumbers = [];
|
|
16571
|
+
const strippedLines = [];
|
|
16572
|
+
for (const line of lines) {
|
|
16573
|
+
const match = /^(\d+): ?(.*)$/.exec(line);
|
|
16574
|
+
if (!match) {
|
|
16575
|
+
return null;
|
|
16576
|
+
}
|
|
16577
|
+
lineNumbers.push(Number(match[1]));
|
|
16578
|
+
strippedLines.push(match[2]);
|
|
16579
|
+
}
|
|
16580
|
+
return {
|
|
16581
|
+
lineNumbers,
|
|
16582
|
+
strippedText: strippedLines.join("\n"),
|
|
16583
|
+
isContiguous: lineNumbers.every((lineNumber, index) => index === 0 || lineNumber === lineNumbers[index - 1] + 1),
|
|
16584
|
+
startLine: lineNumbers[0],
|
|
16585
|
+
endLine: lineNumbers[lineNumbers.length - 1]
|
|
16586
|
+
};
|
|
16587
|
+
}
|
|
16545
16588
|
function matchLooseBlock(contentLines, oldLines, newLines, newlineStyle, content) {
|
|
16546
16589
|
for (let start = 0; start < contentLines.length; start += 1) {
|
|
16547
16590
|
let contentIndex = start;
|
|
@@ -16658,7 +16701,7 @@ async function readWholeGbkTextFile(input) {
|
|
|
16658
16701
|
}
|
|
16659
16702
|
async function visitDecodedTextChunks(input, visitor) {
|
|
16660
16703
|
const decoder = import_iconv_lite.default.getDecoder(input.encoding);
|
|
16661
|
-
const stream = createReadStream(input.filePath);
|
|
16704
|
+
const stream = createReadStream(input.filePath, { highWaterMark: STREAM_READ_CHUNK_SIZE_BYTES });
|
|
16662
16705
|
try {
|
|
16663
16706
|
for await (const chunk of stream) {
|
|
16664
16707
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
@@ -16681,6 +16724,90 @@ async function visitDecodedTextChunks(input, visitor) {
|
|
|
16681
16724
|
stream.destroy();
|
|
16682
16725
|
}
|
|
16683
16726
|
}
|
|
16727
|
+
async function getGbkLineIndex(input) {
|
|
16728
|
+
const cached2 = gbkLineIndexCache.get(input.filePath);
|
|
16729
|
+
if (cached2 && cached2.fileSize === toSafeNumber(input.stat.size) && cached2.mtimeMs === toSafeNumber(input.stat.mtimeMs)) {
|
|
16730
|
+
return cached2;
|
|
16731
|
+
}
|
|
16732
|
+
const lineStartOffsets = [0];
|
|
16733
|
+
let byteOffset = 0;
|
|
16734
|
+
let previousByteWasCR = false;
|
|
16735
|
+
let crlfCount = 0;
|
|
16736
|
+
let lfCount = 0;
|
|
16737
|
+
const stream = createReadStream(input.filePath, { highWaterMark: STREAM_READ_CHUNK_SIZE_BYTES });
|
|
16738
|
+
try {
|
|
16739
|
+
for await (const chunk of stream) {
|
|
16740
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
16741
|
+
assertNotBinary(buffer);
|
|
16742
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
16743
|
+
const byte = buffer[index];
|
|
16744
|
+
if (byte === 10) {
|
|
16745
|
+
if (previousByteWasCR) {
|
|
16746
|
+
crlfCount += 1;
|
|
16747
|
+
} else {
|
|
16748
|
+
lfCount += 1;
|
|
16749
|
+
}
|
|
16750
|
+
lineStartOffsets.push(byteOffset + index + 1);
|
|
16751
|
+
previousByteWasCR = false;
|
|
16752
|
+
continue;
|
|
16753
|
+
}
|
|
16754
|
+
previousByteWasCR = byte === 13;
|
|
16755
|
+
}
|
|
16756
|
+
byteOffset += buffer.length;
|
|
16757
|
+
}
|
|
16758
|
+
} catch (error45) {
|
|
16759
|
+
if (error45 instanceof Error && "code" in error45) {
|
|
16760
|
+
throw error45;
|
|
16761
|
+
}
|
|
16762
|
+
throw createGbkError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${input.filePath}`, error45);
|
|
16763
|
+
} finally {
|
|
16764
|
+
stream.destroy();
|
|
16765
|
+
}
|
|
16766
|
+
const result = {
|
|
16767
|
+
filePath: input.filePath,
|
|
16768
|
+
fileSize: toSafeNumber(input.stat.size),
|
|
16769
|
+
mtimeMs: toSafeNumber(input.stat.mtimeMs),
|
|
16770
|
+
lineStartOffsets,
|
|
16771
|
+
totalLines: lineStartOffsets.length,
|
|
16772
|
+
newlineStyle: finalizeNewlineStyle(crlfCount, lfCount)
|
|
16773
|
+
};
|
|
16774
|
+
gbkLineIndexCache.set(input.filePath, result);
|
|
16775
|
+
return result;
|
|
16776
|
+
}
|
|
16777
|
+
async function readDecodedGbkByteRange(input, start, endExclusive) {
|
|
16778
|
+
if (endExclusive <= start) {
|
|
16779
|
+
return "";
|
|
16780
|
+
}
|
|
16781
|
+
const decoder = import_iconv_lite.default.getDecoder(input.encoding);
|
|
16782
|
+
const stream = createReadStream(input.filePath, {
|
|
16783
|
+
start,
|
|
16784
|
+
end: endExclusive - 1,
|
|
16785
|
+
highWaterMark: STREAM_READ_CHUNK_SIZE_BYTES
|
|
16786
|
+
});
|
|
16787
|
+
const parts = [];
|
|
16788
|
+
try {
|
|
16789
|
+
for await (const chunk of stream) {
|
|
16790
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
16791
|
+
assertNotBinary(buffer);
|
|
16792
|
+
const text = decoder.write(buffer);
|
|
16793
|
+
if (text.length > 0) {
|
|
16794
|
+
parts.push(text);
|
|
16795
|
+
}
|
|
16796
|
+
}
|
|
16797
|
+
const trailingText = decoder.end() ?? "";
|
|
16798
|
+
if (trailingText.length > 0) {
|
|
16799
|
+
parts.push(trailingText);
|
|
16800
|
+
}
|
|
16801
|
+
return parts.join("");
|
|
16802
|
+
} catch (error45) {
|
|
16803
|
+
if (error45 instanceof Error && "code" in error45) {
|
|
16804
|
+
throw error45;
|
|
16805
|
+
}
|
|
16806
|
+
throw createGbkError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${input.filePath}`, error45);
|
|
16807
|
+
} finally {
|
|
16808
|
+
stream.destroy();
|
|
16809
|
+
}
|
|
16810
|
+
}
|
|
16684
16811
|
async function writeAll(handle, buffer) {
|
|
16685
16812
|
let offset = 0;
|
|
16686
16813
|
while (offset < buffer.length) {
|
|
@@ -16696,6 +16823,249 @@ async function writeEncodedText(handle, encoding, text) {
|
|
|
16696
16823
|
await writeAll(handle, buffer);
|
|
16697
16824
|
return buffer.byteLength;
|
|
16698
16825
|
}
|
|
16826
|
+
async function copyFileByteRangeToHandle(sourcePath, handle, start, endExclusive) {
|
|
16827
|
+
if (endExclusive <= start) {
|
|
16828
|
+
return 0;
|
|
16829
|
+
}
|
|
16830
|
+
const stream = createReadStream(sourcePath, {
|
|
16831
|
+
start,
|
|
16832
|
+
end: endExclusive - 1,
|
|
16833
|
+
highWaterMark: STREAM_READ_CHUNK_SIZE_BYTES
|
|
16834
|
+
});
|
|
16835
|
+
let bytesWritten = 0;
|
|
16836
|
+
try {
|
|
16837
|
+
for await (const chunk of stream) {
|
|
16838
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
16839
|
+
await writeAll(handle, buffer);
|
|
16840
|
+
bytesWritten += buffer.byteLength;
|
|
16841
|
+
}
|
|
16842
|
+
return bytesWritten;
|
|
16843
|
+
} catch (error45) {
|
|
16844
|
+
if (error45 instanceof Error && "code" in error45) {
|
|
16845
|
+
throw error45;
|
|
16846
|
+
}
|
|
16847
|
+
throw createGbkError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${sourcePath}`, error45);
|
|
16848
|
+
} finally {
|
|
16849
|
+
stream.destroy();
|
|
16850
|
+
}
|
|
16851
|
+
}
|
|
16852
|
+
function alignTextToNewlineStyle(text, newlineStyle) {
|
|
16853
|
+
return newlineStyle === "crlf" ? text.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n") : newlineStyle === "lf" ? text.replace(/\r\n/g, "\n") : text;
|
|
16854
|
+
}
|
|
16855
|
+
function replaceScopedTextContent(scopeText, oldString, newString, replaceAll, newlineStyle) {
|
|
16856
|
+
const occurrencesBefore = countOccurrences(scopeText, oldString);
|
|
16857
|
+
if (!replaceAll && occurrencesBefore === 0) {
|
|
16858
|
+
const loose = tryLooseBlockReplace(scopeText, oldString, newString);
|
|
16859
|
+
if (loose !== null) {
|
|
16860
|
+
return {
|
|
16861
|
+
replacedText: loose.content,
|
|
16862
|
+
replacements: 1,
|
|
16863
|
+
occurrencesBefore: loose.occurrencesBefore
|
|
16864
|
+
};
|
|
16865
|
+
}
|
|
16866
|
+
}
|
|
16867
|
+
if (replaceAll) {
|
|
16868
|
+
if (occurrencesBefore === 0) {
|
|
16869
|
+
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scopeText, oldString));
|
|
16870
|
+
}
|
|
16871
|
+
} else if (occurrencesBefore === 0) {
|
|
16872
|
+
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scopeText, oldString));
|
|
16873
|
+
} else if (occurrencesBefore > 1) {
|
|
16874
|
+
throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${oldString}`);
|
|
16875
|
+
}
|
|
16876
|
+
const alignedNewString = alignTextToNewlineStyle(newString, newlineStyle);
|
|
16877
|
+
return {
|
|
16878
|
+
replacedText: replaceAll ? scopeText.split(oldString).join(alignedNewString) : scopeText.replace(oldString, alignedNewString),
|
|
16879
|
+
replacements: replaceAll ? occurrencesBefore : 1,
|
|
16880
|
+
occurrencesBefore
|
|
16881
|
+
};
|
|
16882
|
+
}
|
|
16883
|
+
async function replaceLargeGbkFileTextInLineRange(input) {
|
|
16884
|
+
const lineIndex = await getGbkLineIndex(input);
|
|
16885
|
+
const requestedStartLine = input.startLine ?? 1;
|
|
16886
|
+
const requestedEndLine = input.endLine ?? lineIndex.totalLines;
|
|
16887
|
+
if (requestedEndLine < requestedStartLine) {
|
|
16888
|
+
throw createGbkError("GBK_INVALID_ARGUMENT", "endLine \u4E0D\u80FD\u5C0F\u4E8E startLine");
|
|
16889
|
+
}
|
|
16890
|
+
const actualStartLine = Math.min(requestedStartLine, lineIndex.totalLines);
|
|
16891
|
+
const actualEndLine = Math.min(requestedEndLine, lineIndex.totalLines);
|
|
16892
|
+
const rangeStart = lineIndex.lineStartOffsets[Math.max(0, actualStartLine - 1)] ?? toSafeNumber(input.stat.size);
|
|
16893
|
+
const rangeEnd = actualEndLine < lineIndex.totalLines ? lineIndex.lineStartOffsets[actualEndLine] : toSafeNumber(input.stat.size);
|
|
16894
|
+
const scopeText = await readDecodedGbkByteRange(input, rangeStart, rangeEnd);
|
|
16895
|
+
const replaced = replaceScopedTextContent(
|
|
16896
|
+
scopeText,
|
|
16897
|
+
input.oldString,
|
|
16898
|
+
input.newString,
|
|
16899
|
+
input.replaceAll,
|
|
16900
|
+
lineIndex.newlineStyle
|
|
16901
|
+
);
|
|
16902
|
+
const tempPath = path2.join(
|
|
16903
|
+
path2.dirname(input.filePath),
|
|
16904
|
+
`${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
|
|
16905
|
+
);
|
|
16906
|
+
const handle = await fs2.open(tempPath, "w");
|
|
16907
|
+
let bytesWritten = 0;
|
|
16908
|
+
try {
|
|
16909
|
+
bytesWritten += await copyFileByteRangeToHandle(input.filePath, handle, 0, rangeStart);
|
|
16910
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, replaced.replacedText);
|
|
16911
|
+
bytesWritten += await copyFileByteRangeToHandle(input.filePath, handle, rangeEnd, toSafeNumber(input.stat.size));
|
|
16912
|
+
await handle.close();
|
|
16913
|
+
const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
|
|
16914
|
+
await fs2.chmod(tempPath, mode);
|
|
16915
|
+
await fs2.rename(tempPath, input.filePath);
|
|
16916
|
+
return {
|
|
16917
|
+
mode: "replace",
|
|
16918
|
+
filePath: input.filePath,
|
|
16919
|
+
encoding: input.encoding,
|
|
16920
|
+
replacements: replaced.replacements,
|
|
16921
|
+
occurrencesBefore: replaced.occurrencesBefore,
|
|
16922
|
+
bytesRead: input.stat.size,
|
|
16923
|
+
bytesWritten
|
|
16924
|
+
};
|
|
16925
|
+
} catch (error45) {
|
|
16926
|
+
await handle.close().catch(() => void 0);
|
|
16927
|
+
await fs2.rm(tempPath, { force: true }).catch(() => void 0);
|
|
16928
|
+
throw error45;
|
|
16929
|
+
}
|
|
16930
|
+
}
|
|
16931
|
+
async function replaceLargeGbkFileByAnchor(input) {
|
|
16932
|
+
const tempPath = path2.join(
|
|
16933
|
+
path2.dirname(input.filePath),
|
|
16934
|
+
`${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
|
|
16935
|
+
);
|
|
16936
|
+
const handle = await fs2.open(tempPath, "w");
|
|
16937
|
+
const alignedContent = input.content.replace(/\r\n/g, "\n");
|
|
16938
|
+
const anchorLength = input.anchor.length;
|
|
16939
|
+
const carryLength = Math.max(anchorLength + alignedContent.length, 1);
|
|
16940
|
+
let decoded = "";
|
|
16941
|
+
let scanFrom = 0;
|
|
16942
|
+
let totalMatches = 0;
|
|
16943
|
+
let inserted = false;
|
|
16944
|
+
let skipped = false;
|
|
16945
|
+
let bytesWritten = 0;
|
|
16946
|
+
const flushPrefix = async (preserveTailLength) => {
|
|
16947
|
+
const flushLength = Math.max(0, decoded.length - preserveTailLength);
|
|
16948
|
+
if (flushLength === 0) {
|
|
16949
|
+
return;
|
|
16950
|
+
}
|
|
16951
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, decoded.slice(0, flushLength));
|
|
16952
|
+
decoded = decoded.slice(flushLength);
|
|
16953
|
+
scanFrom = Math.max(0, scanFrom - flushLength);
|
|
16954
|
+
};
|
|
16955
|
+
const finalizeInserted = async () => {
|
|
16956
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, decoded);
|
|
16957
|
+
decoded = "";
|
|
16958
|
+
};
|
|
16959
|
+
try {
|
|
16960
|
+
await visitDecodedTextChunks(input, async (text) => {
|
|
16961
|
+
decoded += text;
|
|
16962
|
+
while (!inserted) {
|
|
16963
|
+
const foundAt = decoded.indexOf(input.anchor, scanFrom);
|
|
16964
|
+
if (foundAt === -1) {
|
|
16965
|
+
break;
|
|
16966
|
+
}
|
|
16967
|
+
totalMatches += 1;
|
|
16968
|
+
const afterAnchor = foundAt + anchorLength;
|
|
16969
|
+
if (totalMatches !== input.occurrence) {
|
|
16970
|
+
scanFrom = afterAnchor;
|
|
16971
|
+
continue;
|
|
16972
|
+
}
|
|
16973
|
+
const before = decoded.slice(0, foundAt);
|
|
16974
|
+
const after = decoded.slice(afterAnchor);
|
|
16975
|
+
const anchorAndAfter = decoded.slice(foundAt);
|
|
16976
|
+
const alreadyExists = input.mode === "insertAfter" ? after.startsWith(alignedContent) : before.endsWith(alignedContent);
|
|
16977
|
+
if (alreadyExists) {
|
|
16978
|
+
if (input.ifExists === "error") {
|
|
16979
|
+
throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
|
|
16980
|
+
}
|
|
16981
|
+
if (input.ifExists === "skip") {
|
|
16982
|
+
skipped = true;
|
|
16983
|
+
inserted = true;
|
|
16984
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, decoded);
|
|
16985
|
+
decoded = "";
|
|
16986
|
+
return;
|
|
16987
|
+
}
|
|
16988
|
+
}
|
|
16989
|
+
if (input.mode === "insertAfter") {
|
|
16990
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, before);
|
|
16991
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, input.anchor);
|
|
16992
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, alignedContent);
|
|
16993
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, after);
|
|
16994
|
+
} else {
|
|
16995
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, before);
|
|
16996
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, alignedContent);
|
|
16997
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, anchorAndAfter);
|
|
16998
|
+
}
|
|
16999
|
+
decoded = "";
|
|
17000
|
+
scanFrom = 0;
|
|
17001
|
+
inserted = true;
|
|
17002
|
+
return;
|
|
17003
|
+
}
|
|
17004
|
+
if (!inserted) {
|
|
17005
|
+
await flushPrefix(carryLength);
|
|
17006
|
+
}
|
|
17007
|
+
});
|
|
17008
|
+
if (!inserted) {
|
|
17009
|
+
while (true) {
|
|
17010
|
+
const foundAt = decoded.indexOf(input.anchor, scanFrom);
|
|
17011
|
+
if (foundAt === -1) {
|
|
17012
|
+
break;
|
|
17013
|
+
}
|
|
17014
|
+
totalMatches += 1;
|
|
17015
|
+
const afterAnchor = foundAt + anchorLength;
|
|
17016
|
+
if (totalMatches !== input.occurrence) {
|
|
17017
|
+
scanFrom = afterAnchor;
|
|
17018
|
+
continue;
|
|
17019
|
+
}
|
|
17020
|
+
const before = decoded.slice(0, foundAt);
|
|
17021
|
+
const after = decoded.slice(afterAnchor);
|
|
17022
|
+
const anchorAndAfter = decoded.slice(foundAt);
|
|
17023
|
+
const alreadyExists = input.mode === "insertAfter" ? after.startsWith(alignedContent) : before.endsWith(alignedContent);
|
|
17024
|
+
if (alreadyExists) {
|
|
17025
|
+
if (input.ifExists === "error") {
|
|
17026
|
+
throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
|
|
17027
|
+
}
|
|
17028
|
+
if (input.ifExists === "skip") {
|
|
17029
|
+
skipped = true;
|
|
17030
|
+
inserted = true;
|
|
17031
|
+
break;
|
|
17032
|
+
}
|
|
17033
|
+
}
|
|
17034
|
+
decoded = input.mode === "insertAfter" ? `${before}${input.anchor}${alignedContent}${after}` : `${before}${alignedContent}${anchorAndAfter}`;
|
|
17035
|
+
inserted = true;
|
|
17036
|
+
break;
|
|
17037
|
+
}
|
|
17038
|
+
}
|
|
17039
|
+
if (!inserted && totalMatches === 0) {
|
|
17040
|
+
throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\u70B9: ${input.anchor}`);
|
|
17041
|
+
}
|
|
17042
|
+
if (!inserted && totalMatches > 0) {
|
|
17043
|
+
throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${input.anchor} \u53EA\u627E\u5230 ${totalMatches} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7B2C ${input.occurrence} \u5904`);
|
|
17044
|
+
}
|
|
17045
|
+
await finalizeInserted();
|
|
17046
|
+
await handle.close();
|
|
17047
|
+
const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
|
|
17048
|
+
await fs2.chmod(tempPath, mode);
|
|
17049
|
+
await fs2.rename(tempPath, input.filePath);
|
|
17050
|
+
invalidateGbkLineIndex(input.filePath);
|
|
17051
|
+
return {
|
|
17052
|
+
mode: input.mode,
|
|
17053
|
+
filePath: input.filePath,
|
|
17054
|
+
encoding: input.encoding,
|
|
17055
|
+
anchor: input.anchor,
|
|
17056
|
+
occurrence: input.occurrence,
|
|
17057
|
+
anchorMatches: totalMatches,
|
|
17058
|
+
inserted: !skipped,
|
|
17059
|
+
skipped,
|
|
17060
|
+
bytesRead: input.stat.size,
|
|
17061
|
+
bytesWritten
|
|
17062
|
+
};
|
|
17063
|
+
} catch (error45) {
|
|
17064
|
+
await handle.close().catch(() => void 0);
|
|
17065
|
+
await fs2.rm(tempPath, { force: true }).catch(() => void 0);
|
|
17066
|
+
throw error45;
|
|
17067
|
+
}
|
|
17068
|
+
}
|
|
16699
17069
|
async function replaceLargeGbkFileText(input) {
|
|
16700
17070
|
const tempPath = path2.join(
|
|
16701
17071
|
path2.dirname(input.filePath),
|
|
@@ -16742,6 +17112,7 @@ async function replaceLargeGbkFileText(input) {
|
|
|
16742
17112
|
const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
|
|
16743
17113
|
await fs2.chmod(tempPath, mode);
|
|
16744
17114
|
await fs2.rename(tempPath, input.filePath);
|
|
17115
|
+
invalidateGbkLineIndex(input.filePath);
|
|
16745
17116
|
return {
|
|
16746
17117
|
filePath: input.filePath,
|
|
16747
17118
|
encoding: input.encoding,
|
|
@@ -16766,8 +17137,18 @@ async function replaceGbkFileText(input) {
|
|
|
16766
17137
|
if (mode === "insertAfter" || mode === "insertBefore") {
|
|
16767
17138
|
assertInsertArguments(input);
|
|
16768
17139
|
const resolved2 = await resolveReadableGbkFile(normalizedInput);
|
|
16769
|
-
const current2 = await readWholeGbkTextFile(resolved2);
|
|
16770
17140
|
const occurrence = normalizeOptionalPositiveInteger(input.occurrence, "occurrence") ?? 1;
|
|
17141
|
+
if (resolved2.stat.size >= STREAMING_FILE_SIZE_THRESHOLD_BYTES) {
|
|
17142
|
+
return await replaceLargeGbkFileByAnchor({
|
|
17143
|
+
...resolved2,
|
|
17144
|
+
mode,
|
|
17145
|
+
anchor: input.anchor,
|
|
17146
|
+
content: input.content,
|
|
17147
|
+
occurrence,
|
|
17148
|
+
ifExists: input.ifExists ?? "skip"
|
|
17149
|
+
});
|
|
17150
|
+
}
|
|
17151
|
+
const current2 = await readWholeGbkTextFile(resolved2);
|
|
16771
17152
|
const insertResult = insertByAnchor(
|
|
16772
17153
|
current2.content,
|
|
16773
17154
|
mode,
|
|
@@ -16812,24 +17193,47 @@ async function replaceGbkFileText(input) {
|
|
|
16812
17193
|
}
|
|
16813
17194
|
const replaceAll = normalizedInput.replaceAll ?? false;
|
|
16814
17195
|
const resolved = await resolveReadableGbkFile(normalizedInput);
|
|
16815
|
-
const
|
|
17196
|
+
const prefixedBlock = parseLineNumberPrefixedBlock(input.oldString);
|
|
17197
|
+
const derivedScopedRange = prefixedBlock?.isContiguous ? {
|
|
17198
|
+
startLine: normalizedInput.startLine ?? prefixedBlock.startLine,
|
|
17199
|
+
endLine: normalizedInput.endLine ?? prefixedBlock.endLine,
|
|
17200
|
+
oldString: prefixedBlock.strippedText
|
|
17201
|
+
} : null;
|
|
17202
|
+
const effectiveOldString = derivedScopedRange?.oldString ?? input.oldString;
|
|
17203
|
+
const hasScopedRange = normalizedInput.startLine !== void 0 || normalizedInput.endLine !== void 0 || normalizedInput.startAnchor !== void 0 || normalizedInput.endAnchor !== void 0 || derivedScopedRange !== null;
|
|
16816
17204
|
if (resolved.stat.size >= STREAMING_FILE_SIZE_THRESHOLD_BYTES && !hasScopedRange) {
|
|
16817
17205
|
return await replaceLargeGbkFileText({
|
|
16818
17206
|
...resolved,
|
|
16819
|
-
oldString:
|
|
17207
|
+
oldString: effectiveOldString,
|
|
16820
17208
|
newString: input.newString,
|
|
16821
17209
|
replaceAll
|
|
16822
17210
|
});
|
|
16823
17211
|
}
|
|
17212
|
+
if (resolved.stat.size >= STREAMING_FILE_SIZE_THRESHOLD_BYTES && normalizedInput.startAnchor === void 0 && normalizedInput.endAnchor === void 0 && (normalizedInput.startLine !== void 0 || normalizedInput.endLine !== void 0 || derivedScopedRange !== null)) {
|
|
17213
|
+
return await replaceLargeGbkFileTextInLineRange({
|
|
17214
|
+
...resolved,
|
|
17215
|
+
oldString: effectiveOldString,
|
|
17216
|
+
newString: input.newString,
|
|
17217
|
+
replaceAll,
|
|
17218
|
+
startLine: derivedScopedRange?.startLine ?? normalizedInput.startLine,
|
|
17219
|
+
endLine: derivedScopedRange?.endLine ?? normalizedInput.endLine
|
|
17220
|
+
});
|
|
17221
|
+
}
|
|
16824
17222
|
const current = await readWholeGbkTextFile(resolved);
|
|
16825
|
-
const
|
|
16826
|
-
|
|
17223
|
+
const scopedInput = derivedScopedRange === null ? normalizedInput : {
|
|
17224
|
+
...normalizedInput,
|
|
17225
|
+
startLine: derivedScopedRange.startLine,
|
|
17226
|
+
endLine: derivedScopedRange.endLine
|
|
17227
|
+
};
|
|
17228
|
+
const scope = resolveEditScope(current.content, scopedInput);
|
|
17229
|
+
const occurrencesBefore = countOccurrences(scope.selectedText, effectiveOldString);
|
|
16827
17230
|
if (!replaceAll && occurrencesBefore === 0) {
|
|
16828
|
-
const loose = tryLooseBlockReplace(scope.selectedText,
|
|
17231
|
+
const loose = tryLooseBlockReplace(scope.selectedText, effectiveOldString, input.newString);
|
|
16829
17232
|
if (loose !== null) {
|
|
16830
17233
|
const outputText2 = `${current.content.slice(0, scope.rangeStart)}${loose.content}${current.content.slice(scope.rangeEnd)}`;
|
|
16831
17234
|
const buffer2 = import_iconv_lite.default.encode(outputText2, current.encoding);
|
|
16832
17235
|
await fs2.writeFile(current.filePath, buffer2);
|
|
17236
|
+
invalidateGbkLineIndex(current.filePath);
|
|
16833
17237
|
return {
|
|
16834
17238
|
mode: "replace",
|
|
16835
17239
|
filePath: current.filePath,
|
|
@@ -16843,19 +17247,20 @@ async function replaceGbkFileText(input) {
|
|
|
16843
17247
|
}
|
|
16844
17248
|
if (replaceAll) {
|
|
16845
17249
|
if (occurrencesBefore === 0) {
|
|
16846
|
-
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText,
|
|
17250
|
+
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, effectiveOldString));
|
|
16847
17251
|
}
|
|
16848
17252
|
} else if (occurrencesBefore === 0) {
|
|
16849
|
-
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText,
|
|
17253
|
+
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, effectiveOldString));
|
|
16850
17254
|
} else if (occurrencesBefore > 1) {
|
|
16851
|
-
throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${
|
|
17255
|
+
throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${effectiveOldString}`);
|
|
16852
17256
|
}
|
|
16853
17257
|
const fileNewlineStyle = detectNewlineStyle(current.content);
|
|
16854
17258
|
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;
|
|
16855
|
-
const replaced = replaceAll ? scope.selectedText.split(
|
|
17259
|
+
const replaced = replaceAll ? scope.selectedText.split(effectiveOldString).join(alignedNewString) : scope.selectedText.replace(effectiveOldString, alignedNewString);
|
|
16856
17260
|
const outputText = `${current.content.slice(0, scope.rangeStart)}${replaced}${current.content.slice(scope.rangeEnd)}`;
|
|
16857
17261
|
const buffer = import_iconv_lite.default.encode(outputText, current.encoding);
|
|
16858
17262
|
await fs2.writeFile(current.filePath, buffer);
|
|
17263
|
+
invalidateGbkLineIndex(current.filePath);
|
|
16859
17264
|
return {
|
|
16860
17265
|
mode: "replace",
|
|
16861
17266
|
filePath: current.filePath,
|
|
@@ -16871,7 +17276,8 @@ async function replaceGbkFileText(input) {
|
|
|
16871
17276
|
var gbk_edit_default = tool({
|
|
16872
17277
|
description: `Edit GBK/GB18030 encoded text files with exact string replacement.
|
|
16873
17278
|
|
|
16874
|
-
|
|
17279
|
+
For whole-file replace, large files use a streaming path.
|
|
17280
|
+
For line-scoped replace, large files use a cached line-byte index and only decode the selected line window.
|
|
16875
17281
|
Safe to use on files with more than 2000 lines.
|
|
16876
17282
|
|
|
16877
17283
|
CRITICAL \u2014 do NOT include line number prefixes in oldString or newString:
|
|
@@ -16915,7 +17321,7 @@ Insert mode:
|
|
|
16915
17321
|
},
|
|
16916
17322
|
async execute(args, context) {
|
|
16917
17323
|
const result = await replaceGbkFileText({ ...args, context });
|
|
16918
|
-
const isReplace =
|
|
17324
|
+
const isReplace = "replacements" in result;
|
|
16919
17325
|
const title = isReplace ? `GBK \u7F16\u8F91 ${result.encoding.toUpperCase()} x${result.replacements}` : result.inserted ? `GBK \u63D2\u5165 ${result.encoding.toUpperCase()} #${result.occurrence}` : `GBK \u8DF3\u8FC7 ${result.encoding.toUpperCase()} #${result.occurrence}`;
|
|
16920
17326
|
context.metadata({
|
|
16921
17327
|
title,
|