pi-supernova 0.9.0 → 0.9.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/package.json +1 -1
- package/src/context/fuzzy.js +116 -43
- package/src/context/query.js +14 -5
- package/src/context/repo-index.js +15 -4
- package/src/context/snap-search.js +14 -13
package/package.json
CHANGED
package/src/context/fuzzy.js
CHANGED
|
@@ -40,10 +40,13 @@ export class Frecency {
|
|
|
40
40
|
score(filePath, mtimeSec, now = Date.now() / 1000) {
|
|
41
41
|
let total = 0;
|
|
42
42
|
const cutoff = now - AI_MAX_HISTORY_DAYS * 86400;
|
|
43
|
+
const stamps = this.access.get(filePath);
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
if (stamps) {
|
|
46
|
+
for (const t of stamps) {
|
|
47
|
+
if (t < cutoff) continue;
|
|
48
|
+
total += Math.exp(-AI_DECAY * ((now - t) / 86400));
|
|
49
|
+
}
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
if (mtimeSec) {
|
|
@@ -74,33 +77,42 @@ function isBoundary(hay, i) {
|
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
/**
|
|
77
|
-
* Greedy forward match
|
|
78
|
-
*
|
|
80
|
+
* Greedy forward scan. Returns the match end or the needle index that failed
|
|
81
|
+
* (failAt), which the typo retry uses to prune deletions provably unable to
|
|
82
|
+
* match (see matchWithTypos).
|
|
79
83
|
*/
|
|
80
|
-
function
|
|
81
|
-
const hayCmp = caseSensitive ? hay : hay.toLowerCase();
|
|
82
|
-
const nCmp = caseSensitive ? needle : needle.toLowerCase();
|
|
84
|
+
function scanForward(nCmp, hayCmp) {
|
|
83
85
|
let hi = 0;
|
|
84
|
-
let firstAt = -1;
|
|
85
86
|
|
|
86
87
|
for (let ni = 0; ni < nCmp.length; ni++) {
|
|
87
88
|
hi = hayCmp.indexOf(nCmp[ni], hi);
|
|
88
89
|
|
|
89
|
-
if (hi < 0) return
|
|
90
|
-
|
|
91
|
-
if (firstAt < 0) firstAt = hi;
|
|
90
|
+
if (hi < 0) return { failAt: ni };
|
|
92
91
|
hi++;
|
|
93
92
|
}
|
|
94
93
|
|
|
95
|
-
|
|
94
|
+
return { end: hi, failAt: -1 };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Greedy forward match with backward tightening (fzf v1). Returns null or
|
|
99
|
+
* { score, start, end }. Score: +16 boundary, +8 consecutive, +4 case match, −1 per gap char.
|
|
100
|
+
* Lowered strings arrive precomputed: the needle once per query, the haystack
|
|
101
|
+
* once per path — never re-lowered per part or per typo variant.
|
|
102
|
+
*/
|
|
103
|
+
function matchOnce(part, pCmp, hay, hayCmp) {
|
|
104
|
+
const scan = scanForward(pCmp, hayCmp);
|
|
105
|
+
|
|
106
|
+
if (scan.failAt >= 0) return null;
|
|
107
|
+
const end = scan.end;
|
|
96
108
|
// Tighten: walk backwards from end to find the latest possible start.
|
|
97
109
|
let start = end;
|
|
98
110
|
|
|
99
|
-
for (let ni =
|
|
100
|
-
start = hayCmp.lastIndexOf(
|
|
111
|
+
for (let ni = pCmp.length - 1; ni >= 0; ni--) {
|
|
112
|
+
start = hayCmp.lastIndexOf(pCmp[ni], start - 1);
|
|
101
113
|
}
|
|
102
114
|
|
|
103
|
-
return { score: scoreAlignment(
|
|
115
|
+
return { score: scoreAlignment(part, pCmp, hay, hayCmp, start), start, end };
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
/** +16 boundary, +8 consecutive, +4 exact-case, −1 per skipped haystack char. */
|
|
@@ -122,9 +134,14 @@ function scoreAlignment(needle, nCmp, hay, hayCmp, start) {
|
|
|
122
134
|
return score;
|
|
123
135
|
}
|
|
124
136
|
|
|
125
|
-
function considerShorter(
|
|
126
|
-
for (let i = 0; i
|
|
127
|
-
const m = visit(
|
|
137
|
+
function considerShorter(sub, subCmp, subLower, typosLeft, visit, best, maxDel) {
|
|
138
|
+
for (let i = 0; i <= maxDel; i++) {
|
|
139
|
+
const m = visit(
|
|
140
|
+
sub.slice(0, i) + sub.slice(i + 1),
|
|
141
|
+
subCmp.slice(0, i) + subCmp.slice(i + 1),
|
|
142
|
+
subLower.slice(0, i) + subLower.slice(i + 1),
|
|
143
|
+
typosLeft - 1,
|
|
144
|
+
);
|
|
128
145
|
|
|
129
146
|
if (!m) continue;
|
|
130
147
|
const scored = { ...m, score: m.score - 12, typos: m.typos + 1, exact: false };
|
|
@@ -135,46 +152,95 @@ function considerShorter(part, typosLeft, visit, best) {
|
|
|
135
152
|
return best;
|
|
136
153
|
}
|
|
137
154
|
|
|
138
|
-
|
|
155
|
+
// Failure pruning (exact, not heuristic): a deletion strictly after the
|
|
156
|
+
// fail index preserves the failing prefix, so that child fails too — and
|
|
157
|
+
// every deeper success deletes an early char first, which the unpruned
|
|
158
|
+
// order reaches with the same typo count via memo. On success all
|
|
159
|
+
// deletions are still explored (a shorter variant can outscore the -12).
|
|
160
|
+
// This turns full-miss retries from O(len^typos) attempts into O(len×typos).
|
|
161
|
+
function matchWithTypos(part, pCmp, partLower, hay, hayCmp, hayLowerOrNull, maxTypos) {
|
|
139
162
|
const memo = new Map();
|
|
163
|
+
let hayLower = hayLowerOrNull;
|
|
140
164
|
|
|
141
|
-
const visit = (
|
|
142
|
-
const key =
|
|
165
|
+
const visit = (sub, subCmp, subLower, typosLeft) => {
|
|
166
|
+
const key = sub + "\0" + typosLeft;
|
|
143
167
|
|
|
144
168
|
if (memo.has(key)) return memo.get(key);
|
|
145
|
-
|
|
169
|
+
const scan = scanForward(subCmp, hayCmp);
|
|
170
|
+
let best = null;
|
|
171
|
+
let maxDel = sub.length - 1;
|
|
172
|
+
|
|
173
|
+
if (scan.failAt < 0) {
|
|
174
|
+
let start = scan.end;
|
|
175
|
+
|
|
176
|
+
for (let ni = subCmp.length - 1; ni >= 0; ni--) {
|
|
177
|
+
start = hayCmp.lastIndexOf(subCmp[ni], start - 1);
|
|
178
|
+
}
|
|
146
179
|
|
|
147
|
-
|
|
180
|
+
if (hayLower === null) hayLower = hay.toLowerCase();
|
|
181
|
+
best = {
|
|
182
|
+
score: scoreAlignment(sub, subCmp, hay, hayCmp, start),
|
|
183
|
+
start,
|
|
184
|
+
end: scan.end,
|
|
185
|
+
typos: 0,
|
|
186
|
+
exact: hayLower === subLower,
|
|
187
|
+
};
|
|
188
|
+
} else {
|
|
189
|
+
maxDel = scan.failAt;
|
|
190
|
+
}
|
|
148
191
|
|
|
149
|
-
if (typosLeft > 0) best = considerShorter(
|
|
192
|
+
if (typosLeft > 0) best = considerShorter(sub, subCmp, subLower, typosLeft, visit, best, maxDel);
|
|
150
193
|
memo.set(key, best);
|
|
151
194
|
|
|
152
195
|
return best;
|
|
153
196
|
};
|
|
154
197
|
|
|
155
|
-
return visit(
|
|
198
|
+
return visit(part, pCmp, partLower, maxTypos);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function matchPart(part, pCmp, partLower, hay, hayCmp, hayLowerOrNull, maxTypos) {
|
|
202
|
+
const direct = matchOnce(part, pCmp, hay, hayCmp);
|
|
203
|
+
|
|
204
|
+
if (direct) {
|
|
205
|
+
const hayLower = hayLowerOrNull === null ? hay.toLowerCase() : hayLowerOrNull;
|
|
206
|
+
|
|
207
|
+
return { ...direct, typos: 0, exact: hayLower === partLower };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (maxTypos <= 0 || part.length < 3 || part.length > 128) return null;
|
|
211
|
+
|
|
212
|
+
return matchWithTypos(part, pCmp, partLower, hay, hayCmp, hayLowerOrNull, maxTypos);
|
|
156
213
|
}
|
|
157
214
|
|
|
158
215
|
/** Best match allowing up to maxTypos skipped needle characters. */
|
|
159
216
|
export function fuzzyMatch(needle, hay, { maxTypos = 0, caseSensitive = false } = {}) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (direct) return { ...direct, typos: 0, exact: hay.toLowerCase() === needle.toLowerCase() };
|
|
217
|
+
if (caseSensitive) return matchPart(needle, needle, needle.toLowerCase(), hay, hay, null, maxTypos);
|
|
163
218
|
|
|
164
|
-
|
|
219
|
+
const needleLower = needle.toLowerCase();
|
|
220
|
+
const hayLower = hay.toLowerCase();
|
|
165
221
|
|
|
166
|
-
return
|
|
222
|
+
return matchPart(needle, needleLower, needleLower, hay, hayLower, hayLower, maxTypos);
|
|
167
223
|
}
|
|
168
224
|
|
|
169
225
|
export function smartCase(query) {
|
|
170
226
|
return /[A-Z]/.test(query);
|
|
171
227
|
}
|
|
172
228
|
|
|
229
|
+
function splitDirSegs(dir) {
|
|
230
|
+
return dir.split("/").filter(Boolean);
|
|
231
|
+
}
|
|
232
|
+
|
|
173
233
|
/** fff distance penalty: directory hops from the current file's directory, floor −20. */
|
|
174
|
-
function distancePenalty(
|
|
175
|
-
if (!
|
|
176
|
-
|
|
177
|
-
|
|
234
|
+
function distancePenalty(currentSegs, candidateDir, dirCache) {
|
|
235
|
+
if (!currentSegs) return 0;
|
|
236
|
+
let b = dirCache.get(candidateDir);
|
|
237
|
+
|
|
238
|
+
if (!b) {
|
|
239
|
+
b = splitDirSegs(candidateDir);
|
|
240
|
+
dirCache.set(candidateDir, b);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const a = currentSegs;
|
|
178
244
|
let common = 0;
|
|
179
245
|
|
|
180
246
|
while (common < a.length && common < b.length && a[common] === b[common]) common++;
|
|
@@ -191,13 +257,14 @@ function partTypos(parts, ctx) {
|
|
|
191
257
|
return ctx.maxTypos ?? (parts[0].length >= 6 ? 2 : parts[0].length >= 4 ? 1 : 0);
|
|
192
258
|
}
|
|
193
259
|
|
|
194
|
-
function scoredPath(rel, parts, maxTypos, caseSensitive, ctx,
|
|
195
|
-
const
|
|
260
|
+
function scoredPath(rel, parts, partLower, maxTypos, caseSensitive, ctx, currentSegs, dirCache) {
|
|
261
|
+
const hayCmp = caseSensitive ? rel : rel.toLowerCase();
|
|
262
|
+
const matched = matchParts(parts, partLower, rel, hayCmp, caseSensitive ? null : hayCmp, maxTypos, caseSensitive);
|
|
196
263
|
|
|
197
264
|
if (!matched) return null;
|
|
198
265
|
const { base, first, exact } = matched;
|
|
199
266
|
const filenameStart = rel.lastIndexOf("/") + 1;
|
|
200
|
-
const boosts = filenameBonus(base, rel, filenameStart, first,
|
|
267
|
+
const boosts = filenameBonus(base, rel, filenameStart, first, partLower[0]) + contextBoost(base, rel, ctx) + distancePenalty(currentSegs, rel.slice(0, filenameStart), dirCache);
|
|
201
268
|
|
|
202
269
|
return { path: rel, score: base + boosts, exact, typos: first.typos };
|
|
203
270
|
}
|
|
@@ -208,11 +275,17 @@ export function rankPaths(query, paths, ctx = {}) {
|
|
|
208
275
|
if (parts.length === 0 || parts.length > 16) return [];
|
|
209
276
|
const caseSensitive = smartCase(query);
|
|
210
277
|
const maxTypos = partTypos(parts, ctx);
|
|
278
|
+
// Per-query hoists: lowered parts once (not once per path per part), the
|
|
279
|
+
// current directory split once (not once per candidate), plus a
|
|
280
|
+
// per-call cache for candidate directory segments (paths share dirs).
|
|
281
|
+
const partLower = parts.map((p) => p.toLowerCase());
|
|
211
282
|
const currentDir = ctx.currentFile ? ctx.currentFile.slice(0, ctx.currentFile.lastIndexOf("/") + 1) : "";
|
|
283
|
+
const currentSegs = currentDir ? splitDirSegs(currentDir) : null;
|
|
284
|
+
const dirCache = new Map();
|
|
212
285
|
const out = [];
|
|
213
286
|
|
|
214
287
|
for (const rel of paths) {
|
|
215
|
-
const scored = scoredPath(rel, parts, maxTypos, caseSensitive, ctx,
|
|
288
|
+
const scored = scoredPath(rel, parts, partLower, maxTypos, caseSensitive, ctx, currentSegs, dirCache);
|
|
216
289
|
|
|
217
290
|
if (scored) out.push(scored);
|
|
218
291
|
}
|
|
@@ -223,13 +296,13 @@ export function rankPaths(query, paths, ctx = {}) {
|
|
|
223
296
|
}
|
|
224
297
|
|
|
225
298
|
/** Every query part must match; later parts get at most one typo (fff narrows per part). Score is the average. */
|
|
226
|
-
function matchParts(parts, rel, maxTypos, caseSensitive) {
|
|
299
|
+
function matchParts(parts, partLower, rel, hayCmp, hayLowerOrNull, maxTypos, caseSensitive) {
|
|
227
300
|
let sum = 0;
|
|
228
301
|
let first = null;
|
|
229
302
|
let exact = true;
|
|
230
303
|
|
|
231
304
|
for (let pi = 0; pi < parts.length; pi++) {
|
|
232
|
-
const m =
|
|
305
|
+
const m = matchPart(parts[pi], caseSensitive ? parts[pi] : partLower[pi], partLower[pi], rel, hayCmp, hayLowerOrNull, pi === 0 ? maxTypos : Math.min(maxTypos, 1));
|
|
233
306
|
|
|
234
307
|
if (!m) return null;
|
|
235
308
|
first ??= m;
|
|
@@ -241,10 +314,10 @@ function matchParts(parts, rel, maxTypos, caseSensitive) {
|
|
|
241
314
|
}
|
|
242
315
|
|
|
243
316
|
/** fff: exact filename +40% of base, any filename match +20%. */
|
|
244
|
-
function filenameBonus(base, rel, filenameStart, first,
|
|
317
|
+
function filenameBonus(base, rel, filenameStart, first, needleLower) {
|
|
245
318
|
if (first.start < filenameStart) return 0;
|
|
246
319
|
|
|
247
|
-
return rel.slice(filenameStart).toLowerCase() ===
|
|
320
|
+
return rel.slice(filenameStart).toLowerCase() === needleLower ? Math.floor((base * 2) / 5) : Math.floor(base / 5);
|
|
248
321
|
}
|
|
249
322
|
|
|
250
323
|
/** fff: frecency boost base·f/100 and +15% for git-modified files. */
|
package/src/context/query.js
CHANGED
|
@@ -12,6 +12,14 @@ const SOURCE_EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".rs",
|
|
|
12
12
|
|
|
13
13
|
const TYPED_EXT = new Set([".ts", ".tsx", ".rs", ".go"]);
|
|
14
14
|
|
|
15
|
+
const BUILD_DIRS = new Set(["node_modules", "dist", "target"]);
|
|
16
|
+
|
|
17
|
+
const TEST_WORDS = new Set(["test", "tests", "testing", "spec", "specs"]);
|
|
18
|
+
|
|
19
|
+
const TYPE_WORDS = new Set(["type", "types", "interface", "interfaces", "schema", "schemas"]);
|
|
20
|
+
|
|
21
|
+
const DOC_WORDS = new Set(["doc", "docs", "documentation", "readme"]);
|
|
22
|
+
|
|
15
23
|
const MAX_NEEDLE_CHARS = 128;
|
|
16
24
|
|
|
17
25
|
/** Light suffix stripping so "terminated" ⊇ "terminat" matches "terminate"; deterministic, no dictionary. */
|
|
@@ -27,9 +35,9 @@ export function tokenizeQuery(query) {
|
|
|
27
35
|
|
|
28
36
|
return {
|
|
29
37
|
tokens: [...new Set(words.filter(word => word.length > 1 && !STOP_WORDS.has(word)))],
|
|
30
|
-
wantsTest: words.some(word =>
|
|
31
|
-
wantsType: words.some(word =>
|
|
32
|
-
wantsDoc: words.some(word =>
|
|
38
|
+
wantsTest: words.some(word => TEST_WORDS.has(word)),
|
|
39
|
+
wantsType: words.some(word => TYPE_WORDS.has(word)),
|
|
40
|
+
wantsDoc: words.some(word => DOC_WORDS.has(word)),
|
|
33
41
|
};
|
|
34
42
|
}
|
|
35
43
|
|
|
@@ -37,7 +45,8 @@ function tokenPathScore(base, words, normalized, tokens) {
|
|
|
37
45
|
let score = 0;
|
|
38
46
|
|
|
39
47
|
for (const token of tokens) {
|
|
40
|
-
|
|
48
|
+
// base === token + "." without the concat alloc: same verdict, no garbage.
|
|
49
|
+
if (base === token || (base.length > token.length && base[token.length] === "." && base.startsWith(token))) score += 60;
|
|
41
50
|
else if (base.includes(token)) score += 30;
|
|
42
51
|
else if (words.includes(token)) score += 15;
|
|
43
52
|
else if (normalized.includes(token)) score += 5;
|
|
@@ -49,7 +58,7 @@ function tokenPathScore(base, words, normalized, tokens) {
|
|
|
49
58
|
function topologyPenalty(normalized, flags) {
|
|
50
59
|
const parts = normalized.split("/");
|
|
51
60
|
|
|
52
|
-
if (parts.some(part =>
|
|
61
|
+
if (parts.some(part => BUILD_DIRS.has(part))) return -100;
|
|
53
62
|
const test = isTestPath(normalized);
|
|
54
63
|
|
|
55
64
|
if (test && !flags.wantsTest) return -50;
|
|
@@ -113,6 +113,19 @@ function grepEntryRows(e, filePath, root, regex, nameRegex, out) {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// One alternation scan per file instead of one full scan per needle — same
|
|
117
|
+
// verdict as needles.some(includes). Needles are escaped: they arrive as
|
|
118
|
+
// literals that may carry regex syntax.
|
|
119
|
+
function anyOfProbe(needles) {
|
|
120
|
+
return new RegExp(needles.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function fileMatchesNeedles(entry, needles, anyOf, probe) {
|
|
124
|
+
if (probe) return probe.test(entry.lower);
|
|
125
|
+
|
|
126
|
+
return anyOf ? needles.some((n) => entry.lower.includes(n)) : needles.every((n) => entry.lower.includes(n));
|
|
127
|
+
}
|
|
128
|
+
|
|
116
129
|
export class WorkspaceIndex {
|
|
117
130
|
constructor(runCommand) {
|
|
118
131
|
this.runCommand = runCommand;
|
|
@@ -358,14 +371,12 @@ export class WorkspaceIndex {
|
|
|
358
371
|
/** Files whose lowercase text contains any (or every) needle; needles are lowercase. */
|
|
359
372
|
filesContaining(files, needles, anyOf) {
|
|
360
373
|
const hits = [];
|
|
374
|
+
const probe = anyOf && needles.length > 1 ? anyOfProbe(needles) : null;
|
|
361
375
|
|
|
362
376
|
for (const filePath of files) {
|
|
363
377
|
const e = this.entry(filePath);
|
|
364
378
|
|
|
365
|
-
if (
|
|
366
|
-
const found = anyOf ? needles.some((n) => e.lower.includes(n)) : needles.every((n) => e.lower.includes(n));
|
|
367
|
-
|
|
368
|
-
if (found) hits.push(filePath);
|
|
379
|
+
if (e && fileMatchesNeedles(e, needles, anyOf, probe)) hits.push(filePath);
|
|
369
380
|
}
|
|
370
381
|
|
|
371
382
|
return hits;
|
|
@@ -15,17 +15,15 @@ function inScope(filePath, dir, includeHidden) {
|
|
|
15
15
|
return !parts.includes(".git") && (includeHidden || !parts.some(part => part.startsWith(".") && part.length > 1));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function makeCandidate(filePath, dir, query, tokens, flags) {
|
|
18
|
+
function makeCandidate(filePath, dir, query, tokens, flags, needles = tokens.map(token => stem(token).slice(0, MAX_NEEDLE_CHARS))) {
|
|
19
19
|
const relative = path.relative(dir, filePath);
|
|
20
20
|
const lower = relative.toLowerCase();
|
|
21
21
|
const base = path.basename(lower);
|
|
22
22
|
|
|
23
23
|
const extension = path.extname(base);
|
|
24
24
|
const stemBase = extension ? base.slice(0, -extension.length) : base;
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const needles = tokens.map(token => stem(token).slice(0, MAX_NEEDLE_CHARS));
|
|
25
|
+
const queryLower = query.toLowerCase();
|
|
26
|
+
const exactPath = lower === queryLower || base === queryLower || stemBase === queryLower;
|
|
29
27
|
|
|
30
28
|
return { path: filePath, pathScore: scorePathTopology(relative, tokens, flags), exactPath,
|
|
31
29
|
pathCoverage: tokens.filter((token, index) => lower.includes(needles[index] ?? token)).length,
|
|
@@ -120,7 +118,7 @@ function parseRgRecord(line, truncated, isLast) {
|
|
|
120
118
|
}
|
|
121
119
|
}
|
|
122
120
|
|
|
123
|
-
function absorbRgHit(candidates, record, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles) {
|
|
121
|
+
function absorbRgHit(candidates, record, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles, candidateNeedles) {
|
|
124
122
|
if (record.type !== "match" && record.type !== "context") return;
|
|
125
123
|
const data = record.data;
|
|
126
124
|
|
|
@@ -131,21 +129,21 @@ function absorbRgHit(candidates, record, dir, includeHidden, overlayText, candid
|
|
|
131
129
|
let candidate = candidates.get(filePath);
|
|
132
130
|
|
|
133
131
|
if (!candidate) {
|
|
134
|
-
candidate = makeCandidate(filePath, candidateRoot, query, tokens, flags);
|
|
132
|
+
candidate = makeCandidate(filePath, candidateRoot, query, tokens, flags, candidateNeedles);
|
|
135
133
|
candidates.set(filePath, candidate);
|
|
136
134
|
}
|
|
137
135
|
|
|
138
136
|
inspectLine(candidate, data.line_number, data.lines.text, query, tokens, needles, record.type === "match");
|
|
139
137
|
}
|
|
140
138
|
|
|
141
|
-
function overlayCandidates(candidates, pendingPaths, overlayText, candidateRoot, query, tokens, flags, needles, signal) {
|
|
139
|
+
function overlayCandidates(candidates, pendingPaths, overlayText, candidateRoot, query, tokens, flags, needles, candidateNeedles, signal) {
|
|
142
140
|
let overlayTruncated = false;
|
|
143
141
|
|
|
144
142
|
for (const filePath of pendingPaths) {
|
|
145
143
|
const pending = overlayText(filePath);
|
|
146
144
|
|
|
147
145
|
if (pending === undefined) continue;
|
|
148
|
-
const candidate = makeCandidate(filePath, candidateRoot, query, tokens, flags);
|
|
146
|
+
const candidate = makeCandidate(filePath, candidateRoot, query, tokens, flags, candidateNeedles);
|
|
149
147
|
overlayTruncated = inspectOverlay(candidate, pending, needles, query, tokens, signal) || overlayTruncated;
|
|
150
148
|
|
|
151
149
|
if (candidate.matched.size) candidates.set(filePath, candidate);
|
|
@@ -176,7 +174,7 @@ async function runContentSearch({ dir, includeHidden, searchNeedles, run, overla
|
|
|
176
174
|
return response;
|
|
177
175
|
}
|
|
178
176
|
|
|
179
|
-
function absorbRgRecords(candidates, response, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles, signal) {
|
|
177
|
+
function absorbRgRecords(candidates, response, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles, candidateNeedles, signal) {
|
|
180
178
|
const records = response.stdout.split("\n");
|
|
181
179
|
|
|
182
180
|
for (let i = 0; i < records.length; i++) {
|
|
@@ -185,17 +183,20 @@ function absorbRgRecords(candidates, response, dir, includeHidden, overlayText,
|
|
|
185
183
|
|
|
186
184
|
if (record === undefined) break;
|
|
187
185
|
if (!record) continue;
|
|
188
|
-
absorbRgHit(candidates, record, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles);
|
|
186
|
+
absorbRgHit(candidates, record, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles, candidateNeedles);
|
|
189
187
|
}
|
|
190
188
|
}
|
|
191
189
|
|
|
192
190
|
async function contentCandidates({ dir, includeHidden, query, tokens, flags, pendingPaths, run, overlayText, signal, exact, diskFiles, focusFile }) {
|
|
193
191
|
const needles = exact ? [query.toLowerCase().slice(0, MAX_NEEDLE_CHARS)] : tokens.map(token => stem(token).slice(0, MAX_NEEDLE_CHARS));
|
|
192
|
+
// Coverage needles are always token-derived (even in exact mode, where the
|
|
193
|
+
// search needles collapse to the query): computed once, not once per file.
|
|
194
|
+
const candidateNeedles = exact ? tokens.map(token => stem(token).slice(0, MAX_NEEDLE_CHARS)) : needles;
|
|
194
195
|
const candidateRoot = focusFile ? path.dirname(focusFile) : dir;
|
|
195
196
|
const candidates = new Map();
|
|
196
197
|
const response = await runContentSearch({ dir, includeHidden, searchNeedles: [...new Set(needles)], run, overlayText, signal, diskFiles, focusFile });
|
|
197
|
-
absorbRgRecords(candidates, response, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles, signal);
|
|
198
|
-
const overlayTruncated = overlayCandidates(candidates, pendingPaths, overlayText, candidateRoot, query, tokens, flags, needles, signal);
|
|
198
|
+
absorbRgRecords(candidates, response, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles, candidateNeedles, signal);
|
|
199
|
+
const overlayTruncated = overlayCandidates(candidates, pendingPaths, overlayText, candidateRoot, query, tokens, flags, needles, candidateNeedles, signal);
|
|
199
200
|
|
|
200
201
|
return { candidates, truncated: response.outputTruncated === true || overlayTruncated };
|
|
201
202
|
}
|