pi-hashline-edit-pro 4.2.11 → 4.3.0
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 +19 -11
- package/index.ts +29 -8
- package/package.json +1 -1
- package/src/anchor-registry.ts +21 -31
- package/src/auto-read-all.ts +305 -0
- package/src/batch.ts +3 -3
- package/src/commit.ts +10 -19
- package/src/config-ui.ts +2 -1
- package/src/config.ts +52 -29
- package/src/constants.ts +6 -0
- package/src/fs-write.ts +1 -1
- package/src/grep.ts +12 -18
- package/src/hash-store/cache.ts +0 -18
- package/src/hash-store/retry.ts +0 -4
- package/src/hash-store/validation.ts +0 -31
- package/src/hash-store.ts +22 -32
- package/src/hashline/apply.ts +2 -2
- package/src/hashline/hash.ts +7 -8
- package/src/hashline/index.ts +1 -0
- package/src/hashline/parse.ts +3 -16
- package/src/hashline/resolve.ts +19 -39
- package/src/payload-contract.ts +3 -11
- package/src/read.ts +3 -3
- package/src/replace-diff.ts +107 -108
- package/src/replace-response.ts +37 -3
- package/src/replace-undo.ts +9 -9
- package/src/replace.ts +11 -5
- package/src/served.ts +7 -3
- package/src/utils.ts +9 -8
- package/src/write-hook.ts +0 -4
package/src/hashline/parse.ts
CHANGED
|
@@ -55,43 +55,30 @@ export const parseHashRef = parseRef;
|
|
|
55
55
|
|
|
56
56
|
const JSON_ENVELOPE_RE = /^\s*\["(.*)"\]\.\s*$/;
|
|
57
57
|
|
|
58
|
-
function unwrapJsonEnvelope(line: string
|
|
58
|
+
function unwrapJsonEnvelope(line: string): string {
|
|
59
59
|
const match = line.match(JSON_ENVELOPE_RE);
|
|
60
60
|
if (!match) return line;
|
|
61
61
|
const withoutDot = line.trim().slice(0, -1);
|
|
62
62
|
try {
|
|
63
63
|
const parsed: unknown = JSON.parse(withoutDot);
|
|
64
64
|
if (Array.isArray(parsed) && parsed.length === 1 && typeof parsed[0] === "string") {
|
|
65
|
-
warnings?.push(
|
|
66
|
-
'[W_BAD_SHAPE] Unwrapped JSON array syntax from a replacement_lines element.',
|
|
67
|
-
);
|
|
68
65
|
return parsed[0];
|
|
69
66
|
}
|
|
70
67
|
return line;
|
|
71
68
|
} catch {
|
|
72
|
-
warnings?.push(
|
|
73
|
-
'[W_BAD_SHAPE] Unwrapped JSON array syntax from a replacement_lines element.',
|
|
74
|
-
);
|
|
75
69
|
return match[1]!;
|
|
76
70
|
}
|
|
77
71
|
}
|
|
78
72
|
|
|
79
|
-
export function parseText(edit: string[]
|
|
73
|
+
export function parseText(edit: string[]): string[] {
|
|
80
74
|
if (!Array.isArray(edit) || edit.some((line) => typeof line !== "string")) {
|
|
81
75
|
throw new Error(NEW_CONTENT_NOT_ARRAY_MSG);
|
|
82
76
|
}
|
|
83
77
|
const out: string[] = [];
|
|
84
|
-
let split = false;
|
|
85
78
|
for (const line of edit) {
|
|
86
|
-
const unwrapped = unwrapJsonEnvelope(line
|
|
79
|
+
const unwrapped = unwrapJsonEnvelope(line);
|
|
87
80
|
const normalized = unwrapped.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
88
|
-
if (normalized.includes("\n")) split = true;
|
|
89
81
|
out.push(...normalized.split("\n"));
|
|
90
82
|
}
|
|
91
|
-
if (split) {
|
|
92
|
-
warnings?.push(
|
|
93
|
-
"[W_BAD_SHAPE] replacement_lines contained embedded newlines; split into one line each.",
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
83
|
return out;
|
|
97
84
|
}
|
package/src/hashline/resolve.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { abortIf, rejectUnknownFields, firstNonEmptyIndex, lastNonEmptyIndex, clipLine, getCached, decodeStringArray, assertNoNul } from "../utils";
|
|
2
2
|
import { parseHashRef, parseText, type Anchor } from "./parse";
|
|
3
|
-
import { HASH_SEP, stripRowPrefix, canon } from "./hash";
|
|
3
|
+
import { HASH_SEP, stripRowPrefix, canon, lineChecksum, type RowPrefixKind } from "./hash";
|
|
4
4
|
import { HASH_RUN } from "./alphabet";
|
|
5
5
|
import { NEW_CONTENT_NOT_ARRAY_MSG, MAX_RANGE_STALE_LINES } from "../constants";
|
|
6
|
-
import { contentChecksum } from "./hasher";
|
|
7
|
-
import { hashSource } from "./hash";
|
|
8
6
|
|
|
9
7
|
export type RAnchor = {
|
|
10
8
|
line: number;
|
|
@@ -110,7 +108,7 @@ export function fmtMismatchWithHashes(
|
|
|
110
108
|
const h = fileHashes[ln - 1]!;
|
|
111
109
|
const c = fileLines[ln - 1] ?? "";
|
|
112
110
|
hashes.push(h);
|
|
113
|
-
servedMap.set(h,
|
|
111
|
+
servedMap.set(h, lineChecksum(c));
|
|
114
112
|
rows.push(` ${ln}: ${h}│${clipLine(c)}`);
|
|
115
113
|
}
|
|
116
114
|
out.push("");
|
|
@@ -171,7 +169,7 @@ export function stripAnchorRow(
|
|
|
171
169
|
export function resEdit(edit: HTEdit, warnings?: string[]): HEdit {
|
|
172
170
|
assertItem(edit as Record<string, unknown>);
|
|
173
171
|
|
|
174
|
-
const replaceLines = parseText(decodeStringArray(edit.replacement_lines, warnings) ?? edit.replacement_lines
|
|
172
|
+
const replaceLines = parseText(decodeStringArray(edit.replacement_lines, warnings) ?? edit.replacement_lines);
|
|
175
173
|
assertNoNul(replaceLines);
|
|
176
174
|
const bounds = [edit.remove_from, edit.remove_to].map((ref) => {
|
|
177
175
|
return stripAnchorRow(ref.trim(), "remove_from/remove_to entry", warnings);
|
|
@@ -193,52 +191,37 @@ function warnUnicodeEsc(
|
|
|
193
191
|
}
|
|
194
192
|
}
|
|
195
193
|
|
|
196
|
-
|
|
197
|
-
edit: HEdit,
|
|
198
|
-
fileHashes: string[],
|
|
199
|
-
warnings: string[],
|
|
200
|
-
): HEdit {
|
|
201
|
-
const fileHashSet = new Set(fileHashes);
|
|
202
|
-
const stripped: { lineIndex: number; matched: boolean }[] = [];
|
|
203
|
-
const contentLines = edit.content_lines.map((line, lineIndex) => {
|
|
204
|
-
const result = stripRowPrefix(line);
|
|
205
|
-
if (result.kind !== "bare") return line;
|
|
206
|
-
stripped.push({ lineIndex, matched: fileHashSet.has(result.hash ?? "") });
|
|
207
|
-
return result.text;
|
|
208
|
-
});
|
|
209
|
-
if (stripped.length === 0) return edit;
|
|
210
|
-
const locations = stripped
|
|
211
|
-
.map((s) => `replacement_lines line ${s.lineIndex + 1}`)
|
|
212
|
-
.join(", ");
|
|
213
|
-
warnings.push(
|
|
214
|
-
`[W_BARE_HASH_PREFIX] Stripped "anchor│" prefix from ${locations}.`
|
|
215
|
-
);
|
|
216
|
-
return { ...edit, content_lines: contentLines };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function stripDiffPrefixes(
|
|
194
|
+
function stripRowPrefixes(
|
|
220
195
|
edit: HEdit,
|
|
221
196
|
warnings: string[],
|
|
197
|
+
kinds: RowPrefixKind[],
|
|
198
|
+
code: string,
|
|
199
|
+
marker: string,
|
|
222
200
|
): HEdit {
|
|
223
201
|
const stripped: number[] = [];
|
|
224
202
|
const contentLines = edit.content_lines.map((line, lineIndex) => {
|
|
225
203
|
const result = stripRowPrefix(line);
|
|
226
|
-
if (result.kind
|
|
204
|
+
if (result.kind === null || !kinds.includes(result.kind)) return line;
|
|
227
205
|
stripped.push(lineIndex);
|
|
228
206
|
return result.text;
|
|
229
207
|
});
|
|
230
208
|
if (stripped.length === 0) return edit;
|
|
231
209
|
const locations = stripped.map((i) => `replacement_lines line ${i + 1}`).join(", ");
|
|
232
|
-
warnings.push(
|
|
233
|
-
`[W_INVALID_PATCH] Stripped diff-preview marker from ${locations}.`
|
|
234
|
-
);
|
|
210
|
+
warnings.push(`${code} Stripped ${marker} from ${locations}.`);
|
|
235
211
|
return { ...edit, content_lines: contentLines };
|
|
236
212
|
}
|
|
237
213
|
|
|
214
|
+
export function stripBarePrefixes(edit: HEdit, warnings: string[]): HEdit {
|
|
215
|
+
return stripRowPrefixes(edit, warnings, ["bare"], "[W_BARE_HASH_PREFIX]", '"anchor│" prefix');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function stripDiffPrefixes(edit: HEdit, warnings: string[]): HEdit {
|
|
219
|
+
return stripRowPrefixes(edit, warnings, ["plus", "minus"], "[W_INVALID_PATCH]", "diff-preview marker");
|
|
220
|
+
}
|
|
221
|
+
|
|
238
222
|
export function swapReversedRanges(
|
|
239
223
|
edit: HEdit,
|
|
240
224
|
fileHashes: string[],
|
|
241
|
-
warnings: string[],
|
|
242
225
|
): HEdit {
|
|
243
226
|
const lineByHash = new Map<string, number>();
|
|
244
227
|
for (let i = 0; i < fileHashes.length; i++) {
|
|
@@ -254,9 +237,6 @@ export function swapReversedRanges(
|
|
|
254
237
|
) {
|
|
255
238
|
return edit;
|
|
256
239
|
}
|
|
257
|
-
warnings.push(
|
|
258
|
-
`[W_BAD_OP] Swapped reversed remove_from/remove_to.`
|
|
259
|
-
);
|
|
260
240
|
return { ...edit, hash_bounds: [endRef, startRef] as [Anchor, Anchor] };
|
|
261
241
|
}
|
|
262
242
|
|
|
@@ -515,7 +495,7 @@ export function assertRangeServed(
|
|
|
515
495
|
const hash = fileHashes[line - 1]!;
|
|
516
496
|
const content = fileLines[line - 1]!;
|
|
517
497
|
const servedContent = served?.get(hash);
|
|
518
|
-
if (servedContent === undefined || servedContent !==
|
|
498
|
+
if (servedContent === undefined || servedContent !== lineChecksum(content)) mismatchLines.push(line);
|
|
519
499
|
}
|
|
520
500
|
if (mismatchLines.length === 0) return;
|
|
521
501
|
const rangeLength = endLine - startLine + 1;
|
|
@@ -527,7 +507,7 @@ export function assertRangeServed(
|
|
|
527
507
|
const hash = fileHashes[line - 1]!;
|
|
528
508
|
const content = fileLines[line - 1]!;
|
|
529
509
|
shownHashes.push(hash);
|
|
530
|
-
shownMap.set(hash,
|
|
510
|
+
shownMap.set(hash, lineChecksum(content));
|
|
531
511
|
rows.push(fmtRow(hash, clipLine(content)));
|
|
532
512
|
}
|
|
533
513
|
const location = filePath ? ` in ${filePath}` : "";
|
package/src/payload-contract.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import { isRec,
|
|
2
|
+
import { isRec, normalizeRequest, rejectUnknownFields, assertNoNul } from "./utils";
|
|
3
3
|
|
|
4
4
|
const replacementLinesSchema = Type.Array(
|
|
5
5
|
Type.String({
|
|
@@ -77,20 +77,12 @@ export function assertReq(request: unknown): asserts request is ReqParams {
|
|
|
77
77
|
assertNoNul(request.replacement_lines);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
export
|
|
81
|
-
if (!isRec(input)) {
|
|
82
|
-
return input;
|
|
83
|
-
}
|
|
84
|
-
const record: Record<string, unknown> = { ...input };
|
|
85
|
-
normalizeFilePath(record);
|
|
86
|
-
normalizeAnchors(record);
|
|
87
|
-
return record;
|
|
88
|
-
}
|
|
80
|
+
export { normalizeRequest as normReq } from "./utils";
|
|
89
81
|
|
|
90
82
|
export function getPreviewInput(args: unknown): { path?: string; remove_from: string; remove_to: string; replacement_lines: string[] } | null {
|
|
91
83
|
let normalized: unknown;
|
|
92
84
|
try {
|
|
93
|
-
normalized =
|
|
85
|
+
normalized = normalizeRequest(args);
|
|
94
86
|
} catch {
|
|
95
87
|
return null;
|
|
96
88
|
}
|
package/src/read.ts
CHANGED
|
@@ -17,8 +17,8 @@ import { abortIf, makePrepareArguments, numberedRead, visLines, splitLines } fro
|
|
|
17
17
|
import { loadP, loadGuide } from "./prompts";
|
|
18
18
|
import { withReadPrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
|
|
19
19
|
import { valAccess } from "./validation";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { withAnchorSession } from "./anchor-registry";
|
|
21
|
+
import { serveRows } from "./served";
|
|
22
22
|
import { Text } from "@earendil-works/pi-tui";
|
|
23
23
|
const R_DESC = loadP("../prompts/read.md");
|
|
24
24
|
const R_SNIPPET = loadP("../prompts/read-snippet.md");
|
|
@@ -237,7 +237,7 @@ export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
|
|
|
237
237
|
fileHashes,
|
|
238
238
|
resolvedPath,
|
|
239
239
|
);
|
|
240
|
-
|
|
240
|
+
serveRows(resolvedPath, fileHashes, fileLines, preview.servedHashes);
|
|
241
241
|
const snapshotId = await safeSnapId(absolutePath, "read");
|
|
242
242
|
const previewText =
|
|
243
243
|
hadUtf8DecodeErrors
|
package/src/replace-diff.ts
CHANGED
|
@@ -201,6 +201,74 @@ function trimPlacedSpans(
|
|
|
201
201
|
return out;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
function createRowEmitter(
|
|
205
|
+
output: string[],
|
|
206
|
+
lineNumbers: (number | undefined)[],
|
|
207
|
+
maxLineBytes: number,
|
|
208
|
+
maxBytes: number,
|
|
209
|
+
): {
|
|
210
|
+
readonly stopped: boolean;
|
|
211
|
+
readonly truncated: boolean;
|
|
212
|
+
emitPlain(line: string, num?: number): void;
|
|
213
|
+
emitRow(prefix: " " | "+" | "-", line: string, hash: string | undefined, num?: number): void;
|
|
214
|
+
emitDedup(line: string): boolean;
|
|
215
|
+
} {
|
|
216
|
+
let outBytes = 0;
|
|
217
|
+
let stopped = false;
|
|
218
|
+
let truncated = false;
|
|
219
|
+
const emitPlain = (line: string, num?: number): void => {
|
|
220
|
+
if (stopped) return;
|
|
221
|
+
const lineBytes = Buffer.byteLength(line, "utf-8") + 1;
|
|
222
|
+
if (outBytes + lineBytes > maxBytes) {
|
|
223
|
+
stopped = true;
|
|
224
|
+
truncated = true;
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
outBytes += lineBytes;
|
|
228
|
+
output.push(line);
|
|
229
|
+
lineNumbers.push(num);
|
|
230
|
+
};
|
|
231
|
+
const emitRow = (prefix: " " | "+" | "-", line: string, hash: string | undefined, num?: number): void => {
|
|
232
|
+
if (stopped) return;
|
|
233
|
+
const full = fmtDiffLine(prefix, line, hash);
|
|
234
|
+
const rowBytes = Buffer.byteLength(full, "utf-8");
|
|
235
|
+
if (rowBytes > maxLineBytes) {
|
|
236
|
+
const marker = `[Row is ${formatSize(rowBytes)}, exceeds ${formatSize(maxLineBytes)}; content not shown. Use read to see the full line.]`;
|
|
237
|
+
emitPlain(fmtDiffLine(prefix, marker, hash), num);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (outBytes + rowBytes + 1 > maxBytes) {
|
|
241
|
+
stopped = true;
|
|
242
|
+
truncated = true;
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
outBytes += rowBytes + 1;
|
|
246
|
+
output.push(full);
|
|
247
|
+
lineNumbers.push(num);
|
|
248
|
+
};
|
|
249
|
+
const emitDedup = (line: string): boolean => {
|
|
250
|
+
if (stopped) return false;
|
|
251
|
+
const row = fmtDedupRow(line);
|
|
252
|
+
const rowBytes = Buffer.byteLength(row, "utf-8") + 1;
|
|
253
|
+
if (outBytes + rowBytes > maxBytes) {
|
|
254
|
+
stopped = true;
|
|
255
|
+
truncated = true;
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
outBytes += rowBytes;
|
|
259
|
+
output.push(row);
|
|
260
|
+
lineNumbers.push(undefined);
|
|
261
|
+
return true;
|
|
262
|
+
};
|
|
263
|
+
return {
|
|
264
|
+
get stopped() { return stopped; },
|
|
265
|
+
get truncated() { return truncated; },
|
|
266
|
+
emitPlain,
|
|
267
|
+
emitRow,
|
|
268
|
+
emitDedup,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
204
272
|
function genSpanDiff(
|
|
205
273
|
oldContent: string,
|
|
206
274
|
newContent: string,
|
|
@@ -235,50 +303,18 @@ function genSpanDiff(
|
|
|
235
303
|
let dedupEmitted = false;
|
|
236
304
|
const lineNumbers: (number | undefined)[] = [];
|
|
237
305
|
let firstChangedLine: number | undefined;
|
|
238
|
-
|
|
239
|
-
let stopped = false;
|
|
240
|
-
let diffTruncated = false;
|
|
241
|
-
const emitPlain = (line: string, num?: number): void => {
|
|
242
|
-
if (stopped) return;
|
|
243
|
-
const lineBytes = Buffer.byteLength(line, "utf-8") + 1;
|
|
244
|
-
if (outBytes + lineBytes > maxBytes) {
|
|
245
|
-
stopped = true;
|
|
246
|
-
diffTruncated = true;
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
outBytes += lineBytes;
|
|
250
|
-
output.push(line);
|
|
251
|
-
lineNumbers.push(num);
|
|
252
|
-
};
|
|
253
|
-
const emitRow = (prefix: " " | "+" | "-", line: string, hash: string | undefined, num?: number): void => {
|
|
254
|
-
if (stopped) return;
|
|
255
|
-
const full = fmtDiffLine(prefix, line, hash);
|
|
256
|
-
const rowBytes = Buffer.byteLength(full, "utf-8");
|
|
257
|
-
if (rowBytes > maxLineBytes) {
|
|
258
|
-
const marker = `[Row is ${formatSize(rowBytes)}, exceeds ${formatSize(maxLineBytes)}; content not shown. Use read to see the full line.]`;
|
|
259
|
-
emitPlain(fmtDiffLine(prefix, marker, hash), num);
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
if (outBytes + rowBytes + 1 > maxBytes) {
|
|
263
|
-
stopped = true;
|
|
264
|
-
diffTruncated = true;
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
outBytes += rowBytes + 1;
|
|
268
|
-
output.push(full);
|
|
269
|
-
lineNumbers.push(num);
|
|
270
|
-
};
|
|
306
|
+
const em = createRowEmitter(output, lineNumbers, maxLineBytes, maxBytes);
|
|
271
307
|
const emitGap = (gapStartNew: number, gapLen: number, position: "leading" | "middle" | "trailing"): void => {
|
|
272
|
-
if (stopped || gapLen <= 0) return;
|
|
308
|
+
if (em.stopped || gapLen <= 0) return;
|
|
273
309
|
if (position === "leading") {
|
|
274
310
|
let count = contextLines;
|
|
275
311
|
if (contextLines > 0 && gapLen > count && isBlankLine(newLines[gapStartNew + gapLen - 1]!)) count += 1;
|
|
276
312
|
count = Math.min(count, gapLen);
|
|
277
313
|
const skipStart = gapLen - count;
|
|
278
|
-
if (skipStart > 0) emitPlain(" ...", undefined);
|
|
279
|
-
for (let k = gapLen - count; k < gapLen && !stopped; k++) {
|
|
314
|
+
if (skipStart > 0) em.emitPlain(" ...", undefined);
|
|
315
|
+
for (let k = gapLen - count; k < gapLen && !em.stopped; k++) {
|
|
280
316
|
const newIdx = gapStartNew + k;
|
|
281
|
-
emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
317
|
+
em.emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
282
318
|
}
|
|
283
319
|
return;
|
|
284
320
|
}
|
|
@@ -286,11 +322,11 @@ function genSpanDiff(
|
|
|
286
322
|
let count = contextLines;
|
|
287
323
|
if (contextLines > 0 && gapLen > count && isBlankLine(newLines[gapStartNew]!)) count += 1;
|
|
288
324
|
count = Math.min(count, gapLen);
|
|
289
|
-
for (let k = 0; k < count && !stopped; k++) {
|
|
325
|
+
for (let k = 0; k < count && !em.stopped; k++) {
|
|
290
326
|
const newIdx = gapStartNew + k;
|
|
291
|
-
emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
327
|
+
em.emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
292
328
|
}
|
|
293
|
-
if (gapLen > count) emitPlain(" ...", undefined);
|
|
329
|
+
if (gapLen > count) em.emitPlain(" ...", undefined);
|
|
294
330
|
return;
|
|
295
331
|
}
|
|
296
332
|
let headCount = contextLines;
|
|
@@ -299,56 +335,52 @@ function genSpanDiff(
|
|
|
299
335
|
if (contextLines > 0 && gapLen - tailCount > headCount && isBlankLine(newLines[gapStartNew + gapLen - tailCount]!)) tailCount += 1;
|
|
300
336
|
const middleLen = gapLen - headCount - tailCount;
|
|
301
337
|
if (middleLen <= 0) {
|
|
302
|
-
for (let k = 0; k < gapLen && !stopped; k++) {
|
|
338
|
+
for (let k = 0; k < gapLen && !em.stopped; k++) {
|
|
303
339
|
const newIdx = gapStartNew + k;
|
|
304
|
-
emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
340
|
+
em.emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
305
341
|
}
|
|
306
342
|
return;
|
|
307
343
|
}
|
|
308
|
-
for (let k = 0; k < headCount && !stopped; k++) {
|
|
344
|
+
for (let k = 0; k < headCount && !em.stopped; k++) {
|
|
309
345
|
const newIdx = gapStartNew + k;
|
|
310
|
-
emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
346
|
+
em.emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
311
347
|
}
|
|
312
|
-
if (!stopped) emitPlain(" ...", undefined);
|
|
313
|
-
for (let k = gapLen - tailCount; k < gapLen && !stopped; k++) {
|
|
348
|
+
if (!em.stopped) em.emitPlain(" ...", undefined);
|
|
349
|
+
for (let k = gapLen - tailCount; k < gapLen && !em.stopped; k++) {
|
|
314
350
|
const newIdx = gapStartNew + k;
|
|
315
|
-
emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
351
|
+
em.emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
316
352
|
}
|
|
317
353
|
};
|
|
318
354
|
oldPos = 0;
|
|
319
355
|
newPos = 0;
|
|
320
|
-
for (let spanIdx = 0; spanIdx < ordered.length && !stopped; spanIdx++) {
|
|
356
|
+
for (let spanIdx = 0; spanIdx < ordered.length && !em.stopped; spanIdx++) {
|
|
321
357
|
const span = ordered[spanIdx]!;
|
|
322
358
|
const gapLen = span.oldStart - oldPos;
|
|
323
359
|
emitGap(newPos, gapLen, spanIdx === 0 ? "leading" : "middle");
|
|
324
|
-
if (stopped) break;
|
|
360
|
+
if (em.stopped) break;
|
|
325
361
|
if (firstChangedLine === undefined) firstChangedLine = span.newStart + 1;
|
|
326
362
|
for (const dedup of span.dedupAbove ?? []) {
|
|
327
|
-
if (stopped) break;
|
|
328
|
-
dedupEmitted = true;
|
|
329
|
-
output.push(fmtDedupRow(dedup));
|
|
330
|
-
lineNumbers.push(undefined);
|
|
363
|
+
if (em.stopped) break;
|
|
364
|
+
if (em.emitDedup(dedup)) dedupEmitted = true;
|
|
331
365
|
}
|
|
332
|
-
for (let oldIdx = span.oldStart; oldIdx <= span.oldEnd && !stopped; oldIdx++) {
|
|
333
|
-
emitRow("-", oldLines[oldIdx]!, oldHashes[oldIdx], oldIdx + 1);
|
|
366
|
+
for (let oldIdx = span.oldStart; oldIdx <= span.oldEnd && !em.stopped; oldIdx++) {
|
|
367
|
+
em.emitRow("-", oldLines[oldIdx]!, oldHashes[oldIdx], oldIdx + 1);
|
|
334
368
|
}
|
|
335
|
-
for (let newIdx = span.newStart; newIdx <= span.newEnd && !stopped; newIdx++) {
|
|
336
|
-
emitRow("+", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
369
|
+
for (let newIdx = span.newStart; newIdx <= span.newEnd && !em.stopped; newIdx++) {
|
|
370
|
+
em.emitRow("+", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
337
371
|
}
|
|
338
372
|
for (const dedup of span.dedupBelow ?? []) {
|
|
339
|
-
if (stopped) break;
|
|
340
|
-
dedupEmitted = true;
|
|
341
|
-
output.push(fmtDedupRow(dedup));
|
|
342
|
-
lineNumbers.push(undefined);
|
|
373
|
+
if (em.stopped) break;
|
|
374
|
+
if (em.emitDedup(dedup)) dedupEmitted = true;
|
|
343
375
|
}
|
|
344
376
|
oldPos = span.oldEnd + 1;
|
|
345
377
|
newPos = span.newEnd + 1;
|
|
346
378
|
}
|
|
347
|
-
if (!stopped) {
|
|
379
|
+
if (!em.stopped) {
|
|
348
380
|
const trailingLen = oldLines.length - oldPos;
|
|
349
381
|
emitGap(newPos, trailingLen, "trailing");
|
|
350
382
|
}
|
|
351
|
-
if (
|
|
383
|
+
if (em.truncated) {
|
|
352
384
|
output.push(" ...");
|
|
353
385
|
lineNumbers.push(undefined);
|
|
354
386
|
output.push(`[diff truncated at ${formatSize(maxBytes)}; use read to see the rest.]`);
|
|
@@ -441,45 +473,12 @@ export function genDiff(
|
|
|
441
473
|
let oldLineNum = 1;
|
|
442
474
|
let lastWasChange = false;
|
|
443
475
|
let firstChangedLine: number | undefined;
|
|
444
|
-
let outBytes = 0;
|
|
445
|
-
let stopped = false;
|
|
446
|
-
let diffTruncated = false;
|
|
447
476
|
const lineNumbers: (number|undefined)[] = [];
|
|
448
477
|
|
|
449
|
-
const
|
|
450
|
-
if (stopped) return;
|
|
451
|
-
const lineBytes = Buffer.byteLength(line, "utf-8") + 1;
|
|
452
|
-
if (outBytes + lineBytes > maxBytes) {
|
|
453
|
-
stopped = true;
|
|
454
|
-
diffTruncated = true;
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
outBytes += lineBytes;
|
|
458
|
-
output.push(line);
|
|
459
|
-
lineNumbers.push(num);
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
const emitRow = (prefix: " " | "+" | "-", line: string, hash: string | undefined, num?: number): void => {
|
|
463
|
-
if (stopped) return;
|
|
464
|
-
const full = fmtDiffLine(prefix, line, hash);
|
|
465
|
-
const rowBytes = Buffer.byteLength(full, "utf-8");
|
|
466
|
-
if (rowBytes > maxLineBytes) {
|
|
467
|
-
const marker = `[Row is ${formatSize(rowBytes)}, exceeds ${formatSize(maxLineBytes)}; content not shown. Use read to see the full line.]`;
|
|
468
|
-
emitPlain(fmtDiffLine(prefix, marker, hash), num);
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
if (outBytes + rowBytes + 1 > maxBytes) {
|
|
472
|
-
stopped = true;
|
|
473
|
-
diffTruncated = true;
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
outBytes += rowBytes + 1;
|
|
477
|
-
output.push(full);
|
|
478
|
-
lineNumbers.push(num);
|
|
479
|
-
};
|
|
478
|
+
const em = createRowEmitter(output, lineNumbers, maxLineBytes, maxBytes);
|
|
480
479
|
|
|
481
480
|
for (let i = 0; i < parts.length; i++) {
|
|
482
|
-
if (stopped) break;
|
|
481
|
+
if (em.stopped) break;
|
|
483
482
|
const part = parts[i]!;
|
|
484
483
|
const raw = part.value.split("\n");
|
|
485
484
|
if (raw[raw.length - 1] === "") raw.pop();
|
|
@@ -488,18 +487,18 @@ export function genDiff(
|
|
|
488
487
|
if (part.added || part.removed) {
|
|
489
488
|
if (firstChangedLine === undefined) firstChangedLine = newLineNum;
|
|
490
489
|
for (let k = 0; k < displayLines.length; k++) {
|
|
491
|
-
if (stopped) break;
|
|
490
|
+
if (em.stopped) break;
|
|
492
491
|
if (part.added) {
|
|
493
492
|
const hash = effectiveNewHashes[newLineNum - 1];
|
|
494
|
-
emitRow("+", displayLines[k]!, hash, newLineNum);
|
|
493
|
+
em.emitRow("+", displayLines[k]!, hash, newLineNum);
|
|
495
494
|
newLineNum++;
|
|
496
495
|
} else {
|
|
497
496
|
const hash = oldContentHashes?.[oldLineNum - 1];
|
|
498
|
-
emitRow("-", displayLines[k]!, hash, oldLineNum);
|
|
497
|
+
em.emitRow("-", displayLines[k]!, hash, oldLineNum);
|
|
499
498
|
oldLineNum++;
|
|
500
499
|
}
|
|
501
500
|
}
|
|
502
|
-
if (stopped) break;
|
|
501
|
+
if (em.stopped) break;
|
|
503
502
|
lastWasChange = true;
|
|
504
503
|
continue;
|
|
505
504
|
}
|
|
@@ -562,25 +561,25 @@ export function genDiff(
|
|
|
562
561
|
}
|
|
563
562
|
|
|
564
563
|
if (skipStart > 0) {
|
|
565
|
-
emitPlain(" ...", undefined);
|
|
564
|
+
em.emitPlain(" ...", undefined);
|
|
566
565
|
newLineNum += skipStart;
|
|
567
566
|
oldLineNum += skipStart;
|
|
568
567
|
}
|
|
569
568
|
for (const line of linesToShow) {
|
|
570
|
-
if (stopped) break;
|
|
569
|
+
if (em.stopped) break;
|
|
571
570
|
if (isEllipsisMarker(line)) {
|
|
572
|
-
emitPlain(" ...", undefined);
|
|
571
|
+
em.emitPlain(" ...", undefined);
|
|
573
572
|
newLineNum += skipMiddle;
|
|
574
573
|
oldLineNum += skipMiddle;
|
|
575
574
|
continue;
|
|
576
575
|
}
|
|
577
576
|
const hash = effectiveNewHashes[newLineNum - 1];
|
|
578
|
-
emitRow(" ", line, hash, newLineNum);
|
|
577
|
+
em.emitRow(" ", line, hash, newLineNum);
|
|
579
578
|
newLineNum++;
|
|
580
579
|
oldLineNum++;
|
|
581
580
|
}
|
|
582
581
|
if (skipTail > 0) {
|
|
583
|
-
emitPlain(" ...", undefined);
|
|
582
|
+
em.emitPlain(" ...", undefined);
|
|
584
583
|
}
|
|
585
584
|
} else {
|
|
586
585
|
newLineNum += displayLines.length;
|
|
@@ -589,7 +588,7 @@ export function genDiff(
|
|
|
589
588
|
lastWasChange = false;
|
|
590
589
|
}
|
|
591
590
|
|
|
592
|
-
if (
|
|
591
|
+
if (em.truncated) {
|
|
593
592
|
output.push(" ...");
|
|
594
593
|
lineNumbers.push(undefined);
|
|
595
594
|
output.push(`[diff truncated at ${formatSize(maxBytes)}; use read to see the rest.]`);
|
package/src/replace-response.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatSize, DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import type { NEdit } from "./hashline";
|
|
2
3
|
import { HASH_SEP } from "./hashline";
|
|
3
4
|
import type { ReplaceDetails } from "./replace";
|
|
@@ -140,9 +141,42 @@ export function isChangeRow(line: string): boolean {
|
|
|
140
141
|
return line.startsWith("+") || line.startsWith("-");
|
|
141
142
|
}
|
|
142
143
|
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
144
|
+
function capDedupRows(rows: string[], budget: number): { rows: string[]; bytes: number; omitted: number } {
|
|
145
|
+
const kept: string[] = [];
|
|
146
|
+
let bytes = 0;
|
|
147
|
+
for (const row of rows) {
|
|
148
|
+
const rowBytes = Buffer.byteLength(row, "utf-8") + 1;
|
|
149
|
+
if (bytes + rowBytes > budget) break;
|
|
150
|
+
kept.push(row);
|
|
151
|
+
bytes += rowBytes;
|
|
152
|
+
}
|
|
153
|
+
return { rows: kept, bytes, omitted: rows.length - kept.length };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function dedupOmissionNote(omitted: number, maxBytes: number): string {
|
|
157
|
+
return fmtDedupRow(`[${omitted} line(s) not shown again; output capped at ${formatSize(maxBytes)}.]`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function withDedupRows(diff: string, lineNumbers: (number | undefined)[], above: string[] | undefined, below: string[] | undefined, maxBytes = DEFAULT_MAX_BYTES): { diff: string; lineNumbers: (number | undefined)[] } {
|
|
161
|
+
const topAll = (above ?? []).map(fmtDedupRow);
|
|
162
|
+
const bottomAll = (below ?? []).map(fmtDedupRow);
|
|
163
|
+
if (topAll.length === 0 && bottomAll.length === 0) return { diff, lineNumbers };
|
|
164
|
+
const usedBytes = Buffer.byteLength(diff, "utf-8");
|
|
165
|
+
const fullBudget = Math.max(0, maxBytes - usedBytes);
|
|
166
|
+
const initialTop = capDedupRows(topAll, fullBudget);
|
|
167
|
+
const initialBottom = capDedupRows(bottomAll, Math.max(0, fullBudget - initialTop.bytes));
|
|
168
|
+
const anyOmitted = initialTop.omitted + initialBottom.omitted > 0;
|
|
169
|
+
const reserved = anyOmitted ? Buffer.byteLength(dedupOmissionNote(topAll.length + bottomAll.length, maxBytes), "utf-8") + 1 : 0;
|
|
170
|
+
const budget = Math.max(0, fullBudget - reserved);
|
|
171
|
+
const topCapped = anyOmitted ? capDedupRows(topAll, budget) : initialTop;
|
|
172
|
+
const bottomCapped = anyOmitted ? capDedupRows(bottomAll, Math.max(0, budget - topCapped.bytes)) : initialBottom;
|
|
173
|
+
const top = topCapped.rows;
|
|
174
|
+
const bottom = [...bottomCapped.rows];
|
|
175
|
+
const omitted = topCapped.omitted + bottomCapped.omitted;
|
|
176
|
+
if (omitted > 0) {
|
|
177
|
+
const note = dedupOmissionNote(omitted, maxBytes);
|
|
178
|
+
if (usedBytes + topCapped.bytes + bottomCapped.bytes + Buffer.byteLength(note, "utf-8") + 1 <= maxBytes) bottom.push(note);
|
|
179
|
+
}
|
|
146
180
|
if (top.length === 0 && bottom.length === 0) return { diff, lineNumbers };
|
|
147
181
|
if (diff.length === 0) return { diff: [...top, ...bottom].join("\n"), lineNumbers: [...lineNumbers, ...[...top, ...bottom].map(() => undefined)] };
|
|
148
182
|
const lines = diff.split("\n");
|
package/src/replace-undo.ts
CHANGED
|
@@ -4,10 +4,9 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
4
4
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
import { loadHashStore, persistSnapshot, upsertUndo, getUndoEntry, deleteUndo, type UndoRecord } from "./hash-store";
|
|
7
|
-
import { servedHashesFromDiff,
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { markServed as markServedScoped, freeAnchors, adoptAnchors, withAnchorSession } from "./anchor-registry";
|
|
7
|
+
import { servedHashesFromDiff, serveRows } from "./served";
|
|
8
|
+
import { lineChecksum } from "./hashline";
|
|
9
|
+
import { freeAnchors, adoptAnchors, withAnchorSession } from "./anchor-registry";
|
|
11
10
|
import { resolveInCwd, writeAtomic, type FileIdentity } from "./fs-write";
|
|
12
11
|
import { toLF, stripBOM, restoreEndings, type LineEnding } from "./normalize";
|
|
13
12
|
import { genDiff, genPatch, spansFromHashes } from "./replace-diff";
|
|
@@ -200,16 +199,17 @@ export function regUndo(pi: ExtensionAPI): void {
|
|
|
200
199
|
try {
|
|
201
200
|
const store = await loadHashStore();
|
|
202
201
|
const undoLines = splitLines(undo.content);
|
|
203
|
-
persistSnapshot(store, mutationTargetPath, undo.content, undo.hashes, undoLines.map(
|
|
202
|
+
persistSnapshot(store, mutationTargetPath, undo.content, undo.hashes, undoLines.map(lineChecksum));
|
|
204
203
|
freeAnchors(mutationTargetPath);
|
|
205
204
|
adoptAnchors(
|
|
206
205
|
mutationTargetPath,
|
|
207
|
-
new Map(undoLines.map((line, i) => [undo.hashes[i]!,
|
|
206
|
+
new Map(undoLines.map((line, i) => [undo.hashes[i]!, lineChecksum(line)])),
|
|
208
207
|
);
|
|
209
|
-
|
|
208
|
+
serveRows(
|
|
210
209
|
mutationTargetPath,
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
undo.hashes,
|
|
211
|
+
undoLines,
|
|
212
|
+
servedHashesFromDiff(undoDiff),
|
|
213
213
|
);
|
|
214
214
|
} catch (error) {
|
|
215
215
|
console.error("Failed to restore hash store snapshot after undo:", error);
|