@d3ara1n/pi-hashline-edit 0.5.1 → 0.5.3
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/package.json +1 -1
- package/src/integration/grep-rg.test.ts +6 -5
- package/src/pi/grep-tool.ts +10 -12
- package/src/pi/grep.test.ts +33 -1
- package/src/pi/pi.test.ts +7 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d3ara1n/pi-hashline-edit",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hashline-style file editing for pi — line-anchored edits verified by content hash, replacing oldText/newText matching",
|
|
6
6
|
"homepage": "https://github.com/d3ara1n/pi-extensions/tree/main/packages/pi-hashline-edit#readme",
|
|
@@ -12,7 +12,7 @@ import { makeGrepOverrideWithBackend } from "../pi/grep-tool.ts";
|
|
|
12
12
|
async function findPathRg(): Promise<string | null> {
|
|
13
13
|
for (const directory of process.env.PATH?.split(delimiter) ?? []) {
|
|
14
14
|
if (!directory) continue;
|
|
15
|
-
const candidate = join(directory, "rg");
|
|
15
|
+
const candidate = join(directory, process.platform === "win32" ? "rg.exe" : "rg");
|
|
16
16
|
try {
|
|
17
17
|
await access(candidate, constants.X_OK);
|
|
18
18
|
return candidate;
|
|
@@ -31,15 +31,16 @@ test("real rg emits anchored matches from a temporary directory", {
|
|
|
31
31
|
const file = join(directory, "fixture.ts");
|
|
32
32
|
await writeFile(file, "needle\nother\n");
|
|
33
33
|
const tool = makeGrepOverrideWithBackend(directory, {
|
|
34
|
-
findRg: async () => rgPath,
|
|
35
34
|
delegate: async () => {
|
|
36
35
|
throw new Error("integration test must not invoke the built-in grep delegate");
|
|
37
36
|
},
|
|
38
37
|
});
|
|
39
38
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
for (const pattern of ["needle", ["needle"]]) {
|
|
40
|
+
const result: any = await tool.execute("0", { pattern }, undefined, undefined);
|
|
41
|
+
assert.match(result.content[0].text, /fixture\.ts · 1 match/);
|
|
42
|
+
assert.match(result.content[0].text, /1#[0-9A-Z]+│needle/);
|
|
43
|
+
}
|
|
43
44
|
} finally {
|
|
44
45
|
await rm(directory, { recursive: true, force: true });
|
|
45
46
|
}
|
package/src/pi/grep-tool.ts
CHANGED
|
@@ -42,7 +42,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
42
42
|
import { spawn } from "node:child_process";
|
|
43
43
|
import { createInterface } from "node:readline";
|
|
44
44
|
import { access, constants, readFile, stat } from "node:fs/promises";
|
|
45
|
-
import {
|
|
45
|
+
import { delimiter, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
46
46
|
import { hashFileLines } from "../core/hash.ts";
|
|
47
47
|
import { splitLines } from "../core/lines.ts";
|
|
48
48
|
import { getState } from "./state.ts";
|
|
@@ -55,15 +55,16 @@ const GREP_MAX_LINE_LENGTH = 500;
|
|
|
55
55
|
|
|
56
56
|
/** Locate ripgrep: pi's bundled bin first, then PATH. Returns null if not found. */
|
|
57
57
|
async function findRg(): Promise<string | null> {
|
|
58
|
+
const executable = process.platform === "win32" ? "rg.exe" : "rg";
|
|
58
59
|
const agentDir = getAgentDir();
|
|
59
|
-
const piRg = join(agentDir, "bin",
|
|
60
|
+
const piRg = join(agentDir, "bin", executable);
|
|
60
61
|
try {
|
|
61
62
|
await access(piRg, constants.X_OK);
|
|
62
63
|
return piRg;
|
|
63
64
|
} catch {}
|
|
64
65
|
for (const dir of process.env.PATH?.split(delimiter) ?? []) {
|
|
65
66
|
if (!dir) continue;
|
|
66
|
-
const p = join(dir,
|
|
67
|
+
const p = join(dir, executable);
|
|
67
68
|
try {
|
|
68
69
|
await access(p, constants.X_OK);
|
|
69
70
|
return p;
|
|
@@ -410,11 +411,9 @@ export function makeGrepOverrideWithBackend(cwd: string, overrides: Partial<Grep
|
|
|
410
411
|
})();
|
|
411
412
|
const hashLen = state.config.hashLen;
|
|
412
413
|
|
|
413
|
-
// Verify search paths upfront; remember dir-ness for relative display.
|
|
414
|
-
const roots: { path: string; isDir: boolean }[] = [];
|
|
415
414
|
for (const sp of searchPaths) {
|
|
416
415
|
try {
|
|
417
|
-
|
|
416
|
+
await stat(sp);
|
|
418
417
|
} catch {
|
|
419
418
|
throw new Error(`Path not found: ${sp}`);
|
|
420
419
|
}
|
|
@@ -527,12 +526,11 @@ export function makeGrepOverrideWithBackend(cwd: string, overrides: Partial<Grep
|
|
|
527
526
|
};
|
|
528
527
|
|
|
529
528
|
const formatPath = (fp: string): string => {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
return basename(fp);
|
|
529
|
+
const abs = resolve(cwd, fp);
|
|
530
|
+
const rel = relative(cwd, abs);
|
|
531
|
+
return rel && rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)
|
|
532
|
+
? rel.replace(/\\/g, "/")
|
|
533
|
+
: abs;
|
|
536
534
|
};
|
|
537
535
|
|
|
538
536
|
const blocks: string[] = [];
|
package/src/pi/grep.test.ts
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { test } from "node:test";
|
|
6
6
|
import assert from "node:assert/strict";
|
|
7
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
7
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { computeLineHash } from "../core/hash.ts";
|
|
11
11
|
import { makeGrepOverrideWithBackend, type GrepBackend } from "./grep-tool.ts";
|
|
12
|
+
import { makeEditOverride } from "./edit-tool.ts";
|
|
12
13
|
import { getState } from "./state.ts";
|
|
13
14
|
|
|
14
15
|
type FakeOptions = {
|
|
@@ -111,6 +112,37 @@ test("formats parsed rg matches with full-line hash anchors", async () => {
|
|
|
111
112
|
);
|
|
112
113
|
});
|
|
113
114
|
|
|
115
|
+
test("grep in a subdirectory returns a path that edits the matching file", async () => {
|
|
116
|
+
await withDir(async (dir) =>
|
|
117
|
+
withEnabled(true, async () => {
|
|
118
|
+
await mkdir(join(dir, "src"));
|
|
119
|
+
const original = "export const status = 1;\n";
|
|
120
|
+
const rootFile = join(dir, "status.ts");
|
|
121
|
+
const matchedFile = join(dir, "src", "status.ts");
|
|
122
|
+
await writeFile(rootFile, original);
|
|
123
|
+
await writeFile(matchedFile, original);
|
|
124
|
+
const fake = fakeBackend({ lines: [rgMatch(matchedFile, 1, original)] });
|
|
125
|
+
const result = await call(makeGrepOverrideWithBackend(dir, fake.backend), {
|
|
126
|
+
pattern: "status",
|
|
127
|
+
path: "src",
|
|
128
|
+
});
|
|
129
|
+
const output = text(result);
|
|
130
|
+
const displayPath = output.split(" · ")[0];
|
|
131
|
+
const edit: any = makeEditOverride(dir);
|
|
132
|
+
await call(edit, {
|
|
133
|
+
path: displayPath,
|
|
134
|
+
edits: [{
|
|
135
|
+
op: "replace",
|
|
136
|
+
anchor: { line: 1, hash: computeLineHash(1, original.trimEnd()) },
|
|
137
|
+
body: ["export const status = 2;"],
|
|
138
|
+
}],
|
|
139
|
+
});
|
|
140
|
+
assert.equal(await readFile(matchedFile, "utf-8"), "export const status = 2;\n");
|
|
141
|
+
assert.equal(await readFile(rootFile, "utf-8"), original);
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
114
146
|
test("applies all, exclude, context, and CRLF filtering after rg output", async () => {
|
|
115
147
|
await withDir(async (dir) =>
|
|
116
148
|
withEnabled(true, async () => {
|
package/src/pi/pi.test.ts
CHANGED
|
@@ -2,15 +2,18 @@ import { test } from "node:test";
|
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import { canonicalPath } from "./read-tool.ts";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
5
6
|
|
|
6
7
|
test("canonicalPath resolves relative and absolute", () => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
assert.equal(canonicalPath(
|
|
8
|
+
const cwd = resolve("/cwd");
|
|
9
|
+
const absolute = resolve("/abs/x.ts");
|
|
10
|
+
assert.equal(canonicalPath(cwd, "foo.ts"), join(cwd, "foo.ts"));
|
|
11
|
+
assert.equal(canonicalPath(cwd, "./foo.ts"), join(cwd, "foo.ts"));
|
|
12
|
+
assert.equal(canonicalPath(cwd, absolute), absolute);
|
|
10
13
|
});
|
|
11
14
|
|
|
12
15
|
test("canonicalPath expands ~ to home directory", () => {
|
|
13
16
|
const home = homedir();
|
|
14
17
|
assert.equal(canonicalPath("/cwd", "~"), home);
|
|
15
|
-
assert.equal(canonicalPath("/cwd", "~/foo.ts"),
|
|
18
|
+
assert.equal(canonicalPath("/cwd", "~/foo.ts"), join(home, "foo.ts"));
|
|
16
19
|
});
|