pi-hashline-edit-pro 4.2.0 → 4.2.2

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/commit.ts CHANGED
@@ -1,16 +1,17 @@
1
+ import { readFile } from "fs/promises";
1
2
  import type { PipelineResult } from "./replace";
2
- import { abortIf } from "./utils";
3
+ import { abortIf, errCode, splitLines } from "./utils";
3
4
  import { DEDUP_ANCHOR } from "./constants";
4
5
  import { HASH_SEP } from "./hashline";
5
6
  import { buildChanged, buildNoop, type RMeta, type TResult } from "./replace-response";
6
7
  import { saveUndo } from "./replace-undo";
8
+ import { getDiffContextLines } from "./config";
7
9
  import { safeSnapId } from "./file-reader";
8
10
  import { writeAtomic } from "./fs-write";
9
11
  import { servedHashesFromDiff, buildServedMap } from "./served";
10
12
  import { lineHashes } from "./hashline";
11
13
  import { hashSpan } from "./replace";
12
- import { restoreEndings } from "./normalize";
13
- import { splitLines } from "./utils";
14
+ import { restoreEndings, stripBOM, toLF } from "./normalize";
14
15
  import { markServed as markServedScoped } from "./anchor-registry";
15
16
  export interface CommitMeta {
16
17
  editAnchors?: [string, string];
@@ -70,6 +71,22 @@ export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promis
70
71
  }
71
72
 
72
73
  abortIf(signal);
74
+ let currentRaw: string | undefined;
75
+ try {
76
+ currentRaw = await readFile(mutationTargetPath, "utf-8");
77
+ } catch (error) {
78
+ const code = errCode(error);
79
+ if (code === "ENOENT") currentRaw = undefined;
80
+ else if (code === "EACCES" || code === "EPERM") throw new Error(`[E_ACCESS] File is not readable: ${path}`);
81
+ else if (code === "ELOOP") throw new Error(`[E_ACCESS] Too many symbolic links while resolving: ${path}`);
82
+ else throw error;
83
+ }
84
+ if (currentRaw === undefined) {
85
+ throw new Error(`[E_OP_ABORTED] Edit aborted: the file was deleted after the edit started.`);
86
+ }
87
+ if (toLF(stripBOM(currentRaw).text) !== pipe.originalNormalized) {
88
+ throw new Error(`[E_OP_ABORTED] Edit aborted: the file changed after the edit started. Call read for fresh anchors and retry.`);
89
+ }
73
90
  const undo = await saveUndo(mutationTargetPath, {
74
91
  content: pipe.originalNormalized,
75
92
  bom: pipe.bom,
@@ -79,7 +96,7 @@ export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promis
79
96
  });
80
97
  if (!undo.persisted) {
81
98
  throw new Error(
82
- `[E_UNDO_UNAVAILABLE] Could not persist undo history; the edit was not applied and ${path} is unchanged.`
99
+ `[E_UNDO_UNAVAILABLE] Could not persist undo history for ${path}.`
83
100
  );
84
101
  }
