pi-hashline-edit-pro 4.2.11 → 4.3.1
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 +30 -8
- package/package.json +1 -1
- package/prompts/grep.md +1 -1
- package/prompts/insert-guidelines.md +0 -1
- package/prompts/insert.md +1 -1
- package/prompts/replace-guidelines.md +1 -1
- package/prompts/undo-last-change-guidelines.md +1 -1
- package/prompts/undo-last-change-snippet.md +1 -1
- package/src/anchor-registry.ts +21 -31
- package/src/auto-read-all.ts +309 -0
- package/src/batch.ts +3 -3
- package/src/commit.ts +10 -19
- package/src/config-ui.ts +2 -1
- package/src/config.ts +68 -28
- package/src/constants.ts +6 -0
- package/src/edit-common.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 +4 -12
- 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/grep.ts
CHANGED
|
@@ -12,8 +12,8 @@ import { toCwd } from "./paths";
|
|
|
12
12
|
import { loadP, loadGuide } from "./prompts";
|
|
13
13
|
import { normReq } from "./payload-contract";
|
|
14
14
|
import { abortIf, errCode, isRec, makePrepareArguments, rejectUnknownFields, truncateToBytes, visLines } from "./utils";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { withAnchorSession } from "./anchor-registry";
|
|
16
|
+
import { serveRows } from "./served";
|
|
17
17
|
import { Text } from "@earendil-works/pi-tui";
|
|
18
18
|
import { expandHint, getResultText, reuseText, type CallT, type FgT } from "./replace-render";
|
|
19
19
|
const GREP_KS = new Set(["pattern", "path", "glob", "context", "ignoreCase", "literal", "limit"]);
|
|
@@ -281,11 +281,8 @@ function makeHitFromIndices(
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
let cachedRgPath: string | undefined;
|
|
284
|
-
export function clearRgPathCache(): void {
|
|
285
|
-
cachedRgPath = undefined;
|
|
286
|
-
}
|
|
287
284
|
|
|
288
|
-
async function resolveRgPath(): Promise<string> {
|
|
285
|
+
export async function resolveRgPath(): Promise<string> {
|
|
289
286
|
if (cachedRgPath !== undefined) return cachedRgPath;
|
|
290
287
|
try {
|
|
291
288
|
const r = spawnSync("rg", ["--version"], { stdio: "pipe" });
|
|
@@ -572,6 +569,12 @@ export function regGrep(pi: ExtensionAPI): void {
|
|
|
572
569
|
}
|
|
573
570
|
const globRoot = baseStat.isFile() ? dirname(base) : base;
|
|
574
571
|
const globRegex = req.glob === undefined ? undefined : globToRegex(req.glob);
|
|
572
|
+
const matchesGlob = (absPath: string): boolean => {
|
|
573
|
+
if (globRegex === undefined) return true;
|
|
574
|
+
const displayPath = relative(ctx.cwd, absPath).replace(/\\/g, "/");
|
|
575
|
+
const globPath = relative(globRoot, absPath).replace(/\\/g, "/");
|
|
576
|
+
return globRegex.test(globPath) || globRegex.test(displayPath);
|
|
577
|
+
};
|
|
575
578
|
const validatedRegex = buildRegex(req.pattern, req.literal === true, req.ignoreCase === true);
|
|
576
579
|
const rgPath = await resolveRgPath();
|
|
577
580
|
const hits: FileHit[] = [];
|
|
@@ -607,11 +610,7 @@ export function regGrep(pi: ExtensionAPI): void {
|
|
|
607
610
|
const sortedNums = [...allNums].sort((a, b) => a - b);
|
|
608
611
|
const indices = sortedNums.map((n) => n - 1).filter((n) => n >= 0);
|
|
609
612
|
if (countOnly) {
|
|
610
|
-
if (
|
|
611
|
-
const displayPath = relative(ctx.cwd, absPath).replace(/\\/g, "/");
|
|
612
|
-
const globPath = relative(globRoot, absPath).replace(/\\/g, "/");
|
|
613
|
-
if (!globRegex.test(globPath) && !globRegex.test(displayPath)) continue;
|
|
614
|
-
}
|
|
613
|
+
if (!matchesGlob(absPath)) continue;
|
|
615
614
|
const norm = await readGrepFileShadow(absPath);
|
|
616
615
|
if (!norm) continue;
|
|
617
616
|
const hit = makeHitFromIndices(norm, relative(ctx.cwd, absPath).replace(/\\/g, "/"), indices, context, validatedRegex, totalForFile, indices.length);
|
|
@@ -633,15 +632,10 @@ export function regGrep(pi: ExtensionAPI): void {
|
|
|
633
632
|
limitTruncated = true;
|
|
634
633
|
break;
|
|
635
634
|
}
|
|
636
|
-
if (
|
|
637
|
-
const displayPath = relative(ctx.cwd, absPath).replace(/\\/g, "/");
|
|
638
|
-
const globPath = relative(globRoot, absPath).replace(/\\/g, "/");
|
|
639
|
-
if (!globRegex.test(globPath) && !globRegex.test(displayPath)) continue;
|
|
640
|
-
}
|
|
635
|
+
if (!matchesGlob(absPath)) continue;
|
|
641
636
|
const norm = await readGrepFile(absPath);
|
|
642
637
|
if (!norm) continue;
|
|
643
638
|
const hit = makeHitFromIndices(norm, relative(ctx.cwd, absPath).replace(/\\/g, "/"), indices, context, validatedRegex, totalForFile, Math.min(totalForFile, remaining));
|
|
644
|
-
if (!hit) continue;
|
|
645
639
|
const display = displayRowsForHit(hit);
|
|
646
640
|
const keptRows: string[] = [];
|
|
647
641
|
const keptHashes: string[] = [];
|
|
@@ -677,7 +671,7 @@ export function regGrep(pi: ExtensionAPI): void {
|
|
|
677
671
|
}
|
|
678
672
|
hits.sort((a, b) => cmp(a.displayPath, b.displayPath));
|
|
679
673
|
for (const hit of hits) {
|
|
680
|
-
|
|
674
|
+
serveRows(hit.path, hit.fileHashes, hit.fileLines, hit.hashes);
|
|
681
675
|
}
|
|
682
676
|
const blocks = hits
|
|
683
677
|
.map((hit) => `=== ${hit.displayPath} ===\n${hit.rows.join("\n")}`)
|
package/src/hash-store/cache.ts
CHANGED
|
@@ -16,21 +16,3 @@ export function cacheSnapshot(path: string, checksum: string, lineCount: number,
|
|
|
16
16
|
if (oldest !== undefined) snapshotCache.delete(oldest);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
let sessionSeq = 0;
|
|
21
|
-
export const sessionTouched = new Map<string, number>();
|
|
22
|
-
|
|
23
|
-
export function touchSession(path: string): void {
|
|
24
|
-
sessionSeq += 1;
|
|
25
|
-
sessionTouched.delete(path);
|
|
26
|
-
sessionTouched.set(path, sessionSeq);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function clearSession(): void {
|
|
30
|
-
sessionTouched.clear();
|
|
31
|
-
sessionSeq = 0;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function forgetSession(path: string): void {
|
|
35
|
-
sessionTouched.delete(path);
|
|
36
|
-
}
|
package/src/hash-store/retry.ts
CHANGED
|
@@ -9,15 +9,6 @@ export function isValidHashList(value: unknown): value is string[] {
|
|
|
9
9
|
return true;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function isValidServedMap(value: unknown): value is Record<string, string> {
|
|
13
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
14
|
-
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
15
|
-
if (typeof k !== "string" || !HASH_RE.test(k)) return false;
|
|
16
|
-
if (typeof v !== "string") return false;
|
|
17
|
-
}
|
|
18
|
-
return true;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
12
|
export function parseHashList(raw: string, onInvalid: () => void, context?: string): string[] | undefined {
|
|
22
13
|
let parsed: unknown;
|
|
23
14
|
try {
|
|
@@ -35,33 +26,11 @@ export function parseHashList(raw: string, onInvalid: () => void, context?: stri
|
|
|
35
26
|
return parsed;
|
|
36
27
|
}
|
|
37
28
|
|
|
38
|
-
export function parseServedMap(raw: string, onInvalid: () => void, context?: string): Map<string, string> | undefined {
|
|
39
|
-
let parsed: unknown;
|
|
40
|
-
try {
|
|
41
|
-
parsed = JSON.parse(raw);
|
|
42
|
-
} catch (error) {
|
|
43
|
-
console.error(`[parseServedMap]${context ? ` ${context}:` : ""} failed to parse stored served JSON:`, error);
|
|
44
|
-
onInvalid();
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
if (!isValidServedMap(parsed)) {
|
|
48
|
-
console.error(`[parseServedMap]${context ? ` ${context}:` : ""} stored served did not pass validation:`, (() => { try { return JSON.stringify(parsed)?.slice(0, 500) ?? String(parsed).slice(0, 500); } catch { return String(parsed).slice(0, 500); } })());
|
|
49
|
-
onInvalid();
|
|
50
|
-
return undefined;
|
|
51
|
-
}
|
|
52
|
-
return new Map(Object.entries(parsed as Record<string, string>));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
29
|
export function parseStoredHashes(row: Record<string, unknown> | undefined, onInvalid: () => void): string[] | undefined {
|
|
56
30
|
if (!row) return undefined;
|
|
57
31
|
return parseHashList(row.hashes as string, onInvalid);
|
|
58
32
|
}
|
|
59
33
|
|
|
60
|
-
export function parseStoredServed(row: Record<string, unknown> | undefined, onInvalid: () => void): Map<string, string> | undefined {
|
|
61
|
-
if (!row) return undefined;
|
|
62
|
-
return parseServedMap(row.hashes as string, onInvalid);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
34
|
export function isValidSnapshot(value: unknown): value is { content: string; hashes: string[] } {
|
|
66
35
|
if (typeof value !== "object" || value === null) return false;
|
|
67
36
|
const v = value as Record<string, unknown>;
|
package/src/hash-store.ts
CHANGED
|
@@ -7,22 +7,19 @@ import { initHasher, contentChecksum } from "./hashline/hasher";
|
|
|
7
7
|
import { HASH_STORE_VERSION, HASH_STORE_BUSY_TIMEOUT } from "./constants";
|
|
8
8
|
import {
|
|
9
9
|
isValidHashList,
|
|
10
|
-
isValidServedMap,
|
|
11
10
|
parseStoredHashes,
|
|
12
|
-
parseStoredServed,
|
|
13
11
|
isValidSnapshot,
|
|
14
12
|
isCorruptionError,
|
|
15
13
|
parseHashList,
|
|
16
|
-
parseServedMap,
|
|
17
14
|
} from "./hash-store/validation";
|
|
18
15
|
import {
|
|
19
16
|
withBusyRetry,
|
|
20
17
|
retriedWrite,
|
|
21
|
-
|
|
18
|
+
withBusyRetryAsync,
|
|
22
19
|
} from "./hash-store/retry";
|
|
23
|
-
import { snapshotCache, cacheSnapshot, SNAPSHOT_CACHE_LIMIT
|
|
20
|
+
import { snapshotCache, cacheSnapshot, SNAPSHOT_CACHE_LIMIT } from "./hash-store/cache";
|
|
24
21
|
|
|
25
|
-
export { isValidHashList,
|
|
22
|
+
export { isValidHashList, parseHashList, parseStoredHashes, isCorruptionError };
|
|
26
23
|
export { SNAPSHOT_CACHE_LIMIT };
|
|
27
24
|
export const STORE_NOT_OPEN_MESSAGE = "Hash store is not open; transactional update aborted";
|
|
28
25
|
|
|
@@ -283,19 +280,19 @@ async function openStore(storePath: string): Promise<HashStore> {
|
|
|
283
280
|
let existed = existsSync(storePath);
|
|
284
281
|
let opened: { db: RawDb; stmts: Prepared };
|
|
285
282
|
try {
|
|
286
|
-
opened = await
|
|
283
|
+
opened = await withBusyRetryAsync(() => openDb(storePath));
|
|
287
284
|
} catch (error) {
|
|
288
285
|
if (!isCorruptionError(error)) throw error;
|
|
289
286
|
console.error("Hash store failed to open, rebuilding:", error);
|
|
290
287
|
await quarantineStore(storePath);
|
|
291
288
|
existed = false;
|
|
292
|
-
opened = await
|
|
289
|
+
opened = await withBusyRetryAsync(() => openDb(storePath));
|
|
293
290
|
}
|
|
294
291
|
if (!isHealthy(opened.db)) {
|
|
295
292
|
shutdownDb(opened.db);
|
|
296
293
|
await quarantineStore(storePath);
|
|
297
294
|
existed = false;
|
|
298
|
-
opened = await
|
|
295
|
+
opened = await withBusyRetryAsync(() => openDb(storePath));
|
|
299
296
|
}
|
|
300
297
|
const { db, stmts } = opened;
|
|
301
298
|
try {
|
|
@@ -367,25 +364,28 @@ export function shutdownHashStore(): void {
|
|
|
367
364
|
cachedDb = null;
|
|
368
365
|
}
|
|
369
366
|
snapshotCache.clear();
|
|
370
|
-
clearSession();
|
|
371
367
|
}
|
|
372
368
|
|
|
373
|
-
|
|
374
|
-
if (!cachedDb || !cachedDb.db.isOpen) {
|
|
375
|
-
throw new Error(STORE_NOT_OPEN_MESSAGE);
|
|
376
|
-
}
|
|
369
|
+
function withTransaction(db: RawDb, fn: () => void): void {
|
|
377
370
|
withBusyRetry(() => {
|
|
378
|
-
|
|
371
|
+
db.exec("BEGIN IMMEDIATE");
|
|
379
372
|
try {
|
|
380
373
|
fn();
|
|
381
|
-
|
|
374
|
+
db.exec("COMMIT");
|
|
382
375
|
} catch (e) {
|
|
383
|
-
try {
|
|
376
|
+
try { db.exec("ROLLBACK"); } catch {}
|
|
384
377
|
throw e;
|
|
385
378
|
}
|
|
386
379
|
});
|
|
387
380
|
}
|
|
388
381
|
|
|
382
|
+
export function withStore(fn: () => void): void {
|
|
383
|
+
if (!cachedDb || !cachedDb.db.isOpen) {
|
|
384
|
+
throw new Error(STORE_NOT_OPEN_MESSAGE);
|
|
385
|
+
}
|
|
386
|
+
withTransaction(cachedDb.db, fn);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
389
|
async function migrateLegacy(db: RawDb): Promise<void> {
|
|
390
390
|
const legacyPath = legacyHashStorePath();
|
|
391
391
|
let content: string;
|
|
@@ -431,18 +431,11 @@ async function migrateLegacy(db: RawDb): Promise<void> {
|
|
|
431
431
|
]);
|
|
432
432
|
}
|
|
433
433
|
if (rows.length > 0) {
|
|
434
|
-
|
|
435
|
-
db.
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
);
|
|
440
|
-
for (const row of rows) stmt.run(...row);
|
|
441
|
-
db.exec("COMMIT");
|
|
442
|
-
} catch (e) {
|
|
443
|
-
try { db.exec("ROLLBACK"); } catch {}
|
|
444
|
-
throw e;
|
|
445
|
-
}
|
|
434
|
+
withTransaction(db, () => {
|
|
435
|
+
const stmt = db.prepare(
|
|
436
|
+
"INSERT OR REPLACE INTO snapshots (path, checksum, line_count, hashes, line_checksums, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
437
|
+
);
|
|
438
|
+
for (const row of rows) stmt.run(...row);
|
|
446
439
|
});
|
|
447
440
|
}
|
|
448
441
|
|
|
@@ -487,7 +480,6 @@ export function upsertSnapshot(
|
|
|
487
480
|
): void {
|
|
488
481
|
store.stmts.upsert(path, checksum, lineCount, JSON.stringify(hashes), lineChecksums ? JSON.stringify(lineChecksums) : "", Date.now());
|
|
489
482
|
cacheSnapshot(path, checksum, lineCount, hashes);
|
|
490
|
-
touchSession(path);
|
|
491
483
|
}
|
|
492
484
|
export function persistSnapshot(
|
|
493
485
|
store: HashStore,
|
|
@@ -540,7 +532,6 @@ export function upsertUndo(store: HashStore, path: string, entry: UndoRecord): v
|
|
|
540
532
|
typeof entry.mode === "number" ? entry.mode : null,
|
|
541
533
|
Date.now(),
|
|
542
534
|
);
|
|
543
|
-
touchSession(path);
|
|
544
535
|
}
|
|
545
536
|
|
|
546
537
|
export function getUndoEntry(store: HashStore, path: string): UndoRecord | undefined {
|
|
@@ -600,7 +591,6 @@ export async function pruneMissing(store: HashStore): Promise<string[]> {
|
|
|
600
591
|
}
|
|
601
592
|
});
|
|
602
593
|
for (const path of missing) snapshotCache.delete(path);
|
|
603
|
-
for (const path of missing) forgetSession(path);
|
|
604
594
|
return missing;
|
|
605
595
|
}
|
|
606
596
|
|
package/src/hashline/apply.ts
CHANGED
|
@@ -184,9 +184,9 @@ export function planEdit(
|
|
|
184
184
|
const fileHashes = precomputedHashes;
|
|
185
185
|
const warnings: string[] = [];
|
|
186
186
|
|
|
187
|
-
const rangeFixed = swapReversedRanges(edit, fileHashes
|
|
187
|
+
const rangeFixed = swapReversedRanges(edit, fileHashes);
|
|
188
188
|
const prefixFixed = stripDiffPrefixes(
|
|
189
|
-
stripBarePrefixes(rangeFixed,
|
|
189
|
+
stripBarePrefixes(rangeFixed, warnings),
|
|
190
190
|
warnings,
|
|
191
191
|
);
|
|
192
192
|
|
package/src/hashline/hash.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { splitLines, truncateToBytes, getCached } from "../utils";
|
|
|
2
2
|
import { MAX_HASH_SOURCE_BYTES } from "../constants";
|
|
3
3
|
import { loadHashStore, type HashStore } from "../hash-store";
|
|
4
4
|
import { allocateFileAnchors } from "../anchor-registry";
|
|
5
|
-
import { xxh32, initHasher } from "./hasher";
|
|
5
|
+
import { xxh32, initHasher, contentChecksum } from "./hasher";
|
|
6
6
|
import { HASH_LEN, ANCHOR_COUNT, anchorAt, HASH_CLASS, HASH_RUN } from "./alphabet";
|
|
7
7
|
export { initHasher, HASH_LEN, HASH_CLASS, HASH_RUN };
|
|
8
8
|
|
|
@@ -15,11 +15,6 @@ export const MAX_HASH_LINES = HASH_SPACE;
|
|
|
15
15
|
|
|
16
16
|
export const HASH_PROBE_STRIDE = 836286;
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
function hashAt(idx: number): string {
|
|
20
|
-
return anchorAt(idx);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
18
|
export const HL_PREFIX_PLUS_RE = new RegExp(
|
|
24
19
|
`^\\+${HASH_RUN}│`,
|
|
25
20
|
);
|
|
@@ -61,6 +56,10 @@ export function hashSource(line: string): string {
|
|
|
61
56
|
return truncateToBytes(canon(line), MAX_HASH_SOURCE_BYTES);
|
|
62
57
|
}
|
|
63
58
|
|
|
59
|
+
export function lineChecksum(line: string): string {
|
|
60
|
+
return contentChecksum(hashSource(line));
|
|
61
|
+
}
|
|
62
|
+
|
|
64
63
|
const BITSET_WORDS = Math.ceil(HASH_SPACE / 32);
|
|
65
64
|
|
|
66
65
|
function getBit(bits: Uint32Array, idx: number): boolean {
|
|
@@ -88,12 +87,12 @@ function assignHash(used: Uint32Array, baseIdx: number, hint: { value: number })
|
|
|
88
87
|
if (!getBit(used, baseIdx)) {
|
|
89
88
|
setBit(used, baseIdx);
|
|
90
89
|
hint.value = baseIdx + HASH_PROBE_STRIDE;
|
|
91
|
-
return
|
|
90
|
+
return anchorAt(baseIdx);
|
|
92
91
|
}
|
|
93
92
|
const nextIdx = nextZeroBit(used, hint.value);
|
|
94
93
|
setBit(used, nextIdx);
|
|
95
94
|
hint.value = nextIdx + HASH_PROBE_STRIDE;
|
|
96
|
-
return
|
|
95
|
+
return anchorAt(nextIdx);
|
|
97
96
|
}
|
|
98
97
|
|
|
99
98
|
export function _lineHashesPure(content: string): string[] {
|
package/src/hashline/index.ts
CHANGED
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({
|
|
@@ -8,7 +8,7 @@ const replacementLinesSchema = Type.Array(
|
|
|
8
8
|
}),
|
|
9
9
|
{
|
|
10
10
|
description:
|
|
11
|
-
"One string per line. Use [] to delete the range.",
|
|
11
|
+
"One string per line. Use [] to delete the range; [\"\"] is a single blank line.",
|
|
12
12
|
},
|
|
13
13
|
);
|
|
14
14
|
|
|
@@ -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
|