@d3ara1n/pi-hashline-edit 0.5.7 → 0.6.0

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 CHANGED
@@ -165,6 +165,7 @@ The `grep` override also covers the compound queries that otherwise push models
165
165
  - `wordMatch` — whole words only (`rg -w`)
166
166
  - `outputMode: "files"` / `"count"` — just the file paths (`rg -l`) or per-file counts + total (`grep -c`); `"files"` output pastes straight back as a `path` array
167
167
  - `pattern` and `path` accept arrays — several patterns combined per `matchMode`, several search roots in one call
168
+ - `glob` accepts one pattern or an ordered array of ripgrep globs, e.g. `["*.ts", "*.md", "!**/*.test.ts"]` to include TypeScript and Markdown files but exclude tests
168
169
 
169
170
  Filters run before the match limit counts, and context windows are rebuilt from surviving matches, so `limit` and `context` compose cleanly with `matchMode`/`excludePattern`.
170
171
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d3ara1n/pi-hashline-edit",
3
- "version": "0.5.7",
3
+ "version": "0.6.0",
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",
@@ -46,6 +46,31 @@ test("real rg emits anchored matches from a temporary directory", {
46
46
  }
47
47
  });
48
48
 
49
+ test("real rg combines include and exclude globs in order", {
50
+ skip: rgPath === null,
51
+ }, async () => {
52
+ const directory = await mkdtemp(join(tmpdir(), "hl-grep-globs-"));
53
+ try {
54
+ for (const name of ["first.ts", "second.ts", "first.test.ts", "notes.md", "notes.txt"]) {
55
+ await writeFile(join(directory, name), "needle\n");
56
+ }
57
+ const tool = makeGrepOverrideWithBackend(directory, {
58
+ findRg: async () => rgPath,
59
+ delegate: async () => {
60
+ throw new Error("integration test must not invoke the built-in grep delegate");
61
+ },
62
+ });
63
+ const result: any = await tool.execute("0", {
64
+ pattern: "needle",
65
+ glob: ["*.ts", "*.md", "!**/*.test.ts"],
66
+ outputMode: "files",
67
+ }, undefined, undefined);
68
+ assert.deepEqual(result.content[0].text.split("\n").sort(), ["first.ts", "notes.md", "second.ts"]);
69
+ } finally {
70
+ await rm(directory, { recursive: true, force: true });
71
+ }
72
+ });
73
+
49
74
  test("real rg and line filters honor explicit case settings and Unicode folding", {
50
75
  skip: rgPath === null,
51
76
  }, async () => {
@@ -140,7 +140,9 @@ const grepOverrideSchema = Type.Object({
140
140
  "Directory or file to search (string or array of paths; default: current directory)",
141
141
  })),
142
142
  glob: Type.Optional(
143
- Type.String({ description: "Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'" }),
143
+ Type.Union([Type.String(), Type.Array(Type.String())], {
144
+ description: "Filter files by glob pattern; pass an array for multiple filters and prefix exclusions with `!`, e.g. ['*.ts', '!**/*.test.ts']",
145
+ }),
144
146
  ),
145
147
  ignoreCase: Type.Optional(
146
148
  Type.Boolean({ description: "Case-insensitive search (default: false); applies to pattern and excludePattern." }),
@@ -340,7 +342,7 @@ export function makeGrepOverrideWithBackend(cwd: string, overrides: Partial<Grep
340
342
  text += theme.fg("toolOutput", ` -v:${ex}`);
341
343
  }
342
344
  if (args?.wordMatch) text += theme.fg("toolOutput", " -w");
343
- if (args?.glob) text += theme.fg("toolOutput", ` (${args.glob})`);
345
+ if (args?.glob) text += theme.fg("toolOutput", ` (${toArray(args.glob).join(", ")})`);
344
346
  if (args?.outputMode && args.outputMode !== "content")
345
347
  text += theme.fg("success", ` → ${args.outputMode}`);
346
348
  if (args?.limit !== undefined) text += theme.fg("toolOutput", ` limit ${args.limit}`);
@@ -388,6 +390,7 @@ export function makeGrepOverrideWithBackend(cwd: string, overrides: Partial<Grep
388
390
  params.excludePattern === undefined &&
389
391
  params.outputMode === undefined &&
390
392
  params.wordMatch === undefined &&
393
+ !Array.isArray(params.glob) &&
391
394
  !Array.isArray(params.path);
392
395
 
393
396
  const rgPath = await backend.findRg();
@@ -402,7 +405,8 @@ export function makeGrepOverrideWithBackend(cwd: string, overrides: Partial<Grep
402
405
  const excludes = toArray(params.excludePattern);
403
406
  const matchMode: "any" | "all" = params.matchMode ?? "any";
404
407
  const outputMode: "content" | "files" | "count" = params.outputMode ?? "content";
405
- const { glob, ignoreCase, literal, wordMatch, limit } = params;
408
+ const globs = toArray(params.glob);
409
+ const { ignoreCase, literal, wordMatch, limit } = params;
406
410
  const searchPaths = (() => {
407
411
  const raw = toArray(params.path);
408
412
  return (raw.length ? raw : ["."]).map((p) => canonicalPath(cwd, p));
@@ -446,7 +450,7 @@ export function makeGrepOverrideWithBackend(cwd: string, overrides: Partial<Grep
446
450
  if (ignoreCase) args.push("--ignore-case");
447
451
  if (literal) args.push("--fixed-strings");
448
452
  if (wordMatch) args.push("--word-regexp");
449
- if (glob) args.push("--glob", glob);
453
+ for (const glob of globs) args.push("--glob", glob);
450
454
  for (const p of patterns) args.push("-e", p);
451
455
  args.push("--", ...searchPaths);
452
456
 
@@ -219,10 +219,13 @@ test("passes output flags and formats files and counts", async () => {
219
219
  await writeFile(b, "foo a.b\n");
220
220
  const fake = fakeBackend({ lines: [rgMatch(a, 1, "Foo a.b\n"), rgMatch(b, 1, "foo a.b\n")] });
221
221
 
222
- const files = await call(makeGrepOverrideWithBackend(dir, fake.backend), {
222
+ const tool = makeGrepOverrideWithBackend(dir, fake.backend);
223
+ const globSchema: any = tool.parameters.properties.glob;
224
+ assert.deepEqual(globSchema.anyOf.map((option: any) => option.type), ["string", "array"]);
225
+ const files = await call(tool, {
223
226
  pattern: ["Foo", "a.b"],
224
227
  path: ["a.ts", "b.ts"],
225
- glob: "*.ts",
228
+ glob: ["*.ts", "!**/*.test.ts"],
226
229
  ignoreCase: true,
227
230
  literal: true,
228
231
  wordMatch: true,
@@ -239,6 +242,8 @@ test("passes output flags and formats files and counts", async () => {
239
242
  "--word-regexp",
240
243
  "--glob",
241
244
  "*.ts",
245
+ "--glob",
246
+ "!**/*.test.ts",
242
247
  "-e",
243
248
  "Foo",
244
249
  "-e",
@@ -337,6 +342,12 @@ test("delegates only safe fallbacks and rejects extended missing-rg requests", a
337
342
  }),
338
343
  /ripgrep \(rg\) not found/,
339
344
  );
345
+ await call(tool, { pattern: "x", glob: "*.ts" });
346
+ assert.deepEqual(absent.delegates.at(-1)?.[1], { pattern: "x", glob: "*.ts" });
347
+ await assert.rejects(
348
+ call(tool, { pattern: "x", glob: ["*.ts", "!**/*.test.ts"] }),
349
+ /ripgrep \(rg\) not found/,
350
+ );
340
351
  });
341
352
  });
342
353
  });