pi-hashline-edit-pro 0.9.8 → 0.11.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/index.ts +21 -21
- package/package.json +5 -3
- package/prompts/replace.md +27 -1
- package/src/constants.ts +6 -6
- package/src/file-kind.ts +24 -24
- package/src/file-reader.ts +48 -0
- package/src/fs-write.ts +14 -13
- package/src/hashline/apply.ts +73 -81
- package/src/hashline/hash.ts +37 -51
- package/src/hashline/index.ts +26 -30
- package/src/hashline/parse.ts +22 -22
- package/src/hashline/resolve.ts +73 -84
- package/src/path-utils.ts +3 -3
- package/src/prompts.ts +3 -3
- package/src/read.ts +34 -38
- package/src/replace-diff.ts +13 -121
- package/src/replace-normalize.ts +4 -4
- package/src/replace-render.ts +45 -47
- package/src/replace-response.ts +34 -44
- package/src/replace.ts +117 -143
- package/src/runtime.ts +1 -1
- package/src/snapshot.ts +6 -6
- package/src/utils.ts +20 -6
- package/src/validation.ts +11 -7
package/src/hashline/apply.ts
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { abortIf } from "../runtime";
|
|
2
|
+
import { lineHashes } from "./hash";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type
|
|
10
|
-
type
|
|
11
|
-
type
|
|
12
|
-
type
|
|
4
|
+
valEdits,
|
|
5
|
+
assertNoBarePrefix,
|
|
6
|
+
warnUnicodeEsc,
|
|
7
|
+
fmtMismatch,
|
|
8
|
+
descEdit,
|
|
9
|
+
type RHEdit,
|
|
10
|
+
type NEdit,
|
|
11
|
+
type HEdit,
|
|
12
|
+
type BDupWarn,
|
|
13
13
|
} from "./resolve";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { cntLines } from "../utils";
|
|
15
|
+
import { CTX_LINES, MAX_OUT } from "../constants";
|
|
16
16
|
|
|
17
|
-
type
|
|
17
|
+
type LIdx = {
|
|
18
18
|
fileLines: string[];
|
|
19
19
|
lineStarts: number[];
|
|
20
20
|
hasTerminalNewline: boolean;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
export function
|
|
23
|
+
export function buildIdx(content: string): LIdx {
|
|
24
24
|
const fileLines = content.split("\n");
|
|
25
25
|
const lineStarts: number[] = [];
|
|
26
26
|
let offset = 0;
|
|
@@ -40,8 +40,7 @@ export function buildLineIndex(content: string): LineIndex {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
type ResolvedEditSpan = {
|
|
43
|
+
type RESpan = {
|
|
45
44
|
kind: "replace";
|
|
46
45
|
index: number;
|
|
47
46
|
label: string;
|
|
@@ -50,7 +49,7 @@ type ResolvedEditSpan = {
|
|
|
50
49
|
replacement: string;
|
|
51
50
|
};
|
|
52
51
|
|
|
53
|
-
function
|
|
52
|
+
function assertNotEmpty(originalContent: string, result: string): void {
|
|
54
53
|
if (originalContent.length > 0 && result.length === 0) {
|
|
55
54
|
throw new Error(
|
|
56
55
|
"[E_WOULD_EMPTY] Cannot empty a non-empty file via edit."
|
|
@@ -58,8 +57,7 @@ function assertDoesNotEmptyFile(originalContent: string, result: string): void {
|
|
|
58
57
|
}
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
function throwEditConflict(
|
|
60
|
+
function throwConflict(
|
|
63
61
|
left: { index: number; label: string },
|
|
64
62
|
right: { index: number; label: string },
|
|
65
63
|
reason: string,
|
|
@@ -69,14 +67,13 @@ function throwEditConflict(
|
|
|
69
67
|
);
|
|
70
68
|
}
|
|
71
69
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
edit: ResolvedHashlineEdit,
|
|
70
|
+
function resToSpan(
|
|
71
|
+
edit: RHEdit,
|
|
75
72
|
index: number,
|
|
76
73
|
content: string,
|
|
77
|
-
lineIndex:
|
|
78
|
-
noopEdits:
|
|
79
|
-
):
|
|
74
|
+
lineIndex: LIdx,
|
|
75
|
+
noopEdits: NEdit[],
|
|
76
|
+
): RESpan | null {
|
|
80
77
|
const { fileLines, lineStarts, hasTerminalNewline } = lineIndex;
|
|
81
78
|
|
|
82
79
|
const startLine = edit.old_range[0].line;
|
|
@@ -100,7 +97,7 @@ function resolveEditToSpan(
|
|
|
100
97
|
return {
|
|
101
98
|
kind: "replace",
|
|
102
99
|
index,
|
|
103
|
-
label:
|
|
100
|
+
label: descEdit(edit),
|
|
104
101
|
start: lineStarts[startLine - 1]!,
|
|
105
102
|
end: lineStarts[endLine - 1]! + fileLines[endLine - 1]!.length,
|
|
106
103
|
replacement: edit.new_lines.join("\n"),
|
|
@@ -111,7 +108,7 @@ function resolveEditToSpan(
|
|
|
111
108
|
return {
|
|
112
109
|
kind: "replace",
|
|
113
110
|
index,
|
|
114
|
-
label:
|
|
111
|
+
label: descEdit(edit),
|
|
115
112
|
start: 0,
|
|
116
113
|
end: content.length,
|
|
117
114
|
replacement: "",
|
|
@@ -122,7 +119,7 @@ function resolveEditToSpan(
|
|
|
122
119
|
return {
|
|
123
120
|
kind: "replace",
|
|
124
121
|
index,
|
|
125
|
-
label:
|
|
122
|
+
label: descEdit(edit),
|
|
126
123
|
start: lineStarts[startLine - 1]!,
|
|
127
124
|
end: lineStarts[endLine]!,
|
|
128
125
|
replacement: "",
|
|
@@ -132,14 +129,14 @@ function resolveEditToSpan(
|
|
|
132
129
|
return {
|
|
133
130
|
kind: "replace",
|
|
134
131
|
index,
|
|
135
|
-
label:
|
|
132
|
+
label: descEdit(edit),
|
|
136
133
|
start: Math.max(0, lineStarts[startLine - 1]! - 1),
|
|
137
134
|
end: lineStarts[endLine - 1]! + fileLines[endLine - 1]!.length,
|
|
138
135
|
replacement: "",
|
|
139
136
|
};
|
|
140
137
|
}
|
|
141
138
|
|
|
142
|
-
function
|
|
139
|
+
function assertNoConflict(spans: RESpan[]): void {
|
|
143
140
|
for (let leftIndex = 0; leftIndex < spans.length; leftIndex++) {
|
|
144
141
|
const left = spans[leftIndex]!;
|
|
145
142
|
for (
|
|
@@ -150,7 +147,7 @@ function assertNoConflictingSpans(spans: ResolvedEditSpan[]): void {
|
|
|
150
147
|
const right = spans[rightIndex]!;
|
|
151
148
|
|
|
152
149
|
if (left.start < right.end && right.start < left.end) {
|
|
153
|
-
|
|
150
|
+
throwConflict(
|
|
154
151
|
left,
|
|
155
152
|
right,
|
|
156
153
|
"overlap on the same original line range",
|
|
@@ -160,18 +157,18 @@ function assertNoConflictingSpans(spans: ResolvedEditSpan[]): void {
|
|
|
160
157
|
}
|
|
161
158
|
}
|
|
162
159
|
|
|
163
|
-
function
|
|
164
|
-
edits:
|
|
160
|
+
function resSpans(
|
|
161
|
+
edits: RHEdit[],
|
|
165
162
|
content: string,
|
|
166
|
-
lineIndex:
|
|
167
|
-
noopEdits:
|
|
163
|
+
lineIndex: LIdx,
|
|
164
|
+
noopEdits: NEdit[],
|
|
168
165
|
signal: AbortSignal | undefined,
|
|
169
|
-
):
|
|
166
|
+
): RESpan[] {
|
|
170
167
|
const seenSpanKeys = new Set<string>();
|
|
171
|
-
const resolvedSpans:
|
|
168
|
+
const resolvedSpans: RESpan[] = [];
|
|
172
169
|
for (const [index, edit] of edits.entries()) {
|
|
173
|
-
|
|
174
|
-
const span =
|
|
170
|
+
abortIf(signal);
|
|
171
|
+
const span = resToSpan(
|
|
175
172
|
edit,
|
|
176
173
|
index,
|
|
177
174
|
content,
|
|
@@ -191,7 +188,7 @@ function resolveEditSpans(
|
|
|
191
188
|
resolvedSpans.push(span);
|
|
192
189
|
}
|
|
193
190
|
|
|
194
|
-
|
|
191
|
+
assertNoConflict(resolvedSpans);
|
|
195
192
|
return [...resolvedSpans].sort((left, right) => {
|
|
196
193
|
if (right.end !== left.end) {
|
|
197
194
|
return right.end - left.end;
|
|
@@ -200,24 +197,23 @@ function resolveEditSpans(
|
|
|
200
197
|
});
|
|
201
198
|
}
|
|
202
199
|
|
|
203
|
-
function
|
|
200
|
+
function assemble(
|
|
204
201
|
content: string,
|
|
205
|
-
spans:
|
|
202
|
+
spans: RESpan[],
|
|
206
203
|
signal: AbortSignal | undefined,
|
|
207
204
|
): string {
|
|
208
205
|
let result = content;
|
|
209
206
|
for (const span of spans) {
|
|
210
|
-
|
|
207
|
+
abortIf(signal);
|
|
211
208
|
result =
|
|
212
209
|
result.slice(0, span.start) + span.replacement + result.slice(span.end);
|
|
213
210
|
}
|
|
214
211
|
return result;
|
|
215
212
|
}
|
|
216
213
|
|
|
217
|
-
|
|
218
|
-
export function applyHashlineEdits(
|
|
214
|
+
export function applyEdits(
|
|
219
215
|
content: string,
|
|
220
|
-
edits: import("./resolve").
|
|
216
|
+
edits: import("./resolve").HEdit[],
|
|
221
217
|
signal?: AbortSignal,
|
|
222
218
|
precomputedHashes?: string[],
|
|
223
219
|
): {
|
|
@@ -225,9 +221,9 @@ export function applyHashlineEdits(
|
|
|
225
221
|
firstChangedLine: number | undefined;
|
|
226
222
|
lastChangedLine: number | undefined;
|
|
227
223
|
warnings?: string[];
|
|
228
|
-
noopEdits?:
|
|
224
|
+
noopEdits?: NEdit[];
|
|
229
225
|
} {
|
|
230
|
-
|
|
226
|
+
abortIf(signal);
|
|
231
227
|
if (!edits.length)
|
|
232
228
|
return {
|
|
233
229
|
content,
|
|
@@ -242,12 +238,12 @@ export function applyHashlineEdits(
|
|
|
242
238
|
: edit,
|
|
243
239
|
);
|
|
244
240
|
|
|
245
|
-
const lineIndex =
|
|
246
|
-
const fileHashes = precomputedHashes ??
|
|
247
|
-
const noopEdits:
|
|
241
|
+
const lineIndex = buildIdx(content);
|
|
242
|
+
const fileHashes = precomputedHashes ?? lineHashes(content);
|
|
243
|
+
const noopEdits: NEdit[] = [];
|
|
248
244
|
const warnings: string[] = [];
|
|
249
245
|
|
|
250
|
-
const { resolved, mismatches, boundaryWarnings } =
|
|
246
|
+
const { resolved, mismatches, boundaryWarnings } = valEdits(
|
|
251
247
|
edits,
|
|
252
248
|
lineIndex.fileLines,
|
|
253
249
|
fileHashes,
|
|
@@ -256,15 +252,15 @@ export function applyHashlineEdits(
|
|
|
256
252
|
);
|
|
257
253
|
if (mismatches.length) {
|
|
258
254
|
throw new Error(
|
|
259
|
-
|
|
255
|
+
fmtMismatch(mismatches, lineIndex.fileLines, fileHashes),
|
|
260
256
|
);
|
|
261
257
|
}
|
|
262
258
|
|
|
263
|
-
const barePrefixWarnings =
|
|
259
|
+
const barePrefixWarnings = assertNoBarePrefix(edits, lineIndex.fileLines, fileHashes);
|
|
264
260
|
warnings.push(...barePrefixWarnings);
|
|
265
|
-
|
|
261
|
+
warnUnicodeEsc(edits, warnings);
|
|
266
262
|
|
|
267
|
-
const orderedSpans =
|
|
263
|
+
const orderedSpans = resSpans(
|
|
268
264
|
resolved,
|
|
269
265
|
content,
|
|
270
266
|
lineIndex,
|
|
@@ -272,12 +268,12 @@ export function applyHashlineEdits(
|
|
|
272
268
|
signal,
|
|
273
269
|
);
|
|
274
270
|
|
|
275
|
-
const result =
|
|
276
|
-
|
|
277
|
-
const
|
|
271
|
+
const result = assemble(content, orderedSpans, signal);
|
|
272
|
+
assertNotEmpty(content, result);
|
|
273
|
+
const range = changedRange(content, result);
|
|
278
274
|
|
|
279
275
|
const resultLines = result.split("\n");
|
|
280
|
-
const resultHashes =
|
|
276
|
+
const resultHashes = lineHashes(result);
|
|
281
277
|
for (const bw of boundaryWarnings) {
|
|
282
278
|
let seen = 0;
|
|
283
279
|
let matchIndex = -1;
|
|
@@ -303,15 +299,14 @@ export function applyHashlineEdits(
|
|
|
303
299
|
|
|
304
300
|
return {
|
|
305
301
|
content: result,
|
|
306
|
-
firstChangedLine:
|
|
307
|
-
lastChangedLine:
|
|
302
|
+
firstChangedLine: range?.firstChangedLine,
|
|
303
|
+
lastChangedLine: range?.lastChangedLine,
|
|
308
304
|
...(warnings.length ? { warnings } : {}),
|
|
309
305
|
...(noopEdits.length ? { noopEdits } : {}),
|
|
310
306
|
};
|
|
311
307
|
}
|
|
312
308
|
|
|
313
|
-
|
|
314
|
-
export function computeAffectedLineRange(params: {
|
|
309
|
+
export function affRange(params: {
|
|
315
310
|
firstChangedLine: number | undefined;
|
|
316
311
|
lastChangedLine: number | undefined;
|
|
317
312
|
resultLineCount: number;
|
|
@@ -322,8 +317,8 @@ export function computeAffectedLineRange(params: {
|
|
|
322
317
|
firstChangedLine,
|
|
323
318
|
lastChangedLine,
|
|
324
319
|
resultLineCount,
|
|
325
|
-
contextLines =
|
|
326
|
-
maxOutputLines =
|
|
320
|
+
contextLines = CTX_LINES,
|
|
321
|
+
maxOutputLines = MAX_OUT,
|
|
327
322
|
} = params;
|
|
328
323
|
|
|
329
324
|
if (firstChangedLine === undefined || lastChangedLine === undefined) {
|
|
@@ -352,13 +347,13 @@ export function computeAffectedLineRange(params: {
|
|
|
352
347
|
return { start, end };
|
|
353
348
|
}
|
|
354
349
|
|
|
355
|
-
export function
|
|
350
|
+
export function fmtRegion(
|
|
356
351
|
hashes: string[],
|
|
357
352
|
lines: string[],
|
|
358
353
|
): string {
|
|
359
354
|
if (hashes.length !== lines.length) {
|
|
360
355
|
throw new Error(
|
|
361
|
-
`
|
|
356
|
+
`fmtRegion: hashes.length (${hashes.length}) must match lines.length (${lines.length}).`,
|
|
362
357
|
);
|
|
363
358
|
}
|
|
364
359
|
return lines
|
|
@@ -366,25 +361,23 @@ export function formatHashlineRegion(
|
|
|
366
361
|
.join("\n");
|
|
367
362
|
}
|
|
368
363
|
|
|
369
|
-
|
|
370
|
-
export function computeChangedLineRange(
|
|
364
|
+
export function changedRange(
|
|
371
365
|
original: string,
|
|
372
366
|
result: string,
|
|
373
367
|
): { firstChangedLine: number; lastChangedLine: number } | null {
|
|
374
368
|
if (original === result) return null;
|
|
375
369
|
|
|
376
|
-
|
|
377
370
|
if (original.length === 0) {
|
|
378
371
|
return {
|
|
379
372
|
firstChangedLine: 1,
|
|
380
|
-
lastChangedLine:
|
|
373
|
+
lastChangedLine: cntLines(result),
|
|
381
374
|
};
|
|
382
375
|
}
|
|
383
376
|
|
|
384
377
|
if (result.startsWith(original) && original.endsWith("\n")) {
|
|
385
378
|
return {
|
|
386
|
-
firstChangedLine:
|
|
387
|
-
lastChangedLine:
|
|
379
|
+
firstChangedLine: cntLines(original) + 1,
|
|
380
|
+
lastChangedLine: cntLines(result),
|
|
388
381
|
};
|
|
389
382
|
}
|
|
390
383
|
|
|
@@ -395,7 +388,6 @@ export function computeChangedLineRange(
|
|
|
395
388
|
}
|
|
396
389
|
if (firstDiff === minLen && original.length === result.length) return null;
|
|
397
390
|
|
|
398
|
-
|
|
399
391
|
let lastOrig = original.length - 1;
|
|
400
392
|
let lastRes = result.length - 1;
|
|
401
393
|
while (
|
|
@@ -407,7 +399,7 @@ export function computeChangedLineRange(
|
|
|
407
399
|
lastRes--;
|
|
408
400
|
}
|
|
409
401
|
|
|
410
|
-
function
|
|
402
|
+
function idxToLine(charIdx: number, text: string): number {
|
|
411
403
|
let line = 1;
|
|
412
404
|
for (let i = 0; i < charIdx && i < text.length; i++) {
|
|
413
405
|
if (text[i] === "\n") line++;
|
|
@@ -415,10 +407,10 @@ export function computeChangedLineRange(
|
|
|
415
407
|
return line;
|
|
416
408
|
}
|
|
417
409
|
|
|
418
|
-
const firstChangedLine =
|
|
410
|
+
const firstChangedLine = idxToLine(firstDiff + 1, result);
|
|
419
411
|
let lastChangedLine: number;
|
|
420
412
|
if (lastRes < firstDiff) {
|
|
421
|
-
lastChangedLine = result.length === 0 ? 1 :
|
|
413
|
+
lastChangedLine = result.length === 0 ? 1 : cntLines(result);
|
|
422
414
|
} else if (
|
|
423
415
|
firstDiff === 0 &&
|
|
424
416
|
original.length > 0 &&
|
|
@@ -426,8 +418,8 @@ export function computeChangedLineRange(
|
|
|
426
418
|
) {
|
|
427
419
|
lastChangedLine = firstChangedLine;
|
|
428
420
|
} else {
|
|
429
|
-
lastChangedLine =
|
|
421
|
+
lastChangedLine = idxToLine(lastRes + 1, result);
|
|
430
422
|
}
|
|
431
423
|
|
|
432
424
|
return { firstChangedLine, lastChangedLine };
|
|
433
|
-
}
|
|
425
|
+
}
|
package/src/hashline/hash.ts
CHANGED
|
@@ -1,82 +1,78 @@
|
|
|
1
1
|
import xxhash from "xxhash-wasm";
|
|
2
2
|
|
|
3
|
+
export const HASH_LEN = 3;
|
|
4
|
+
export const ANCHOR_LEN = HASH_LEN;
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
export const HASH_PREFIX = "";
|
|
6
|
-
export const ANCHOR_LENGTH = HASH_LENGTH;
|
|
7
|
-
|
|
8
|
-
const HASH_ALPHABET =
|
|
6
|
+
const ALPH =
|
|
9
7
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
export const
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
const ALPH_BITS = 6;
|
|
9
|
+
const ALPH_MASK = (1 << ALPH_BITS) - 1;
|
|
10
|
+
const ALPH_SAFE = ALPH.replace(/-/g, "\\-");
|
|
11
|
+
const ALPH_RE = new RegExp(`^[${ALPH_SAFE}]+$`);
|
|
12
|
+
export const HASH_CLASS = `[${ALPH_SAFE}]{${HASH_LEN}}`;
|
|
13
|
+
|
|
14
|
+
function h2s(h: number): string {
|
|
15
|
+
const totalBits = HASH_LEN * ALPH_BITS;
|
|
17
16
|
const shift = 32 - totalBits;
|
|
18
17
|
let n = h >>> shift;
|
|
19
18
|
let out = "";
|
|
20
|
-
for (let j = 0; j <
|
|
19
|
+
for (let j = 0; j < HASH_LEN; j++) {
|
|
21
20
|
out +=
|
|
22
|
-
|
|
23
|
-
(n >>> ((
|
|
24
|
-
|
|
21
|
+
ALPH[
|
|
22
|
+
(n >>> ((HASH_LEN - 1 - j) * ALPH_BITS)) &
|
|
23
|
+
ALPH_MASK
|
|
25
24
|
]!;
|
|
26
25
|
}
|
|
27
26
|
return out;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
export const
|
|
31
|
-
`^\\s*(?:>>>|>>)?\\s*${
|
|
29
|
+
export const HL_PREFIX_RE = new RegExp(
|
|
30
|
+
`^\\s*(?:>>>|>>)?\\s*${HASH_CLASS}│`,
|
|
32
31
|
);
|
|
33
|
-
export const
|
|
34
|
-
`^\\+\\s*${
|
|
32
|
+
export const HL_PREFIX_PLUS_RE = new RegExp(
|
|
33
|
+
`^\\+\\s*${HASH_CLASS}│`,
|
|
35
34
|
);
|
|
36
35
|
export const DIFF_MINUS_RE = /^-\s*\d+\s{4}/;
|
|
37
36
|
|
|
38
|
-
export const
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
export const HL_BARE_PREFIX_RE = new RegExp(`^\\s*(${HASH_CLASS})│`);
|
|
41
38
|
|
|
42
39
|
type Hasher = { h32(input: string, seed?: number): number };
|
|
43
|
-
let
|
|
44
|
-
let
|
|
40
|
+
let hasherP: Promise<Hasher> | null = null;
|
|
41
|
+
let hasher: Hasher | null = null;
|
|
45
42
|
|
|
46
|
-
function
|
|
47
|
-
if (
|
|
43
|
+
function getH(): Hasher {
|
|
44
|
+
if (hasher) return hasher;
|
|
48
45
|
throw new Error("xxhash-wasm not initialized yet. This should not happen.");
|
|
49
46
|
}
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
hasherP = xxhash().then((h) => {
|
|
49
|
+
hasher = h;
|
|
53
50
|
return h;
|
|
54
51
|
});
|
|
55
52
|
|
|
56
|
-
export function
|
|
57
|
-
return
|
|
53
|
+
export function initHasher(): Promise<Hasher> {
|
|
54
|
+
return hasherP!
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
function xxh32(input: string, seed = 0): number {
|
|
61
|
-
return
|
|
58
|
+
return getH().h32(input, seed) >>> 0;
|
|
62
59
|
}
|
|
63
60
|
|
|
64
|
-
|
|
65
|
-
function canonicalizeLine(line: string): string {
|
|
61
|
+
function canon(line: string): string {
|
|
66
62
|
return line.replace(/\r/g, "").trimEnd();
|
|
67
63
|
}
|
|
68
64
|
|
|
69
|
-
export function
|
|
65
|
+
export function lineHashes(content: string): string[] {
|
|
70
66
|
const lines = content.split("\n");
|
|
71
67
|
const hashes = new Array<string>(lines.length);
|
|
72
68
|
const assigned = new Set<string>();
|
|
73
69
|
for (let i = 0; i < lines.length; i++) {
|
|
74
|
-
const
|
|
75
|
-
let hash =
|
|
70
|
+
const c = canon(lines[i]!);
|
|
71
|
+
let hash = h2s(xxh32(c));
|
|
76
72
|
let retry = 0;
|
|
77
73
|
while (assigned.has(hash)) {
|
|
78
74
|
retry++;
|
|
79
|
-
hash =
|
|
75
|
+
hash = h2s(xxh32(`${c}:R${retry}`));
|
|
80
76
|
}
|
|
81
77
|
assigned.add(hash);
|
|
82
78
|
hashes[i] = hash;
|
|
@@ -84,18 +80,8 @@ export function computeLineHashes(content: string): string[] {
|
|
|
84
80
|
return hashes;
|
|
85
81
|
}
|
|
86
82
|
|
|
87
|
-
export function
|
|
88
|
-
|
|
89
|
-
return hashToString(xxh32(canonical));
|
|
83
|
+
export function lineHash(idx: number, line: string): string {
|
|
84
|
+
return h2s(xxh32(canon(line)));
|
|
90
85
|
}
|
|
91
86
|
|
|
92
|
-
export
|
|
93
|
-
prefix: HASH_PREFIX,
|
|
94
|
-
length: HASH_LENGTH,
|
|
95
|
-
anchorLength: ANCHOR_LENGTH,
|
|
96
|
-
bitsPerChar: HASH_ALPHABET_BITS,
|
|
97
|
-
alphabet: HASH_ALPHABET,
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
export { HASH_ALPHABET_RE };
|
|
87
|
+
export { ALPH_RE };
|
package/src/hashline/index.ts
CHANGED
|
@@ -1,44 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
1
|
export {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
HASHLINE_PREFIX_RE,
|
|
10
|
-
HASHLINE_PREFIX_PLUS_RE,
|
|
2
|
+
HASH_LEN,
|
|
3
|
+
ANCHOR_LEN,
|
|
4
|
+
HASH_CLASS,
|
|
5
|
+
HL_PREFIX_RE,
|
|
6
|
+
HL_PREFIX_PLUS_RE,
|
|
11
7
|
DIFF_MINUS_RE,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
HL_BARE_PREFIX_RE,
|
|
9
|
+
lineHashes,
|
|
10
|
+
lineHash,
|
|
11
|
+
initHasher,
|
|
16
12
|
} from "./hash";
|
|
17
13
|
|
|
18
14
|
export {
|
|
19
15
|
parseHashRef,
|
|
20
|
-
|
|
16
|
+
parseText,
|
|
21
17
|
type Anchor,
|
|
22
18
|
} from "./parse";
|
|
23
19
|
|
|
24
20
|
export {
|
|
25
|
-
type
|
|
26
|
-
type
|
|
27
|
-
type
|
|
28
|
-
type
|
|
29
|
-
type
|
|
30
|
-
type
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
21
|
+
type RAnchor,
|
|
22
|
+
type HEdit,
|
|
23
|
+
type RHEdit,
|
|
24
|
+
type HTEdit,
|
|
25
|
+
type NEdit,
|
|
26
|
+
type BDupWarn,
|
|
27
|
+
descEdit,
|
|
28
|
+
resEdits,
|
|
29
|
+
valEdits,
|
|
30
|
+
assertNoBarePrefix,
|
|
31
|
+
fmtMismatch,
|
|
36
32
|
} from "./resolve";
|
|
37
33
|
|
|
38
34
|
export {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
buildIdx,
|
|
36
|
+
applyEdits,
|
|
37
|
+
affRange,
|
|
38
|
+
fmtRegion,
|
|
39
|
+
changedRange,
|
|
44
40
|
} from "./apply";
|
package/src/hashline/parse.ts
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
|
|
2
1
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
ANCHOR_LEN,
|
|
3
|
+
ALPH_RE,
|
|
4
|
+
HASH_CLASS,
|
|
5
|
+
HL_PREFIX_PLUS_RE,
|
|
7
6
|
DIFF_MINUS_RE,
|
|
8
7
|
} from "./hash";
|
|
9
8
|
|
|
10
|
-
|
|
11
9
|
export type Anchor = { hash: string };
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
function diagnoseHashRef(ref: string): string {
|
|
11
|
+
function diagRef(ref: string): string {
|
|
15
12
|
const trimmed = ref.trim();
|
|
16
13
|
|
|
17
14
|
if (!trimmed.length) {
|
|
18
|
-
return `[E_BAD_REF] Invalid anchor. Expected a 3-
|
|
15
|
+
return `[E_BAD_REF] Invalid anchor. Expected a 3-char base64 anchor (e.g. "aB3").`;
|
|
19
16
|
}
|
|
20
17
|
|
|
21
18
|
if (/^\d+/.test(trimmed)) {
|
|
22
|
-
return `[E_BAD_REF] Invalid anchor. Use the hash alone (e.g.
|
|
19
|
+
return `[E_BAD_REF] Invalid anchor. Use the hash alone (e.g. "aB3") — no line numbers or trailing content.`;
|
|
23
20
|
}
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
if (trimmed.includes("│")) {
|
|
23
|
+
return `[E_BAD_REF] Invalid anchor "${trimmed}". old_range must contain the 3-char hash only — remove everything from "│" onward.`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return `[E_BAD_REF] Invalid anchor "${trimmed}". Expected a 3-char base64 anchor (e.g. "aB3").`;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
function
|
|
29
|
+
function parseRef(ref: string): Anchor {
|
|
29
30
|
const trimmed = ref.trim();
|
|
30
31
|
|
|
31
32
|
if (
|
|
32
|
-
trimmed.length ===
|
|
33
|
-
|
|
33
|
+
trimmed.length === ANCHOR_LEN &&
|
|
34
|
+
ALPH_RE.test(trimmed)
|
|
34
35
|
) {
|
|
35
36
|
return { hash: trimmed };
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
throw new Error(
|
|
39
|
+
throw new Error(diagRef(ref));
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
export const parseHashRef =
|
|
42
|
-
|
|
42
|
+
export const parseHashRef = parseRef;
|
|
43
43
|
|
|
44
|
-
function
|
|
44
|
+
function assertNoPrefixes(lines: string[]): void {
|
|
45
45
|
for (const line of lines) {
|
|
46
46
|
if (!line.length) continue;
|
|
47
47
|
if (
|
|
48
|
-
|
|
48
|
+
HL_PREFIX_PLUS_RE.test(line) ||
|
|
49
49
|
DIFF_MINUS_RE.test(line)
|
|
50
50
|
) {
|
|
51
51
|
throw new Error(
|
|
52
|
-
`[E_INVALID_PATCH]
|
|
52
|
+
`[E_INVALID_PATCH] "lines" must contain literal file content, not HASH| or diff prefixes. Offending line: ${JSON.stringify(line)}`
|
|
53
53
|
);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
export function
|
|
58
|
+
export function parseText(edit: string[] | string | null): string[] {
|
|
59
59
|
if (edit === null) return [];
|
|
60
60
|
const lines =
|
|
61
61
|
typeof edit === "string"
|
|
@@ -63,6 +63,6 @@ export function hashlineParseText(edit: string[] | string | null): string[] {
|
|
|
63
63
|
.replaceAll("\r", "")
|
|
64
64
|
.split("\n")
|
|
65
65
|
: edit;
|
|
66
|
-
|
|
66
|
+
assertNoPrefixes(lines);
|
|
67
67
|
return lines;
|
|
68
68
|
}
|