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/README.md +8 -7
- package/index.ts +10 -5
- package/package.json +1 -1
- package/src/anchor-registry.ts +69 -6
- package/src/batch.ts +129 -31
- package/src/commit.ts +31 -8
- package/src/config-ui.ts +45 -13
- package/src/config.ts +111 -32
- package/src/grep.ts +12 -4
- package/src/hashline/alphabet.ts +5 -5
- package/src/hashline/anchor-table.json +1 -1
- package/src/hashline/apply.ts +3 -1
- package/src/hashline/resolve.ts +1 -1
- package/src/insert.ts +44 -40
- package/src/payload-contract.ts +2 -2
- package/src/replace-response.ts +2 -2
- package/src/replace-undo.ts +3 -2
- package/src/replace.ts +29 -20
- package/src/validation.ts +2 -2
- package/src/write-hook.ts +3 -3
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
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
return parseConfig(content);
|
|
79
|
+
return { config: parseConfig(content), corrupted: false };
|
|
58
80
|
} catch (error: unknown) {
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
86
|
-
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
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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);
|
package/src/hashline/alphabet.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import anchorData from "./anchor-table.json";
|
|
2
|
-
|
|
3
|
-
|
|
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}]+$`);
|