@xynogen/pix-pretty 1.3.3 → 1.5.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/package.json +1 -1
- package/src/fff.ts +1 -262
- package/src/index.ts +1 -10
- package/src/paste-chips.test.ts +28 -32
- package/src/paste-chips.ts +29 -7
- package/src/tools/bash.test.ts +0 -5
- package/src/tools/context.ts +1 -3
- package/src/types.ts +0 -38
- package/src/utils.ts +0 -31
- package/src/tools/multi-grep.ts +0 -311
package/package.json
CHANGED
package/src/fff.ts
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { join } from "node:path";
|
|
3
2
|
|
|
4
3
|
import type { GrepCursor, GrepMatch } from "@ff-labs/fff-node";
|
|
5
4
|
|
|
6
|
-
import type {
|
|
7
|
-
ConstraintParseResult,
|
|
8
|
-
FffBackedFinder,
|
|
9
|
-
MultiGrepRipgrepFallbackParams,
|
|
10
|
-
MultiGrepRipgrepFallbackResult,
|
|
11
|
-
OptionalFffModule,
|
|
12
|
-
} from "./types.js";
|
|
5
|
+
import type { FffBackedFinder, OptionalFffModule } from "./types.js";
|
|
13
6
|
|
|
14
7
|
export interface FffState {
|
|
15
8
|
module: OptionalFffModule | null;
|
|
@@ -159,257 +152,3 @@ export function fffFormatGrepText(items: GrepMatch[], limit: number): string {
|
|
|
159
152
|
|
|
160
153
|
return lines.join("\n");
|
|
161
154
|
}
|
|
162
|
-
|
|
163
|
-
// ---------------------------------------------------------------------------
|
|
164
|
-
// Multi-grep ripgrep fallback
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
|
|
167
|
-
const GLOB_META_RE = /[*?[{]/;
|
|
168
|
-
|
|
169
|
-
function trimSlashes(value: string): string {
|
|
170
|
-
return value.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function normalizeConstraintPath(value: string): string {
|
|
174
|
-
let normalized = value.replace(/\\/g, "/").trim();
|
|
175
|
-
while (normalized.startsWith("./")) normalized = normalized.slice(2);
|
|
176
|
-
return normalized;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function tokenizeConstraints(constraints: string): ConstraintParseResult {
|
|
180
|
-
const tokens: string[] = [];
|
|
181
|
-
let current = "";
|
|
182
|
-
let quote: '"' | "'" | null = null;
|
|
183
|
-
|
|
184
|
-
for (let i = 0; i < constraints.length; i++) {
|
|
185
|
-
const char = constraints[i];
|
|
186
|
-
if (quote) {
|
|
187
|
-
if (char === quote) quote = null;
|
|
188
|
-
else current += char;
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (char === '"' || char === "'") {
|
|
193
|
-
quote = char;
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (/\s/.test(char)) {
|
|
198
|
-
if (current) {
|
|
199
|
-
tokens.push(current);
|
|
200
|
-
current = "";
|
|
201
|
-
}
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
current += char;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (quote) return { ok: false, error: "unterminated quoted constraint" };
|
|
209
|
-
if (current) tokens.push(current);
|
|
210
|
-
|
|
211
|
-
return { ok: true, globs: [], tokens };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function tokenToRipgrepGlob(
|
|
215
|
-
token: string,
|
|
216
|
-
): { ok: true; glob: string } | { ok: false; error: string } {
|
|
217
|
-
let negated = false;
|
|
218
|
-
let body = token;
|
|
219
|
-
|
|
220
|
-
if (body.startsWith("!")) {
|
|
221
|
-
negated = true;
|
|
222
|
-
body = body.slice(1);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
body = normalizeConstraintPath(body);
|
|
226
|
-
if (!body) return { ok: false, error: `empty constraint token: ${token}` };
|
|
227
|
-
if (body.includes("\0"))
|
|
228
|
-
return {
|
|
229
|
-
ok: false,
|
|
230
|
-
error: `invalid NUL byte in constraint token: ${token}`,
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
let glob: string;
|
|
234
|
-
if (body.endsWith("/")) {
|
|
235
|
-
const dir = trimSlashes(body);
|
|
236
|
-
if (!dir)
|
|
237
|
-
return { ok: false, error: `empty directory constraint: ${token}` };
|
|
238
|
-
glob = `**/${dir}/**`;
|
|
239
|
-
} else if (GLOB_META_RE.test(body) || body.includes("/")) {
|
|
240
|
-
glob = body.replace(/^\/+/, "");
|
|
241
|
-
} else if (body.includes(".")) {
|
|
242
|
-
glob = `**/${body}`;
|
|
243
|
-
} else {
|
|
244
|
-
glob = `**/${body}/**`;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return { ok: true, glob: negated ? `!${glob}` : glob };
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export function parseMultiGrepConstraints(
|
|
251
|
-
constraints: string | undefined,
|
|
252
|
-
): ConstraintParseResult {
|
|
253
|
-
const trimmed = constraints?.trim();
|
|
254
|
-
if (!trimmed) return { ok: true, globs: [], tokens: [] };
|
|
255
|
-
|
|
256
|
-
const tokenized = tokenizeConstraints(trimmed);
|
|
257
|
-
if (!tokenized.ok) return tokenized;
|
|
258
|
-
|
|
259
|
-
const globs: string[] = [];
|
|
260
|
-
for (const token of tokenized.tokens) {
|
|
261
|
-
const parsed = tokenToRipgrepGlob(token);
|
|
262
|
-
if (!parsed.ok) return parsed;
|
|
263
|
-
globs.push(parsed.glob);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return { ok: true, globs, tokens: tokenized.tokens };
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function isRipgrepMatchLine(line: string): boolean {
|
|
270
|
-
return /^.+?:\d+:/.test(line);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function buildRipgrepArgs(
|
|
274
|
-
params: MultiGrepRipgrepFallbackParams,
|
|
275
|
-
globs: string[],
|
|
276
|
-
): string[] {
|
|
277
|
-
const args = [
|
|
278
|
-
"--line-number",
|
|
279
|
-
"--with-filename",
|
|
280
|
-
"--color=never",
|
|
281
|
-
"--hidden",
|
|
282
|
-
"--fixed-strings",
|
|
283
|
-
];
|
|
284
|
-
|
|
285
|
-
if (params.ignoreCase) args.push("--ignore-case");
|
|
286
|
-
if (params.context && params.context > 0)
|
|
287
|
-
args.push("--context", String(params.context));
|
|
288
|
-
|
|
289
|
-
for (const glob of globs) args.push("--glob", glob);
|
|
290
|
-
for (const pattern of params.patterns) args.push("-e", pattern);
|
|
291
|
-
|
|
292
|
-
const searchPath = params.path?.trim();
|
|
293
|
-
if (searchPath) args.push("--", searchPath);
|
|
294
|
-
|
|
295
|
-
return args;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
export function getMultiGrepRipgrepArgs(
|
|
299
|
-
params: MultiGrepRipgrepFallbackParams,
|
|
300
|
-
): ConstraintParseResult & { args?: string[] } {
|
|
301
|
-
const parsed = parseMultiGrepConstraints(params.constraints);
|
|
302
|
-
if (!parsed.ok) return parsed;
|
|
303
|
-
return { ...parsed, args: buildRipgrepArgs(params, parsed.globs) };
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
export async function runMultiGrepRipgrepFallback(
|
|
307
|
-
params: MultiGrepRipgrepFallbackParams,
|
|
308
|
-
): Promise<MultiGrepRipgrepFallbackResult> {
|
|
309
|
-
const parsed = parseMultiGrepConstraints(params.constraints);
|
|
310
|
-
if (!parsed.ok) throw new Error(`unsupported constraints: ${parsed.error}`);
|
|
311
|
-
|
|
312
|
-
const args = buildRipgrepArgs(params, parsed.globs);
|
|
313
|
-
|
|
314
|
-
return new Promise((resolve, reject) => {
|
|
315
|
-
if (params.signal?.aborted) {
|
|
316
|
-
reject(new Error("Operation aborted"));
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const child = spawn("rg", args, {
|
|
321
|
-
cwd: params.cwd,
|
|
322
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
323
|
-
});
|
|
324
|
-
const outputLines: string[] = [];
|
|
325
|
-
let stderr = "";
|
|
326
|
-
let buffer = "";
|
|
327
|
-
let matchCount = 0;
|
|
328
|
-
let limitReached = false;
|
|
329
|
-
let killedForLimit = false;
|
|
330
|
-
let settled = false;
|
|
331
|
-
|
|
332
|
-
const settle = (fn: () => void): void => {
|
|
333
|
-
if (settled) return;
|
|
334
|
-
settled = true;
|
|
335
|
-
fn();
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
const stopChild = (dueToLimit = false): void => {
|
|
339
|
-
if (!child.killed) {
|
|
340
|
-
killedForLimit = dueToLimit;
|
|
341
|
-
child.kill();
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
const onAbort = (): void => stopChild(false);
|
|
346
|
-
params.signal?.addEventListener("abort", onAbort, { once: true });
|
|
347
|
-
|
|
348
|
-
const cleanup = (): void => {
|
|
349
|
-
params.signal?.removeEventListener("abort", onAbort);
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
const handleLine = (line: string): void => {
|
|
353
|
-
if (limitReached) return;
|
|
354
|
-
outputLines.push(line);
|
|
355
|
-
if (isRipgrepMatchLine(line)) {
|
|
356
|
-
matchCount++;
|
|
357
|
-
if (matchCount >= params.limit) {
|
|
358
|
-
limitReached = true;
|
|
359
|
-
stopChild(true);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
child.stdout?.on("data", (chunk: Buffer) => {
|
|
365
|
-
buffer += chunk.toString("utf8");
|
|
366
|
-
let newlineIndex = buffer.indexOf("\n");
|
|
367
|
-
while (newlineIndex >= 0) {
|
|
368
|
-
const line = buffer.slice(0, newlineIndex).replace(/\r$/, "");
|
|
369
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
370
|
-
handleLine(line);
|
|
371
|
-
newlineIndex = buffer.indexOf("\n");
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
child.stderr?.on("data", (chunk: Buffer) => {
|
|
376
|
-
stderr += chunk.toString("utf8");
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
child.on("error", (error: NodeJS.ErrnoException) => {
|
|
380
|
-
cleanup();
|
|
381
|
-
const message =
|
|
382
|
-
error.code === "ENOENT"
|
|
383
|
-
? "ripgrep (rg) is not available"
|
|
384
|
-
: `Failed to run ripgrep: ${error.message}`;
|
|
385
|
-
settle(() => reject(new Error(message)));
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
child.on("close", (code) => {
|
|
389
|
-
cleanup();
|
|
390
|
-
|
|
391
|
-
if (params.signal?.aborted) {
|
|
392
|
-
settle(() => reject(new Error("Operation aborted")));
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (buffer && !limitReached) handleLine(buffer.replace(/\r$/, ""));
|
|
397
|
-
|
|
398
|
-
if (!killedForLimit && code !== 0 && code !== 1) {
|
|
399
|
-
const message = stderr.trim() || `ripgrep exited with code ${code}`;
|
|
400
|
-
settle(() => reject(new Error(message)));
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
settle(() =>
|
|
405
|
-
resolve({
|
|
406
|
-
text: outputLines.length
|
|
407
|
-
? outputLines.join("\n")
|
|
408
|
-
: "No matches found",
|
|
409
|
-
matchCount,
|
|
410
|
-
limitReached,
|
|
411
|
-
}),
|
|
412
|
-
);
|
|
413
|
-
});
|
|
414
|
-
});
|
|
415
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* fff.ts Fast File Finder + cursor store + multi-grep fallback
|
|
17
17
|
* diff.ts unified diff parser
|
|
18
18
|
* diff-render.ts split/word-level diff renderer
|
|
19
|
-
* tools/ per-tool registrars (read/bash/ls/find/grep/
|
|
19
|
+
* tools/ per-tool registrars (read/bash/ls/find/grep/edit/write)
|
|
20
20
|
* commands/ slash command registrars (fff)
|
|
21
21
|
*/
|
|
22
22
|
|
|
@@ -40,7 +40,6 @@ import {
|
|
|
40
40
|
fffEnsureFinder,
|
|
41
41
|
fffState,
|
|
42
42
|
getPiPrettyFffDir,
|
|
43
|
-
runMultiGrepRipgrepFallback,
|
|
44
43
|
} from "./fff.js";
|
|
45
44
|
import { clearHighlightCache } from "./highlight.js";
|
|
46
45
|
import { registerBashTool } from "./tools/bash.js";
|
|
@@ -49,7 +48,6 @@ import { registerEditTool } from "./tools/edit.js";
|
|
|
49
48
|
import { registerFindTool } from "./tools/find.js";
|
|
50
49
|
import { registerGrepTool } from "./tools/grep.js";
|
|
51
50
|
import { registerLsTool } from "./tools/ls.js";
|
|
52
|
-
import { registerMultiGrepTool } from "./tools/multi-grep.js";
|
|
53
51
|
import { registerReadTool } from "./tools/read.js";
|
|
54
52
|
import { registerWriteTool } from "./tools/write.js";
|
|
55
53
|
import type {
|
|
@@ -134,9 +132,6 @@ export default function piPrettyExtension(
|
|
|
134
132
|
const cwd = process.cwd();
|
|
135
133
|
const home = process.env.HOME ?? "";
|
|
136
134
|
const sp = (p: string) => shortPath(cwd, home, p);
|
|
137
|
-
const multiGrepRipgrepFallback =
|
|
138
|
-
deps?.multiGrepRipgrepFallback ?? runMultiGrepRipgrepFallback;
|
|
139
|
-
|
|
140
135
|
// Respect PRETTY_DISABLE_TOOLS env var
|
|
141
136
|
const disabledTools = new Set(
|
|
142
137
|
(process.env.PRETTY_DISABLE_TOOLS ?? "")
|
|
@@ -224,7 +219,6 @@ export default function piPrettyExtension(
|
|
|
224
219
|
TextComponent: TextComponent!,
|
|
225
220
|
fffState,
|
|
226
221
|
cursorStore,
|
|
227
|
-
multiGrepRipgrepFallback,
|
|
228
222
|
};
|
|
229
223
|
|
|
230
224
|
// ── Register tools ──────────────────────────────────────────────────
|
|
@@ -244,9 +238,6 @@ export default function piPrettyExtension(
|
|
|
244
238
|
if (isToolEnabled("grep") && createGrepTool) {
|
|
245
239
|
registerGrepTool(pi, createGrepTool, toolCtx);
|
|
246
240
|
}
|
|
247
|
-
if (isToolEnabled("multi_grep") && (fffState.module || createGrepTool)) {
|
|
248
|
-
registerMultiGrepTool(pi, createGrepTool ?? null, toolCtx);
|
|
249
|
-
}
|
|
250
241
|
if (isToolEnabled("edit") && createEditTool) {
|
|
251
242
|
registerEditTool(pi, createEditTool, toolCtx, trackInvalidator);
|
|
252
243
|
}
|
package/src/paste-chips.test.ts
CHANGED
|
@@ -10,37 +10,41 @@ import { describe, expect, it } from "bun:test";
|
|
|
10
10
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
11
11
|
import { restyleMarkers } from "./paste-chips";
|
|
12
12
|
|
|
13
|
+
const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
14
|
+
|
|
13
15
|
// ─── restyleMarkers ──────────────────────────────────────────────────────────
|
|
14
16
|
|
|
15
17
|
describe("paste-chips restyleMarkers", () => {
|
|
16
18
|
describe("text markers", () => {
|
|
17
|
-
it("restyles chars marker to text
|
|
19
|
+
it("restyles chars marker to colored icon text chip", () => {
|
|
18
20
|
const result = restyleMarkers("[paste #1 2232 chars]", new Set());
|
|
19
|
-
expect(result).
|
|
21
|
+
expect(result).toContain("\x1b[");
|
|
22
|
+
expect(stripAnsi(result)).toBe(" text 2.2k chars");
|
|
20
23
|
});
|
|
21
24
|
|
|
22
|
-
it("restyles lines marker to text
|
|
25
|
+
it("restyles lines marker to colored icon text chip", () => {
|
|
23
26
|
const result = restyleMarkers("[paste #2 +42 lines]", new Set());
|
|
24
|
-
expect(result).toBe("
|
|
27
|
+
expect(stripAnsi(result)).toBe(" text 42 lines");
|
|
25
28
|
});
|
|
26
29
|
|
|
27
|
-
it("restyles bare marker (no size info) to text
|
|
30
|
+
it("restyles bare marker (no size info) to text chip with id", () => {
|
|
28
31
|
const result = restyleMarkers("[paste #3]", new Set());
|
|
29
|
-
expect(result).toBe("
|
|
32
|
+
expect(stripAnsi(result)).toBe(" text #3");
|
|
30
33
|
});
|
|
31
34
|
});
|
|
32
35
|
|
|
33
36
|
describe("image markers", () => {
|
|
34
|
-
it("restyles marker to image
|
|
37
|
+
it("restyles marker to colored icon image chip when ID is in imageIds", () => {
|
|
35
38
|
const imageIds = new Set([1]);
|
|
36
39
|
const result = restyleMarkers("[paste #1 58 chars]", imageIds);
|
|
37
|
-
expect(result).
|
|
40
|
+
expect(result).toContain("\x1b[");
|
|
41
|
+
expect(stripAnsi(result)).toBe(" image #1");
|
|
38
42
|
});
|
|
39
43
|
|
|
40
44
|
it("does not restyle non-image ID as image", () => {
|
|
41
45
|
const imageIds = new Set([1]);
|
|
42
46
|
const result = restyleMarkers("[paste #2 100 chars]", imageIds);
|
|
43
|
-
expect(result).toBe("
|
|
47
|
+
expect(stripAnsi(result)).toBe(" text 100 chars");
|
|
44
48
|
});
|
|
45
49
|
});
|
|
46
50
|
|
|
@@ -50,8 +54,8 @@ describe("paste-chips restyleMarkers", () => {
|
|
|
50
54
|
const line =
|
|
51
55
|
"before [paste #1 58 chars] middle [paste #2 +10 lines] after";
|
|
52
56
|
const result = restyleMarkers(line, imageIds);
|
|
53
|
-
expect(result).toBe(
|
|
54
|
-
"before
|
|
57
|
+
expect(stripAnsi(result)).toBe(
|
|
58
|
+
"before image #1 middle text 10 lines after",
|
|
55
59
|
);
|
|
56
60
|
});
|
|
57
61
|
});
|
|
@@ -71,7 +75,7 @@ describe("paste-chips restyleMarkers", () => {
|
|
|
71
75
|
it("restyles markers embedded in ANSI sequences", () => {
|
|
72
76
|
const line = "\x1b[38;2;84;92;126m[paste #1 500 chars]\x1b[0m";
|
|
73
77
|
const result = restyleMarkers(line, new Set());
|
|
74
|
-
expect(result).toBe("
|
|
78
|
+
expect(stripAnsi(result)).toBe(" text 500 chars");
|
|
75
79
|
});
|
|
76
80
|
});
|
|
77
81
|
});
|
|
@@ -79,11 +83,10 @@ describe("paste-chips restyleMarkers", () => {
|
|
|
79
83
|
// ─── Width safety (regression for crash) ─────────────────────────────────────
|
|
80
84
|
|
|
81
85
|
describe("paste-chips width safety", () => {
|
|
82
|
-
it("restyling a chars marker can
|
|
83
|
-
// This is the core issue: "#1" (2 chars) → "text" (4 chars) = +2 width
|
|
86
|
+
it("restyling a chars marker can decrease visible width", () => {
|
|
84
87
|
const original = "[paste #1 2232 chars]";
|
|
85
88
|
const restyled = restyleMarkers(original, new Set());
|
|
86
|
-
expect(visibleWidth(restyled)).
|
|
89
|
+
expect(visibleWidth(restyled)).toBeLessThan(visibleWidth(original));
|
|
87
90
|
});
|
|
88
91
|
|
|
89
92
|
it("restyling a lines marker can decrease visible width", () => {
|
|
@@ -101,38 +104,31 @@ describe("paste-chips width safety", () => {
|
|
|
101
104
|
expect(visibleWidth(restyled)).not.toBe(visibleWidth(original));
|
|
102
105
|
});
|
|
103
106
|
|
|
104
|
-
it("chars marker with large char count
|
|
105
|
-
// "[paste #N CCCC chars]" → "[paste text CCCC chars]"
|
|
106
|
-
// "#N" (2 chars for single digit) → "text" (4 chars) = exactly +2
|
|
107
|
+
it("chars marker with large char count compacts metadata", () => {
|
|
107
108
|
const original = "[paste #1 9999 chars]";
|
|
108
109
|
const restyled = restyleMarkers(original, new Set());
|
|
109
|
-
expect(
|
|
110
|
+
expect(stripAnsi(restyled)).toBe(" text 10k chars");
|
|
111
|
+
expect(visibleWidth(restyled)).toBeLessThan(visibleWidth(original));
|
|
110
112
|
});
|
|
111
113
|
|
|
112
|
-
it("chars marker with multi-digit ID
|
|
113
|
-
// "#10" (3 chars) → "text" (4 chars) = +1
|
|
114
|
+
it("chars marker with multi-digit ID omits id from text chip", () => {
|
|
114
115
|
const original = "[paste #10 9999 chars]";
|
|
115
116
|
const restyled = restyleMarkers(original, new Set());
|
|
116
|
-
expect(
|
|
117
|
+
expect(stripAnsi(restyled)).toBe(" text 10k chars");
|
|
118
|
+
expect(visibleWidth(restyled)).toBeLessThan(visibleWidth(original));
|
|
117
119
|
});
|
|
118
120
|
|
|
119
|
-
describe("
|
|
120
|
-
it("
|
|
121
|
-
// Simulate the crash: a line at exactly terminal width (283)
|
|
122
|
-
// containing a marker that widens by 2 after restyling.
|
|
121
|
+
describe("width behavior", () => {
|
|
122
|
+
it("icon chip stays within terminal width when marker line fits", () => {
|
|
123
123
|
const terminalWidth = 283;
|
|
124
|
-
const marker = "[paste #1 2232 chars]";
|
|
124
|
+
const marker = "[paste #1 2232 chars]";
|
|
125
125
|
const padding = " ".repeat(terminalWidth - visibleWidth(marker));
|
|
126
126
|
const line = marker + padding;
|
|
127
127
|
|
|
128
|
-
// Verify the original line fits
|
|
129
128
|
expect(visibleWidth(line)).toBe(terminalWidth);
|
|
130
129
|
|
|
131
|
-
// Restyle it — this WOULD exceed width without the fix
|
|
132
130
|
const restyled = restyleMarkers(line, new Set());
|
|
133
|
-
expect(visibleWidth(restyled)).
|
|
134
|
-
// Specifically: "[paste text 2232 chars]" is 23 chars, +2 over original 21
|
|
135
|
-
expect(visibleWidth(restyled)).toBe(terminalWidth + 2);
|
|
131
|
+
expect(visibleWidth(restyled)).toBeLessThanOrEqual(terminalWidth);
|
|
136
132
|
});
|
|
137
133
|
});
|
|
138
134
|
});
|
package/src/paste-chips.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import type { EditorTheme, TUI } from "@earendil-works/pi-tui";
|
|
22
22
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
23
|
+
import { BOLD, FG_BLUE, FG_DIM, FG_GREEN, RST } from "./ansi.js";
|
|
23
24
|
|
|
24
25
|
// Upstream stopped re-exporting `EditorFactory` from the package entry point,
|
|
25
26
|
// so we reconstruct its signature locally from the still-exported primitives.
|
|
@@ -100,8 +101,8 @@ function replaceImagePaths(
|
|
|
100
101
|
|
|
101
102
|
/**
|
|
102
103
|
* Re-style every paste marker in a rendered line:
|
|
103
|
-
* image →
|
|
104
|
-
* text →
|
|
104
|
+
* image → ` image #N` with blue icon/label
|
|
105
|
+
* text → ` text Lines lines` or ` text Chars chars` with green icon/label
|
|
105
106
|
*
|
|
106
107
|
* Width-preserving is not required — Pi re-wraps each render call.
|
|
107
108
|
*/
|
|
@@ -111,19 +112,36 @@ export function restyleMarkers(line: string, imageIds: Set<number>): string {
|
|
|
111
112
|
(_full, idStr, _g2, _g3, linesStr, charsStr) => {
|
|
112
113
|
const id = Number.parseInt(idStr, 10);
|
|
113
114
|
if (imageIds.has(id)) {
|
|
114
|
-
return
|
|
115
|
+
return chip(FG_BLUE, "", "image", `#${id}`);
|
|
115
116
|
}
|
|
116
117
|
if (linesStr) {
|
|
117
|
-
return
|
|
118
|
+
return chip(FG_GREEN, "", "text", `${linesStr} lines`);
|
|
118
119
|
}
|
|
119
120
|
if (charsStr) {
|
|
120
|
-
return
|
|
121
|
+
return chip(FG_GREEN, "", "text", `${compactNumber(charsStr)} chars`);
|
|
121
122
|
}
|
|
122
|
-
return
|
|
123
|
+
return chip(FG_GREEN, "", "text", `#${id}`);
|
|
123
124
|
},
|
|
124
125
|
);
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
function chip(
|
|
129
|
+
color: string,
|
|
130
|
+
icon: string,
|
|
131
|
+
label: string,
|
|
132
|
+
meta: string,
|
|
133
|
+
): string {
|
|
134
|
+
return `${color}${BOLD}${icon} ${label}${RST}${FG_DIM} ${meta}${RST}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function compactNumber(raw: string): string {
|
|
138
|
+
const n = Number.parseInt(raw, 10);
|
|
139
|
+
if (!Number.isFinite(n)) return raw;
|
|
140
|
+
if (n < 1_000) return `${n}`;
|
|
141
|
+
if (n < 1_000_000) return `${(n / 1_000).toFixed(1).replace(/\.0$/, "")}k`;
|
|
142
|
+
return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, "")}m`;
|
|
143
|
+
}
|
|
144
|
+
|
|
127
145
|
// ─── Custom editor ────────────────────────────────────────────────────────────
|
|
128
146
|
|
|
129
147
|
class ChipEditor extends CustomEditor {
|
|
@@ -131,7 +149,11 @@ class ChipEditor extends CustomEditor {
|
|
|
131
149
|
|
|
132
150
|
override insertTextAtCursor(text: string): void {
|
|
133
151
|
const internals = this as unknown as EditorInternals;
|
|
134
|
-
|
|
152
|
+
const replaced = replaceImagePaths(text, internals, this.imageIds);
|
|
153
|
+
// Append a trailing space when the insertion ends with a paste marker so
|
|
154
|
+
// the cursor lands after the chip rather than inside it.
|
|
155
|
+
const needsSpace = /\[paste #\d+[^\]]*\]$/.test(replaced);
|
|
156
|
+
super.insertTextAtCursor(needsSpace ? `${replaced} ` : replaced);
|
|
135
157
|
}
|
|
136
158
|
|
|
137
159
|
override render(width: number): string[] {
|
package/src/tools/bash.test.ts
CHANGED
package/src/tools/context.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CursorStore, FffState } from "../fff.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { TextComponentCtor } from "../types.js";
|
|
3
3
|
|
|
4
4
|
// ── Shared context passed to each tool registrar ───────────────────────
|
|
5
5
|
|
|
@@ -14,6 +14,4 @@ export interface ToolContext {
|
|
|
14
14
|
fffState: FffState;
|
|
15
15
|
/** FFF cursor store */
|
|
16
16
|
cursorStore: CursorStore;
|
|
17
|
-
/** Ripgrep fallback for multi_grep */
|
|
18
|
-
multiGrepRipgrepFallback: MultiGrepRipgrepFallback;
|
|
19
17
|
}
|
package/src/types.ts
CHANGED
|
@@ -20,35 +20,6 @@ export type BundledLanguage = string;
|
|
|
20
20
|
|
|
21
21
|
export type BundledTheme = string;
|
|
22
22
|
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Multi-grep ripgrep fallback contracts
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
export type ConstraintParseResult =
|
|
28
|
-
| { ok: true; globs: string[]; tokens: string[] }
|
|
29
|
-
| { ok: false; error: string };
|
|
30
|
-
|
|
31
|
-
export type MultiGrepRipgrepFallbackParams = {
|
|
32
|
-
cwd: string;
|
|
33
|
-
patterns: string[];
|
|
34
|
-
path?: string;
|
|
35
|
-
constraints?: string;
|
|
36
|
-
context?: number;
|
|
37
|
-
limit: number;
|
|
38
|
-
ignoreCase: boolean;
|
|
39
|
-
signal?: AbortSignal;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export type MultiGrepRipgrepFallbackResult = {
|
|
43
|
-
text: string;
|
|
44
|
-
matchCount: number;
|
|
45
|
-
limitReached: boolean;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export type MultiGrepRipgrepFallback = (
|
|
49
|
-
params: MultiGrepRipgrepFallbackParams,
|
|
50
|
-
) => Promise<MultiGrepRipgrepFallbackResult>;
|
|
51
|
-
|
|
52
23
|
// ---------------------------------------------------------------------------
|
|
53
24
|
// Config
|
|
54
25
|
// ---------------------------------------------------------------------------
|
|
@@ -203,14 +174,6 @@ export type FindParams = FindToolInput;
|
|
|
203
174
|
|
|
204
175
|
export type GrepParams = GrepToolInput;
|
|
205
176
|
|
|
206
|
-
export type MultiGrepParams = {
|
|
207
|
-
patterns: string[];
|
|
208
|
-
path?: string;
|
|
209
|
-
constraints?: string;
|
|
210
|
-
context?: number;
|
|
211
|
-
limit?: number;
|
|
212
|
-
};
|
|
213
|
-
|
|
214
177
|
export type EditRenderState = {
|
|
215
178
|
_pk?: string;
|
|
216
179
|
_pt?: string;
|
|
@@ -322,5 +285,4 @@ export interface PiPrettyDeps {
|
|
|
322
285
|
sdk: PiPrettySdk;
|
|
323
286
|
TextComponent: TextComponentCtor;
|
|
324
287
|
fffModule?: OptionalFffModule;
|
|
325
|
-
multiGrepRipgrepFallback?: MultiGrepRipgrepFallback;
|
|
326
288
|
}
|
package/src/utils.ts
CHANGED
|
@@ -258,34 +258,3 @@ export function trimToUndefined(value: string | undefined): string | undefined {
|
|
|
258
258
|
const trimmed = value?.trim();
|
|
259
259
|
return trimmed ? trimmed : undefined;
|
|
260
260
|
}
|
|
261
|
-
|
|
262
|
-
function escapeRegexLiteral(text: string): string {
|
|
263
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export function buildLiteralAlternationPattern(patterns: string[]): string {
|
|
267
|
-
return patterns
|
|
268
|
-
.map(escapeRegexLiteral)
|
|
269
|
-
.sort((a, b) => b.length - a.length)
|
|
270
|
-
.join("|");
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
export function shouldIgnoreCaseForPatterns(patterns: string[]): boolean {
|
|
274
|
-
return patterns.every((pattern) => pattern.toLowerCase() === pattern);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export function getConstraintBackedPath(
|
|
278
|
-
constraints: string | undefined,
|
|
279
|
-
): string | undefined {
|
|
280
|
-
const trimmed = trimToUndefined(constraints);
|
|
281
|
-
if (
|
|
282
|
-
!trimmed ||
|
|
283
|
-
/\s/.test(trimmed) ||
|
|
284
|
-
trimmed.includes("!") ||
|
|
285
|
-
trimmed.endsWith("/") ||
|
|
286
|
-
/[*?[{]/.test(trimmed)
|
|
287
|
-
) {
|
|
288
|
-
return undefined;
|
|
289
|
-
}
|
|
290
|
-
return trimmed;
|
|
291
|
-
}
|
package/src/tools/multi-grep.ts
DELETED
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ExtensionContext,
|
|
3
|
-
GrepToolInput,
|
|
4
|
-
ToolRenderResultOptions,
|
|
5
|
-
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
|
|
7
|
-
import { fffFormatGrepText } from "../fff.js";
|
|
8
|
-
import type {
|
|
9
|
-
GrepResultDetails,
|
|
10
|
-
MultiGrepParams,
|
|
11
|
-
PiPrettyApi,
|
|
12
|
-
RenderContextLike,
|
|
13
|
-
ThemeLike,
|
|
14
|
-
ToolFactory,
|
|
15
|
-
ToolResultLike,
|
|
16
|
-
} from "../types.js";
|
|
17
|
-
import {
|
|
18
|
-
appendNotices,
|
|
19
|
-
buildLiteralAlternationPattern,
|
|
20
|
-
countRipgrepMatches,
|
|
21
|
-
fillToolBackground,
|
|
22
|
-
getConstraintBackedPath,
|
|
23
|
-
getErrorMessage,
|
|
24
|
-
getTextContent,
|
|
25
|
-
makeTextResult,
|
|
26
|
-
normalizeLineEndings,
|
|
27
|
-
pluralize,
|
|
28
|
-
renderDimPreview,
|
|
29
|
-
renderToolError,
|
|
30
|
-
shouldIgnoreCaseForPatterns,
|
|
31
|
-
trimToUndefined,
|
|
32
|
-
} from "../utils.js";
|
|
33
|
-
import type { ToolContext } from "./context.js";
|
|
34
|
-
|
|
35
|
-
export function registerMultiGrepTool(
|
|
36
|
-
pi: PiPrettyApi,
|
|
37
|
-
createGrepTool: ToolFactory<GrepToolInput> | null,
|
|
38
|
-
ctx: ToolContext,
|
|
39
|
-
): void {
|
|
40
|
-
const {
|
|
41
|
-
cwd,
|
|
42
|
-
sp,
|
|
43
|
-
TextComponent,
|
|
44
|
-
fffState,
|
|
45
|
-
cursorStore,
|
|
46
|
-
multiGrepRipgrepFallback,
|
|
47
|
-
} = ctx;
|
|
48
|
-
const multiGrepFallback = createGrepTool ? createGrepTool(cwd) : null;
|
|
49
|
-
|
|
50
|
-
pi.registerTool({
|
|
51
|
-
name: "multi_grep",
|
|
52
|
-
label: "multi_grep",
|
|
53
|
-
renderShell: "self",
|
|
54
|
-
description: [
|
|
55
|
-
"Search file contents for lines matching ANY of multiple patterns (OR logic).",
|
|
56
|
-
"Uses SIMD-accelerated Aho-Corasick multi-pattern matching when FFF is available.",
|
|
57
|
-
"Falls back to ripgrep while preserving literal OR semantics and file constraints when needed.",
|
|
58
|
-
"Patterns are literal text — never escape special characters.",
|
|
59
|
-
"Use path to scope a directory/file and constraints for file filtering ('*.rs', 'src/', '!test/').",
|
|
60
|
-
].join(" "),
|
|
61
|
-
promptSnippet:
|
|
62
|
-
"Multi-pattern OR search across file contents (FFF-accelerated with grep fallback)",
|
|
63
|
-
promptGuidelines: [
|
|
64
|
-
"Use multi_grep when you need to find multiple identifiers at once (OR logic).",
|
|
65
|
-
"Include all naming conventions: snake_case, PascalCase, camelCase variants.",
|
|
66
|
-
"Patterns are literal text. Never escape special characters.",
|
|
67
|
-
"Use path to scope a directory or file when you need fresh on-disk results.",
|
|
68
|
-
"Use the constraints parameter for additional file filtering, not inside patterns.",
|
|
69
|
-
],
|
|
70
|
-
|
|
71
|
-
parameters: {
|
|
72
|
-
type: "object",
|
|
73
|
-
properties: {
|
|
74
|
-
patterns: {
|
|
75
|
-
type: "array",
|
|
76
|
-
items: { type: "string" },
|
|
77
|
-
description:
|
|
78
|
-
"Patterns to search for (OR logic — matches lines containing ANY pattern).",
|
|
79
|
-
},
|
|
80
|
-
path: {
|
|
81
|
-
type: "string",
|
|
82
|
-
description:
|
|
83
|
-
"Directory or file path to search (default: current directory)",
|
|
84
|
-
},
|
|
85
|
-
constraints: {
|
|
86
|
-
type: "string",
|
|
87
|
-
description:
|
|
88
|
-
"File constraints, e.g. '*.{ts,tsx} !test/' to filter files.",
|
|
89
|
-
},
|
|
90
|
-
context: {
|
|
91
|
-
type: "number",
|
|
92
|
-
description:
|
|
93
|
-
"Number of context lines before and after each match (default: 0)",
|
|
94
|
-
},
|
|
95
|
-
limit: {
|
|
96
|
-
type: "number",
|
|
97
|
-
description: "Maximum number of matches to return (default: 100)",
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
required: ["patterns"],
|
|
101
|
-
},
|
|
102
|
-
|
|
103
|
-
async execute(
|
|
104
|
-
tid: string,
|
|
105
|
-
params: MultiGrepParams,
|
|
106
|
-
sig: AbortSignal | undefined,
|
|
107
|
-
upd: unknown,
|
|
108
|
-
toolCtx: ExtensionContext,
|
|
109
|
-
) {
|
|
110
|
-
if (sig?.aborted) return makeTextResult("Aborted", {});
|
|
111
|
-
|
|
112
|
-
if (!params.patterns || params.patterns.length === 0) {
|
|
113
|
-
return makeTextResult(
|
|
114
|
-
"Error: patterns array must have at least 1 element",
|
|
115
|
-
{ error: "empty patterns" },
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const effectiveLimit = Math.max(1, params.limit ?? 100);
|
|
120
|
-
const pattern = buildLiteralAlternationPattern(params.patterns);
|
|
121
|
-
const requestedPath = trimToUndefined(params.path);
|
|
122
|
-
const requestedConstraints = trimToUndefined(params.constraints);
|
|
123
|
-
const effectivePath =
|
|
124
|
-
requestedPath ?? getConstraintBackedPath(requestedConstraints);
|
|
125
|
-
const hasNativeConstraints = Boolean(
|
|
126
|
-
requestedPath || requestedConstraints,
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
// FFF path (no constraints — constrained searches use ripgrep)
|
|
130
|
-
if (
|
|
131
|
-
fffState.finder &&
|
|
132
|
-
!fffState.finder.isDestroyed &&
|
|
133
|
-
!hasNativeConstraints
|
|
134
|
-
) {
|
|
135
|
-
try {
|
|
136
|
-
const grepResult = fffState.finder.multiGrep({
|
|
137
|
-
patterns: params.patterns,
|
|
138
|
-
maxMatchesPerFile: Math.min(effectiveLimit, 50),
|
|
139
|
-
smartCase: true,
|
|
140
|
-
cursor: null,
|
|
141
|
-
beforeContext: params.context ?? 0,
|
|
142
|
-
afterContext: params.context ?? 0,
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
if (!grepResult.ok) {
|
|
146
|
-
return makeTextResult(`multi_grep error: ${grepResult.error}`, {
|
|
147
|
-
error: grepResult.error,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const grep = grepResult.value;
|
|
152
|
-
const notices: string[] = [];
|
|
153
|
-
if (fffState.partialIndex)
|
|
154
|
-
notices.push("Warning: partial file index");
|
|
155
|
-
if (grep.items.length >= effectiveLimit)
|
|
156
|
-
notices.push(`${effectiveLimit} limit reached`);
|
|
157
|
-
if (grep.nextCursor) {
|
|
158
|
-
const cursorId = cursorStore.store(grep.nextCursor);
|
|
159
|
-
notices.push(`More results: cursor="${cursorId}"`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const textContent = appendNotices(
|
|
163
|
-
fffFormatGrepText(grep.items, effectiveLimit),
|
|
164
|
-
notices,
|
|
165
|
-
);
|
|
166
|
-
return makeTextResult<GrepResultDetails>(textContent, {
|
|
167
|
-
_type: "grepResult",
|
|
168
|
-
text: textContent,
|
|
169
|
-
pattern,
|
|
170
|
-
matchCount: Math.min(grep.items.length, effectiveLimit),
|
|
171
|
-
});
|
|
172
|
-
} catch {
|
|
173
|
-
/* fall through to SDK */
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Ripgrep path (constrained, or FFF unavailable)
|
|
178
|
-
if (requestedConstraints || !multiGrepFallback) {
|
|
179
|
-
try {
|
|
180
|
-
const pathBackedConstraint = Boolean(
|
|
181
|
-
requestedConstraints &&
|
|
182
|
-
!requestedPath &&
|
|
183
|
-
requestedConstraints === effectivePath,
|
|
184
|
-
);
|
|
185
|
-
const constraintsForRipgrep = pathBackedConstraint
|
|
186
|
-
? undefined
|
|
187
|
-
: requestedConstraints;
|
|
188
|
-
const notices: string[] = [];
|
|
189
|
-
|
|
190
|
-
if (!fffState.finder || fffState.finder.isDestroyed)
|
|
191
|
-
notices.push("FFF unavailable, used ripgrep fallback");
|
|
192
|
-
else if (hasNativeConstraints)
|
|
193
|
-
notices.push("Used ripgrep fallback for constrained search");
|
|
194
|
-
else notices.push("Used ripgrep fallback");
|
|
195
|
-
|
|
196
|
-
const rgResult = await multiGrepRipgrepFallback({
|
|
197
|
-
cwd,
|
|
198
|
-
patterns: params.patterns,
|
|
199
|
-
path: effectivePath,
|
|
200
|
-
constraints: constraintsForRipgrep,
|
|
201
|
-
ignoreCase: shouldIgnoreCaseForPatterns(params.patterns),
|
|
202
|
-
context: params.context,
|
|
203
|
-
limit: effectiveLimit,
|
|
204
|
-
signal: sig,
|
|
205
|
-
});
|
|
206
|
-
const textContent =
|
|
207
|
-
normalizeLineEndings(rgResult.text) || "No matches found";
|
|
208
|
-
if (rgResult.limitReached)
|
|
209
|
-
notices.push(`${effectiveLimit} limit reached`);
|
|
210
|
-
const finalText = appendNotices(textContent, notices);
|
|
211
|
-
|
|
212
|
-
return makeTextResult<GrepResultDetails>(finalText, {
|
|
213
|
-
_type: "grepResult",
|
|
214
|
-
text: finalText,
|
|
215
|
-
pattern,
|
|
216
|
-
matchCount: rgResult.matchCount,
|
|
217
|
-
});
|
|
218
|
-
} catch (error: unknown) {
|
|
219
|
-
const message = getErrorMessage(error);
|
|
220
|
-
return makeTextResult(`multi_grep error: ${message}`, {
|
|
221
|
-
error: message,
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// SDK grep fallback
|
|
227
|
-
try {
|
|
228
|
-
const notices: string[] = [];
|
|
229
|
-
if (!fffState.finder || fffState.finder.isDestroyed)
|
|
230
|
-
notices.push("FFF unavailable, used SDK grep fallback");
|
|
231
|
-
|
|
232
|
-
const result = await multiGrepFallback.execute(
|
|
233
|
-
tid,
|
|
234
|
-
{
|
|
235
|
-
pattern,
|
|
236
|
-
path: effectivePath,
|
|
237
|
-
ignoreCase: shouldIgnoreCaseForPatterns(params.patterns),
|
|
238
|
-
context: params.context,
|
|
239
|
-
limit: params.limit,
|
|
240
|
-
},
|
|
241
|
-
sig,
|
|
242
|
-
upd as never,
|
|
243
|
-
toolCtx,
|
|
244
|
-
);
|
|
245
|
-
const textContent =
|
|
246
|
-
normalizeLineEndings(getTextContent(result)) || "No matches found";
|
|
247
|
-
const finalText = appendNotices(textContent, notices);
|
|
248
|
-
|
|
249
|
-
return makeTextResult<GrepResultDetails>(finalText, {
|
|
250
|
-
_type: "grepResult",
|
|
251
|
-
text: finalText,
|
|
252
|
-
pattern,
|
|
253
|
-
matchCount: textContent ? countRipgrepMatches(textContent) : 0,
|
|
254
|
-
});
|
|
255
|
-
} catch (error: unknown) {
|
|
256
|
-
const message = getErrorMessage(error);
|
|
257
|
-
return makeTextResult(`multi_grep error: ${message}`, {
|
|
258
|
-
error: message,
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
},
|
|
262
|
-
|
|
263
|
-
renderCall(
|
|
264
|
-
args: MultiGrepParams,
|
|
265
|
-
theme: ThemeLike,
|
|
266
|
-
renderCtx: RenderContextLike,
|
|
267
|
-
) {
|
|
268
|
-
const patterns = args.patterns ?? [];
|
|
269
|
-
const path = args.path
|
|
270
|
-
? ` ${theme.fg("muted", `in ${sp(args.path)}`)}`
|
|
271
|
-
: "";
|
|
272
|
-
const constraints = args.constraints;
|
|
273
|
-
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
274
|
-
let content =
|
|
275
|
-
theme.fg("toolTitle", theme.bold("multi_grep")) +
|
|
276
|
-
" " +
|
|
277
|
-
theme.fg("accent", patterns.map((p) => `"${p}"`).join(", "));
|
|
278
|
-
content += path;
|
|
279
|
-
if (constraints) content += theme.fg("muted", ` (${constraints})`);
|
|
280
|
-
text.setText(fillToolBackground(content));
|
|
281
|
-
return text;
|
|
282
|
-
},
|
|
283
|
-
|
|
284
|
-
renderResult(
|
|
285
|
-
result: ToolResultLike<GrepResultDetails | { error?: string }>,
|
|
286
|
-
_opt: ToolRenderResultOptions,
|
|
287
|
-
theme: ThemeLike,
|
|
288
|
-
renderCtx: RenderContextLike,
|
|
289
|
-
) {
|
|
290
|
-
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
291
|
-
|
|
292
|
-
if (renderCtx.isError) {
|
|
293
|
-
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
294
|
-
return text;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const d = result.details;
|
|
298
|
-
const isGrep = d && "_type" in d && d._type === "grepResult";
|
|
299
|
-
const output = getTextContent(result) || "searched";
|
|
300
|
-
text.setText(
|
|
301
|
-
renderDimPreview(output, theme, {
|
|
302
|
-
header: isGrep
|
|
303
|
-
? pluralize(d.matchCount, "match", "matches")
|
|
304
|
-
: undefined,
|
|
305
|
-
highlight: isGrep ? d.pattern : undefined,
|
|
306
|
-
}),
|
|
307
|
-
);
|
|
308
|
-
return text;
|
|
309
|
-
},
|
|
310
|
-
});
|
|
311
|
-
}
|