@d3ara1n/pi-hashline-edit 0.5.5 → 0.5.7
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/pi/grep-tool.ts +17 -9
- package/src/pi/grep.test.ts +46 -3
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.7",
|
|
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",
|
package/src/pi/grep-tool.ts
CHANGED
|
@@ -52,6 +52,7 @@ import { parseHashline } from "./render.ts";
|
|
|
52
52
|
const DEFAULT_LIMIT = 100;
|
|
53
53
|
/** Max chars per result line for display (mirrors pi's truncate.ts; not exported there). */
|
|
54
54
|
const GREP_MAX_LINE_LENGTH = 500;
|
|
55
|
+
const GREP_CONTEXT_MAX = 20;
|
|
55
56
|
|
|
56
57
|
/** Locate ripgrep: pi's bundled bin first, then PATH. Returns null if not found. */
|
|
57
58
|
async function findRg(): Promise<string | null> {
|
|
@@ -107,6 +108,11 @@ function toArray(v: string | string[] | undefined): string[] {
|
|
|
107
108
|
return Array.isArray(v) ? v : [v];
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
function clampContext(context: number | undefined): number {
|
|
112
|
+
if (!context || !Number.isFinite(context) || context < 0) return 0;
|
|
113
|
+
return Math.min(Math.floor(context), GREP_CONTEXT_MAX);
|
|
114
|
+
}
|
|
115
|
+
|
|
110
116
|
const grepOverrideSchema = Type.Object({
|
|
111
117
|
pattern: Type.Union([Type.String(), Type.Array(Type.String())], {
|
|
112
118
|
description:
|
|
@@ -129,10 +135,10 @@ const grepOverrideSchema = Type.Object({
|
|
|
129
135
|
}),
|
|
130
136
|
),
|
|
131
137
|
wordMatch: Type.Optional(Type.Boolean({ description: "Match whole words only (rg -w)" })),
|
|
132
|
-
path: Type.Union([Type.String(), Type.Array(Type.String())], {
|
|
138
|
+
path: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())], {
|
|
133
139
|
description:
|
|
134
140
|
"Directory or file to search (string or array of paths; default: current directory)",
|
|
135
|
-
}),
|
|
141
|
+
})),
|
|
136
142
|
glob: Type.Optional(
|
|
137
143
|
Type.String({ description: "Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'" }),
|
|
138
144
|
),
|
|
@@ -145,9 +151,10 @@ const grepOverrideSchema = Type.Object({
|
|
|
145
151
|
}),
|
|
146
152
|
),
|
|
147
153
|
context: Type.Optional(
|
|
148
|
-
Type.
|
|
149
|
-
|
|
150
|
-
|
|
154
|
+
Type.Integer({
|
|
155
|
+
minimum: 0,
|
|
156
|
+
maximum: GREP_CONTEXT_MAX,
|
|
157
|
+
description: `Number of lines on each side of a match (0-${GREP_CONTEXT_MAX}; default: 0); context lines are anchored too`,
|
|
151
158
|
}),
|
|
152
159
|
),
|
|
153
160
|
limit: Type.Optional(
|
|
@@ -365,8 +372,10 @@ export function makeGrepOverrideWithBackend(cwd: string, overrides: Partial<Grep
|
|
|
365
372
|
onUpdate: any,
|
|
366
373
|
): Promise<any> {
|
|
367
374
|
const state = getState();
|
|
375
|
+
const ctx = clampContext(params.context);
|
|
376
|
+
const delegatedParams = params.context === undefined ? params : { ...params, context: ctx };
|
|
368
377
|
// aborted → built-in grep (it handles abort itself)
|
|
369
|
-
if (signal?.aborted) return backend.delegate(toolCallId,
|
|
378
|
+
if (signal?.aborted) return backend.delegate(toolCallId, delegatedParams, signal, onUpdate);
|
|
370
379
|
|
|
371
380
|
const patterns = toArray(params.pattern);
|
|
372
381
|
if (patterns.length === 0) throw new Error("pattern is required (got an empty array)");
|
|
@@ -384,7 +393,7 @@ export function makeGrepOverrideWithBackend(cwd: string, overrides: Partial<Grep
|
|
|
384
393
|
const rgPath = await backend.findRg();
|
|
385
394
|
// ripgrep unavailable → built-in (it can auto-download rg), but only for plain params
|
|
386
395
|
if (!rgPath) {
|
|
387
|
-
if (legacyShaped) return backend.delegate(toolCallId,
|
|
396
|
+
if (legacyShaped) return backend.delegate(toolCallId, delegatedParams, signal, onUpdate);
|
|
388
397
|
throw new Error(
|
|
389
398
|
"ripgrep (rg) not found; extended grep params cannot fall back to the built-in grep. Retry with a simple pattern first, or use bash",
|
|
390
399
|
);
|
|
@@ -393,8 +402,7 @@ export function makeGrepOverrideWithBackend(cwd: string, overrides: Partial<Grep
|
|
|
393
402
|
const excludes = toArray(params.excludePattern);
|
|
394
403
|
const matchMode: "any" | "all" = params.matchMode ?? "any";
|
|
395
404
|
const outputMode: "content" | "files" | "count" = params.outputMode ?? "content";
|
|
396
|
-
const { glob, ignoreCase, literal, wordMatch,
|
|
397
|
-
const ctx = context && context > 0 ? context : 0;
|
|
405
|
+
const { glob, ignoreCase, literal, wordMatch, limit } = params;
|
|
398
406
|
const searchPaths = (() => {
|
|
399
407
|
const raw = toArray(params.path);
|
|
400
408
|
return (raw.length ? raw : ["."]).map((p) => canonicalPath(cwd, p));
|
package/src/pi/grep.test.ts
CHANGED
|
@@ -96,7 +96,9 @@ test("formats parsed rg matches with full-line hash anchors", async () => {
|
|
|
96
96
|
],
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
const
|
|
99
|
+
const tool = makeGrepOverrideWithBackend(dir, fake.backend);
|
|
100
|
+
assert.deepEqual(tool.parameters.required, ["pattern"]);
|
|
101
|
+
const result = await call(tool, {
|
|
100
102
|
pattern: "alpha",
|
|
101
103
|
});
|
|
102
104
|
const output = text(result);
|
|
@@ -161,11 +163,16 @@ test("applies all, exclude, context, and CRLF filtering after rg output", async
|
|
|
161
163
|
],
|
|
162
164
|
});
|
|
163
165
|
|
|
164
|
-
const
|
|
166
|
+
const tool = makeGrepOverrideWithBackend(dir, fake.backend);
|
|
167
|
+
const contextSchema: any = tool.parameters.properties.context;
|
|
168
|
+
assert.equal(contextSchema.type, "integer");
|
|
169
|
+
assert.equal(contextSchema.minimum, 0);
|
|
170
|
+
assert.equal(contextSchema.maximum, 20);
|
|
171
|
+
const result = await call(tool, {
|
|
165
172
|
pattern: ["alpha", "beta$"],
|
|
166
173
|
matchMode: "all",
|
|
167
174
|
excludePattern: "drop",
|
|
168
|
-
context: 1,
|
|
175
|
+
context: 1.9,
|
|
169
176
|
});
|
|
170
177
|
assert.equal(
|
|
171
178
|
text(result),
|
|
@@ -180,6 +187,29 @@ test("applies all, exclude, context, and CRLF filtering after rg output", async
|
|
|
180
187
|
);
|
|
181
188
|
});
|
|
182
189
|
|
|
190
|
+
test("context bounds apply to each side of a match, including direct runtime calls", async () => {
|
|
191
|
+
await withDir(async (dir) => {
|
|
192
|
+
const file = join(dir, "a.ts");
|
|
193
|
+
const lines = Array.from({ length: 45 }, (_, i) => i === 22 ? "needle" : `line ${i + 1}`);
|
|
194
|
+
await writeFile(file, lines.join("\n"));
|
|
195
|
+
const fake = fakeBackend({ lines: [rgMatch(file, 23, "needle\n")] });
|
|
196
|
+
const tool = makeGrepOverrideWithBackend(dir, fake.backend);
|
|
197
|
+
|
|
198
|
+
for (const context of [20, 1_000]) {
|
|
199
|
+
const output = text(await call(tool, { pattern: "needle", context })).split("\n");
|
|
200
|
+
assert.equal(output.length, 42);
|
|
201
|
+
assert.match(output[1], /^3#[0-9A-Z]+│line 3$/);
|
|
202
|
+
assert.match(output[21], /^23#[0-9A-Z]+│needle$/);
|
|
203
|
+
assert.match(output[41], /^43#[0-9A-Z]+│line 43$/);
|
|
204
|
+
}
|
|
205
|
+
for (const context of [-1, Number.NaN, Number.POSITIVE_INFINITY]) {
|
|
206
|
+
const output = text(await call(tool, { pattern: "needle", context })).split("\n");
|
|
207
|
+
assert.equal(output.length, 2);
|
|
208
|
+
assert.match(output[1], /^23#[0-9A-Z]+│needle$/);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
183
213
|
test("passes output flags and formats files and counts", async () => {
|
|
184
214
|
await withDir(async (dir) =>
|
|
185
215
|
withEnabled(true, async () => {
|
|
@@ -287,6 +317,19 @@ test("delegates only safe fallbacks and rejects extended missing-rg requests", a
|
|
|
287
317
|
text(await call(makeGrepOverrideWithBackend(dir, absent.backend), { pattern: "x" })),
|
|
288
318
|
"delegated",
|
|
289
319
|
);
|
|
320
|
+
assert.deepEqual(absent.delegates.at(-1)?.[1], { pattern: "x" });
|
|
321
|
+
const tool = makeGrepOverrideWithBackend(dir, absent.backend);
|
|
322
|
+
for (const [context, expected] of [
|
|
323
|
+
[1_000, 20],
|
|
324
|
+
[1.9, 1],
|
|
325
|
+
[-1, 0],
|
|
326
|
+
[Number.POSITIVE_INFINITY, 0],
|
|
327
|
+
]) {
|
|
328
|
+
const params = { pattern: "x", context };
|
|
329
|
+
await call(tool, params);
|
|
330
|
+
assert.equal(absent.delegates.at(-1)?.[1].context, expected);
|
|
331
|
+
assert.equal(params.context, context);
|
|
332
|
+
}
|
|
290
333
|
await assert.rejects(
|
|
291
334
|
call(makeGrepOverrideWithBackend(dir, absent.backend), {
|
|
292
335
|
pattern: ["x", "y"],
|