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/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 { markServed as markServedScoped, withAnchorSession } from "./anchor-registry";
16
- import { buildServedMap } from "./served";
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 (globRegex) {
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 (globRegex) {
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
- markServedScoped(hit.path, buildServedMap(hit.fileHashes, hit.fileLines, hit.hashes), new Set(hit.fileHashes));
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")}`)
@@ -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
- }
@@ -42,7 +42,3 @@ export function retriedWrite(stmt: { run(...params: (string | number | null)[]):
42
42
  withBusyRetry(() => { stmt.run(...params); });
43
43
  };
44
44
  }
45
-
46
- export async function openDbWithBusyRetryAsync<T>(fn: () => T): Promise<T> {
47
- return withBusyRetryAsync(fn);
48
- }
@@ -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
- openDbWithBusyRetryAsync,
18
+ withBusyRetryAsync,
22
19
  } from "./hash-store/retry";
23
- import { snapshotCache, cacheSnapshot, SNAPSHOT_CACHE_LIMIT, touchSession, clearSession, forgetSession } from "./hash-store/cache";
20
+ import { snapshotCache, cacheSnapshot, SNAPSHOT_CACHE_LIMIT } from "./hash-store/cache";
24
21
 
25
- export { isValidHashList, isValidServedMap, parseHashList, parseServedMap, parseStoredHashes, parseStoredServed, isCorruptionError };
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 openDbWithBusyRetryAsync(() => openDb(storePath));
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 openDbWithBusyRetryAsync(() => openDb(storePath));
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 openDbWithBusyRetryAsync(() => openDb(storePath));
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
- export function withStore(fn: () => void): void {
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
- cachedDb!.db.exec("BEGIN IMMEDIATE");
371
+ db.exec("BEGIN IMMEDIATE");
379
372
  try {
380
373
  fn();
381
- cachedDb!.db.exec("COMMIT");
374
+ db.exec("COMMIT");
382
375
  } catch (e) {
383
- try { cachedDb!.db.exec("ROLLBACK"); } catch {}
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
- withBusyRetry(() => {
435
- db.exec("BEGIN IMMEDIATE");
436
- try {
437
- const stmt = db.prepare(
438
- "INSERT OR REPLACE INTO snapshots (path, checksum, line_count, hashes, line_checksums, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
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
 
@@ -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, warnings);
187
+ const rangeFixed = swapReversedRanges(edit, fileHashes);
188
188
  const prefixFixed = stripDiffPrefixes(
189
- stripBarePrefixes(rangeFixed, fileHashes, warnings),
189
+ stripBarePrefixes(rangeFixed, warnings),
190
190
  warnings,
191
191
  );
192
192
 
@@ -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 hashAt(baseIdx);
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 hashAt(nextIdx);
95
+ return anchorAt(nextIdx);
97
96
  }
98
97
 
99
98
  export function _lineHashesPure(content: string): string[] {
@@ -12,6 +12,7 @@ export {
12
12
  initHasher,
13
13
  canon,
14
14
  hashSource,
15
+ lineChecksum,
15
16
  } from "./hash";
16
17
 
17
18
  export {
@@ -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, warnings?: string[]): 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[], warnings?: string[]): 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, warnings);
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
  }
@@ -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, contentChecksum(hashSource(c)));
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, warnings);
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
- export function stripBarePrefixes(
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 !== "plus" && result.kind !== "minus") return line;
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 !== contentChecksum(hashSource(content))) mismatchLines.push(line);
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, contentChecksum(hashSource(content)));
510
+ shownMap.set(hash, lineChecksum(content));
531
511
  rows.push(fmtRow(hash, clipLine(content)));
532
512
  }
533
513
  const location = filePath ? ` in ${filePath}` : "";
@@ -1,5 +1,5 @@
1
1
  import { Type } from "typebox";
2
- import { isRec, normalizeAnchors, normalizeFilePath, rejectUnknownFields, assertNoNul } from "./utils";
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 function normReq(input: unknown): unknown {
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 = normReq(args);
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 { markServed as markServedScoped, withAnchorSession } from "./anchor-registry";
21
- import { buildServedMap } from "./served";
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
- markServedScoped(resolvedPath, buildServedMap(fileHashes, fileLines, preview.servedHashes), new Set(fileHashes));
240
+ serveRows(resolvedPath, fileHashes, fileLines, preview.servedHashes);
241
241
  const snapshotId = await safeSnapId(absolutePath, "read");
242
242
  const previewText =
243
243
  hadUtf8DecodeErrors