opencode-gbk-tools 0.1.14 → 0.1.16
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 +16 -4
- package/dist/cli/index.js +11 -2
- 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 +540 -174
- package/dist/plugins/opencode-gbk-tools.js +18818 -2
- package/dist/release-manifest.json +2 -50
- package/package.json +1 -1
- package/dist/agents/gbk-engine.md +0 -39
package/dist/plugin/index.js
CHANGED
|
@@ -16306,6 +16306,14 @@ async function assertPathAllowed(filePath, context, allowExternal = false) {
|
|
|
16306
16306
|
|
|
16307
16307
|
// src/lib/gbk-file.ts
|
|
16308
16308
|
var STREAMING_FILE_SIZE_THRESHOLD_BYTES = 1024 * 1024;
|
|
16309
|
+
var STREAM_READ_CHUNK_SIZE_BYTES = 1024 * 1024;
|
|
16310
|
+
var gbkLineIndexCache = /* @__PURE__ */ new Map();
|
|
16311
|
+
function toSafeNumber(value) {
|
|
16312
|
+
return typeof value === "bigint" ? Number(value) : value;
|
|
16313
|
+
}
|
|
16314
|
+
function invalidateGbkLineIndex(filePath) {
|
|
16315
|
+
gbkLineIndexCache.delete(filePath);
|
|
16316
|
+
}
|
|
16309
16317
|
function assertStringArgument(value, name) {
|
|
16310
16318
|
if (typeof value !== "string") {
|
|
16311
16319
|
throw createGbkError("GBK_INVALID_ARGUMENT", `${name} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32`);
|
|
@@ -16469,6 +16477,16 @@ function finalizeNewlineStyle(crlfCount, lfCount) {
|
|
|
16469
16477
|
}
|
|
16470
16478
|
return "none";
|
|
16471
16479
|
}
|
|
16480
|
+
function formatLineWindowContent(text, startLine, expectedLineCount) {
|
|
16481
|
+
const lines = text.length === 0 ? [""] : text.split(/\r?\n/);
|
|
16482
|
+
while (lines.length > expectedLineCount && lines[lines.length - 1] === "") {
|
|
16483
|
+
lines.pop();
|
|
16484
|
+
}
|
|
16485
|
+
while (lines.length < expectedLineCount) {
|
|
16486
|
+
lines.push("");
|
|
16487
|
+
}
|
|
16488
|
+
return lines.slice(0, expectedLineCount).map((line, index) => `${startLine + index}: ${line}`).join("\n");
|
|
16489
|
+
}
|
|
16472
16490
|
function applyLineRange(text, startLine, endLine) {
|
|
16473
16491
|
if (startLine === void 0 && endLine === void 0) {
|
|
16474
16492
|
return {
|
|
@@ -16594,6 +16612,29 @@ function hasLineNumberPrefixes(lines) {
|
|
|
16594
16612
|
function stripLineNumberPrefixes(lines) {
|
|
16595
16613
|
return lines.map((l) => l.replace(/^\d+: /, ""));
|
|
16596
16614
|
}
|
|
16615
|
+
function parseLineNumberPrefixedBlock(text) {
|
|
16616
|
+
const lines = splitNormalizedLines(text);
|
|
16617
|
+
if (!hasLineNumberPrefixes(lines)) {
|
|
16618
|
+
return null;
|
|
16619
|
+
}
|
|
16620
|
+
const lineNumbers = [];
|
|
16621
|
+
const strippedLines = [];
|
|
16622
|
+
for (const line of lines) {
|
|
16623
|
+
const match = /^(\d+): ?(.*)$/.exec(line);
|
|
16624
|
+
if (!match) {
|
|
16625
|
+
return null;
|
|
16626
|
+
}
|
|
16627
|
+
lineNumbers.push(Number(match[1]));
|
|
16628
|
+
strippedLines.push(match[2]);
|
|
16629
|
+
}
|
|
16630
|
+
return {
|
|
16631
|
+
lineNumbers,
|
|
16632
|
+
strippedText: strippedLines.join("\n"),
|
|
16633
|
+
isContiguous: lineNumbers.every((lineNumber, index) => index === 0 || lineNumber === lineNumbers[index - 1] + 1),
|
|
16634
|
+
startLine: lineNumbers[0],
|
|
16635
|
+
endLine: lineNumbers[lineNumbers.length - 1]
|
|
16636
|
+
};
|
|
16637
|
+
}
|
|
16597
16638
|
function matchLooseBlock(contentLines, oldLines, newLines, newlineStyle, content) {
|
|
16598
16639
|
for (let start = 0; start < contentLines.length; start += 1) {
|
|
16599
16640
|
let contentIndex = start;
|
|
@@ -16710,7 +16751,7 @@ async function readWholeGbkTextFile(input) {
|
|
|
16710
16751
|
}
|
|
16711
16752
|
async function visitDecodedTextChunks(input, visitor) {
|
|
16712
16753
|
const decoder = import_iconv_lite.default.getDecoder(input.encoding);
|
|
16713
|
-
const stream = createReadStream(input.filePath);
|
|
16754
|
+
const stream = createReadStream(input.filePath, { highWaterMark: STREAM_READ_CHUNK_SIZE_BYTES });
|
|
16714
16755
|
try {
|
|
16715
16756
|
for await (const chunk of stream) {
|
|
16716
16757
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
@@ -16733,109 +16774,89 @@ async function visitDecodedTextChunks(input, visitor) {
|
|
|
16733
16774
|
stream.destroy();
|
|
16734
16775
|
}
|
|
16735
16776
|
}
|
|
16736
|
-
function
|
|
16737
|
-
const
|
|
16738
|
-
|
|
16739
|
-
|
|
16777
|
+
async function getGbkLineIndex(input) {
|
|
16778
|
+
const cached2 = gbkLineIndexCache.get(input.filePath);
|
|
16779
|
+
if (cached2 && cached2.fileSize === toSafeNumber(input.stat.size) && cached2.mtimeMs === toSafeNumber(input.stat.mtimeMs)) {
|
|
16780
|
+
return cached2;
|
|
16781
|
+
}
|
|
16782
|
+
const lineStartOffsets = [0];
|
|
16783
|
+
let byteOffset = 0;
|
|
16784
|
+
let previousByteWasCR = false;
|
|
16740
16785
|
let crlfCount = 0;
|
|
16741
16786
|
let lfCount = 0;
|
|
16742
|
-
const
|
|
16743
|
-
|
|
16744
|
-
|
|
16745
|
-
|
|
16746
|
-
|
|
16747
|
-
|
|
16748
|
-
|
|
16749
|
-
|
|
16750
|
-
|
|
16751
|
-
|
|
16752
|
-
|
|
16753
|
-
|
|
16754
|
-
|
|
16755
|
-
|
|
16756
|
-
|
|
16757
|
-
|
|
16758
|
-
break;
|
|
16759
|
-
}
|
|
16760
|
-
let line = combined.slice(start, newlineIndex);
|
|
16761
|
-
if (line.endsWith("\r")) {
|
|
16762
|
-
line = line.slice(0, -1);
|
|
16763
|
-
crlfCount += 1;
|
|
16764
|
-
} else {
|
|
16765
|
-
lfCount += 1;
|
|
16787
|
+
const stream = createReadStream(input.filePath, { highWaterMark: STREAM_READ_CHUNK_SIZE_BYTES });
|
|
16788
|
+
try {
|
|
16789
|
+
for await (const chunk of stream) {
|
|
16790
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
16791
|
+
assertNotBinary(buffer);
|
|
16792
|
+
for (let index = 0; index < buffer.length; index += 1) {
|
|
16793
|
+
const byte = buffer[index];
|
|
16794
|
+
if (byte === 10) {
|
|
16795
|
+
if (previousByteWasCR) {
|
|
16796
|
+
crlfCount += 1;
|
|
16797
|
+
} else {
|
|
16798
|
+
lfCount += 1;
|
|
16799
|
+
}
|
|
16800
|
+
lineStartOffsets.push(byteOffset + index + 1);
|
|
16801
|
+
previousByteWasCR = false;
|
|
16802
|
+
continue;
|
|
16766
16803
|
}
|
|
16767
|
-
|
|
16768
|
-
start = newlineIndex + 1;
|
|
16804
|
+
previousByteWasCR = byte === 13;
|
|
16769
16805
|
}
|
|
16770
|
-
|
|
16771
|
-
},
|
|
16772
|
-
finish() {
|
|
16773
|
-
emitLine(pending);
|
|
16774
|
-
const endLine = lines.length === 0 ? totalLines : offset + lines.length - 1;
|
|
16775
|
-
return {
|
|
16776
|
-
startLine: offset,
|
|
16777
|
-
endLine,
|
|
16778
|
-
totalLines,
|
|
16779
|
-
content: lines.join("\n"),
|
|
16780
|
-
tail: false,
|
|
16781
|
-
truncated: endLine < totalLines,
|
|
16782
|
-
newlineStyle: finalizeNewlineStyle(crlfCount, lfCount)
|
|
16783
|
-
};
|
|
16806
|
+
byteOffset += buffer.length;
|
|
16784
16807
|
}
|
|
16785
|
-
}
|
|
16786
|
-
|
|
16787
|
-
|
|
16788
|
-
assertPositiveInteger(limit, "limit");
|
|
16789
|
-
const lines = [];
|
|
16790
|
-
let pending = "";
|
|
16791
|
-
let totalLines = 0;
|
|
16792
|
-
let crlfCount = 0;
|
|
16793
|
-
let lfCount = 0;
|
|
16794
|
-
const emitLine = (line) => {
|
|
16795
|
-
totalLines += 1;
|
|
16796
|
-
lines.push(line);
|
|
16797
|
-
if (lines.length > limit) {
|
|
16798
|
-
lines.shift();
|
|
16808
|
+
} catch (error45) {
|
|
16809
|
+
if (error45 instanceof Error && "code" in error45) {
|
|
16810
|
+
throw error45;
|
|
16799
16811
|
}
|
|
16812
|
+
throw createGbkError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${input.filePath}`, error45);
|
|
16813
|
+
} finally {
|
|
16814
|
+
stream.destroy();
|
|
16815
|
+
}
|
|
16816
|
+
const result = {
|
|
16817
|
+
filePath: input.filePath,
|
|
16818
|
+
fileSize: toSafeNumber(input.stat.size),
|
|
16819
|
+
mtimeMs: toSafeNumber(input.stat.mtimeMs),
|
|
16820
|
+
lineStartOffsets,
|
|
16821
|
+
totalLines: lineStartOffsets.length,
|
|
16822
|
+
newlineStyle: finalizeNewlineStyle(crlfCount, lfCount)
|
|
16800
16823
|
};
|
|
16801
|
-
|
|
16802
|
-
|
|
16803
|
-
|
|
16804
|
-
|
|
16805
|
-
|
|
16806
|
-
|
|
16807
|
-
|
|
16808
|
-
|
|
16809
|
-
|
|
16810
|
-
|
|
16811
|
-
|
|
16812
|
-
|
|
16813
|
-
|
|
16814
|
-
|
|
16815
|
-
|
|
16816
|
-
|
|
16817
|
-
|
|
16818
|
-
|
|
16819
|
-
|
|
16820
|
-
|
|
16821
|
-
|
|
16824
|
+
gbkLineIndexCache.set(input.filePath, result);
|
|
16825
|
+
return result;
|
|
16826
|
+
}
|
|
16827
|
+
async function readDecodedGbkByteRange(input, start, endExclusive) {
|
|
16828
|
+
if (endExclusive <= start) {
|
|
16829
|
+
return "";
|
|
16830
|
+
}
|
|
16831
|
+
const decoder = import_iconv_lite.default.getDecoder(input.encoding);
|
|
16832
|
+
const stream = createReadStream(input.filePath, {
|
|
16833
|
+
start,
|
|
16834
|
+
end: endExclusive - 1,
|
|
16835
|
+
highWaterMark: STREAM_READ_CHUNK_SIZE_BYTES
|
|
16836
|
+
});
|
|
16837
|
+
const parts = [];
|
|
16838
|
+
try {
|
|
16839
|
+
for await (const chunk of stream) {
|
|
16840
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
16841
|
+
assertNotBinary(buffer);
|
|
16842
|
+
const text = decoder.write(buffer);
|
|
16843
|
+
if (text.length > 0) {
|
|
16844
|
+
parts.push(text);
|
|
16822
16845
|
}
|
|
16823
|
-
pending = combined.slice(start);
|
|
16824
|
-
},
|
|
16825
|
-
finish() {
|
|
16826
|
-
emitLine(pending);
|
|
16827
|
-
const startLine = Math.max(1, totalLines - lines.length + 1);
|
|
16828
|
-
return {
|
|
16829
|
-
startLine,
|
|
16830
|
-
endLine: totalLines,
|
|
16831
|
-
totalLines,
|
|
16832
|
-
content: lines.map((line, index) => `${startLine + index}: ${line}`).join("\n"),
|
|
16833
|
-
truncated: startLine > 1,
|
|
16834
|
-
tail: true,
|
|
16835
|
-
newlineStyle: finalizeNewlineStyle(crlfCount, lfCount)
|
|
16836
|
-
};
|
|
16837
16846
|
}
|
|
16838
|
-
|
|
16847
|
+
const trailingText = decoder.end() ?? "";
|
|
16848
|
+
if (trailingText.length > 0) {
|
|
16849
|
+
parts.push(trailingText);
|
|
16850
|
+
}
|
|
16851
|
+
return parts.join("");
|
|
16852
|
+
} catch (error45) {
|
|
16853
|
+
if (error45 instanceof Error && "code" in error45) {
|
|
16854
|
+
throw error45;
|
|
16855
|
+
}
|
|
16856
|
+
throw createGbkError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${input.filePath}`, error45);
|
|
16857
|
+
} finally {
|
|
16858
|
+
stream.destroy();
|
|
16859
|
+
}
|
|
16839
16860
|
}
|
|
16840
16861
|
async function writeAll(handle, buffer) {
|
|
16841
16862
|
let offset = 0;
|
|
@@ -16852,6 +16873,257 @@ async function writeEncodedText(handle, encoding, text) {
|
|
|
16852
16873
|
await writeAll(handle, buffer);
|
|
16853
16874
|
return buffer.byteLength;
|
|
16854
16875
|
}
|
|
16876
|
+
async function appendEncodedText(filePath, encoding, text) {
|
|
16877
|
+
if (text.length === 0) {
|
|
16878
|
+
return 0;
|
|
16879
|
+
}
|
|
16880
|
+
const buffer = import_iconv_lite.default.encode(text, encoding);
|
|
16881
|
+
await fs2.appendFile(filePath, buffer);
|
|
16882
|
+
return buffer.byteLength;
|
|
16883
|
+
}
|
|
16884
|
+
async function copyFileByteRangeToHandle(sourcePath, handle, start, endExclusive) {
|
|
16885
|
+
if (endExclusive <= start) {
|
|
16886
|
+
return 0;
|
|
16887
|
+
}
|
|
16888
|
+
const stream = createReadStream(sourcePath, {
|
|
16889
|
+
start,
|
|
16890
|
+
end: endExclusive - 1,
|
|
16891
|
+
highWaterMark: STREAM_READ_CHUNK_SIZE_BYTES
|
|
16892
|
+
});
|
|
16893
|
+
let bytesWritten = 0;
|
|
16894
|
+
try {
|
|
16895
|
+
for await (const chunk of stream) {
|
|
16896
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
16897
|
+
await writeAll(handle, buffer);
|
|
16898
|
+
bytesWritten += buffer.byteLength;
|
|
16899
|
+
}
|
|
16900
|
+
return bytesWritten;
|
|
16901
|
+
} catch (error45) {
|
|
16902
|
+
if (error45 instanceof Error && "code" in error45) {
|
|
16903
|
+
throw error45;
|
|
16904
|
+
}
|
|
16905
|
+
throw createGbkError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${sourcePath}`, error45);
|
|
16906
|
+
} finally {
|
|
16907
|
+
stream.destroy();
|
|
16908
|
+
}
|
|
16909
|
+
}
|
|
16910
|
+
function alignTextToNewlineStyle(text, newlineStyle) {
|
|
16911
|
+
return newlineStyle === "crlf" ? text.replace(/\r\n/g, "\n").replace(/\n/g, "\r\n") : newlineStyle === "lf" ? text.replace(/\r\n/g, "\n") : text;
|
|
16912
|
+
}
|
|
16913
|
+
function replaceScopedTextContent(scopeText, oldString, newString, replaceAll, newlineStyle) {
|
|
16914
|
+
const occurrencesBefore = countOccurrences(scopeText, oldString);
|
|
16915
|
+
if (!replaceAll && occurrencesBefore === 0) {
|
|
16916
|
+
const loose = tryLooseBlockReplace(scopeText, oldString, newString);
|
|
16917
|
+
if (loose !== null) {
|
|
16918
|
+
return {
|
|
16919
|
+
replacedText: loose.content,
|
|
16920
|
+
replacements: 1,
|
|
16921
|
+
occurrencesBefore: loose.occurrencesBefore
|
|
16922
|
+
};
|
|
16923
|
+
}
|
|
16924
|
+
}
|
|
16925
|
+
if (replaceAll) {
|
|
16926
|
+
if (occurrencesBefore === 0) {
|
|
16927
|
+
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scopeText, oldString));
|
|
16928
|
+
}
|
|
16929
|
+
} else if (occurrencesBefore === 0) {
|
|
16930
|
+
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scopeText, oldString));
|
|
16931
|
+
} else if (occurrencesBefore > 1) {
|
|
16932
|
+
throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${oldString}`);
|
|
16933
|
+
}
|
|
16934
|
+
const alignedNewString = alignTextToNewlineStyle(newString, newlineStyle);
|
|
16935
|
+
return {
|
|
16936
|
+
replacedText: replaceAll ? scopeText.split(oldString).join(alignedNewString) : scopeText.replace(oldString, alignedNewString),
|
|
16937
|
+
replacements: replaceAll ? occurrencesBefore : 1,
|
|
16938
|
+
occurrencesBefore
|
|
16939
|
+
};
|
|
16940
|
+
}
|
|
16941
|
+
async function replaceLargeGbkFileTextInLineRange(input) {
|
|
16942
|
+
const lineIndex = await getGbkLineIndex(input);
|
|
16943
|
+
const requestedStartLine = input.startLine ?? 1;
|
|
16944
|
+
const requestedEndLine = input.endLine ?? lineIndex.totalLines;
|
|
16945
|
+
if (requestedEndLine < requestedStartLine) {
|
|
16946
|
+
throw createGbkError("GBK_INVALID_ARGUMENT", "endLine \u4E0D\u80FD\u5C0F\u4E8E startLine");
|
|
16947
|
+
}
|
|
16948
|
+
const actualStartLine = Math.min(requestedStartLine, lineIndex.totalLines);
|
|
16949
|
+
const actualEndLine = Math.min(requestedEndLine, lineIndex.totalLines);
|
|
16950
|
+
const rangeStart = lineIndex.lineStartOffsets[Math.max(0, actualStartLine - 1)] ?? toSafeNumber(input.stat.size);
|
|
16951
|
+
const rangeEnd = actualEndLine < lineIndex.totalLines ? lineIndex.lineStartOffsets[actualEndLine] : toSafeNumber(input.stat.size);
|
|
16952
|
+
const scopeText = await readDecodedGbkByteRange(input, rangeStart, rangeEnd);
|
|
16953
|
+
const replaced = replaceScopedTextContent(
|
|
16954
|
+
scopeText,
|
|
16955
|
+
input.oldString,
|
|
16956
|
+
input.newString,
|
|
16957
|
+
input.replaceAll,
|
|
16958
|
+
lineIndex.newlineStyle
|
|
16959
|
+
);
|
|
16960
|
+
const tempPath = path2.join(
|
|
16961
|
+
path2.dirname(input.filePath),
|
|
16962
|
+
`${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
|
|
16963
|
+
);
|
|
16964
|
+
const handle = await fs2.open(tempPath, "w");
|
|
16965
|
+
let bytesWritten = 0;
|
|
16966
|
+
try {
|
|
16967
|
+
bytesWritten += await copyFileByteRangeToHandle(input.filePath, handle, 0, rangeStart);
|
|
16968
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, replaced.replacedText);
|
|
16969
|
+
bytesWritten += await copyFileByteRangeToHandle(input.filePath, handle, rangeEnd, toSafeNumber(input.stat.size));
|
|
16970
|
+
await handle.close();
|
|
16971
|
+
const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
|
|
16972
|
+
await fs2.chmod(tempPath, mode);
|
|
16973
|
+
await fs2.rename(tempPath, input.filePath);
|
|
16974
|
+
return {
|
|
16975
|
+
mode: "replace",
|
|
16976
|
+
filePath: input.filePath,
|
|
16977
|
+
encoding: input.encoding,
|
|
16978
|
+
replacements: replaced.replacements,
|
|
16979
|
+
occurrencesBefore: replaced.occurrencesBefore,
|
|
16980
|
+
bytesRead: input.stat.size,
|
|
16981
|
+
bytesWritten
|
|
16982
|
+
};
|
|
16983
|
+
} catch (error45) {
|
|
16984
|
+
await handle.close().catch(() => void 0);
|
|
16985
|
+
await fs2.rm(tempPath, { force: true }).catch(() => void 0);
|
|
16986
|
+
throw error45;
|
|
16987
|
+
}
|
|
16988
|
+
}
|
|
16989
|
+
async function replaceLargeGbkFileByAnchor(input) {
|
|
16990
|
+
const tempPath = path2.join(
|
|
16991
|
+
path2.dirname(input.filePath),
|
|
16992
|
+
`${path2.basename(input.filePath)}.opencode-gbk-${crypto.randomUUID()}.tmp`
|
|
16993
|
+
);
|
|
16994
|
+
const handle = await fs2.open(tempPath, "w");
|
|
16995
|
+
const alignedContent = input.content.replace(/\r\n/g, "\n");
|
|
16996
|
+
const anchorLength = input.anchor.length;
|
|
16997
|
+
const carryLength = Math.max(anchorLength + alignedContent.length, 1);
|
|
16998
|
+
let decoded = "";
|
|
16999
|
+
let scanFrom = 0;
|
|
17000
|
+
let totalMatches = 0;
|
|
17001
|
+
let inserted = false;
|
|
17002
|
+
let skipped = false;
|
|
17003
|
+
let bytesWritten = 0;
|
|
17004
|
+
const flushPrefix = async (preserveTailLength) => {
|
|
17005
|
+
const flushLength = Math.max(0, decoded.length - preserveTailLength);
|
|
17006
|
+
if (flushLength === 0) {
|
|
17007
|
+
return;
|
|
17008
|
+
}
|
|
17009
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, decoded.slice(0, flushLength));
|
|
17010
|
+
decoded = decoded.slice(flushLength);
|
|
17011
|
+
scanFrom = Math.max(0, scanFrom - flushLength);
|
|
17012
|
+
};
|
|
17013
|
+
const finalizeInserted = async () => {
|
|
17014
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, decoded);
|
|
17015
|
+
decoded = "";
|
|
17016
|
+
};
|
|
17017
|
+
try {
|
|
17018
|
+
await visitDecodedTextChunks(input, async (text) => {
|
|
17019
|
+
decoded += text;
|
|
17020
|
+
while (!inserted) {
|
|
17021
|
+
const foundAt = decoded.indexOf(input.anchor, scanFrom);
|
|
17022
|
+
if (foundAt === -1) {
|
|
17023
|
+
break;
|
|
17024
|
+
}
|
|
17025
|
+
totalMatches += 1;
|
|
17026
|
+
const afterAnchor = foundAt + anchorLength;
|
|
17027
|
+
if (totalMatches !== input.occurrence) {
|
|
17028
|
+
scanFrom = afterAnchor;
|
|
17029
|
+
continue;
|
|
17030
|
+
}
|
|
17031
|
+
const before = decoded.slice(0, foundAt);
|
|
17032
|
+
const after = decoded.slice(afterAnchor);
|
|
17033
|
+
const anchorAndAfter = decoded.slice(foundAt);
|
|
17034
|
+
const alreadyExists = input.mode === "insertAfter" ? after.startsWith(alignedContent) : before.endsWith(alignedContent);
|
|
17035
|
+
if (alreadyExists) {
|
|
17036
|
+
if (input.ifExists === "error") {
|
|
17037
|
+
throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
|
|
17038
|
+
}
|
|
17039
|
+
if (input.ifExists === "skip") {
|
|
17040
|
+
skipped = true;
|
|
17041
|
+
inserted = true;
|
|
17042
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, decoded);
|
|
17043
|
+
decoded = "";
|
|
17044
|
+
return;
|
|
17045
|
+
}
|
|
17046
|
+
}
|
|
17047
|
+
if (input.mode === "insertAfter") {
|
|
17048
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, before);
|
|
17049
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, input.anchor);
|
|
17050
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, alignedContent);
|
|
17051
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, after);
|
|
17052
|
+
} else {
|
|
17053
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, before);
|
|
17054
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, alignedContent);
|
|
17055
|
+
bytesWritten += await writeEncodedText(handle, input.encoding, anchorAndAfter);
|
|
17056
|
+
}
|
|
17057
|
+
decoded = "";
|
|
17058
|
+
scanFrom = 0;
|
|
17059
|
+
inserted = true;
|
|
17060
|
+
return;
|
|
17061
|
+
}
|
|
17062
|
+
if (!inserted) {
|
|
17063
|
+
await flushPrefix(carryLength);
|
|
17064
|
+
}
|
|
17065
|
+
});
|
|
17066
|
+
if (!inserted) {
|
|
17067
|
+
while (true) {
|
|
17068
|
+
const foundAt = decoded.indexOf(input.anchor, scanFrom);
|
|
17069
|
+
if (foundAt === -1) {
|
|
17070
|
+
break;
|
|
17071
|
+
}
|
|
17072
|
+
totalMatches += 1;
|
|
17073
|
+
const afterAnchor = foundAt + anchorLength;
|
|
17074
|
+
if (totalMatches !== input.occurrence) {
|
|
17075
|
+
scanFrom = afterAnchor;
|
|
17076
|
+
continue;
|
|
17077
|
+
}
|
|
17078
|
+
const before = decoded.slice(0, foundAt);
|
|
17079
|
+
const after = decoded.slice(afterAnchor);
|
|
17080
|
+
const anchorAndAfter = decoded.slice(foundAt);
|
|
17081
|
+
const alreadyExists = input.mode === "insertAfter" ? after.startsWith(alignedContent) : before.endsWith(alignedContent);
|
|
17082
|
+
if (alreadyExists) {
|
|
17083
|
+
if (input.ifExists === "error") {
|
|
17084
|
+
throw createGbkError("GBK_INVALID_ARGUMENT", "\u63D2\u5165\u4F4D\u7F6E\u5DF2\u5B58\u5728\u76F8\u540C\u5185\u5BB9");
|
|
17085
|
+
}
|
|
17086
|
+
if (input.ifExists === "skip") {
|
|
17087
|
+
skipped = true;
|
|
17088
|
+
inserted = true;
|
|
17089
|
+
break;
|
|
17090
|
+
}
|
|
17091
|
+
}
|
|
17092
|
+
decoded = input.mode === "insertAfter" ? `${before}${input.anchor}${alignedContent}${after}` : `${before}${alignedContent}${anchorAndAfter}`;
|
|
17093
|
+
inserted = true;
|
|
17094
|
+
break;
|
|
17095
|
+
}
|
|
17096
|
+
}
|
|
17097
|
+
if (!inserted && totalMatches === 0) {
|
|
17098
|
+
throw createGbkError("GBK_NO_MATCH", `\u672A\u627E\u5230\u951A\u70B9: ${input.anchor}`);
|
|
17099
|
+
}
|
|
17100
|
+
if (!inserted && totalMatches > 0) {
|
|
17101
|
+
throw createGbkError("GBK_NO_MATCH", `\u951A\u70B9 ${input.anchor} \u53EA\u627E\u5230 ${totalMatches} \u5904\uFF0C\u65E0\u6CD5\u4F7F\u7528\u7B2C ${input.occurrence} \u5904`);
|
|
17102
|
+
}
|
|
17103
|
+
await finalizeInserted();
|
|
17104
|
+
await handle.close();
|
|
17105
|
+
const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
|
|
17106
|
+
await fs2.chmod(tempPath, mode);
|
|
17107
|
+
await fs2.rename(tempPath, input.filePath);
|
|
17108
|
+
invalidateGbkLineIndex(input.filePath);
|
|
17109
|
+
return {
|
|
17110
|
+
mode: input.mode,
|
|
17111
|
+
filePath: input.filePath,
|
|
17112
|
+
encoding: input.encoding,
|
|
17113
|
+
anchor: input.anchor,
|
|
17114
|
+
occurrence: input.occurrence,
|
|
17115
|
+
anchorMatches: totalMatches,
|
|
17116
|
+
inserted: !skipped,
|
|
17117
|
+
skipped,
|
|
17118
|
+
bytesRead: input.stat.size,
|
|
17119
|
+
bytesWritten
|
|
17120
|
+
};
|
|
17121
|
+
} catch (error45) {
|
|
17122
|
+
await handle.close().catch(() => void 0);
|
|
17123
|
+
await fs2.rm(tempPath, { force: true }).catch(() => void 0);
|
|
17124
|
+
throw error45;
|
|
17125
|
+
}
|
|
17126
|
+
}
|
|
16855
17127
|
async function replaceLargeGbkFileText(input) {
|
|
16856
17128
|
const tempPath = path2.join(
|
|
16857
17129
|
path2.dirname(input.filePath),
|
|
@@ -16898,6 +17170,7 @@ async function replaceLargeGbkFileText(input) {
|
|
|
16898
17170
|
const mode = typeof input.stat.mode === "bigint" ? Number(input.stat.mode) : input.stat.mode;
|
|
16899
17171
|
await fs2.chmod(tempPath, mode);
|
|
16900
17172
|
await fs2.rename(tempPath, input.filePath);
|
|
17173
|
+
invalidateGbkLineIndex(input.filePath);
|
|
16901
17174
|
return {
|
|
16902
17175
|
filePath: input.filePath,
|
|
16903
17176
|
encoding: input.encoding,
|
|
@@ -16912,22 +17185,6 @@ async function replaceLargeGbkFileText(input) {
|
|
|
16912
17185
|
throw error45;
|
|
16913
17186
|
}
|
|
16914
17187
|
}
|
|
16915
|
-
async function loadGbkTextFile(input) {
|
|
16916
|
-
const resolved = await resolveReadableGbkFile(input);
|
|
16917
|
-
if (resolved.stat.size < STREAMING_FILE_SIZE_THRESHOLD_BYTES) {
|
|
16918
|
-
return await readWholeGbkTextFile(resolved);
|
|
16919
|
-
}
|
|
16920
|
-
const chunks = [];
|
|
16921
|
-
await visitDecodedTextChunks(resolved, (text) => {
|
|
16922
|
-
chunks.push(text);
|
|
16923
|
-
});
|
|
16924
|
-
return {
|
|
16925
|
-
filePath: resolved.filePath,
|
|
16926
|
-
encoding: resolved.encoding,
|
|
16927
|
-
content: chunks.join(""),
|
|
16928
|
-
bytesRead: resolved.stat.size
|
|
16929
|
-
};
|
|
16930
|
-
}
|
|
16931
17188
|
async function readGbkFile(input) {
|
|
16932
17189
|
const offset = normalizeOptionalPositiveInteger(input.offset, "offset") ?? 1;
|
|
16933
17190
|
const limit = normalizeOptionalPositiveInteger(input.limit, "limit") ?? 2e3;
|
|
@@ -16953,11 +17210,49 @@ async function readGbkFile(input) {
|
|
|
16953
17210
|
...lineWindow2
|
|
16954
17211
|
};
|
|
16955
17212
|
}
|
|
16956
|
-
const
|
|
16957
|
-
|
|
16958
|
-
|
|
16959
|
-
|
|
16960
|
-
|
|
17213
|
+
const lineIndex = await getGbkLineIndex(resolved);
|
|
17214
|
+
const totalLines = lineIndex.totalLines;
|
|
17215
|
+
let lineWindow;
|
|
17216
|
+
if (tail) {
|
|
17217
|
+
const startLine = Math.max(1, totalLines - limit + 1);
|
|
17218
|
+
const expectedLineCount = totalLines - startLine + 1;
|
|
17219
|
+
const startOffset = lineIndex.lineStartOffsets[startLine - 1] ?? 0;
|
|
17220
|
+
const text = await readDecodedGbkByteRange(resolved, startOffset, toSafeNumber(resolved.stat.size));
|
|
17221
|
+
lineWindow = {
|
|
17222
|
+
startLine,
|
|
17223
|
+
endLine: totalLines,
|
|
17224
|
+
totalLines,
|
|
17225
|
+
content: formatLineWindowContent(text, startLine, expectedLineCount),
|
|
17226
|
+
truncated: startLine > 1,
|
|
17227
|
+
tail: true,
|
|
17228
|
+
newlineStyle: lineIndex.newlineStyle
|
|
17229
|
+
};
|
|
17230
|
+
} else if (offset > totalLines) {
|
|
17231
|
+
lineWindow = {
|
|
17232
|
+
startLine: offset,
|
|
17233
|
+
endLine: totalLines,
|
|
17234
|
+
totalLines,
|
|
17235
|
+
content: "",
|
|
17236
|
+
tail: false,
|
|
17237
|
+
truncated: false,
|
|
17238
|
+
newlineStyle: lineIndex.newlineStyle
|
|
17239
|
+
};
|
|
17240
|
+
} else {
|
|
17241
|
+
const endLine = Math.min(offset + limit - 1, totalLines);
|
|
17242
|
+
const expectedLineCount = endLine - offset + 1;
|
|
17243
|
+
const startOffset = lineIndex.lineStartOffsets[offset - 1] ?? 0;
|
|
17244
|
+
const endOffset = endLine < totalLines ? lineIndex.lineStartOffsets[endLine] : toSafeNumber(resolved.stat.size);
|
|
17245
|
+
const text = await readDecodedGbkByteRange(resolved, startOffset, endOffset);
|
|
17246
|
+
lineWindow = {
|
|
17247
|
+
startLine: offset,
|
|
17248
|
+
endLine,
|
|
17249
|
+
totalLines,
|
|
17250
|
+
content: formatLineWindowContent(text, offset, expectedLineCount),
|
|
17251
|
+
tail: false,
|
|
17252
|
+
truncated: endLine < totalLines,
|
|
17253
|
+
newlineStyle: lineIndex.newlineStyle
|
|
17254
|
+
};
|
|
17255
|
+
}
|
|
16961
17256
|
return {
|
|
16962
17257
|
filePath: resolved.filePath,
|
|
16963
17258
|
encoding: resolved.encoding,
|
|
@@ -16977,8 +17272,18 @@ async function replaceGbkFileText(input) {
|
|
|
16977
17272
|
if (mode === "insertAfter" || mode === "insertBefore") {
|
|
16978
17273
|
assertInsertArguments(input);
|
|
16979
17274
|
const resolved2 = await resolveReadableGbkFile(normalizedInput);
|
|
16980
|
-
const current2 = await readWholeGbkTextFile(resolved2);
|
|
16981
17275
|
const occurrence = normalizeOptionalPositiveInteger(input.occurrence, "occurrence") ?? 1;
|
|
17276
|
+
if (resolved2.stat.size >= STREAMING_FILE_SIZE_THRESHOLD_BYTES) {
|
|
17277
|
+
return await replaceLargeGbkFileByAnchor({
|
|
17278
|
+
...resolved2,
|
|
17279
|
+
mode,
|
|
17280
|
+
anchor: input.anchor,
|
|
17281
|
+
content: input.content,
|
|
17282
|
+
occurrence,
|
|
17283
|
+
ifExists: input.ifExists ?? "skip"
|
|
17284
|
+
});
|
|
17285
|
+
}
|
|
17286
|
+
const current2 = await readWholeGbkTextFile(resolved2);
|
|
16982
17287
|
const insertResult = insertByAnchor(
|
|
16983
17288
|
current2.content,
|
|
16984
17289
|
mode,
|
|
@@ -17023,24 +17328,47 @@ async function replaceGbkFileText(input) {
|
|
|
17023
17328
|
}
|
|
17024
17329
|
const replaceAll = normalizedInput.replaceAll ?? false;
|
|
17025
17330
|
const resolved = await resolveReadableGbkFile(normalizedInput);
|
|
17026
|
-
const
|
|
17331
|
+
const prefixedBlock = parseLineNumberPrefixedBlock(input.oldString);
|
|
17332
|
+
const derivedScopedRange = prefixedBlock?.isContiguous ? {
|
|
17333
|
+
startLine: normalizedInput.startLine ?? prefixedBlock.startLine,
|
|
17334
|
+
endLine: normalizedInput.endLine ?? prefixedBlock.endLine,
|
|
17335
|
+
oldString: prefixedBlock.strippedText
|
|
17336
|
+
} : null;
|
|
17337
|
+
const effectiveOldString = derivedScopedRange?.oldString ?? input.oldString;
|
|
17338
|
+
const hasScopedRange = normalizedInput.startLine !== void 0 || normalizedInput.endLine !== void 0 || normalizedInput.startAnchor !== void 0 || normalizedInput.endAnchor !== void 0 || derivedScopedRange !== null;
|
|
17027
17339
|
if (resolved.stat.size >= STREAMING_FILE_SIZE_THRESHOLD_BYTES && !hasScopedRange) {
|
|
17028
17340
|
return await replaceLargeGbkFileText({
|
|
17029
17341
|
...resolved,
|
|
17030
|
-
oldString:
|
|
17342
|
+
oldString: effectiveOldString,
|
|
17031
17343
|
newString: input.newString,
|
|
17032
17344
|
replaceAll
|
|
17033
17345
|
});
|
|
17034
17346
|
}
|
|
17347
|
+
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)) {
|
|
17348
|
+
return await replaceLargeGbkFileTextInLineRange({
|
|
17349
|
+
...resolved,
|
|
17350
|
+
oldString: effectiveOldString,
|
|
17351
|
+
newString: input.newString,
|
|
17352
|
+
replaceAll,
|
|
17353
|
+
startLine: derivedScopedRange?.startLine ?? normalizedInput.startLine,
|
|
17354
|
+
endLine: derivedScopedRange?.endLine ?? normalizedInput.endLine
|
|
17355
|
+
});
|
|
17356
|
+
}
|
|
17035
17357
|
const current = await readWholeGbkTextFile(resolved);
|
|
17036
|
-
const
|
|
17037
|
-
|
|
17358
|
+
const scopedInput = derivedScopedRange === null ? normalizedInput : {
|
|
17359
|
+
...normalizedInput,
|
|
17360
|
+
startLine: derivedScopedRange.startLine,
|
|
17361
|
+
endLine: derivedScopedRange.endLine
|
|
17362
|
+
};
|
|
17363
|
+
const scope = resolveEditScope(current.content, scopedInput);
|
|
17364
|
+
const occurrencesBefore = countOccurrences(scope.selectedText, effectiveOldString);
|
|
17038
17365
|
if (!replaceAll && occurrencesBefore === 0) {
|
|
17039
|
-
const loose = tryLooseBlockReplace(scope.selectedText,
|
|
17366
|
+
const loose = tryLooseBlockReplace(scope.selectedText, effectiveOldString, input.newString);
|
|
17040
17367
|
if (loose !== null) {
|
|
17041
17368
|
const outputText2 = `${current.content.slice(0, scope.rangeStart)}${loose.content}${current.content.slice(scope.rangeEnd)}`;
|
|
17042
17369
|
const buffer2 = import_iconv_lite.default.encode(outputText2, current.encoding);
|
|
17043
17370
|
await fs2.writeFile(current.filePath, buffer2);
|
|
17371
|
+
invalidateGbkLineIndex(current.filePath);
|
|
17044
17372
|
return {
|
|
17045
17373
|
mode: "replace",
|
|
17046
17374
|
filePath: current.filePath,
|
|
@@ -17054,19 +17382,20 @@ async function replaceGbkFileText(input) {
|
|
|
17054
17382
|
}
|
|
17055
17383
|
if (replaceAll) {
|
|
17056
17384
|
if (occurrencesBefore === 0) {
|
|
17057
|
-
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText,
|
|
17385
|
+
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, effectiveOldString));
|
|
17058
17386
|
}
|
|
17059
17387
|
} else if (occurrencesBefore === 0) {
|
|
17060
|
-
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText,
|
|
17388
|
+
throw createGbkError("GBK_NO_MATCH", buildNoMatchMessage(scope.selectedText, effectiveOldString));
|
|
17061
17389
|
} else if (occurrencesBefore > 1) {
|
|
17062
|
-
throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${
|
|
17390
|
+
throw createGbkError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${effectiveOldString}`);
|
|
17063
17391
|
}
|
|
17064
17392
|
const fileNewlineStyle = detectNewlineStyle(current.content);
|
|
17065
17393
|
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;
|
|
17066
|
-
const replaced = replaceAll ? scope.selectedText.split(
|
|
17394
|
+
const replaced = replaceAll ? scope.selectedText.split(effectiveOldString).join(alignedNewString) : scope.selectedText.replace(effectiveOldString, alignedNewString);
|
|
17067
17395
|
const outputText = `${current.content.slice(0, scope.rangeStart)}${replaced}${current.content.slice(scope.rangeEnd)}`;
|
|
17068
17396
|
const buffer = import_iconv_lite.default.encode(outputText, current.encoding);
|
|
17069
17397
|
await fs2.writeFile(current.filePath, buffer);
|
|
17398
|
+
invalidateGbkLineIndex(current.filePath);
|
|
17070
17399
|
return {
|
|
17071
17400
|
mode: "replace",
|
|
17072
17401
|
filePath: current.filePath,
|
|
@@ -17083,25 +17412,58 @@ async function searchGbkFile(input) {
|
|
|
17083
17412
|
}
|
|
17084
17413
|
const contextLines = Math.max(0, input.contextLines ?? 3);
|
|
17085
17414
|
const resolved = await resolveReadableGbkFile(input);
|
|
17086
|
-
const loaded = await loadGbkTextFile({
|
|
17087
|
-
filePath: resolved.filePath,
|
|
17088
|
-
encoding: resolved.encoding,
|
|
17089
|
-
allowExternal: input.allowExternal,
|
|
17090
|
-
context: input.context
|
|
17091
|
-
});
|
|
17092
|
-
const lines = loaded.content.split(/\r?\n/);
|
|
17093
|
-
const totalLines = lines.length;
|
|
17094
17415
|
const matches = [];
|
|
17095
|
-
|
|
17096
|
-
|
|
17097
|
-
|
|
17098
|
-
|
|
17099
|
-
|
|
17100
|
-
|
|
17101
|
-
|
|
17102
|
-
|
|
17416
|
+
const beforeBuffer = [];
|
|
17417
|
+
const activeAfterCollectors = [];
|
|
17418
|
+
let pending = "";
|
|
17419
|
+
let totalLines = 0;
|
|
17420
|
+
const emitLine = (line) => {
|
|
17421
|
+
totalLines += 1;
|
|
17422
|
+
for (let index = activeAfterCollectors.length - 1; index >= 0; index -= 1) {
|
|
17423
|
+
const collector = activeAfterCollectors[index];
|
|
17424
|
+
if (collector.remaining > 0) {
|
|
17425
|
+
collector.match.contextAfter.push(line);
|
|
17426
|
+
collector.remaining -= 1;
|
|
17427
|
+
}
|
|
17428
|
+
if (collector.remaining === 0) {
|
|
17429
|
+
activeAfterCollectors.splice(index, 1);
|
|
17430
|
+
}
|
|
17431
|
+
}
|
|
17432
|
+
if (line.includes(input.pattern)) {
|
|
17433
|
+
const match = {
|
|
17434
|
+
lineNumber: totalLines,
|
|
17435
|
+
line,
|
|
17436
|
+
contextBefore: beforeBuffer.slice(Math.max(0, beforeBuffer.length - contextLines)),
|
|
17437
|
+
contextAfter: []
|
|
17438
|
+
};
|
|
17439
|
+
matches.push(match);
|
|
17440
|
+
if (contextLines > 0) {
|
|
17441
|
+
activeAfterCollectors.push({ match, remaining: contextLines });
|
|
17442
|
+
}
|
|
17103
17443
|
}
|
|
17104
|
-
|
|
17444
|
+
beforeBuffer.push(line);
|
|
17445
|
+
if (beforeBuffer.length > contextLines) {
|
|
17446
|
+
beforeBuffer.shift();
|
|
17447
|
+
}
|
|
17448
|
+
};
|
|
17449
|
+
await visitDecodedTextChunks(resolved, (text) => {
|
|
17450
|
+
const combined = pending + text;
|
|
17451
|
+
let start = 0;
|
|
17452
|
+
while (true) {
|
|
17453
|
+
const newlineIndex = combined.indexOf("\n", start);
|
|
17454
|
+
if (newlineIndex === -1) {
|
|
17455
|
+
break;
|
|
17456
|
+
}
|
|
17457
|
+
let line = combined.slice(start, newlineIndex);
|
|
17458
|
+
if (line.endsWith("\r")) {
|
|
17459
|
+
line = line.slice(0, -1);
|
|
17460
|
+
}
|
|
17461
|
+
emitLine(line);
|
|
17462
|
+
start = newlineIndex + 1;
|
|
17463
|
+
}
|
|
17464
|
+
pending = combined.slice(start);
|
|
17465
|
+
});
|
|
17466
|
+
emitLine(pending);
|
|
17105
17467
|
return {
|
|
17106
17468
|
filePath: resolved.filePath,
|
|
17107
17469
|
encoding: resolved.encoding,
|
|
@@ -17134,24 +17496,21 @@ async function writeGbkFile(input) {
|
|
|
17134
17496
|
throw error45;
|
|
17135
17497
|
}
|
|
17136
17498
|
}
|
|
17137
|
-
let existingContent = "";
|
|
17138
17499
|
let existed = false;
|
|
17139
17500
|
try {
|
|
17140
|
-
|
|
17141
|
-
existingContent = import_iconv_lite.default.decode(existingBuffer, encoding);
|
|
17501
|
+
await fs2.stat(candidatePath);
|
|
17142
17502
|
existed = true;
|
|
17143
17503
|
} catch (error45) {
|
|
17144
17504
|
if (!(error45 instanceof Error && "code" in error45 && error45.code === "ENOENT")) {
|
|
17145
17505
|
throw createGbkError("GBK_IO_ERROR", `\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: ${candidatePath}`, error45);
|
|
17146
17506
|
}
|
|
17147
17507
|
}
|
|
17148
|
-
const
|
|
17149
|
-
|
|
17150
|
-
await fs2.writeFile(candidatePath, buffer);
|
|
17508
|
+
const bytesWritten = await appendEncodedText(candidatePath, encoding, input.content);
|
|
17509
|
+
invalidateGbkLineIndex(candidatePath);
|
|
17151
17510
|
return {
|
|
17152
17511
|
filePath: candidatePath,
|
|
17153
17512
|
encoding,
|
|
17154
|
-
bytesWritten
|
|
17513
|
+
bytesWritten,
|
|
17155
17514
|
created: !existed,
|
|
17156
17515
|
overwritten: false,
|
|
17157
17516
|
appended: true
|
|
@@ -17193,6 +17552,7 @@ async function writeGbkFile(input) {
|
|
|
17193
17552
|
const existed = await fs2.stat(candidatePath).then(() => true).catch(() => false);
|
|
17194
17553
|
const buffer = import_iconv_lite.default.encode(input.content, encoding);
|
|
17195
17554
|
await fs2.writeFile(candidatePath, buffer);
|
|
17555
|
+
invalidateGbkLineIndex(candidatePath);
|
|
17196
17556
|
return {
|
|
17197
17557
|
filePath: candidatePath,
|
|
17198
17558
|
encoding,
|
|
@@ -17212,7 +17572,8 @@ async function writeGbkFile(input) {
|
|
|
17212
17572
|
var gbk_edit_default = tool({
|
|
17213
17573
|
description: `Edit GBK/GB18030 encoded text files with exact string replacement.
|
|
17214
17574
|
|
|
17215
|
-
|
|
17575
|
+
For whole-file replace, large files use a streaming path.
|
|
17576
|
+
For line-scoped replace, large files use a cached line-byte index and only decode the selected line window.
|
|
17216
17577
|
Safe to use on files with more than 2000 lines.
|
|
17217
17578
|
|
|
17218
17579
|
CRITICAL \u2014 do NOT include line number prefixes in oldString or newString:
|
|
@@ -17256,7 +17617,7 @@ Insert mode:
|
|
|
17256
17617
|
},
|
|
17257
17618
|
async execute(args, context) {
|
|
17258
17619
|
const result = await replaceGbkFileText({ ...args, context });
|
|
17259
|
-
const isReplace =
|
|
17620
|
+
const isReplace = "replacements" in result;
|
|
17260
17621
|
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}`;
|
|
17261
17622
|
context.metadata({
|
|
17262
17623
|
title,
|
|
@@ -17284,6 +17645,7 @@ var gbk_read_default = tool({
|
|
|
17284
17645
|
description: `Read GBK/GB18030 encoded text files with line numbers.
|
|
17285
17646
|
|
|
17286
17647
|
Returns up to 'limit' lines (default 2000) starting from 'offset'.
|
|
17648
|
+
Large files use a cached line-byte index so reading a small window does not require decoding the whole file each time.
|
|
17287
17649
|
When the file has more lines than the window, 'truncated' is true and 'totalLines' shows the full count.
|
|
17288
17650
|
|
|
17289
17651
|
IMPORTANT \u2014 line number format: each output line is prefixed with "N: " (e.g. "3787: content").
|
|
@@ -17367,7 +17729,7 @@ var gbk_write_default = tool({
|
|
|
17367
17729
|
description: `Write GBK encoded text files.
|
|
17368
17730
|
|
|
17369
17731
|
**append=true** (recommended for adding content to existing files):
|
|
17370
|
-
-
|
|
17732
|
+
- Appends encoded content directly to the end of the file without re-reading the whole file.
|
|
17371
17733
|
- Works whether the file exists or not (creates it if missing).
|
|
17372
17734
|
- Use this whenever you want to add lines/content to an existing GBK file.
|
|
17373
17735
|
- Example: gbk_write(filePath=..., content="\\r\\n\u65B0\u5185\u5BB9", append=true)
|
|
@@ -17680,7 +18042,7 @@ function buildLineDiffPreview(filePath, encoding, beforeText, afterText) {
|
|
|
17680
18042
|
return lines.join("\n");
|
|
17681
18043
|
}
|
|
17682
18044
|
function buildInsertOutput(text, mode, anchor, content, occurrence, ifExists, newlineStyle) {
|
|
17683
|
-
const alignedContent =
|
|
18045
|
+
const alignedContent = alignTextToNewlineStyle2(content, newlineStyle);
|
|
17684
18046
|
const located = findOccurrenceIndex2(text, anchor, occurrence);
|
|
17685
18047
|
const insertionPoint = mode === "insertAfter" ? located.index + anchor.length : located.index;
|
|
17686
18048
|
const alreadyExists = mode === "insertAfter" ? text.slice(insertionPoint, insertionPoint + alignedContent.length) === alignedContent : text.slice(Math.max(0, insertionPoint - alignedContent.length), insertionPoint) === alignedContent;
|
|
@@ -17804,7 +18166,7 @@ async function detectReadableTextFile(input) {
|
|
|
17804
18166
|
hasBom: detected.hasBom
|
|
17805
18167
|
};
|
|
17806
18168
|
}
|
|
17807
|
-
function
|
|
18169
|
+
function createLineCollector(offset, limit) {
|
|
17808
18170
|
const lines = [];
|
|
17809
18171
|
let pending = "";
|
|
17810
18172
|
let totalLines = 0;
|
|
@@ -17854,7 +18216,7 @@ function createLineCollector2(offset, limit) {
|
|
|
17854
18216
|
}
|
|
17855
18217
|
};
|
|
17856
18218
|
}
|
|
17857
|
-
function
|
|
18219
|
+
function createTailCollector(limit) {
|
|
17858
18220
|
assertPositiveInteger(limit, "limit");
|
|
17859
18221
|
const lines = [];
|
|
17860
18222
|
let pending = "";
|
|
@@ -18068,7 +18430,7 @@ function resolveEditScope2(text, input) {
|
|
|
18068
18430
|
rangeEnd: anchored.rangeStart + lineRanged.rangeEnd
|
|
18069
18431
|
};
|
|
18070
18432
|
}
|
|
18071
|
-
function
|
|
18433
|
+
function alignTextToNewlineStyle2(text, newlineStyle) {
|
|
18072
18434
|
assertStringArgument2(text, "text");
|
|
18073
18435
|
const normalized = text.replace(/\r\n/g, "\n");
|
|
18074
18436
|
if (newlineStyle === "crlf") {
|
|
@@ -18133,7 +18495,7 @@ async function readTextFile(input) {
|
|
|
18133
18495
|
...lineWindow2
|
|
18134
18496
|
};
|
|
18135
18497
|
}
|
|
18136
|
-
const collector = tail ?
|
|
18498
|
+
const collector = tail ? createTailCollector(limit) : createLineCollector(offset, limit);
|
|
18137
18499
|
await visitDecodedTextChunks2(resolved, (text) => {
|
|
18138
18500
|
collector.push(text);
|
|
18139
18501
|
});
|
|
@@ -18186,7 +18548,7 @@ async function writeTextFile(input) {
|
|
|
18186
18548
|
const targetHasBom = targetEncoding === "utf8-bom" ? true : existing && preserveEncoding ? existing.hasBom : targetEncoding === "utf16le" || targetEncoding === "utf16be" ? true : false;
|
|
18187
18549
|
const baseContent = append ? existing?.content ?? "" : "";
|
|
18188
18550
|
const rawContent = `${baseContent}${input.content}`;
|
|
18189
|
-
const outputContent = existing && preserveNewlineStyle ?
|
|
18551
|
+
const outputContent = existing && preserveNewlineStyle ? alignTextToNewlineStyle2(rawContent, existing.newlineStyle) : rawContent;
|
|
18190
18552
|
ensureLossless(outputContent, targetEncoding, targetHasBom);
|
|
18191
18553
|
const buffer = encodeText(outputContent, targetEncoding, targetHasBom);
|
|
18192
18554
|
await fs3.writeFile(candidatePath, buffer);
|
|
@@ -18303,7 +18665,7 @@ async function replaceTextFileText(input) {
|
|
|
18303
18665
|
} else if (occurrencesBefore > 1) {
|
|
18304
18666
|
throw createTextError("GBK_MULTIPLE_MATCHES", `\u627E\u5230\u591A\u4E2A\u5339\u914D\u9879: ${input.oldString}`);
|
|
18305
18667
|
}
|
|
18306
|
-
const alignedNewString = normalizedInput.preserveNewlineStyle === false ? input.newString :
|
|
18668
|
+
const alignedNewString = normalizedInput.preserveNewlineStyle === false ? input.newString : alignTextToNewlineStyle2(input.newString, loaded.newlineStyle);
|
|
18307
18669
|
const replaced = replaceAll ? scope.selectedText.split(input.oldString).join(alignedNewString) : scope.selectedText.replace(input.oldString, alignedNewString);
|
|
18308
18670
|
const outputText = `${loaded.content.slice(0, scope.rangeStart)}${replaced}${loaded.content.slice(scope.rangeEnd)}`;
|
|
18309
18671
|
const targetEncoding = preserveEncoding || requestedEncoding === "auto" ? loaded.encoding : resolveExplicitTextEncoding(requestedEncoding, loaded.encoding);
|
|
@@ -18459,26 +18821,30 @@ var text_write_default = tool({
|
|
|
18459
18821
|
});
|
|
18460
18822
|
|
|
18461
18823
|
// src/plugin/index.ts
|
|
18824
|
+
function createOpencodeGbkHooks() {
|
|
18825
|
+
return {
|
|
18826
|
+
tool: {
|
|
18827
|
+
gbk_read: gbk_read_default,
|
|
18828
|
+
gbk_write: gbk_write_default,
|
|
18829
|
+
gbk_edit: gbk_edit_default,
|
|
18830
|
+
gbk_search: gbk_search_default,
|
|
18831
|
+
text_read: text_read_default,
|
|
18832
|
+
text_write: text_write_default,
|
|
18833
|
+
text_edit: text_edit_default
|
|
18834
|
+
},
|
|
18835
|
+
async "experimental.chat.system.transform"(_input, output) {
|
|
18836
|
+
appendTextToolSystemPrompt(output.system);
|
|
18837
|
+
}
|
|
18838
|
+
};
|
|
18839
|
+
}
|
|
18462
18840
|
var pluginModule = {
|
|
18463
18841
|
id: "opencode-gbk-tools",
|
|
18464
18842
|
async server() {
|
|
18465
|
-
return
|
|
18466
|
-
tool: {
|
|
18467
|
-
gbk_read: gbk_read_default,
|
|
18468
|
-
gbk_write: gbk_write_default,
|
|
18469
|
-
gbk_edit: gbk_edit_default,
|
|
18470
|
-
gbk_search: gbk_search_default,
|
|
18471
|
-
text_read: text_read_default,
|
|
18472
|
-
text_write: text_write_default,
|
|
18473
|
-
text_edit: text_edit_default
|
|
18474
|
-
},
|
|
18475
|
-
async "experimental.chat.system.transform"(_input, output) {
|
|
18476
|
-
appendTextToolSystemPrompt(output.system);
|
|
18477
|
-
}
|
|
18478
|
-
};
|
|
18843
|
+
return createOpencodeGbkHooks();
|
|
18479
18844
|
}
|
|
18480
18845
|
};
|
|
18481
18846
|
var plugin_default = pluginModule;
|
|
18482
18847
|
export {
|
|
18848
|
+
createOpencodeGbkHooks,
|
|
18483
18849
|
plugin_default as default
|
|
18484
18850
|
};
|