@ff-labs/pi-fff 0.10.1 → 0.10.2-nightly.031005e
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 +3 -3
- package/src/aux-finders.ts +38 -16
- package/src/index.ts +68 -102
- package/src/query.ts +1 -5
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ff-labs/pi-fff",
|
|
3
3
|
"public": true,
|
|
4
|
-
"version": "0.10.
|
|
4
|
+
"version": "0.10.2-nightly.031005e",
|
|
5
5
|
"description": "pi extension: FFF-powered fuzzy file and content search",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"typecheck": "tsc --noEmit"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@ff-labs/fff-bun": "
|
|
44
|
-
"@ff-labs/fff-node": "
|
|
43
|
+
"@ff-labs/fff-bun": "0.10.2-nightly.031005e",
|
|
44
|
+
"@ff-labs/fff-node": "0.10.2-nightly.031005e"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@earendil-works/pi-coding-agent": "*",
|
package/src/aux-finders.ts
CHANGED
|
@@ -14,13 +14,15 @@ interface AuxPicker {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export interface AuxOpts {
|
|
17
|
-
frecencyDbPath?: string;
|
|
18
|
-
historyDbPath?: string;
|
|
19
17
|
enableFsRootScanning: boolean;
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
export class AuxFinderPool {
|
|
23
21
|
private entries: AuxPicker[] = [];
|
|
22
|
+
// In-flight creations keyed by root. Concurrent acquire() calls for the same
|
|
23
|
+
// (or a covering) root share one finder/scan instead of each starting a full
|
|
24
|
+
// duplicate traversal — issue #746. Mirrors the main finder's finderPromise.
|
|
25
|
+
private pending = new Map<string, Promise<AuxPicker>>();
|
|
24
26
|
constructor(private opts: AuxOpts) {}
|
|
25
27
|
|
|
26
28
|
destroy(): void {
|
|
@@ -29,6 +31,7 @@ export class AuxFinderPool {
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
this.entries = [];
|
|
34
|
+
this.pending.clear();
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
private sweepIdle(now = Date.now()): void {
|
|
@@ -60,31 +63,53 @@ export class AuxFinderPool {
|
|
|
60
63
|
return { finder: covering.finder, root: covering.root };
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
// Coalesce concurrent creations for the same root so we scan once. A slow
|
|
67
|
+
// full-home scan started by one call is awaited by the others instead of
|
|
68
|
+
// each spawning its own traversal (#746).
|
|
69
|
+
const inflight = this.pending.get(maybeRoot);
|
|
70
|
+
if (inflight) {
|
|
71
|
+
const e = await inflight;
|
|
72
|
+
e.lastUsed = Date.now();
|
|
73
|
+
return { finder: e.finder, root: e.root };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const creation = this.create(maybeRoot).finally(() => {
|
|
77
|
+
this.pending.delete(maybeRoot);
|
|
78
|
+
});
|
|
79
|
+
this.pending.set(maybeRoot, creation);
|
|
80
|
+
const entry = await creation;
|
|
81
|
+
return { finder: entry.finder, root: entry.root };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private async create(root: string): Promise<AuxPicker> {
|
|
63
85
|
if (this.entries.length >= MAX_AUX) {
|
|
64
86
|
let oldest = this.entries[0];
|
|
65
|
-
for (const e of this.entries)
|
|
66
|
-
if (e.lastUsed < oldest.lastUsed) oldest = e;
|
|
87
|
+
for (const e of this.entries) if (e.lastUsed < oldest.lastUsed) oldest = e;
|
|
67
88
|
if (!oldest.finder.isDestroyed) oldest.finder.destroy();
|
|
68
89
|
this.entries = this.entries.filter((e) => e !== oldest);
|
|
69
90
|
}
|
|
70
91
|
|
|
71
92
|
const { FileFinder } = await loadSdk();
|
|
93
|
+
// LMDB env can only be opened once per process; the main finder already
|
|
94
|
+
// owns the frecency/history DBs. Aux finders are transient and run without
|
|
95
|
+
// persistent scoring — see issue #700.
|
|
72
96
|
const result = FileFinder.create({
|
|
73
|
-
basePath:
|
|
74
|
-
frecencyDbPath: this.opts.frecencyDbPath,
|
|
75
|
-
historyDbPath: this.opts.historyDbPath,
|
|
97
|
+
basePath: root,
|
|
76
98
|
aiMode: true,
|
|
77
99
|
enableHomeDirScanning: true,
|
|
78
100
|
enableFsRootScanning: this.opts.enableFsRootScanning,
|
|
79
101
|
});
|
|
80
102
|
if (!result.ok)
|
|
81
|
-
throw new Error(
|
|
82
|
-
`Failed to create aux file finder for ${maybeRoot}: ${result.error}`,
|
|
83
|
-
);
|
|
103
|
+
throw new Error(`Failed to create aux file finder for ${root}: ${result.error}`);
|
|
84
104
|
|
|
85
105
|
await result.value.waitForScan(SCAN_TIMEOUT_MS);
|
|
86
|
-
|
|
87
|
-
|
|
106
|
+
const entry: AuxPicker = {
|
|
107
|
+
root,
|
|
108
|
+
finder: result.value,
|
|
109
|
+
lastUsed: Date.now(),
|
|
110
|
+
};
|
|
111
|
+
this.entries.push(entry);
|
|
112
|
+
return entry;
|
|
88
113
|
}
|
|
89
114
|
|
|
90
115
|
size(): number {
|
|
@@ -97,9 +122,7 @@ export class AuxFinderPool {
|
|
|
97
122
|
// remainder usable as a fuzzy path constraint relative to that root. Glob and
|
|
98
123
|
// nonexistent segments both go into the suffix: we walk up to the nearest
|
|
99
124
|
// existing ancestor so partially-wrong paths still resolve to a search root.
|
|
100
|
-
export function resolveAuxRoot(
|
|
101
|
-
absPath: string,
|
|
102
|
-
): { root: string; suffix: string } | null {
|
|
125
|
+
export function resolveAuxRoot(absPath: string): { root: string; suffix: string } | null {
|
|
103
126
|
const trimmed = path.normalize(absPath.trim()).replace(/\/+$/, "") || "/";
|
|
104
127
|
if (!path.isAbsolute(trimmed)) return null;
|
|
105
128
|
if (trimmed === path.sep) return { root: path.sep, suffix: "" };
|
|
@@ -159,7 +182,6 @@ export function routePathConstraint(
|
|
|
159
182
|
return resolveAuxRoot(candidate);
|
|
160
183
|
}
|
|
161
184
|
|
|
162
|
-
|
|
163
185
|
export function rootCovers(root: string, target: string): boolean {
|
|
164
186
|
if (root === target) return true;
|
|
165
187
|
const prefix = root.endsWith(path.sep) ? root : root + path.sep;
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,9 @@ const DEFAULT_FIND_LIMIT = 30;
|
|
|
36
36
|
const GREP_MAX_LINE_LENGTH = 500;
|
|
37
37
|
const MENTION_MAX_RESULTS = 20;
|
|
38
38
|
|
|
39
|
+
// If we exceed 10 seconds for indexed grep - something is definitely off
|
|
40
|
+
const GREP_TIME_BUDGET_MS = 10_000;
|
|
41
|
+
|
|
39
42
|
type FffMode = "tools-and-ui" | "tools-only" | "override";
|
|
40
43
|
|
|
41
44
|
const VALID_MODES: FffMode[] = ["tools-and-ui", "tools-only", "override"];
|
|
@@ -261,9 +264,7 @@ function createFffMentionProvider(
|
|
|
261
264
|
|
|
262
265
|
const query = prefix.startsWith('@"') ? prefix.slice(2) : prefix.slice(1);
|
|
263
266
|
const items = await getItems(query, options.signal);
|
|
264
|
-
return options.signal.aborted || items.length === 0
|
|
265
|
-
? null
|
|
266
|
-
: { items, prefix };
|
|
267
|
+
return options.signal.aborted || items.length === 0 ? null : { items, prefix };
|
|
267
268
|
},
|
|
268
269
|
applyCompletion(_lines, cursorLine, cursorCol, item, prefix) {
|
|
269
270
|
const currentLine = _lines[cursorLine] || "";
|
|
@@ -272,11 +273,7 @@ function createFffMentionProvider(
|
|
|
272
273
|
const newLine = before + item.value + after;
|
|
273
274
|
const newCursorCol = cursorCol - prefix.length + item.value.length;
|
|
274
275
|
return {
|
|
275
|
-
lines: [
|
|
276
|
-
..._lines.slice(0, cursorLine),
|
|
277
|
-
newLine,
|
|
278
|
-
..._lines.slice(cursorLine + 1),
|
|
279
|
-
],
|
|
276
|
+
lines: [..._lines.slice(0, cursorLine), newLine, ..._lines.slice(cursorLine + 1)],
|
|
280
277
|
cursorLine,
|
|
281
278
|
cursorCol: newCursorCol,
|
|
282
279
|
};
|
|
@@ -344,8 +341,6 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
344
341
|
}
|
|
345
342
|
|
|
346
343
|
let auxPool = new AuxFinderPool({
|
|
347
|
-
frecencyDbPath,
|
|
348
|
-
historyDbPath,
|
|
349
344
|
enableFsRootScanning,
|
|
350
345
|
});
|
|
351
346
|
|
|
@@ -409,9 +404,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
409
404
|
const aux = await auxPool.acquire(route.root);
|
|
410
405
|
// A broader covering picker may have been reused; rebase the suffix so the
|
|
411
406
|
// constraint stays relative to the picker's actual root.
|
|
412
|
-
const rebase = nodePath
|
|
413
|
-
.relative(aux.root, route.root)
|
|
414
|
-
.replaceAll(nodePath.sep, "/");
|
|
407
|
+
const rebase = nodePath.relative(aux.root, route.root).replaceAll(nodePath.sep, "/");
|
|
415
408
|
const suffix = [rebase, route.suffix].filter(Boolean).join("/");
|
|
416
409
|
const query = buildQuery(suffix || undefined, pattern, exclude, aux.root);
|
|
417
410
|
return { finder: aux.finder, query, root: aux.root };
|
|
@@ -428,22 +421,20 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
428
421
|
const result = f.mixedSearch(query, { pageSize: MENTION_MAX_RESULTS });
|
|
429
422
|
if (!result.ok) return [];
|
|
430
423
|
|
|
431
|
-
return result.value.items
|
|
432
|
-
.
|
|
433
|
-
.map((mixed: MixedItem) => {
|
|
434
|
-
if (mixed.type === "directory") {
|
|
435
|
-
return {
|
|
436
|
-
value: buildAtCompletionValue(mixed.item.relativePath),
|
|
437
|
-
label: mixed.item.dirName,
|
|
438
|
-
description: mixed.item.relativePath,
|
|
439
|
-
};
|
|
440
|
-
}
|
|
424
|
+
return result.value.items.slice(0, MENTION_MAX_RESULTS).map((mixed: MixedItem) => {
|
|
425
|
+
if (mixed.type === "directory") {
|
|
441
426
|
return {
|
|
442
427
|
value: buildAtCompletionValue(mixed.item.relativePath),
|
|
443
|
-
label: mixed.item.
|
|
428
|
+
label: mixed.item.dirName,
|
|
444
429
|
description: mixed.item.relativePath,
|
|
445
430
|
};
|
|
446
|
-
}
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
value: buildAtCompletionValue(mixed.item.relativePath),
|
|
434
|
+
label: mixed.item.fileName,
|
|
435
|
+
description: mixed.item.relativePath,
|
|
436
|
+
};
|
|
437
|
+
});
|
|
447
438
|
}
|
|
448
439
|
|
|
449
440
|
function registerAutocompleteProvider(ctx: {
|
|
@@ -479,21 +470,11 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
479
470
|
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
480
471
|
},
|
|
481
472
|
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
482
|
-
return current.applyCompletion(
|
|
483
|
-
lines,
|
|
484
|
-
cursorLine,
|
|
485
|
-
cursorCol,
|
|
486
|
-
item,
|
|
487
|
-
prefix,
|
|
488
|
-
);
|
|
473
|
+
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
489
474
|
},
|
|
490
475
|
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
|
|
491
476
|
return (
|
|
492
|
-
current.shouldTriggerFileCompletion?.(
|
|
493
|
-
lines,
|
|
494
|
-
cursorLine,
|
|
495
|
-
cursorCol,
|
|
496
|
-
) ?? true
|
|
477
|
+
current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true
|
|
497
478
|
);
|
|
498
479
|
},
|
|
499
480
|
};
|
|
@@ -508,14 +489,12 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
508
489
|
});
|
|
509
490
|
|
|
510
491
|
pi.registerFlag("fff-frecency-db", {
|
|
511
|
-
description:
|
|
512
|
-
"Path to the frecency database (overrides FFF_FRECENCY_DB env)",
|
|
492
|
+
description: "Path to the frecency database (overrides FFF_FRECENCY_DB env)",
|
|
513
493
|
type: "string",
|
|
514
494
|
});
|
|
515
495
|
|
|
516
496
|
pi.registerFlag("fff-history-db", {
|
|
517
|
-
description:
|
|
518
|
-
"Path to the query history database (overrides FFF_HISTORY_DB env)",
|
|
497
|
+
description: "Path to the query history database (overrides FFF_HISTORY_DB env)",
|
|
519
498
|
type: "string",
|
|
520
499
|
});
|
|
521
500
|
|
|
@@ -575,20 +554,15 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
575
554
|
context: any,
|
|
576
555
|
maxLines = 15,
|
|
577
556
|
) => {
|
|
578
|
-
const text =
|
|
579
|
-
|
|
580
|
-
const output =
|
|
581
|
-
result.content?.find((c) => c.type === "text")?.text?.trim() ?? "";
|
|
557
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
558
|
+
const output = result.content?.find((c) => c.type === "text")?.text?.trim() ?? "";
|
|
582
559
|
if (!output) {
|
|
583
560
|
text.setText(theme.fg("muted", "No output"));
|
|
584
561
|
return text;
|
|
585
562
|
}
|
|
586
563
|
|
|
587
564
|
const lines = output.split("\n");
|
|
588
|
-
const displayLines = lines.slice(
|
|
589
|
-
0,
|
|
590
|
-
options.expanded ? lines.length : maxLines,
|
|
591
|
-
);
|
|
565
|
+
const displayLines = lines.slice(0, options.expanded ? lines.length : maxLines);
|
|
592
566
|
let content = `\n${displayLines.map((line: string) => theme.fg("toolOutput", line)).join("\n")}`;
|
|
593
567
|
if (lines.length > displayLines.length) {
|
|
594
568
|
content += theme.fg(
|
|
@@ -643,10 +617,10 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
643
617
|
description: `Grep file contents. Smart-case, auto-detects regex vs literal, git-aware. Results are ranked by frecency (most-accessed files first); matches within a file stay in source order. Default limit ${DEFAULT_GREP_LIMIT}.`,
|
|
644
618
|
promptSnippet: "Grep contents",
|
|
645
619
|
promptGuidelines: [
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
620
|
+
`${toolNames.grep}: prefer bare identifiers as patterns. Literal queries are most efficient.`,
|
|
621
|
+
`${toolNames.grep}: use path for include ('src/', '*.ts') and exclude for noise ('test/,*.min.js').`,
|
|
622
|
+
`${toolNames.grep}: caseSensitive: true when you need exact case (smart-case otherwise).`,
|
|
623
|
+
`${toolNames.grep}: after 1-2 greps, read the top match instead of more greps.`,
|
|
650
624
|
],
|
|
651
625
|
parameters: grepSchema,
|
|
652
626
|
|
|
@@ -654,11 +628,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
654
628
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
655
629
|
|
|
656
630
|
const pattern = params.pattern;
|
|
657
|
-
const aux = await resolveFinderForPath(
|
|
658
|
-
params.path,
|
|
659
|
-
pattern,
|
|
660
|
-
params.exclude,
|
|
661
|
-
);
|
|
631
|
+
const aux = await resolveFinderForPath(params.path, pattern, params.exclude);
|
|
662
632
|
|
|
663
633
|
const picker = aux ? aux.finder : await ensureFinder(activeCwd);
|
|
664
634
|
const effectiveLimit = Math.max(1, params.limit ?? DEFAULT_GREP_LIMIT);
|
|
@@ -669,8 +639,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
669
639
|
// Auto-detect: regex if the pattern has regex metacharacters AND parses
|
|
670
640
|
// as a valid regex, otherwise plain literal. The fuzzy fallback below
|
|
671
641
|
// only kicks in for plain mode — regex queries are intentional.
|
|
672
|
-
const hasRegexSyntax =
|
|
673
|
-
pattern !== pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
642
|
+
const hasRegexSyntax = pattern !== pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
674
643
|
|
|
675
644
|
let mode: GrepMode = hasRegexSyntax ? "regex" : "plain";
|
|
676
645
|
if (mode === "regex") {
|
|
@@ -715,6 +684,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
715
684
|
beforeContext: params.context ?? 0,
|
|
716
685
|
afterContext: params.context ?? 0,
|
|
717
686
|
classifyDefinitions: true,
|
|
687
|
+
timeBudgetMs: GREP_TIME_BUDGET_MS,
|
|
718
688
|
});
|
|
719
689
|
|
|
720
690
|
if (!grepResult.ok) throw new Error(grepResult.error);
|
|
@@ -722,9 +692,23 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
722
692
|
let result = grepResult.value;
|
|
723
693
|
let fuzzyNotice: string | null = null;
|
|
724
694
|
|
|
725
|
-
//
|
|
726
|
-
|
|
727
|
-
|
|
695
|
+
// if we hit the timeout do not run the fuzzy fallback
|
|
696
|
+
// cause it will only consumer more time
|
|
697
|
+
if (
|
|
698
|
+
result.items.length === 0 &&
|
|
699
|
+
!result.nextCursor &&
|
|
700
|
+
!params.cursor &&
|
|
701
|
+
mode !== "regex"
|
|
702
|
+
) {
|
|
703
|
+
// When the caller pinned a specific file (path has an extension), the
|
|
704
|
+
// fuzzy fallback broadens across the whole picker — the file may just
|
|
705
|
+
// be misnamed. For directory constraints (or no path), we keep the
|
|
706
|
+
// constrained query so the fallback does not leak matches from
|
|
707
|
+
// excluded / out-of-scope directories.
|
|
708
|
+
const lastSeg = params.path?.split(/[\\/]/).pop() ?? "";
|
|
709
|
+
const pathTargetsFile = /\.[a-zA-Z][a-zA-Z0-9]{0,9}$/.test(lastSeg);
|
|
710
|
+
const fuzzyQuery = pathTargetsFile ? pattern : query;
|
|
711
|
+
const fuzzy = picker.grep(fuzzyQuery, {
|
|
728
712
|
mode: "fuzzy",
|
|
729
713
|
smartCase,
|
|
730
714
|
maxMatchesPerFile: Math.min(effectiveLimit, 50),
|
|
@@ -732,6 +716,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
732
716
|
beforeContext: 0,
|
|
733
717
|
afterContext: 0,
|
|
734
718
|
classifyDefinitions: true,
|
|
719
|
+
timeBudgetMs: GREP_TIME_BUDGET_MS,
|
|
735
720
|
});
|
|
736
721
|
|
|
737
722
|
if (fuzzy.ok && fuzzy.value.items.length > 0) {
|
|
@@ -743,14 +728,10 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
743
728
|
let output = formatGrepOutput(result);
|
|
744
729
|
const notices: string[] = [];
|
|
745
730
|
if (result.regexFallbackError) {
|
|
746
|
-
notices.push(
|
|
747
|
-
`Invalid regex: ${result.regexFallbackError}, used literal match`,
|
|
748
|
-
);
|
|
731
|
+
notices.push(`Invalid regex: ${result.regexFallbackError}, used literal match`);
|
|
749
732
|
}
|
|
750
733
|
if (result.nextCursor) {
|
|
751
|
-
notices.push(
|
|
752
|
-
`Continue with cursor="${storeCursor(result.nextCursor)}"`,
|
|
753
|
-
);
|
|
734
|
+
notices.push(`Continue with cursor="${storeCursor(result.nextCursor)}"`);
|
|
754
735
|
}
|
|
755
736
|
|
|
756
737
|
if (notices.length > 0) output += `\n\n[${notices.join(". ")}]`;
|
|
@@ -766,8 +747,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
766
747
|
},
|
|
767
748
|
|
|
768
749
|
renderCall(args, theme, context) {
|
|
769
|
-
const text =
|
|
770
|
-
(context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
750
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
771
751
|
const pattern = args?.pattern ?? "";
|
|
772
752
|
const path = args?.path ?? ".";
|
|
773
753
|
let content =
|
|
@@ -822,12 +802,12 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
822
802
|
description: `Fuzzy path search and glob search. Matches against the whole repo-relative path, not just the filename. Frecency-ranked, git-aware. Multi-word = narrower (AND). Default limit ${DEFAULT_FIND_LIMIT}.`,
|
|
823
803
|
promptSnippet: "Find files by path or glob",
|
|
824
804
|
promptGuidelines: [
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
805
|
+
`${toolNames.find}: matches the WHOLE path, not just the filename — \`profile\` hits \`chrome/browser/profiles/x.cc\` too.`,
|
|
806
|
+
`${toolNames.find}: keep queries to 1-2 terms; extra words narrow.`,
|
|
807
|
+
`${toolNames.find}: use for paths, not content. Use ${toolNames.grep} for content.`,
|
|
808
|
+
`${toolNames.find}: for exact path matches use a glob in \`path\` — e.g. path: '**/profile.h' for exact filename, or path: 'src/**/profile.h' scoped to a subtree. Bare patterns are fuzzy.`,
|
|
809
|
+
`${toolNames.find}: to list everything inside a directory, pass path: 'dir/**' with an empty or wildcard pattern instead of using pattern alone.`,
|
|
810
|
+
`${toolNames.find}: use exclude: 'test/,*.min.js' to cut noise in large repos.`,
|
|
831
811
|
],
|
|
832
812
|
parameters: findSchema,
|
|
833
813
|
|
|
@@ -839,16 +819,11 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
839
819
|
const aux = resumed
|
|
840
820
|
? resumed.auxRoot
|
|
841
821
|
? {
|
|
842
|
-
finder: (await auxPool.acquire(resumed.auxRoot, { exact: true }))
|
|
843
|
-
.finder,
|
|
822
|
+
finder: (await auxPool.acquire(resumed.auxRoot, { exact: true })).finder,
|
|
844
823
|
root: resumed.auxRoot,
|
|
845
824
|
}
|
|
846
825
|
: null
|
|
847
|
-
: await resolveFinderForPath(
|
|
848
|
-
params.path,
|
|
849
|
-
params.pattern,
|
|
850
|
-
params.exclude,
|
|
851
|
-
);
|
|
826
|
+
: await resolveFinderForPath(params.path, params.pattern, params.exclude);
|
|
852
827
|
|
|
853
828
|
const picker = aux ? aux.finder : await ensureFinder(activeCwd);
|
|
854
829
|
const effectiveLimit = resumed
|
|
@@ -880,8 +855,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
880
855
|
// shown so far there's another page to fetch.
|
|
881
856
|
const shownSoFar = pageIndex * effectiveLimit + result.items.length;
|
|
882
857
|
const hasMore =
|
|
883
|
-
result.items.length >= effectiveLimit &&
|
|
884
|
-
result.totalMatched > shownSoFar;
|
|
858
|
+
result.items.length >= effectiveLimit && result.totalMatched > shownSoFar;
|
|
885
859
|
|
|
886
860
|
const notices: string[] = [];
|
|
887
861
|
if (formatted.weak && formatted.shownCount > 0)
|
|
@@ -916,8 +890,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
916
890
|
},
|
|
917
891
|
|
|
918
892
|
renderCall(args, theme, context) {
|
|
919
|
-
const text =
|
|
920
|
-
(context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
893
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
921
894
|
const pattern = args?.pattern ?? "";
|
|
922
895
|
const path = args?.path ?? ".";
|
|
923
896
|
let content =
|
|
@@ -950,9 +923,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
950
923
|
constraints: Type.Optional(
|
|
951
924
|
Type.String({ description: "File filter, e.g. '*.{ts,tsx} !test/'" }),
|
|
952
925
|
),
|
|
953
|
-
context: Type.Optional(
|
|
954
|
-
Type.Number({ description: "Context lines before+after" }),
|
|
955
|
-
),
|
|
926
|
+
context: Type.Optional(Type.Number({ description: "Context lines before+after" })),
|
|
956
927
|
limit: Type.Optional(
|
|
957
928
|
Type.Number({
|
|
958
929
|
description: `Max matches (default ${DEFAULT_GREP_LIMIT})`,
|
|
@@ -968,9 +939,9 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
968
939
|
"Search file contents for ANY of multiple literal patterns (OR, SIMD Aho-Corasick). Faster than regex alternation.",
|
|
969
940
|
promptSnippet: "Multi-pattern OR content search",
|
|
970
941
|
promptGuidelines: [
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
942
|
+
`${toolNames.multiGrep}: use when searching for several identifiers at once.`,
|
|
943
|
+
`${toolNames.multiGrep}: include all naming-convention variants (snake/camel/Pascal).`,
|
|
944
|
+
`${toolNames.multiGrep}: patterns are literal. Use constraints for file filters.`,
|
|
974
945
|
],
|
|
975
946
|
parameters: multiGrepSchema,
|
|
976
947
|
|
|
@@ -1018,8 +989,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1018
989
|
},
|
|
1019
990
|
|
|
1020
991
|
renderCall(args, theme, context) {
|
|
1021
|
-
const text =
|
|
1022
|
-
(context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
992
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
1023
993
|
const patterns = args?.patterns ?? [];
|
|
1024
994
|
const constraints = args?.constraints;
|
|
1025
995
|
let content =
|
|
@@ -1041,8 +1011,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1041
1011
|
// --- commands ---
|
|
1042
1012
|
|
|
1043
1013
|
pi.registerCommand("fff-mode", {
|
|
1044
|
-
description:
|
|
1045
|
-
"Show or set FFF mode: /fff-mode [tools-and-ui | tools-only | override]",
|
|
1014
|
+
description: "Show or set FFF mode: /fff-mode [tools-and-ui | tools-only | override]",
|
|
1046
1015
|
handler: async (args, ctx) => {
|
|
1047
1016
|
const arg = (args || "").trim();
|
|
1048
1017
|
|
|
@@ -1056,10 +1025,7 @@ export default function fffExtension(pi: ExtensionAPI) {
|
|
|
1056
1025
|
|
|
1057
1026
|
// Validate and set mode
|
|
1058
1027
|
if (!VALID_MODES.includes(arg as FffMode)) {
|
|
1059
|
-
ctx.ui.notify(
|
|
1060
|
-
`Usage: /fff-mode [${VALID_MODES.join(" | ")}]`,
|
|
1061
|
-
"warning",
|
|
1062
|
-
);
|
|
1028
|
+
ctx.ui.notify(`Usage: /fff-mode [${VALID_MODES.join(" | ")}]`, "warning");
|
|
1063
1029
|
return;
|
|
1064
1030
|
}
|
|
1065
1031
|
|
package/src/query.ts
CHANGED
|
@@ -10,11 +10,7 @@ export function normalizePathConstraint(
|
|
|
10
10
|
if (path.isAbsolute(trimmed)) {
|
|
11
11
|
const relative = path.relative(cwd, trimmed).replaceAll(path.sep, "/");
|
|
12
12
|
if (relative === "") return null;
|
|
13
|
-
if (
|
|
14
|
-
relative.startsWith("../") ||
|
|
15
|
-
relative === ".." ||
|
|
16
|
-
path.isAbsolute(relative)
|
|
17
|
-
) {
|
|
13
|
+
if (relative.startsWith("../") || relative === ".." || path.isAbsolute(relative)) {
|
|
18
14
|
throw new Error(
|
|
19
15
|
`Path constraint must be relative to the workspace: ${pathConstraint}`,
|
|
20
16
|
);
|