pi-hashline-edit-pro 4.2.10 → 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 +26 -19
- package/index.ts +29 -10
- package/package.json +1 -1
- package/prompts/insert-guidelines.md +2 -1
- package/prompts/replace.md +1 -1
- package/src/anchor-registry.ts +21 -31
- package/src/auto-read-all.ts +305 -0
- package/src/batch.ts +172 -84
- package/src/commit.ts +26 -26
- 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/insert.ts +2 -4
- package/src/payload-contract.ts +3 -11
- package/src/read.ts +3 -3
- package/src/replace-diff.ts +131 -107
- package/src/replace-render.ts +37 -4
- package/src/replace-response.ts +42 -13
- package/src/replace-undo.ts +10 -10
- package/src/replace.ts +33 -54
- package/src/served.ts +7 -3
- package/src/utils.ts +9 -8
- package/src/write-hook.ts +0 -4
- package/src/boundary-bypass.ts +0 -51
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/insert.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Type } from "typebox";
|
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
4
|
import { execPipeline, type ReqParams, type ReplaceDetails, previewFromPipe, previewError } from "./replace";
|
|
5
5
|
import { commitEdit } from "./commit";
|
|
6
|
-
import { batchMemberFor, ensureBatchBase, executeBatchMember, noteBatchFailure
|
|
6
|
+
import { batchMemberFor, ensureBatchBase, executeBatchMember, noteBatchFailure } from "./batch";
|
|
7
7
|
import { readNormFile, type NormFile } from "./file-reader";
|
|
8
8
|
import { MAX_HASH_LINES, parseHashRef, resEdit, resolveAnchorLine, type Anchor, type HEdit } from "./hashline";
|
|
9
9
|
import { stripAnchorRow } from "./hashline/resolve";
|
|
@@ -11,7 +11,6 @@ import { withAnchorSession } from "./anchor-registry";
|
|
|
11
11
|
import { loadP, loadGuide } from "./prompts";
|
|
12
12
|
import { assertInsertReq, normReq, type InsertReq } from "./payload-contract";
|
|
13
13
|
import { decodeStringArray, isRec, splitLines } from "./utils";
|
|
14
|
-
import { clearBoundaryBypass } from "./boundary-bypass";
|
|
15
14
|
import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper, resolveEditTargetWithRequirement, throwIfStrictInput, withInsertPrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
|
|
16
15
|
import type { RPreview, RRState } from "./replace-render";
|
|
17
16
|
export { assertInsertReq, type InsertReq };
|
|
@@ -189,7 +188,6 @@ export function buildInsertToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): I
|
|
|
189
188
|
}).catch((error: unknown) => {
|
|
190
189
|
const member = batchMemberFor(_toolCallId);
|
|
191
190
|
if (member) noteBatchFailure(member, error);
|
|
192
|
-
else suffixPoisonCause(_toolCallId, error);
|
|
193
191
|
throw error;
|
|
194
192
|
});
|
|
195
193
|
let ref: Anchor;
|
|
@@ -218,6 +216,7 @@ export function buildInsertToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): I
|
|
|
218
216
|
}
|
|
219
217
|
return executeBatchMember({
|
|
220
218
|
kind: "insert",
|
|
219
|
+
direction: req.direction,
|
|
221
220
|
member,
|
|
222
221
|
targetPath,
|
|
223
222
|
mutationTargetPath,
|
|
@@ -251,7 +250,6 @@ export function buildInsertToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): I
|
|
|
251
250
|
noopNoun: "Insertion",
|
|
252
251
|
foldedAnchorLines: anchorLine === undefined ? 0 : 1,
|
|
253
252
|
prefixWarnings: [...anchorWarnings, ...insertWarnings],
|
|
254
|
-
onApplied: () => clearBoundaryBypass(mutationTargetPath),
|
|
255
253
|
});
|
|
256
254
|
});
|
|
257
255
|
});
|
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
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
HASH_SEP,
|
|
7
7
|
changedRange,
|
|
8
8
|
} from "./hashline";
|
|
9
|
-
import { MAX_DIFF_INPUT_BYTES } from "./constants";
|
|
9
|
+
import { MAX_DIFF_INPUT_BYTES, DEDUP_ANCHOR } from "./constants";
|
|
10
10
|
import { splitLines } from "./utils";
|
|
11
11
|
import {
|
|
12
12
|
detectEnding,
|
|
@@ -22,6 +22,8 @@ export interface DiffSpan {
|
|
|
22
22
|
start: number;
|
|
23
23
|
end: number;
|
|
24
24
|
replacementCount: number;
|
|
25
|
+
dedupAbove?: string[];
|
|
26
|
+
dedupBelow?: string[];
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
interface PlacedSpan {
|
|
@@ -29,6 +31,16 @@ interface PlacedSpan {
|
|
|
29
31
|
oldEnd: number;
|
|
30
32
|
newStart: number;
|
|
31
33
|
newEnd: number;
|
|
34
|
+
dedupAbove?: string[];
|
|
35
|
+
dedupBelow?: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function fmtDedupRow(line: string): string {
|
|
39
|
+
const row = `${DEDUP_ANCHOR}${HASH_SEP}${line}`;
|
|
40
|
+
if (Buffer.byteLength(row, "utf-8") <= DEFAULT_MAX_BYTES) return row;
|
|
41
|
+
const size = formatSize(Buffer.byteLength(row, "utf-8"));
|
|
42
|
+
const limit = formatSize(DEFAULT_MAX_BYTES);
|
|
43
|
+
return `${DEDUP_ANCHOR}${HASH_SEP}[Row is ${size}, exceeds ${limit}; content not shown. Use read to see the full line.]`;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
function fmtDiffLine(
|
|
@@ -155,7 +167,7 @@ function placeSpans(
|
|
|
155
167
|
if (span.replacementCount === 0) {
|
|
156
168
|
if (newEnd !== newStart - 1) return undefined;
|
|
157
169
|
} else if (newEnd >= newLen) return undefined;
|
|
158
|
-
placed.push({ oldStart: span.start, oldEnd: span.end, newStart, newEnd });
|
|
170
|
+
placed.push({ oldStart: span.start, oldEnd: span.end, newStart, newEnd, dedupAbove: span.dedupAbove, dedupBelow: span.dedupBelow });
|
|
159
171
|
offset += span.replacementCount - spanLen;
|
|
160
172
|
removedSum += spanLen;
|
|
161
173
|
addedSum += span.replacementCount;
|
|
@@ -184,53 +196,32 @@ function trimPlacedSpans(
|
|
|
184
196
|
newEnd -= 1;
|
|
185
197
|
}
|
|
186
198
|
if (oldStart > oldEnd && newStart > newEnd) continue;
|
|
187
|
-
out.push({ oldStart, oldEnd, newStart, newEnd });
|
|
199
|
+
out.push({ oldStart, oldEnd, newStart, newEnd, dedupAbove: span.dedupAbove, dedupBelow: span.dedupBelow });
|
|
188
200
|
}
|
|
189
201
|
return out;
|
|
190
202
|
}
|
|
191
203
|
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
contextLines: number,
|
|
196
|
-
newHashes: string[],
|
|
197
|
-
oldHashes: string[],
|
|
204
|
+
function createRowEmitter(
|
|
205
|
+
output: string[],
|
|
206
|
+
lineNumbers: (number | undefined)[],
|
|
198
207
|
maxLineBytes: number,
|
|
199
208
|
maxBytes: number,
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const trimmed = trimPlacedSpans(placed, oldLines, newLines);
|
|
208
|
-
const ordered = [...trimmed].sort((a, b) => a.oldStart - b.oldStart || a.newStart - b.newStart);
|
|
209
|
-
let oldPos = 0;
|
|
210
|
-
let newPos = 0;
|
|
211
|
-
for (const span of ordered) {
|
|
212
|
-
if (span.oldStart < oldPos || span.newStart < newPos) return undefined;
|
|
213
|
-
const gapOld = span.oldStart - oldPos;
|
|
214
|
-
const gapNew = span.newStart - newPos;
|
|
215
|
-
if (gapOld !== gapNew || gapOld < 0) return undefined;
|
|
216
|
-
oldPos = span.oldEnd + 1;
|
|
217
|
-
newPos = span.newEnd + 1;
|
|
218
|
-
}
|
|
219
|
-
if (oldLines.length - ordered.reduce((sum, span) => sum + Math.max(0, span.oldEnd - span.oldStart + 1), 0) + ordered.reduce((sum, span) => sum + Math.max(0, span.newEnd - span.newStart + 1), 0) !== newLines.length) {
|
|
220
|
-
return undefined;
|
|
221
|
-
}
|
|
222
|
-
const output: string[] = [];
|
|
223
|
-
const lineNumbers: (number | undefined)[] = [];
|
|
224
|
-
let firstChangedLine: number | undefined;
|
|
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
|
+
} {
|
|
225
216
|
let outBytes = 0;
|
|
226
217
|
let stopped = false;
|
|
227
|
-
let
|
|
218
|
+
let truncated = false;
|
|
228
219
|
const emitPlain = (line: string, num?: number): void => {
|
|
229
220
|
if (stopped) return;
|
|
230
221
|
const lineBytes = Buffer.byteLength(line, "utf-8") + 1;
|
|
231
222
|
if (outBytes + lineBytes > maxBytes) {
|
|
232
223
|
stopped = true;
|
|
233
|
-
|
|
224
|
+
truncated = true;
|
|
234
225
|
return;
|
|
235
226
|
}
|
|
236
227
|
outBytes += lineBytes;
|
|
@@ -248,24 +239,82 @@ function genSpanDiff(
|
|
|
248
239
|
}
|
|
249
240
|
if (outBytes + rowBytes + 1 > maxBytes) {
|
|
250
241
|
stopped = true;
|
|
251
|
-
|
|
242
|
+
truncated = true;
|
|
252
243
|
return;
|
|
253
244
|
}
|
|
254
245
|
outBytes += rowBytes + 1;
|
|
255
246
|
output.push(full);
|
|
256
247
|
lineNumbers.push(num);
|
|
257
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
|
+
|
|
272
|
+
function genSpanDiff(
|
|
273
|
+
oldContent: string,
|
|
274
|
+
newContent: string,
|
|
275
|
+
contextLines: number,
|
|
276
|
+
newHashes: string[],
|
|
277
|
+
oldHashes: string[],
|
|
278
|
+
maxLineBytes: number,
|
|
279
|
+
maxBytes: number,
|
|
280
|
+
spans: DiffSpan[],
|
|
281
|
+
): { diff: string; firstChangedLine: number | undefined; lineNumbers: (number | undefined)[]; dedupEmitted: boolean } | undefined {
|
|
282
|
+
const oldLines = splitLines(oldContent);
|
|
283
|
+
const newLines = splitLines(newContent);
|
|
284
|
+
if (oldHashes.length !== oldLines.length || newHashes.length !== newLines.length) return undefined;
|
|
285
|
+
const placed = placeSpans(oldLines.length, newLines.length, spans);
|
|
286
|
+
if (!placed) return undefined;
|
|
287
|
+
const trimmed = trimPlacedSpans(placed, oldLines, newLines);
|
|
288
|
+
const ordered = [...trimmed].sort((a, b) => a.oldStart - b.oldStart || a.newStart - b.newStart);
|
|
289
|
+
let oldPos = 0;
|
|
290
|
+
let newPos = 0;
|
|
291
|
+
for (const span of ordered) {
|
|
292
|
+
if (span.oldStart < oldPos || span.newStart < newPos) return undefined;
|
|
293
|
+
const gapOld = span.oldStart - oldPos;
|
|
294
|
+
const gapNew = span.newStart - newPos;
|
|
295
|
+
if (gapOld !== gapNew || gapOld < 0) return undefined;
|
|
296
|
+
oldPos = span.oldEnd + 1;
|
|
297
|
+
newPos = span.newEnd + 1;
|
|
298
|
+
}
|
|
299
|
+
if (oldLines.length - ordered.reduce((sum, span) => sum + Math.max(0, span.oldEnd - span.oldStart + 1), 0) + ordered.reduce((sum, span) => sum + Math.max(0, span.newEnd - span.newStart + 1), 0) !== newLines.length) {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
const output: string[] = [];
|
|
303
|
+
let dedupEmitted = false;
|
|
304
|
+
const lineNumbers: (number | undefined)[] = [];
|
|
305
|
+
let firstChangedLine: number | undefined;
|
|
306
|
+
const em = createRowEmitter(output, lineNumbers, maxLineBytes, maxBytes);
|
|
258
307
|
const emitGap = (gapStartNew: number, gapLen: number, position: "leading" | "middle" | "trailing"): void => {
|
|
259
|
-
if (stopped || gapLen <= 0) return;
|
|
308
|
+
if (em.stopped || gapLen <= 0) return;
|
|
260
309
|
if (position === "leading") {
|
|
261
310
|
let count = contextLines;
|
|
262
311
|
if (contextLines > 0 && gapLen > count && isBlankLine(newLines[gapStartNew + gapLen - 1]!)) count += 1;
|
|
263
312
|
count = Math.min(count, gapLen);
|
|
264
313
|
const skipStart = gapLen - count;
|
|
265
|
-
if (skipStart > 0) emitPlain(" ...", undefined);
|
|
266
|
-
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++) {
|
|
267
316
|
const newIdx = gapStartNew + k;
|
|
268
|
-
emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
317
|
+
em.emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
269
318
|
}
|
|
270
319
|
return;
|
|
271
320
|
}
|
|
@@ -273,11 +322,11 @@ function genSpanDiff(
|
|
|
273
322
|
let count = contextLines;
|
|
274
323
|
if (contextLines > 0 && gapLen > count && isBlankLine(newLines[gapStartNew]!)) count += 1;
|
|
275
324
|
count = Math.min(count, gapLen);
|
|
276
|
-
for (let k = 0; k < count && !stopped; k++) {
|
|
325
|
+
for (let k = 0; k < count && !em.stopped; k++) {
|
|
277
326
|
const newIdx = gapStartNew + k;
|
|
278
|
-
emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
327
|
+
em.emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
279
328
|
}
|
|
280
|
-
if (gapLen > count) emitPlain(" ...", undefined);
|
|
329
|
+
if (gapLen > count) em.emitPlain(" ...", undefined);
|
|
281
330
|
return;
|
|
282
331
|
}
|
|
283
332
|
let headCount = contextLines;
|
|
@@ -286,50 +335,58 @@ function genSpanDiff(
|
|
|
286
335
|
if (contextLines > 0 && gapLen - tailCount > headCount && isBlankLine(newLines[gapStartNew + gapLen - tailCount]!)) tailCount += 1;
|
|
287
336
|
const middleLen = gapLen - headCount - tailCount;
|
|
288
337
|
if (middleLen <= 0) {
|
|
289
|
-
for (let k = 0; k < gapLen && !stopped; k++) {
|
|
338
|
+
for (let k = 0; k < gapLen && !em.stopped; k++) {
|
|
290
339
|
const newIdx = gapStartNew + k;
|
|
291
|
-
emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
340
|
+
em.emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
292
341
|
}
|
|
293
342
|
return;
|
|
294
343
|
}
|
|
295
|
-
for (let k = 0; k < headCount && !stopped; k++) {
|
|
344
|
+
for (let k = 0; k < headCount && !em.stopped; k++) {
|
|
296
345
|
const newIdx = gapStartNew + k;
|
|
297
|
-
emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
346
|
+
em.emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
298
347
|
}
|
|
299
|
-
if (!stopped) emitPlain(" ...", undefined);
|
|
300
|
-
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++) {
|
|
301
350
|
const newIdx = gapStartNew + k;
|
|
302
|
-
emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
351
|
+
em.emitRow(" ", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
303
352
|
}
|
|
304
353
|
};
|
|
305
354
|
oldPos = 0;
|
|
306
355
|
newPos = 0;
|
|
307
|
-
for (let spanIdx = 0; spanIdx < ordered.length && !stopped; spanIdx++) {
|
|
356
|
+
for (let spanIdx = 0; spanIdx < ordered.length && !em.stopped; spanIdx++) {
|
|
308
357
|
const span = ordered[spanIdx]!;
|
|
309
358
|
const gapLen = span.oldStart - oldPos;
|
|
310
359
|
emitGap(newPos, gapLen, spanIdx === 0 ? "leading" : "middle");
|
|
311
|
-
if (stopped) break;
|
|
360
|
+
if (em.stopped) break;
|
|
312
361
|
if (firstChangedLine === undefined) firstChangedLine = span.newStart + 1;
|
|
313
|
-
for (
|
|
314
|
-
|
|
362
|
+
for (const dedup of span.dedupAbove ?? []) {
|
|
363
|
+
if (em.stopped) break;
|
|
364
|
+
if (em.emitDedup(dedup)) dedupEmitted = true;
|
|
315
365
|
}
|
|
316
|
-
for (let
|
|
317
|
-
emitRow("
|
|
366
|
+
for (let oldIdx = span.oldStart; oldIdx <= span.oldEnd && !em.stopped; oldIdx++) {
|
|
367
|
+
em.emitRow("-", oldLines[oldIdx]!, oldHashes[oldIdx], oldIdx + 1);
|
|
368
|
+
}
|
|
369
|
+
for (let newIdx = span.newStart; newIdx <= span.newEnd && !em.stopped; newIdx++) {
|
|
370
|
+
em.emitRow("+", newLines[newIdx]!, newHashes[newIdx], newIdx + 1);
|
|
371
|
+
}
|
|
372
|
+
for (const dedup of span.dedupBelow ?? []) {
|
|
373
|
+
if (em.stopped) break;
|
|
374
|
+
if (em.emitDedup(dedup)) dedupEmitted = true;
|
|
318
375
|
}
|
|
319
376
|
oldPos = span.oldEnd + 1;
|
|
320
377
|
newPos = span.newEnd + 1;
|
|
321
378
|
}
|
|
322
|
-
if (!stopped) {
|
|
379
|
+
if (!em.stopped) {
|
|
323
380
|
const trailingLen = oldLines.length - oldPos;
|
|
324
381
|
emitGap(newPos, trailingLen, "trailing");
|
|
325
382
|
}
|
|
326
|
-
if (
|
|
383
|
+
if (em.truncated) {
|
|
327
384
|
output.push(" ...");
|
|
328
385
|
lineNumbers.push(undefined);
|
|
329
386
|
output.push(`[diff truncated at ${formatSize(maxBytes)}; use read to see the rest.]`);
|
|
330
387
|
lineNumbers.push(undefined);
|
|
331
388
|
}
|
|
332
|
-
return { diff: output.join("\n"), firstChangedLine, lineNumbers };
|
|
389
|
+
return { diff: output.join("\n"), firstChangedLine, lineNumbers, dedupEmitted };
|
|
333
390
|
}
|
|
334
391
|
|
|
335
392
|
function overDiffInputLimit(oldContent: string, newContent: string): boolean {
|
|
@@ -344,14 +401,14 @@ export function genDiff(
|
|
|
344
401
|
oldContentHashes?: string[],
|
|
345
402
|
limits?: DiffLimits,
|
|
346
403
|
spans?: DiffSpan[],
|
|
347
|
-
): { diff: string; firstChangedLine: number | undefined; lineNumbers: (number|undefined)[] } {
|
|
404
|
+
): { diff: string; firstChangedLine: number | undefined; lineNumbers: (number|undefined)[]; spanDedupEmitted?: boolean } {
|
|
348
405
|
const maxLineBytes = limits?.unlimited ? Number.POSITIVE_INFINITY : (limits?.maxLineBytes ?? DEFAULT_MAX_BYTES);
|
|
349
406
|
const maxBytes = limits?.unlimited ? Number.POSITIVE_INFINITY : (limits?.maxBytes ?? DEFAULT_MAX_BYTES);
|
|
350
407
|
if (spans && newContentHashes && oldContentHashes) {
|
|
351
408
|
const anchored = genSpanDiff(oldContent, newContent, contextLines, newContentHashes, oldContentHashes, maxLineBytes, maxBytes, spans);
|
|
352
409
|
if (anchored) {
|
|
353
410
|
const diff = disambiguateDuplicateAnchors(anchored.diff);
|
|
354
|
-
return { diff, firstChangedLine: anchored.firstChangedLine, lineNumbers: anchored.lineNumbers };
|
|
411
|
+
return { diff, firstChangedLine: anchored.firstChangedLine, lineNumbers: anchored.lineNumbers, ...(anchored.dedupEmitted ? { spanDedupEmitted: true as const } : {}) };
|
|
355
412
|
}
|
|
356
413
|
}
|
|
357
414
|
if (!limits?.unlimited && overDiffInputLimit(oldContent, newContent)) {
|
|
@@ -416,45 +473,12 @@ export function genDiff(
|
|
|
416
473
|
let oldLineNum = 1;
|
|
417
474
|
let lastWasChange = false;
|
|
418
475
|
let firstChangedLine: number | undefined;
|
|
419
|
-
let outBytes = 0;
|
|
420
|
-
let stopped = false;
|
|
421
|
-
let diffTruncated = false;
|
|
422
476
|
const lineNumbers: (number|undefined)[] = [];
|
|
423
477
|
|
|
424
|
-
const
|
|
425
|
-
if (stopped) return;
|
|
426
|
-
const lineBytes = Buffer.byteLength(line, "utf-8") + 1;
|
|
427
|
-
if (outBytes + lineBytes > maxBytes) {
|
|
428
|
-
stopped = true;
|
|
429
|
-
diffTruncated = true;
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
outBytes += lineBytes;
|
|
433
|
-
output.push(line);
|
|
434
|
-
lineNumbers.push(num);
|
|
435
|
-
};
|
|
436
|
-
|
|
437
|
-
const emitRow = (prefix: " " | "+" | "-", line: string, hash: string | undefined, num?: number): void => {
|
|
438
|
-
if (stopped) return;
|
|
439
|
-
const full = fmtDiffLine(prefix, line, hash);
|
|
440
|
-
const rowBytes = Buffer.byteLength(full, "utf-8");
|
|
441
|
-
if (rowBytes > maxLineBytes) {
|
|
442
|
-
const marker = `[Row is ${formatSize(rowBytes)}, exceeds ${formatSize(maxLineBytes)}; content not shown. Use read to see the full line.]`;
|
|
443
|
-
emitPlain(fmtDiffLine(prefix, marker, hash), num);
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
if (outBytes + rowBytes + 1 > maxBytes) {
|
|
447
|
-
stopped = true;
|
|
448
|
-
diffTruncated = true;
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
outBytes += rowBytes + 1;
|
|
452
|
-
output.push(full);
|
|
453
|
-
lineNumbers.push(num);
|
|
454
|
-
};
|
|
478
|
+
const em = createRowEmitter(output, lineNumbers, maxLineBytes, maxBytes);
|
|
455
479
|
|
|
456
480
|
for (let i = 0; i < parts.length; i++) {
|
|
457
|
-
if (stopped) break;
|
|
481
|
+
if (em.stopped) break;
|
|
458
482
|
const part = parts[i]!;
|
|
459
483
|
const raw = part.value.split("\n");
|
|
460
484
|
if (raw[raw.length - 1] === "") raw.pop();
|
|
@@ -463,18 +487,18 @@ export function genDiff(
|
|
|
463
487
|
if (part.added || part.removed) {
|
|
464
488
|
if (firstChangedLine === undefined) firstChangedLine = newLineNum;
|
|
465
489
|
for (let k = 0; k < displayLines.length; k++) {
|
|
466
|
-
if (stopped) break;
|
|
490
|
+
if (em.stopped) break;
|
|
467
491
|
if (part.added) {
|
|
468
492
|
const hash = effectiveNewHashes[newLineNum - 1];
|
|
469
|
-
emitRow("+", displayLines[k]!, hash, newLineNum);
|
|
493
|
+
em.emitRow("+", displayLines[k]!, hash, newLineNum);
|
|
470
494
|
newLineNum++;
|
|
471
495
|
} else {
|
|
472
496
|
const hash = oldContentHashes?.[oldLineNum - 1];
|
|
473
|
-
emitRow("-", displayLines[k]!, hash, oldLineNum);
|
|
497
|
+
em.emitRow("-", displayLines[k]!, hash, oldLineNum);
|
|
474
498
|
oldLineNum++;
|
|
475
499
|
}
|
|
476
500
|
}
|
|
477
|
-
if (stopped) break;
|
|
501
|
+
if (em.stopped) break;
|
|
478
502
|
lastWasChange = true;
|
|
479
503
|
continue;
|
|
480
504
|
}
|
|
@@ -537,25 +561,25 @@ export function genDiff(
|
|
|
537
561
|
}
|
|
538
562
|
|
|
539
563
|
if (skipStart > 0) {
|
|
540
|
-
emitPlain(" ...", undefined);
|
|
564
|
+
em.emitPlain(" ...", undefined);
|
|
541
565
|
newLineNum += skipStart;
|
|
542
566
|
oldLineNum += skipStart;
|
|
543
567
|
}
|
|
544
568
|
for (const line of linesToShow) {
|
|
545
|
-
if (stopped) break;
|
|
569
|
+
if (em.stopped) break;
|
|
546
570
|
if (isEllipsisMarker(line)) {
|
|
547
|
-
emitPlain(" ...", undefined);
|
|
571
|
+
em.emitPlain(" ...", undefined);
|
|
548
572
|
newLineNum += skipMiddle;
|
|
549
573
|
oldLineNum += skipMiddle;
|
|
550
574
|
continue;
|
|
551
575
|
}
|
|
552
576
|
const hash = effectiveNewHashes[newLineNum - 1];
|
|
553
|
-
emitRow(" ", line, hash, newLineNum);
|
|
577
|
+
em.emitRow(" ", line, hash, newLineNum);
|
|
554
578
|
newLineNum++;
|
|
555
579
|
oldLineNum++;
|
|
556
580
|
}
|
|
557
581
|
if (skipTail > 0) {
|
|
558
|
-
emitPlain(" ...", undefined);
|
|
582
|
+
em.emitPlain(" ...", undefined);
|
|
559
583
|
}
|
|
560
584
|
} else {
|
|
561
585
|
newLineNum += displayLines.length;
|
|
@@ -564,7 +588,7 @@ export function genDiff(
|
|
|
564
588
|
lastWasChange = false;
|
|
565
589
|
}
|
|
566
590
|
|
|
567
|
-
if (
|
|
591
|
+
if (em.truncated) {
|
|
568
592
|
output.push(" ...");
|
|
569
593
|
lineNumbers.push(undefined);
|
|
570
594
|
output.push(`[diff truncated at ${formatSize(maxBytes)}; use read to see the rest.]`);
|