@d3ara1n/pi-hashline-edit 0.5.6 → 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 +15 -7
- package/src/pi/grep.test.ts +43 -2
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:
|
|
@@ -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
|
@@ -163,11 +163,16 @@ test("applies all, exclude, context, and CRLF filtering after rg output", async
|
|
|
163
163
|
],
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
-
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, {
|
|
167
172
|
pattern: ["alpha", "beta$"],
|
|
168
173
|
matchMode: "all",
|
|
169
174
|
excludePattern: "drop",
|
|
170
|
-
context: 1,
|
|
175
|
+
context: 1.9,
|
|
171
176
|
});
|
|
172
177
|
assert.equal(
|
|
173
178
|
text(result),
|
|
@@ -182,6 +187,29 @@ test("applies all, exclude, context, and CRLF filtering after rg output", async
|
|
|
182
187
|
);
|
|
183
188
|
});
|
|
184
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
|
+
|
|
185
213
|
test("passes output flags and formats files and counts", async () => {
|
|
186
214
|
await withDir(async (dir) =>
|
|
187
215
|
withEnabled(true, async () => {
|
|
@@ -289,6 +317,19 @@ test("delegates only safe fallbacks and rejects extended missing-rg requests", a
|
|
|
289
317
|
text(await call(makeGrepOverrideWithBackend(dir, absent.backend), { pattern: "x" })),
|
|
290
318
|
"delegated",
|
|
291
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
|
+
}
|
|
292
333
|
await assert.rejects(
|
|
293
334
|
call(makeGrepOverrideWithBackend(dir, absent.backend), {
|
|
294
335
|
pattern: ["x", "y"],
|