85
102
  try {
@@ -108,13 +125,19 @@ export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promis
108
125
  const span = meta.editAnchors ? hashSpan(pipe.originalHashes, meta.editAnchors[0], meta.editAnchors[1]) : undefined;
109
126
  const resultCount = splitLines(pipe.result).length;
110
127
  const replacementCount = span ? resultCount - (pipe.originalHashes.length - (span[1] - span[0] + 1)) : 0;
111
- const resultHashes = pipe.result === pipe.originalNormalized
112
- ? pipe.originalHashes
113
- : await lineHashes(pipe.result, mutationTargetPath, {
128
+ let resultHashes: string[];
129
+ try {
130
+ resultHashes = pipe.result === pipe.originalNormalized
131
+ ? pipe.originalHashes
132
+ : await lineHashes(pipe.result, mutationTargetPath, {
114
133
  content: pipe.originalNormalized,
115
134
  hashes: pipe.originalHashes,
116
135
  spans: span ? [{ start: span[0], end: span[1], replacementCount }] : undefined,
117
136
  });
137
+ } catch (error) {
138
+ const detail = error instanceof Error ? error.message : String(error);
139
+ throw new Error(`${detail} File was written; anchor finalization failed. One undo reverts. Call read for fresh anchors.`);
140
+ }
118
141
  const successInput = {
119
142
  path,
120
143
  originalNormalized: pipe.originalNormalized,
@@ -127,7 +150,7 @@ export async function commitEdit(pipe: PipelineResult, meta: CommitMeta): Promis
127
150
  boundaryDedupAbove: pipe.boundaryDedupAbove,
128
151
  boundaryDedupBelow: pipe.boundaryDedupBelow,
129
152
  };
130
- const changed = buildChanged(successInput, meta.verb);
153
+ const changed = buildChanged(successInput, meta.verb, await getDiffContextLines());
131
154
  if (changed.details.diff) {
132
155
  markServedScoped(
133
156
  mutationTargetPath,
package/src/config-ui.ts CHANGED
@@ -2,7 +2,7 @@ import { Key, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
2
2
  import type { Theme } from "@earendil-works/pi-coding-agent";
3
3
  import { readConfig, type Config } from "./config";
4
4
 
5
- export type ConfigToggleKey = "autoRead" | "anchorGrepEnabled" | "requirePath" | "strictInput" | "boundaryDedupMode";
5
+ export type ConfigToggleKey = "autoRead" | "anchorGrepEnabled" | "requirePath" | "strictInput" | "boundaryDedupMode" | "diffContextLines";
6
6
 
7
7
  export interface ConfigRow {
8
8
  key: ConfigToggleKey;
@@ -11,11 +11,14 @@ export interface ConfigRow {
11
11
  enabled: boolean;
12
12
  mode?: string;
13
13
  cycle?: string[];
14
+ value?: number;
15
+ disabled?: boolean;
14
16
  }
15
17
 
16
18
  export function configRows(config: Config): ConfigRow[] {
17
19
  return [
18
20
  { key: "autoRead", label: "Auto-read", hint: "Anchors after write + post-edit diffs", enabled: config.autoRead !== false },
21
+ { key: "diffContextLines", label: "Diff context", hint: "Surrounding lines in post-edit diffs (needs Auto-read)", enabled: config.autoRead !== false, value: config.diffContextLines ?? 1, disabled: config.autoRead === false },
19
22
  { key: "anchorGrepEnabled", label: "Anchor grep", hint: "anchor_grep tool (builtin grep off while on)", enabled: config.anchorGrepEnabled === true },
20
23
  { key: "requirePath", label: "Require path", hint: "replace + insert need path (RPC visibility)", enabled: config.requirePath === true },
21
24
  { key: "strictInput", label: "Strict input", hint: "Reject auto-fixable slips instead of warnings", enabled: config.strictInput === true },
@@ -34,11 +37,16 @@ function modeBox(theme: Theme, mode: string): string {
34
37
  return theme.fg("success", `[${mode}]`);
35
38
  }
36
39
 
40
+ function numberBox(theme: Theme, value: number, disabled?: boolean): string {
41
+ if (disabled) return theme.fg("dim", `[${value}]`);
42
+ return theme.fg("accent", `[${value}]`);
43
+ }
44
+
37
45
  export class HashlineConfigOverlay {
38
46
  private rows: ConfigRow[];
39
47
  private selected = 0;
40
48
 
41
- constructor(private readonly opts: { tui: { requestRender(force?: boolean): void }; theme: Theme; done: () => void; onToggle: (key: ConfigToggleKey) => Promise<void> }) {
49
+ constructor(private readonly opts: { tui: { requestRender(force?: boolean): void }; theme: Theme; done: () => void; onToggle: (key: ConfigToggleKey, delta?: number) => Promise<void> }) {
42
50
  this.rows = [];
43
51
  }
44
52
 
@@ -46,9 +54,24 @@ export class HashlineConfigOverlay {
46
54
  this.rows = configRows(await readConfig());
47
55
  }
48
56
 
57
+ private runToggle(row: ConfigRow, delta?: number): void {
58
+ this.opts.tui.requestRender(true);
59
+ void this.opts.onToggle(row.key, delta).then(async () => {
60
+ this.rows = configRows(await readConfig());
61
+ this.opts.tui.requestRender(true);
62
+ }).catch((error: unknown) => {
63
+ console.error("Failed to toggle hashline setting:", error);
64
+ });
65
+ }
66
+
49
67
  private toggleSelected(): void {
50
68
  const row = this.rows[this.selected];
51
- if (!row) return;
69
+ if (!row || row.disabled) return;
70
+ if (row.value !== undefined) {
71
+ row.value += 1;
72
+ this.runToggle(row, 1);
73
+ return;
74
+ }
52
75
  if (row.cycle && row.mode !== undefined) {
53
76
  const next = row.cycle[(row.cycle.indexOf(row.mode) + 1) % row.cycle.length] ?? row.cycle[0];
54
77
  if (next === undefined) return;
@@ -57,13 +80,14 @@ export class HashlineConfigOverlay {
57
80
  } else {
58
81
  row.enabled = !row.enabled;
59
82
  }
60
- this.opts.tui.requestRender(true);
61
- void this.opts.onToggle(row.key).then(async () => {
62
- this.rows = configRows(await readConfig());
63
- this.opts.tui.requestRender(true);
64
- }).catch((error: unknown) => {
65
- console.error("Failed to toggle hashline setting:", error);
66
- });
83
+ this.runToggle(row);
84
+ }
85
+
86
+ private adjustSelected(delta: number): void {
87
+ const row = this.rows[this.selected];
88
+ if (!row || row.disabled || row.value === undefined) return;
89
+ row.value += delta;
90
+ this.runToggle(row, delta);
67
91
  }
68
92
 
69
93
  handleInput(data: string): void {
@@ -75,6 +99,14 @@ export class HashlineConfigOverlay {
75
99
  this.selected = (this.selected + 1) % this.rows.length;
76
100
  return;
77
101
  }
102
+ if (matchesKey(data, Key.right) || data === "+" || data === "=") {
103
+ this.adjustSelected(1);
104
+ return;
105
+ }
106
+ if (matchesKey(data, Key.left) || data === "-" || data === "_") {
107
+ this.adjustSelected(-1);
108
+ return;
109
+ }
78
110
  if (matchesKey(data, Key.space) || matchesKey(data, Key.enter) || data === " " || data === "\r" || data === "\n") {
79
111
  this.toggleSelected();
80
112
  return;
@@ -96,12 +128,12 @@ export class HashlineConfigOverlay {
96
128
  lines.push(theme.fg("border", `├${"─".repeat(innerWidth)}┤`));
97
129
  this.rows.forEach((row, index) => {
98
130
  const cursor = index === this.selected ? theme.fg("accent", "> ") : " ";
99
- const box = row.mode !== undefined ? modeBox(theme, row.mode) : row.enabled ? theme.fg("success", "[x]") : theme.fg("dim", "[ ]");
100
- const label = index === this.selected ? theme.fg("accent", theme.bold(row.label)) : row.label;
131
+ const box = row.value !== undefined ? numberBox(theme, row.value, row.disabled) : row.mode !== undefined ? modeBox(theme, row.mode) : row.enabled ? theme.fg("success", "[x]") : theme.fg("dim", "[ ]");
132
+ const label = row.disabled ? theme.fg("dim", row.label) : index === this.selected ? theme.fg("accent", theme.bold(row.label)) : row.label;
101
133
  lines.push(padRow(theme, innerWidth, `${cursor}${box} ${label} ${theme.fg("dim", `— ${row.hint}`)}`));
102
134
  });
103
135
  lines.push(theme.fg("border", `├${"─".repeat(innerWidth)}┤`));
104
- lines.push(padRow(theme, innerWidth, theme.fg("dim", " ↑↓ navigate · space toggle · q close")));
136
+ lines.push(padRow(theme, innerWidth, theme.fg("dim", " ↑↓ navigate · space toggle · ←/→ or -/+ adjust · q close")));
105
137
  lines.push(theme.fg("border", `╰${"─".repeat(innerWidth)}╯`));
106
138
  return lines;
107
139
  }
package/src/config.ts CHANGED
@@ -1,16 +1,21 @@
1
- import { readFile } from "fs/promises";
1
+ import { mkdir, readFile, rename, rm, stat } from "fs/promises";
2
+ import { dirname } from "path";
2
3
  import { configPath } from "./paths";
3
4
  import { errCode, isRec } from "./utils";
4
5
  import { writeAtomic } from "./fs-write";
5
-
6
6
  export type BoundaryDedupMode = "on" | "off" | "strict";
7
7
 
8
+ export const DEFAULT_DIFF_CONTEXT_LINES = 1;
9
+ export const MIN_DIFF_CONTEXT_LINES = 0;
10
+ export const MAX_DIFF_CONTEXT_LINES = 10;
11
+
8
12
  export interface Config {
9
13
  autoRead: boolean;
10
14
  anchorGrepEnabled: boolean;
11
15
  requirePath?: boolean;
12
16
  strictInput?: boolean;
13
17
  boundaryDedupMode?: BoundaryDedupMode;
18
+ diffContextLines?: number;
14
19
  }
15
20
 
16
21
  const DEFAULT_CONFIG: Config = {
@@ -18,7 +23,8 @@ const DEFAULT_CONFIG: Config = {
18
23
  anchorGrepEnabled: true,
19
24
  requirePath: false,
20
25
  strictInput: false,
21
- boundaryDedupMode: "on"
26
+ boundaryDedupMode: "on",
27
+ diffContextLines: DEFAULT_DIFF_CONTEXT_LINES
22
28
  };
23
29
 
24
30
  const BOUNDARY_DEDUP_MODES: BoundaryDedupMode[] = ["on", "strict", "off"];
@@ -30,6 +36,14 @@ function parseBoundaryDedupMode(mode: unknown, legacy: unknown): BoundaryDedupMo
30
36
  return DEFAULT_CONFIG.boundaryDedupMode ?? "on";
31
37
  }
32
38
 
39
+ export function normalizeDiffContextLines(value: unknown): number {
40
+ if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_DIFF_CONTEXT_LINES;
41
+ const floored = Math.floor(value);
42
+ if (floored < MIN_DIFF_CONTEXT_LINES) return MIN_DIFF_CONTEXT_LINES;
43
+ if (floored > MAX_DIFF_CONTEXT_LINES) return MAX_DIFF_CONTEXT_LINES;
44
+ return floored;
45
+ }
46
+
33
47
  function parseConfig(content: string): Config {
34
48
  const parsed = JSON.parse(content) as unknown;
35
49
  const autoRead = isRec(parsed) ? parsed.autoRead : undefined;
@@ -41,25 +55,90 @@ function parseConfig(content: string): Config {
41
55
  const strictInput = isRec(parsed) ? parsed.strictInput : undefined;
42
56
  const boundaryDedupMode = isRec(parsed) ? parsed.boundaryDedupMode : undefined;
43
57
  const legacyBoundaryDedup = isRec(parsed) ? parsed.boundaryDedupEnabled : undefined;
58
+ const diffContextLines = isRec(parsed) ? parsed.diffContextLines : undefined;
44
59
  return {
45
60
  autoRead,
46
61
  anchorGrepEnabled: typeof anchorGrepEnabled === "boolean" ? anchorGrepEnabled : DEFAULT_CONFIG.anchorGrepEnabled,
47
62
  requirePath: typeof requirePath === "boolean" ? requirePath : DEFAULT_CONFIG.requirePath,
48
63
  strictInput: typeof strictInput === "boolean" ? strictInput : DEFAULT_CONFIG.strictInput,
49
64
  boundaryDedupMode: parseBoundaryDedupMode(boundaryDedupMode, legacyBoundaryDedup),
65
+ diffContextLines: normalizeDiffContextLines(diffContextLines),
50
66
  };
51
67
  }
52
68
 
53
-
54
- export async function readConfig(): Promise<Config> {
69
+ async function loadConfigFile(): Promise<{ config: Config; corrupted: boolean }> {
70
+ let content: string;
71
+ try {
72
+ content = await readFile(configPath(), "utf-8");
73
+ } catch (error: unknown) {
74
+ if (errCode(error) === "ENOENT") return { config: { ...DEFAULT_CONFIG }, corrupted: false };
75
+ console.error("Config file unreadable, using defaults:", error);
76
+ return { config: { ...DEFAULT_CONFIG }, corrupted: false };
77
+ }
55
78
  try {
56
- const content = await readFile(configPath(), "utf-8");
57
- return parseConfig(content);
79
+ return { config: parseConfig(content), corrupted: false };
58
80
  } catch (error: unknown) {
59
- if (errCode(error) !== "ENOENT") {
60
- console.error("Config file corrupted, using defaults:", error);
81
+ try {
82
+ const badPath = configPath();
83
+ await rename(badPath, `${badPath}.corrupt-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2)}`);
84
+ } catch { }
85
+ console.error("Config file corrupted, quarantined, using defaults:", error);
86
+ return { config: { ...DEFAULT_CONFIG }, corrupted: true };
87
+ }
88
+ }
89
+ export async function readConfig(): Promise<Config> {
90
+ return (await loadConfigFile()).config;
91
+ }
92
+ export async function readConfigWithStatus(): Promise<{ config: Config; corrupted: boolean }> {
93
+ return loadConfigFile();
94
+ }
95
+ const CONFIG_LOCK_RETRIES = 80;
96
+ const CONFIG_LOCK_DELAY_MS = 25;
97
+ const CONFIG_LOCK_STALE_MS = 5000;
98
+ async function acquireConfigLock(lockPath: string): Promise<void> {
99
+ try {
100
+ await mkdir(dirname(lockPath), { recursive: true, mode: 0o700 });
101
+ } catch { }
102
+ for (let attempt = 0; attempt < CONFIG_LOCK_RETRIES; attempt++) {
103
+ try {
104
+ await mkdir(lockPath, { mode: 0o700 });
105
+ return;
106
+ } catch (error) {
107
+ if (errCode(error) === "ENOENT") {
108
+ try {
109
+ await mkdir(dirname(lockPath), { recursive: true, mode: 0o700 });
110
+ } catch { }
111
+ continue;
112
+ }
113
+ if (errCode(error) !== "EEXIST") throw error;
114
+ try {
115
+ const st = await stat(lockPath);
116
+ if (Date.now() - st.mtimeMs > CONFIG_LOCK_STALE_MS) {
117
+ await rm(lockPath, { recursive: true, force: true });
118
+ continue;
119
+ }
120
+ } catch { }
121
+ await new Promise<void>((r) => setTimeout(r, CONFIG_LOCK_DELAY_MS));
61
122
  }
62
- return { ...DEFAULT_CONFIG };
123
+ }
124
+ throw new Error(`[E_ACCESS] Could not acquire config lock: ${lockPath}`);
125
+ }
126
+ async function releaseConfigLock(lockPath: string): Promise<void> {
127
+ try {
128
+ await rm(lockPath, { recursive: true, force: true });
129
+ } catch { }
130
+ }
131
+ export async function updateConfig(mut: (config: Config) => void): Promise<Config> {
132
+ const cfgPath = configPath();
133
+ const lockPath = `${cfgPath}.lock`;
134
+ await acquireConfigLock(lockPath);
135
+ try {
136
+ const config = await readConfig();
137
+ mut(config);
138
+ await writeConfig(config);
139
+ return config;
140
+ } finally {
141
+ await releaseConfigLock(lockPath);
63
142
  }
64
143
  }
65
144
  export async function writeConfig(config: Config): Promise<void> {
@@ -68,38 +147,38 @@ export async function writeConfig(config: Config): Promise<void> {
68
147
 
69
148
 
70
149
  export async function toggleAutoRead(): Promise<boolean> {
71
- const config = await readConfig();
72
- config.autoRead = !config.autoRead;
73
- await writeConfig(config);
150
+ const config = await updateConfig((c) => { c.autoRead = !c.autoRead; });
74
151
  return config.autoRead;
75
152
  }
76
-
77
153
  export async function toggleAnchorGrep(): Promise<boolean> {
78
- const config = await readConfig();
79
- config.anchorGrepEnabled = !config.anchorGrepEnabled;
80
- await writeConfig(config);
154
+ const config = await updateConfig((c) => { c.anchorGrepEnabled = !c.anchorGrepEnabled; });
81
155
  return config.anchorGrepEnabled;
82
156
  }
83
-
84
157
  export async function toggleRequirePath(): Promise<boolean> {
85
- const config = await readConfig();
86
- config.requirePath = !config.requirePath;
87
- await writeConfig(config);
88
- return config.requirePath;
158
+ const config = await updateConfig((c) => { c.requirePath = !c.requirePath; });
159
+ return config.requirePath === true;
89
160
  }
90
-
91
161
  export async function toggleStrictInput(): Promise<boolean> {
92
- const config = await readConfig();
93
- config.strictInput = !(config.strictInput === true);
94
- await writeConfig(config);
162
+ const config = await updateConfig((c) => { c.strictInput = !(c.strictInput === true); });
95
163
  return config.strictInput === true;
96
164
  }
97
-
98
165
  export async function cycleBoundaryDedupMode(): Promise<BoundaryDedupMode> {
99
- const config = await readConfig();
100
- const current = config.boundaryDedupMode ?? "on";
101
- const next = BOUNDARY_DEDUP_MODES[(BOUNDARY_DEDUP_MODES.indexOf(current) + 1) % BOUNDARY_DEDUP_MODES.length] ?? "on";
102
- config.boundaryDedupMode = next;
103
- await writeConfig(config);
166
+ let next: BoundaryDedupMode = "on";
167
+ await updateConfig((c) => {
168
+ const current = c.boundaryDedupMode ?? "on";
169
+ next = BOUNDARY_DEDUP_MODES[(BOUNDARY_DEDUP_MODES.indexOf(current) + 1) % BOUNDARY_DEDUP_MODES.length] ?? "on";
170
+ c.boundaryDedupMode = next;
171
+ });
172
+ return next;
173
+ }
174
+ export async function getDiffContextLines(): Promise<number> {
175
+ return normalizeDiffContextLines((await readConfig()).diffContextLines);
176
+ }
177
+ export async function adjustDiffContextLines(delta: number): Promise<number> {
178
+ let next = DEFAULT_DIFF_CONTEXT_LINES;
179
+ await updateConfig((c) => {
180
+ next = normalizeDiffContextLines(normalizeDiffContextLines(c.diffContextLines) + delta);
181
+ c.diffContextLines = next;
182
+ });
104
183
  return next;
105
184
  }
package/src/grep.ts CHANGED
@@ -326,6 +326,12 @@ async function collectRgMatches(
326
326
  signal?: AbortSignal,
327
327
  ): Promise<Map<string, number[]>> {
328
328
  const args = ["--json", "--line-number", "--color=never", "--hidden", "--glob", "!.git"];
329
+ const wanted = req.limit ?? 100;
330
+ args.push("--max-count", String(wanted + 1));
331
+ if (typeof req.glob === "string" && req.glob.length > 0) {
332
+ const stripped = req.glob.startsWith("/") ? req.glob.slice(1) : req.glob;
333
+ if (!stripped.includes("/")) args.push("--glob", stripped);
334
+ }
329
335
  if (req.ignoreCase) args.push("--ignore-case");
330
336
  if (req.literal) args.push("--fixed-strings");
331
337
  args.push("--", pattern, searchPath);
@@ -451,7 +457,7 @@ const grepToolSchema = Type.Object(
451
457
  }),
452
458
  ),
453
459
  },
454
- { additionalProperties: false },
460
+ { additionalProperties: true },
455
461
  );
456
462
 
457
463
 
@@ -579,15 +585,17 @@ export function regGrep(pi: ExtensionAPI): void {
579
585
  let linesReplaced = 0;
580
586
  let countOnly = false;
581
587
  let poolSkipped = 0;
582
- const readGrepFile = async (absPath: string) => {
588
+ const makeGrepReader = (allocation: "real" | "shadow") => async (absPath: string) => {
583
589
  try {
584
- return await tryReadNormFile(absPath, ctx.cwd, { maxLines: MAX_HASH_LINES, noPersist: true, allocation: "real", signal });
590
+ return await tryReadNormFile(absPath, ctx.cwd, { maxLines: MAX_HASH_LINES, noPersist: true, allocation, signal });
585
591
  } catch (error) {
586
592
  if (!isPoolExhaustedError(error)) throw error;
587
593
  poolSkipped += 1;
588
594
  return undefined;
589
595
  }
590
596
  };
597
+ const readGrepFile = makeGrepReader("real");
598
+ const readGrepFileShadow = makeGrepReader("shadow");
591
599
  const rgMatches = await collectRgMatches(rgPath, req.pattern, base, req, signal);
592
600
  const sortedFiles = [...rgMatches.keys()].sort(cmp);
593
601
  for (let f = 0; f < sortedFiles.length; f++) {
@@ -603,7 +611,7 @@ export function regGrep(pi: ExtensionAPI): void {
603
611
  const globPath = relative(globRoot, absPath).replace(/\\/g, "/");
604
612
  if (!globRegex.test(globPath) && !globRegex.test(displayPath)) continue;
605
613
  }
606
- const norm = await readGrepFile(absPath);
614
+ const norm = await readGrepFileShadow(absPath);
607
615
  if (!norm) continue;
608
616
  const hit = makeHitFromIndices(norm, relative(ctx.cwd, absPath).replace(/\\/g, "/"), indices, context, validatedRegex, totalForFile, indices.length);
609
617
  const display = displayRowsForHit(hit);
@@ -1,11 +1,11 @@
1
1
  import anchorData from "./anchor-table.json";
2
-
3
- const TABLE: string = anchorData.anchors;
4
-
2
+ const rawAnchors: unknown = (anchorData as { anchors?: unknown }).anchors;
3
+ if (typeof rawAnchors !== "string" || rawAnchors.length === 0 || rawAnchors.length % 4 !== 0) {
4
+ throw new Error("[E_REGISTRY] Anchor table is missing or corrupt; reinstall pi-hashline-edit-pro.");
5
+ }
6
+ const TABLE: string = rawAnchors;
5
7
  export const HASH_LEN = 4;
6
-
7
8
  export const ANCHOR_COUNT = TABLE.length / HASH_LEN;
8
-
9
9
  const ALNUM = "A-Za-z0-9";
10
10
 
11
11
  export const ALPH_RE = new RegExp(`^[${ALNUM}]+$`);