@d3ara1n/pi-hashline-edit 0.6.0 → 0.6.1
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 +1 -1
- package/package.json +1 -1
- package/src/integration/grep-rg.test.ts +43 -1
- package/src/pi/grep-tool.ts +87 -10
- package/src/pi/grep.test.ts +63 -2
package/README.md
CHANGED
|
@@ -165,7 +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
|
+
- `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. It also filters explicitly named files; this requires a locally available ripgrep binary.
|
|
169
169
|
|
|
170
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`.
|
|
171
171
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d3ara1n/pi-hashline-edit",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { test } from "node:test";
|
|
6
6
|
import assert from "node:assert/strict";
|
|
7
|
-
import { access, constants, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
7
|
+
import { access, constants, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
import { delimiter, join } from "node:path";
|
|
10
10
|
import { makeGrepOverrideWithBackend } from "../pi/grep-tool.ts";
|
|
@@ -71,6 +71,48 @@ test("real rg combines include and exclude globs in order", {
|
|
|
71
71
|
}
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
test("real rg applies globs to explicit files and mixed search paths", {
|
|
75
|
+
skip: rgPath === null,
|
|
76
|
+
}, async () => {
|
|
77
|
+
const directory = await mkdtemp(join(tmpdir(), "hl-grep-explicit-globs-"));
|
|
78
|
+
try {
|
|
79
|
+
await mkdir(join(directory, "sub"));
|
|
80
|
+
await writeFile(join(directory, ".gitignore"), "ignored.ts\n");
|
|
81
|
+
for (const name of ["keep.ts", "drop.test.ts", "ignored.ts", "sub/child.ts"]) {
|
|
82
|
+
await writeFile(join(directory, name), "needle\n");
|
|
83
|
+
}
|
|
84
|
+
const tool = makeGrepOverrideWithBackend(directory, {
|
|
85
|
+
findRg: async () => rgPath,
|
|
86
|
+
delegate: async () => {
|
|
87
|
+
throw new Error("integration test must not invoke the built-in grep delegate");
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
const search = async (path: string | string[], glob: string | string[]) => {
|
|
91
|
+
const result: any = await tool.execute("0", { pattern: "needle", path, glob, outputMode: "files" }, undefined, undefined);
|
|
92
|
+
return result.content[0].text;
|
|
93
|
+
};
|
|
94
|
+
const globs = ["*.ts", "!**/*.test.ts"];
|
|
95
|
+
assert.deepEqual(
|
|
96
|
+
(await search(["drop.test.ts", "keep.ts", "ignored.ts"], globs)).split("\n").sort(),
|
|
97
|
+
["ignored.ts", "keep.ts"],
|
|
98
|
+
);
|
|
99
|
+
assert.equal(await search("drop.test.ts", "*.ts"), "drop.test.ts");
|
|
100
|
+
assert.equal(await search("drop.test.ts", globs), "No matches found");
|
|
101
|
+
assert.equal(await search(["drop.test.ts", "keep.ts"], "!**/*.test.ts"), "keep.ts");
|
|
102
|
+
assert.equal(await search("sub/child.ts", "**/sub/*.ts"), "sub/child.ts");
|
|
103
|
+
assert.deepEqual(
|
|
104
|
+
(await search(["keep.ts", "sub/child.ts"], globs)).split("\n").sort(),
|
|
105
|
+
["keep.ts", "sub/child.ts"],
|
|
106
|
+
);
|
|
107
|
+
assert.deepEqual(
|
|
108
|
+
(await search(["drop.test.ts", "sub", "keep.ts"], globs)).split("\n").sort(),
|
|
109
|
+
["keep.ts", "sub/child.ts"],
|
|
110
|
+
);
|
|
111
|
+
} finally {
|
|
112
|
+
await rm(directory, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
74
116
|
test("real rg and line filters honor explicit case settings and Unicode folding", {
|
|
75
117
|
skip: rgPath === null,
|
|
76
118
|
}, async () => {
|
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 { delimiter, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
45
|
+
import { delimiter, dirname, 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";
|
|
@@ -141,7 +141,7 @@ const grepOverrideSchema = Type.Object({
|
|
|
141
141
|
})),
|
|
142
142
|
glob: Type.Optional(
|
|
143
143
|
Type.Union([Type.String(), Type.Array(Type.String())], {
|
|
144
|
-
description: "Filter files by glob
|
|
144
|
+
description: "Filter files (including explicit file paths) by glob; pass an ordered array for multiple filters and prefix exclusions with `!`, e.g. ['*.ts', '!**/*.test.ts']",
|
|
145
145
|
}),
|
|
146
146
|
),
|
|
147
147
|
ignoreCase: Type.Optional(
|
|
@@ -178,6 +178,12 @@ interface RgRunResult {
|
|
|
178
178
|
/** @internal — injectable process and fallback boundary for deterministic tests. */
|
|
179
179
|
export interface GrepBackend {
|
|
180
180
|
findRg(): Promise<string | null>;
|
|
181
|
+
listFiles(
|
|
182
|
+
rgPath: string,
|
|
183
|
+
directories: string[],
|
|
184
|
+
globs: string[],
|
|
185
|
+
signal: AbortSignal | undefined,
|
|
186
|
+
): Promise<Set<string>>;
|
|
181
187
|
runRg(
|
|
182
188
|
rgPath: string,
|
|
183
189
|
args: string[],
|
|
@@ -192,6 +198,43 @@ export interface GrepBackend {
|
|
|
192
198
|
): Promise<any>;
|
|
193
199
|
}
|
|
194
200
|
|
|
201
|
+
/** List direct children using rg's own ordered glob rules (including ignored files). */
|
|
202
|
+
function listRgFiles(
|
|
203
|
+
rgPath: string,
|
|
204
|
+
directories: string[],
|
|
205
|
+
globs: string[],
|
|
206
|
+
signal: AbortSignal | undefined,
|
|
207
|
+
): Promise<Set<string>> {
|
|
208
|
+
return new Promise((resolveFiles, reject) => {
|
|
209
|
+
if (signal?.aborted) return reject(new Error("Operation aborted"));
|
|
210
|
+
const args = ["--files", "--null", "--hidden", "--no-ignore", "--follow", "--max-depth=1"];
|
|
211
|
+
for (const glob of globs) args.push("--glob", glob);
|
|
212
|
+
args.push("--", ...directories);
|
|
213
|
+
const child = spawn(rgPath, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
214
|
+
const chunks: Buffer[] = [];
|
|
215
|
+
let stderr = "";
|
|
216
|
+
const onAbort = () => child.kill();
|
|
217
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
218
|
+
child.stdout.on("data", (chunk: Buffer) => chunks.push(chunk));
|
|
219
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
220
|
+
stderr += chunk.toString();
|
|
221
|
+
});
|
|
222
|
+
child.on("error", (error) => {
|
|
223
|
+
signal?.removeEventListener("abort", onAbort);
|
|
224
|
+
reject(new Error(`Failed to run ripgrep: ${error.message}`));
|
|
225
|
+
});
|
|
226
|
+
child.on("close", (code) => {
|
|
227
|
+
signal?.removeEventListener("abort", onAbort);
|
|
228
|
+
if (signal?.aborted) return reject(new Error("Operation aborted"));
|
|
229
|
+
if (code !== 0 && code !== 1) {
|
|
230
|
+
return reject(new Error(stderr.trim() || `ripgrep exited with code ${code}`));
|
|
231
|
+
}
|
|
232
|
+
const paths = Buffer.concat(chunks).toString("utf-8").split("\0").filter(Boolean);
|
|
233
|
+
resolveFiles(new Set(paths));
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
195
238
|
/** Run ripgrep and stream its JSON lines to the caller until it asks to stop. */
|
|
196
239
|
function runRg(
|
|
197
240
|
rgPath: string,
|
|
@@ -258,6 +301,10 @@ function countLeading(s: string): number {
|
|
|
258
301
|
function toDisplayLines(raw: string, theme: any): string[] {
|
|
259
302
|
const out: string[] = [];
|
|
260
303
|
const lines = raw.split("\n");
|
|
304
|
+
const lineNoWidth = lines.reduce(
|
|
305
|
+
(width, line) => Math.max(width, parseHashline(line)?.lineNo.length ?? 0),
|
|
306
|
+
0,
|
|
307
|
+
);
|
|
261
308
|
let i = 0;
|
|
262
309
|
while (i < lines.length) {
|
|
263
310
|
const line = lines[i];
|
|
@@ -278,7 +325,7 @@ function toDisplayLines(raw: string, theme: any): string[] {
|
|
|
278
325
|
const marker = base > 0 ? theme.fg("dim", "›") + " " : "";
|
|
279
326
|
for (const g of group) {
|
|
280
327
|
const body = g.content.slice(base);
|
|
281
|
-
out.push(theme.fg("dim", ` ${g.lineNo}: `) + marker + theme.fg("toolOutput", body));
|
|
328
|
+
out.push(theme.fg("dim", ` ${g.lineNo.padStart(lineNoWidth)}: `) + marker + theme.fg("toolOutput", body));
|
|
282
329
|
}
|
|
283
330
|
i = j;
|
|
284
331
|
continue;
|
|
@@ -300,6 +347,7 @@ export function makeGrepOverrideWithBackend(cwd: string, overrides: Partial<Grep
|
|
|
300
347
|
let builtin: ReturnType<typeof createGrepTool> | undefined;
|
|
301
348
|
const backend: GrepBackend = {
|
|
302
349
|
findRg,
|
|
350
|
+
listFiles: listRgFiles,
|
|
303
351
|
runRg,
|
|
304
352
|
delegate(toolCallId, params, signal, onUpdate) {
|
|
305
353
|
builtin ??= createGrepTool(cwd);
|
|
@@ -393,10 +441,19 @@ export function makeGrepOverrideWithBackend(cwd: string, overrides: Partial<Grep
|
|
|
393
441
|
!Array.isArray(params.glob) &&
|
|
394
442
|
!Array.isArray(params.path);
|
|
395
443
|
|
|
444
|
+
const globs = toArray(params.glob);
|
|
396
445
|
const rgPath = await backend.findRg();
|
|
397
|
-
//
|
|
446
|
+
// The built-in grep also lets explicit files bypass globs.
|
|
398
447
|
if (!rgPath) {
|
|
399
|
-
if (legacyShaped)
|
|
448
|
+
if (legacyShaped) {
|
|
449
|
+
if (params.glob !== undefined && params.path !== undefined) {
|
|
450
|
+
const path = canonicalPath(cwd, params.path);
|
|
451
|
+
let explicitFile = false;
|
|
452
|
+
try { explicitFile = (await stat(path)).isFile(); } catch {}
|
|
453
|
+
if (explicitFile) throw new Error("ripgrep (rg) not found; cannot apply glob to an explicit file");
|
|
454
|
+
}
|
|
455
|
+
return backend.delegate(toolCallId, delegatedParams, signal, onUpdate);
|
|
456
|
+
}
|
|
400
457
|
throw new Error(
|
|
401
458
|
"ripgrep (rg) not found; extended grep params cannot fall back to the built-in grep. Retry with a simple pattern first, or use bash",
|
|
402
459
|
);
|
|
@@ -405,7 +462,6 @@ export function makeGrepOverrideWithBackend(cwd: string, overrides: Partial<Grep
|
|
|
405
462
|
const excludes = toArray(params.excludePattern);
|
|
406
463
|
const matchMode: "any" | "all" = params.matchMode ?? "any";
|
|
407
464
|
const outputMode: "content" | "files" | "count" = params.outputMode ?? "content";
|
|
408
|
-
const globs = toArray(params.glob);
|
|
409
465
|
const { ignoreCase, literal, wordMatch, limit } = params;
|
|
410
466
|
const searchPaths = (() => {
|
|
411
467
|
const raw = toArray(params.path);
|
|
@@ -413,15 +469,36 @@ export function makeGrepOverrideWithBackend(cwd: string, overrides: Partial<Grep
|
|
|
413
469
|
})();
|
|
414
470
|
const hashLen = state.config.hashLen;
|
|
415
471
|
|
|
416
|
-
//
|
|
417
|
-
|
|
472
|
+
// rg ignores globs for explicit files, so select those with its file walker first.
|
|
473
|
+
const pathInfo: { path: string; isFile: boolean }[] = [];
|
|
474
|
+
const parents = new Set<string>();
|
|
418
475
|
for (const sp of searchPaths) {
|
|
476
|
+
let info;
|
|
419
477
|
try {
|
|
420
|
-
await stat(sp);
|
|
478
|
+
info = await stat(sp);
|
|
421
479
|
} catch {
|
|
422
480
|
throw new Error(`Path not found: ${sp}`);
|
|
423
481
|
}
|
|
482
|
+
const isFile = info.isFile();
|
|
483
|
+
pathInfo.push({ path: sp, isFile });
|
|
484
|
+
if (globs.length && isFile) parents.add(dirname(sp));
|
|
424
485
|
}
|
|
486
|
+
const fileKey = (path: string) => {
|
|
487
|
+
const absolute = resolve(cwd, path);
|
|
488
|
+
return process.platform === "win32" ? absolute.toLowerCase() : absolute;
|
|
489
|
+
};
|
|
490
|
+
const listed = parents.size
|
|
491
|
+
? await backend.listFiles(rgPath, [...parents], globs, signal)
|
|
492
|
+
: new Set<string>();
|
|
493
|
+
const allowed = new Set([...listed].map(fileKey));
|
|
494
|
+
const selectedPaths = pathInfo
|
|
495
|
+
.filter(({ path, isFile }) => !globs.length || !isFile || allowed.has(fileKey(path)))
|
|
496
|
+
.map(({ path }) => path);
|
|
497
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
498
|
+
if (selectedPaths.length === 0) return {
|
|
499
|
+
content: [{ type: "text", text: "No matches found" }],
|
|
500
|
+
details: undefined,
|
|
501
|
+
};
|
|
425
502
|
|
|
426
503
|
// Client-side line filters — only AND / exclude need them; "any" is native rg (-e OR).
|
|
427
504
|
const excludeMatchers = excludes.map((p) =>
|
|
@@ -452,7 +529,7 @@ export function makeGrepOverrideWithBackend(cwd: string, overrides: Partial<Grep
|
|
|
452
529
|
if (wordMatch) args.push("--word-regexp");
|
|
453
530
|
for (const glob of globs) args.push("--glob", glob);
|
|
454
531
|
for (const p of patterns) args.push("-e", p);
|
|
455
|
-
args.push("--", ...
|
|
532
|
+
args.push("--", ...selectedPaths);
|
|
456
533
|
|
|
457
534
|
const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);
|
|
458
535
|
let matchCount = 0;
|
package/src/pi/grep.test.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { getState } from "./state.ts";
|
|
|
14
14
|
|
|
15
15
|
type FakeOptions = {
|
|
16
16
|
lines?: string[];
|
|
17
|
+
files?: string[];
|
|
17
18
|
code?: number | null;
|
|
18
19
|
stderr?: string;
|
|
19
20
|
error?: Error;
|
|
@@ -38,11 +39,16 @@ function rgMatch(filePath: string, lineNumber: number, text: string): string {
|
|
|
38
39
|
|
|
39
40
|
function fakeBackend(options: FakeOptions = {}) {
|
|
40
41
|
const calls: { path: string; args: string[] }[] = [];
|
|
42
|
+
const fileCalls: { directories: string[]; globs: string[] }[] = [];
|
|
41
43
|
const delegates: any[][] = [];
|
|
42
44
|
const backend: GrepBackend = {
|
|
43
45
|
async findRg() {
|
|
44
46
|
return "/fake/rg";
|
|
45
47
|
},
|
|
48
|
+
async listFiles(_path, directories, globs) {
|
|
49
|
+
fileCalls.push({ directories, globs });
|
|
50
|
+
return new Set(options.files ?? []);
|
|
51
|
+
},
|
|
46
52
|
async runRg(path, args, _signal, onLine) {
|
|
47
53
|
calls.push({ path, args });
|
|
48
54
|
options.onRun?.();
|
|
@@ -61,7 +67,7 @@ function fakeBackend(options: FakeOptions = {}) {
|
|
|
61
67
|
return { content: [{ type: "text", text: "delegated" }], details: undefined };
|
|
62
68
|
},
|
|
63
69
|
};
|
|
64
|
-
return { backend, calls, delegates };
|
|
70
|
+
return { backend, calls, fileCalls, delegates };
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
const text = (result: any): string => result.content[0].text;
|
|
@@ -79,6 +85,27 @@ async function withEnabled<T>(enabled: boolean, fn: () => Promise<T>): Promise<T
|
|
|
79
85
|
}
|
|
80
86
|
}
|
|
81
87
|
|
|
88
|
+
test("grep TUI aligns line-number colons across file groups", () => {
|
|
89
|
+
const raw = [
|
|
90
|
+
"a.ts · 2 matches",
|
|
91
|
+
"99#ABCD│ alpha",
|
|
92
|
+
"100#ABCD│ beta",
|
|
93
|
+
"b.ts · 1 match",
|
|
94
|
+
"7#ABCD│gamma",
|
|
95
|
+
].join("\n");
|
|
96
|
+
const tool = makeGrepOverrideWithBackend(".", {});
|
|
97
|
+
const theme = { fg: (_color: string, value: string) => value };
|
|
98
|
+
const result = { content: [{ type: "text", text: raw }] };
|
|
99
|
+
const rendered = tool.renderResult(result, { isPartial: false, expanded: true }, theme, {}).render(80);
|
|
100
|
+
assert.deepEqual(rendered.map((line) => line.trimEnd()), [
|
|
101
|
+
"a.ts · 2 matches",
|
|
102
|
+
" 99: › alpha",
|
|
103
|
+
" 100: › beta",
|
|
104
|
+
"b.ts · 1 match",
|
|
105
|
+
" 7: gamma",
|
|
106
|
+
]);
|
|
107
|
+
});
|
|
108
|
+
|
|
82
109
|
test("formats parsed rg matches with full-line hash anchors", async () => {
|
|
83
110
|
await withDir(async (dir) =>
|
|
84
111
|
withEnabled(true, async () => {
|
|
@@ -217,7 +244,7 @@ test("passes output flags and formats files and counts", async () => {
|
|
|
217
244
|
const b = join(dir, "b.ts");
|
|
218
245
|
await writeFile(a, "Foo a.b\n");
|
|
219
246
|
await writeFile(b, "foo a.b\n");
|
|
220
|
-
const fake = fakeBackend({ lines: [rgMatch(a, 1, "Foo a.b\n"), rgMatch(b, 1, "foo a.b\n")] });
|
|
247
|
+
const fake = fakeBackend({ files: [a, b], lines: [rgMatch(a, 1, "Foo a.b\n"), rgMatch(b, 1, "foo a.b\n")] });
|
|
221
248
|
|
|
222
249
|
const tool = makeGrepOverrideWithBackend(dir, fake.backend);
|
|
223
250
|
const globSchema: any = tool.parameters.properties.glob;
|
|
@@ -262,6 +289,30 @@ test("passes output flags and formats files and counts", async () => {
|
|
|
262
289
|
);
|
|
263
290
|
});
|
|
264
291
|
|
|
292
|
+
test("glob filters explicit files and batches their parent directories", async () => {
|
|
293
|
+
await withDir(async (dir) => {
|
|
294
|
+
const kept = join(dir, "kept.ts");
|
|
295
|
+
const excluded = join(dir, "excluded.test.ts");
|
|
296
|
+
const nested = join(dir, "sub", "other.ts");
|
|
297
|
+
await mkdir(join(dir, "sub"));
|
|
298
|
+
await writeFile(nested, "needle\n");
|
|
299
|
+
await writeFile(kept, "needle\n");
|
|
300
|
+
await writeFile(excluded, "needle\n");
|
|
301
|
+
const fake = fakeBackend({
|
|
302
|
+
files: [kept, nested],
|
|
303
|
+
lines: [rgMatch(kept, 1, "needle\n"), rgMatch(nested, 1, "needle\n")],
|
|
304
|
+
});
|
|
305
|
+
const tool = makeGrepOverrideWithBackend(dir, fake.backend);
|
|
306
|
+
const query = { pattern: "needle", path: ["kept.ts", "excluded.test.ts", "sub/other.ts"], glob: ["*.ts", "!**/*.test.ts"] };
|
|
307
|
+
assert.match(text(await call(tool, query)), /│needle/);
|
|
308
|
+
assert.deepEqual(fake.fileCalls, [{ directories: [dir, join(dir, "sub")], globs: query.glob }]);
|
|
309
|
+
assert.deepEqual(fake.calls[0].args.slice(-3), ["--", kept, nested]);
|
|
310
|
+
|
|
311
|
+
assert.equal(text(await call(tool, { ...query, path: "excluded.test.ts" })), "No matches found");
|
|
312
|
+
assert.equal(fake.calls.length, 1);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
265
316
|
test("counts only surviving matches toward the limit and stops the fake runner", async () => {
|
|
266
317
|
await withDir(async (dir) =>
|
|
267
318
|
withEnabled(true, async () => {
|
|
@@ -348,6 +399,16 @@ test("delegates only safe fallbacks and rejects extended missing-rg requests", a
|
|
|
348
399
|
call(tool, { pattern: "x", glob: ["*.ts", "!**/*.test.ts"] }),
|
|
349
400
|
/ripgrep \(rg\) not found/,
|
|
350
401
|
);
|
|
402
|
+
const file = join(dir, "fixture.ts");
|
|
403
|
+
await writeFile(file, "x\n");
|
|
404
|
+
const delegatedBefore = absent.delegates.length;
|
|
405
|
+
await assert.rejects(
|
|
406
|
+
call(tool, { pattern: "x", path: file, glob: "*.ts" }),
|
|
407
|
+
/cannot apply glob to an explicit file/,
|
|
408
|
+
);
|
|
409
|
+
assert.equal(absent.delegates.length, delegatedBefore);
|
|
410
|
+
assert.equal(text(await call(tool, { pattern: "x", path: file })), "delegated");
|
|
411
|
+
assert.equal(text(await call(tool, { pattern: "x", path: dir, glob: "*.ts" })), "delegated");
|
|
351
412
|
});
|
|
352
413
|
});
|
|
353
414
|
});
|