@ff-labs/fff-bun 0.5.3-nightly.ea1f980 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/examples/grep.ts +0 -1
- package/examples/search.ts +229 -58
- package/package.json +9 -9
- package/src/ffi.ts +375 -42
- package/src/finder.ts +91 -3
- package/src/index.ts +5 -0
- package/src/types.ts +82 -8
package/examples/grep.ts
CHANGED
package/examples/search.ts
CHANGED
|
@@ -8,10 +8,18 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Indexes the specified directory (or cwd) and provides an interactive
|
|
10
10
|
* search prompt with detailed metadata about results.
|
|
11
|
+
*
|
|
12
|
+
* Modes:
|
|
13
|
+
* :files - fuzzy file search (default)
|
|
14
|
+
* :dirs - fuzzy directory search
|
|
15
|
+
* :mixed - mixed file + directory search
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
18
|
import { FileFinder } from "../src/index";
|
|
19
|
+
import type { DirItem, FileItem, MixedItem, Score } from "../src/index";
|
|
14
20
|
import * as readline from "node:readline";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { homedir } from "node:os";
|
|
15
23
|
|
|
16
24
|
const RESET = "\x1b[0m";
|
|
17
25
|
const BOLD = "\x1b[1m";
|
|
@@ -20,8 +28,11 @@ const GREEN = "\x1b[32m";
|
|
|
20
28
|
const YELLOW = "\x1b[33m";
|
|
21
29
|
const BLUE = "\x1b[34m";
|
|
22
30
|
const CYAN = "\x1b[36m";
|
|
31
|
+
const MAGENTA = "\x1b[35m";
|
|
23
32
|
const RED = "\x1b[31m";
|
|
24
33
|
|
|
34
|
+
type SearchMode = "files" | "dirs" | "mixed";
|
|
35
|
+
|
|
25
36
|
function formatGitStatus(status: string): string {
|
|
26
37
|
switch (status) {
|
|
27
38
|
case "modified":
|
|
@@ -43,12 +54,30 @@ function formatGitStatus(status: string): string {
|
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
function formatScore(score: number): string {
|
|
46
|
-
|
|
47
|
-
if (score >=
|
|
48
|
-
if (score
|
|
57
|
+
const s = String(score);
|
|
58
|
+
if (score >= 100) return `${GREEN}${s}${RESET}`;
|
|
59
|
+
if (score >= 50) return `${YELLOW}${s}${RESET}`;
|
|
60
|
+
if (score > 0) return `${DIM}${s}${RESET}`;
|
|
49
61
|
return `${DIM}0${RESET}`;
|
|
50
62
|
}
|
|
51
63
|
|
|
64
|
+
/** Pad a plain string first, then wrap with ANSI color. */
|
|
65
|
+
function padColor(
|
|
66
|
+
value: string,
|
|
67
|
+
width: number,
|
|
68
|
+
color: string,
|
|
69
|
+
align: "left" | "right" = "right",
|
|
70
|
+
): string {
|
|
71
|
+
const padded = align === "right" ? value.padStart(width) : value.padEnd(width);
|
|
72
|
+
return `${color}${padded}${RESET}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function scoreColor(score: number): string {
|
|
76
|
+
if (score >= 100) return GREEN;
|
|
77
|
+
if (score >= 50) return YELLOW;
|
|
78
|
+
return DIM;
|
|
79
|
+
}
|
|
80
|
+
|
|
52
81
|
function formatSize(bytes: number): string {
|
|
53
82
|
if (bytes < 1024) return `${bytes}B`;
|
|
54
83
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
|
|
@@ -71,6 +100,68 @@ function formatTime(unixSeconds: number): string {
|
|
|
71
100
|
return date.toLocaleDateString();
|
|
72
101
|
}
|
|
73
102
|
|
|
103
|
+
function formatModeTag(mode: SearchMode): string {
|
|
104
|
+
switch (mode) {
|
|
105
|
+
case "files":
|
|
106
|
+
return `${CYAN}files${RESET}`;
|
|
107
|
+
case "dirs":
|
|
108
|
+
return `${MAGENTA}dirs${RESET}`;
|
|
109
|
+
case "mixed":
|
|
110
|
+
return `${YELLOW}mixed${RESET}`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function printScoreBreakdown(score: Score, indent: string) {
|
|
115
|
+
const breakdown: string[] = [];
|
|
116
|
+
if (score.baseScore > 0) breakdown.push(`base:${score.baseScore}`);
|
|
117
|
+
if (score.filenameBonus > 0) breakdown.push(`filename:+${score.filenameBonus}`);
|
|
118
|
+
if (score.frecencyBoost > 0) breakdown.push(`frecency:+${score.frecencyBoost}`);
|
|
119
|
+
if (score.comboMatchBoost > 0) breakdown.push(`combo:+${score.comboMatchBoost}`);
|
|
120
|
+
if (score.distancePenalty < 0) breakdown.push(`distance:${score.distancePenalty}`);
|
|
121
|
+
if (score.exactMatch) breakdown.push(`${GREEN}exact${RESET}`);
|
|
122
|
+
if (score.matchType) breakdown.push(`[${score.matchType}]`);
|
|
123
|
+
|
|
124
|
+
if (breakdown.length > 0) {
|
|
125
|
+
console.log(`${DIM}${indent}└─ ${breakdown.join(", ")}${RESET}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function printFileResult(item: FileItem, score: Score, showBreakdown: boolean) {
|
|
130
|
+
const git = formatGitStatus(item.gitStatus);
|
|
131
|
+
const sc = padColor(String(score.total), 5, scoreColor(score.total));
|
|
132
|
+
const size = formatSize(item.size).padStart(6);
|
|
133
|
+
const modified = formatTime(item.modified).padEnd(10);
|
|
134
|
+
|
|
135
|
+
console.log(` ${git} │ ${sc} │ ${size} │ ${modified} │ ${item.relativePath}`);
|
|
136
|
+
|
|
137
|
+
if (showBreakdown && score.total > 0) {
|
|
138
|
+
printScoreBreakdown(score, " │ │ │ │ ");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function printDirResult(item: DirItem, score: Score, showBreakdown: boolean) {
|
|
143
|
+
const sc = padColor(String(score.total), 5, scoreColor(score.total));
|
|
144
|
+
const frecency = (
|
|
145
|
+
item.maxAccessFrecency > 0 ? `f:${item.maxAccessFrecency}` : "-"
|
|
146
|
+
).padStart(6);
|
|
147
|
+
|
|
148
|
+
console.log(
|
|
149
|
+
` ${MAGENTA}D${RESET} │ ${sc} │ ${frecency} │ ${"".padEnd(10)} │ ${MAGENTA}${item.relativePath}${RESET}`,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (showBreakdown && score.total > 0) {
|
|
153
|
+
printScoreBreakdown(score, " │ │ │ │ ");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function printMixedResult(mixed: MixedItem, score: Score, showBreakdown: boolean) {
|
|
158
|
+
if (mixed.type === "file") {
|
|
159
|
+
printFileResult(mixed.item, score, showBreakdown);
|
|
160
|
+
} else {
|
|
161
|
+
printDirResult(mixed.item, score, showBreakdown);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
74
165
|
async function main() {
|
|
75
166
|
const targetDir = process.argv[2] || process.cwd();
|
|
76
167
|
|
|
@@ -83,10 +174,21 @@ async function main() {
|
|
|
83
174
|
process.exit(1);
|
|
84
175
|
}
|
|
85
176
|
|
|
86
|
-
//
|
|
177
|
+
// Use the same frecency + history databases as the Neovim plugin
|
|
178
|
+
// so the demo benefits from real access history.
|
|
179
|
+
const nvimCache = process.env.XDG_CACHE_HOME || join(homedir(), ".cache", "nvim");
|
|
180
|
+
const nvimData =
|
|
181
|
+
process.env.XDG_DATA_HOME || join(homedir(), ".local", "share", "nvim");
|
|
182
|
+
const frecencyDbPath = join(nvimCache, "fff_nvim");
|
|
183
|
+
const historyDbPath = join(nvimData, "fff_queries");
|
|
184
|
+
|
|
87
185
|
console.log(`${DIM}Initializing index for: ${targetDir}${RESET}`);
|
|
186
|
+
console.log(`${DIM}Frecency DB: ${frecencyDbPath}${RESET}`);
|
|
88
187
|
const createResult = FileFinder.create({
|
|
89
188
|
basePath: targetDir,
|
|
189
|
+
frecencyDbPath,
|
|
190
|
+
historyDbPath,
|
|
191
|
+
useUnsafeNoLock: true,
|
|
90
192
|
});
|
|
91
193
|
|
|
92
194
|
if (!createResult.ok) {
|
|
@@ -128,91 +230,160 @@ async function main() {
|
|
|
128
230
|
}
|
|
129
231
|
}
|
|
130
232
|
|
|
233
|
+
let mode: SearchMode = "files";
|
|
234
|
+
|
|
131
235
|
// Interactive search loop
|
|
132
236
|
const rl = readline.createInterface({
|
|
133
237
|
input: process.stdin,
|
|
134
238
|
output: process.stdout,
|
|
135
239
|
});
|
|
136
240
|
|
|
137
|
-
console.log(
|
|
138
|
-
|
|
139
|
-
);
|
|
241
|
+
console.log(`\n${BOLD}Commands:${RESET}`);
|
|
242
|
+
console.log(` ${DIM}:files${RESET} - file search mode (default)`);
|
|
243
|
+
console.log(` ${DIM}:dirs${RESET} - directory search mode`);
|
|
244
|
+
console.log(` ${DIM}:mixed${RESET} - mixed files + directories mode`);
|
|
245
|
+
console.log(` ${DIM}q${RESET} - quit`);
|
|
246
|
+
console.log();
|
|
140
247
|
|
|
141
248
|
const prompt = () => {
|
|
142
|
-
rl.question(`${CYAN}
|
|
143
|
-
|
|
249
|
+
rl.question(`${CYAN}[${formatModeTag(mode)}]>${RESET} `, (query) => {
|
|
250
|
+
const trimmed = query.trim();
|
|
251
|
+
|
|
252
|
+
if (trimmed.toLowerCase() === "q" || trimmed.toLowerCase() === "quit") {
|
|
144
253
|
console.log(`\n${DIM}Goodbye!${RESET}`);
|
|
145
254
|
finder.destroy();
|
|
146
255
|
rl.close();
|
|
147
256
|
process.exit(0);
|
|
148
257
|
}
|
|
149
258
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (!result.ok) {
|
|
155
|
-
console.log(`${RED}Search error: ${result.error}${RESET}\n`);
|
|
259
|
+
// Mode switching commands
|
|
260
|
+
if (trimmed === ":files") {
|
|
261
|
+
mode = "files";
|
|
262
|
+
console.log(`${GREEN}Switched to file search mode${RESET}\n`);
|
|
156
263
|
prompt();
|
|
157
264
|
return;
|
|
158
265
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
console.log();
|
|
163
|
-
console.log(
|
|
164
|
-
`${DIM}Found ${BOLD}${totalMatched}${RESET}${DIM} matches in ${totalFiles} files (${searchTime}ms)${RESET}`,
|
|
165
|
-
);
|
|
166
|
-
console.log();
|
|
167
|
-
|
|
168
|
-
if (items.length === 0) {
|
|
169
|
-
console.log(`${DIM}No matches found.${RESET}\n`);
|
|
266
|
+
if (trimmed === ":dirs") {
|
|
267
|
+
mode = "dirs";
|
|
268
|
+
console.log(`${MAGENTA}Switched to directory search mode${RESET}\n`);
|
|
170
269
|
prompt();
|
|
171
270
|
return;
|
|
172
271
|
}
|
|
272
|
+
if (trimmed === ":mixed") {
|
|
273
|
+
mode = "mixed";
|
|
274
|
+
console.log(`${YELLOW}Switched to mixed search mode${RESET}\n`);
|
|
275
|
+
prompt();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const searchStart = Date.now();
|
|
173
280
|
|
|
174
|
-
|
|
175
|
-
|
|
281
|
+
if (mode === "files") {
|
|
282
|
+
const result = finder.fileSearch(trimmed, { pageSize: 15 });
|
|
283
|
+
const searchTime = Date.now() - searchStart;
|
|
176
284
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
285
|
+
if (!result.ok) {
|
|
286
|
+
console.log(`${RED}Search error: ${result.error}${RESET}\n`);
|
|
287
|
+
prompt();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
181
290
|
|
|
182
|
-
const
|
|
183
|
-
const totalScore = formatScore(score.total);
|
|
184
|
-
const size = formatSize(item.size).padStart(6);
|
|
185
|
-
const modified = formatTime(item.modified).padEnd(10);
|
|
186
|
-
const path = item.relativePath;
|
|
291
|
+
const { items, scores, totalMatched, totalFiles } = result.value;
|
|
187
292
|
|
|
293
|
+
console.log();
|
|
188
294
|
console.log(
|
|
189
|
-
|
|
295
|
+
`${DIM}Found ${BOLD}${totalMatched}${RESET}${DIM} matches in ${totalFiles} files (${searchTime}ms)${RESET}`,
|
|
190
296
|
);
|
|
297
|
+
console.log();
|
|
191
298
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (score.filenameBonus > 0) breakdown.push(`filename:+${score.filenameBonus}`);
|
|
197
|
-
if (score.frecencyBoost > 0) breakdown.push(`frecency:+${score.frecencyBoost}`);
|
|
198
|
-
if (score.comboMatchBoost > 0)
|
|
199
|
-
breakdown.push(`combo:+${score.comboMatchBoost}`);
|
|
200
|
-
if (score.distancePenalty < 0)
|
|
201
|
-
breakdown.push(`distance:${score.distancePenalty}`);
|
|
202
|
-
if (score.exactMatch) breakdown.push(`${GREEN}exact${RESET}`);
|
|
203
|
-
|
|
204
|
-
if (breakdown.length > 0) {
|
|
205
|
-
console.log(
|
|
206
|
-
`${DIM} │ │ │ │ └─ ${breakdown.join(", ")}${RESET}`,
|
|
207
|
-
);
|
|
208
|
-
}
|
|
299
|
+
if (items.length === 0) {
|
|
300
|
+
console.log(`${DIM}No matches found.${RESET}\n`);
|
|
301
|
+
prompt();
|
|
302
|
+
return;
|
|
209
303
|
}
|
|
210
|
-
}
|
|
211
304
|
|
|
212
|
-
|
|
305
|
+
console.log(`${DIM} Git │ Score │ Size │ Modified │ Path${RESET}`);
|
|
306
|
+
|
|
307
|
+
for (let i = 0; i < items.length; i++) {
|
|
308
|
+
printFileResult(items[i], scores[i], i < 3);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (totalMatched > items.length) {
|
|
312
|
+
console.log(
|
|
313
|
+
`${DIM} │ │ │ │ ... and ${totalMatched - items.length} more${RESET}`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
} else if (mode === "dirs") {
|
|
317
|
+
const result = finder.directorySearch(trimmed, { pageSize: 15 });
|
|
318
|
+
const searchTime = Date.now() - searchStart;
|
|
319
|
+
|
|
320
|
+
if (!result.ok) {
|
|
321
|
+
console.log(`${RED}Search error: ${result.error}${RESET}\n`);
|
|
322
|
+
prompt();
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const { items, scores, totalMatched, totalDirs } = result.value;
|
|
327
|
+
|
|
328
|
+
console.log();
|
|
329
|
+
console.log(
|
|
330
|
+
`${DIM}Found ${BOLD}${totalMatched}${RESET}${DIM} matches in ${totalDirs} directories (${searchTime}ms)${RESET}`,
|
|
331
|
+
);
|
|
332
|
+
console.log();
|
|
333
|
+
|
|
334
|
+
if (items.length === 0) {
|
|
335
|
+
console.log(`${DIM}No matches found.${RESET}\n`);
|
|
336
|
+
prompt();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
console.log(`${DIM} Dir │ Score │ Frecnc │ │ Path${RESET}`);
|
|
341
|
+
|
|
342
|
+
for (let i = 0; i < items.length; i++) {
|
|
343
|
+
printDirResult(items[i], scores[i], i < 3);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (totalMatched > items.length) {
|
|
347
|
+
console.log(
|
|
348
|
+
`${DIM} │ │ │ │ ... and ${totalMatched - items.length} more${RESET}`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
// mixed mode
|
|
353
|
+
const result = finder.mixedSearch(trimmed, { pageSize: 20 });
|
|
354
|
+
const searchTime = Date.now() - searchStart;
|
|
355
|
+
|
|
356
|
+
if (!result.ok) {
|
|
357
|
+
console.log(`${RED}Search error: ${result.error}${RESET}\n`);
|
|
358
|
+
prompt();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const { items, scores, totalMatched, totalFiles, totalDirs } = result.value;
|
|
363
|
+
|
|
364
|
+
console.log();
|
|
213
365
|
console.log(
|
|
214
|
-
`${DIM}
|
|
366
|
+
`${DIM}Found ${BOLD}${totalMatched}${RESET}${DIM} matches (${totalFiles} files, ${totalDirs} dirs) (${searchTime}ms)${RESET}`,
|
|
215
367
|
);
|
|
368
|
+
console.log();
|
|
369
|
+
|
|
370
|
+
if (items.length === 0) {
|
|
371
|
+
console.log(`${DIM}No matches found.${RESET}\n`);
|
|
372
|
+
prompt();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
console.log(`${DIM} Type │ Score │ Info │ Modified │ Path${RESET}`);
|
|
377
|
+
|
|
378
|
+
for (let i = 0; i < items.length; i++) {
|
|
379
|
+
printMixedResult(items[i], scores[i], i < 5);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (totalMatched > items.length) {
|
|
383
|
+
console.log(
|
|
384
|
+
`${DIM} │ │ │ │ ... and ${totalMatched - items.length} more${RESET}`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
216
387
|
}
|
|
217
388
|
|
|
218
389
|
console.log();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ff-labs/fff-bun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "High-performance fuzzy file finder for Bun - perfect for LLM agent tools",
|
|
6
6
|
"type": "module",
|
|
@@ -62,14 +62,14 @@
|
|
|
62
62
|
},
|
|
63
63
|
"homepage": "https://github.com/dmtrKovalenko/fff.nvim#readme",
|
|
64
64
|
"optionalDependencies": {
|
|
65
|
-
"@ff-labs/fff-bin-darwin-arm64": "0.
|
|
66
|
-
"@ff-labs/fff-bin-darwin-x64": "0.
|
|
67
|
-
"@ff-labs/fff-bin-linux-x64-gnu": "0.
|
|
68
|
-
"@ff-labs/fff-bin-linux-arm64-gnu": "0.
|
|
69
|
-
"@ff-labs/fff-bin-linux-x64-musl": "0.
|
|
70
|
-
"@ff-labs/fff-bin-linux-arm64-musl": "0.
|
|
71
|
-
"@ff-labs/fff-bin-win32-x64": "0.
|
|
72
|
-
"@ff-labs/fff-bin-win32-arm64": "0.
|
|
65
|
+
"@ff-labs/fff-bin-darwin-arm64": "0.6.0",
|
|
66
|
+
"@ff-labs/fff-bin-darwin-x64": "0.6.0",
|
|
67
|
+
"@ff-labs/fff-bin-linux-x64-gnu": "0.6.0",
|
|
68
|
+
"@ff-labs/fff-bin-linux-arm64-gnu": "0.6.0",
|
|
69
|
+
"@ff-labs/fff-bin-linux-x64-musl": "0.6.0",
|
|
70
|
+
"@ff-labs/fff-bin-linux-arm64-musl": "0.6.0",
|
|
71
|
+
"@ff-labs/fff-bin-win32-x64": "0.6.0",
|
|
72
|
+
"@ff-labs/fff-bin-win32-arm64": "0.6.0"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@types/bun": "^1.3.8",
|
package/src/ffi.ts
CHANGED
|
@@ -11,10 +11,14 @@
|
|
|
11
11
|
import { CString, dlopen, FFIType, type Pointer, ptr, read } from "bun:ffi";
|
|
12
12
|
import { findBinary } from "./download";
|
|
13
13
|
import type {
|
|
14
|
+
DirItem,
|
|
15
|
+
DirSearchResult,
|
|
14
16
|
FileItem,
|
|
15
17
|
GrepMatch,
|
|
16
18
|
GrepResult,
|
|
17
19
|
Location,
|
|
20
|
+
MixedItem,
|
|
21
|
+
MixedSearchResult,
|
|
18
22
|
Result,
|
|
19
23
|
ScanProgress,
|
|
20
24
|
Score,
|
|
@@ -46,7 +50,9 @@ const ffiDefinition = {
|
|
|
46
50
|
FFIType.cstring, // frecency_db_path
|
|
47
51
|
FFIType.cstring, // history_db_path
|
|
48
52
|
FFIType.bool, // use_unsafe_no_lock
|
|
49
|
-
FFIType.bool, //
|
|
53
|
+
FFIType.bool, // enable_mmap_cache
|
|
54
|
+
FFIType.bool, // enable_content_indexing
|
|
55
|
+
FFIType.bool, // watch
|
|
50
56
|
FFIType.bool, // ai_mode
|
|
51
57
|
],
|
|
52
58
|
returns: FFIType.ptr,
|
|
@@ -71,6 +77,34 @@ const ffiDefinition = {
|
|
|
71
77
|
returns: FFIType.ptr,
|
|
72
78
|
},
|
|
73
79
|
|
|
80
|
+
// Directory search
|
|
81
|
+
fff_search_directories: {
|
|
82
|
+
args: [
|
|
83
|
+
FFIType.ptr, // handle
|
|
84
|
+
FFIType.cstring, // query
|
|
85
|
+
FFIType.cstring, // current_file
|
|
86
|
+
FFIType.u32, // max_threads
|
|
87
|
+
FFIType.u32, // page_index
|
|
88
|
+
FFIType.u32, // page_size
|
|
89
|
+
],
|
|
90
|
+
returns: FFIType.ptr,
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Mixed search (files + directories)
|
|
94
|
+
fff_search_mixed: {
|
|
95
|
+
args: [
|
|
96
|
+
FFIType.ptr, // handle
|
|
97
|
+
FFIType.cstring, // query
|
|
98
|
+
FFIType.cstring, // current_file
|
|
99
|
+
FFIType.u32, // max_threads
|
|
100
|
+
FFIType.u32, // page_index
|
|
101
|
+
FFIType.u32, // page_size
|
|
102
|
+
FFIType.i32, // combo_boost_multiplier
|
|
103
|
+
FFIType.u32, // min_combo_count
|
|
104
|
+
],
|
|
105
|
+
returns: FFIType.ptr,
|
|
106
|
+
},
|
|
107
|
+
|
|
74
108
|
// Live grep (content search)
|
|
75
109
|
fff_live_grep: {
|
|
76
110
|
args: [
|
|
@@ -118,6 +152,10 @@ const ffiDefinition = {
|
|
|
118
152
|
args: [FFIType.ptr],
|
|
119
153
|
returns: FFIType.bool,
|
|
120
154
|
},
|
|
155
|
+
fff_get_base_path: {
|
|
156
|
+
args: [FFIType.ptr],
|
|
157
|
+
returns: FFIType.ptr,
|
|
158
|
+
},
|
|
121
159
|
fff_get_scan_progress: {
|
|
122
160
|
args: [FFIType.ptr],
|
|
123
161
|
returns: FFIType.ptr,
|
|
@@ -171,6 +209,34 @@ const ffiDefinition = {
|
|
|
171
209
|
returns: FFIType.ptr,
|
|
172
210
|
},
|
|
173
211
|
|
|
212
|
+
// Dir search result accessors / free
|
|
213
|
+
fff_free_dir_search_result: {
|
|
214
|
+
args: [FFIType.ptr],
|
|
215
|
+
returns: FFIType.void,
|
|
216
|
+
},
|
|
217
|
+
fff_dir_search_result_get_item: {
|
|
218
|
+
args: [FFIType.ptr, FFIType.u32],
|
|
219
|
+
returns: FFIType.ptr,
|
|
220
|
+
},
|
|
221
|
+
fff_dir_search_result_get_score: {
|
|
222
|
+
args: [FFIType.ptr, FFIType.u32],
|
|
223
|
+
returns: FFIType.ptr,
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
// Mixed search result accessors / free
|
|
227
|
+
fff_free_mixed_search_result: {
|
|
228
|
+
args: [FFIType.ptr],
|
|
229
|
+
returns: FFIType.void,
|
|
230
|
+
},
|
|
231
|
+
fff_mixed_search_result_get_item: {
|
|
232
|
+
args: [FFIType.ptr, FFIType.u32],
|
|
233
|
+
returns: FFIType.ptr,
|
|
234
|
+
},
|
|
235
|
+
fff_mixed_search_result_get_score: {
|
|
236
|
+
args: [FFIType.ptr, FFIType.u32],
|
|
237
|
+
returns: FFIType.ptr,
|
|
238
|
+
},
|
|
239
|
+
|
|
174
240
|
// Grep result accessors / free
|
|
175
241
|
fff_free_grep_result: {
|
|
176
242
|
args: [FFIType.ptr],
|
|
@@ -355,7 +421,9 @@ export function ffiCreate(
|
|
|
355
421
|
frecencyDbPath: string,
|
|
356
422
|
historyDbPath: string,
|
|
357
423
|
useUnsafeNoLock: boolean,
|
|
358
|
-
|
|
424
|
+
enableMmapCache: boolean,
|
|
425
|
+
enableContentIndexing: boolean,
|
|
426
|
+
watch: boolean,
|
|
359
427
|
aiMode: boolean,
|
|
360
428
|
): Result<NativeHandle> {
|
|
361
429
|
const library = loadLibrary();
|
|
@@ -364,7 +432,9 @@ export function ffiCreate(
|
|
|
364
432
|
ptr(encodeString(frecencyDbPath)),
|
|
365
433
|
ptr(encodeString(historyDbPath)),
|
|
366
434
|
useUnsafeNoLock,
|
|
367
|
-
|
|
435
|
+
enableMmapCache,
|
|
436
|
+
enableContentIndexing,
|
|
437
|
+
watch,
|
|
368
438
|
aiMode,
|
|
369
439
|
);
|
|
370
440
|
|
|
@@ -417,18 +487,17 @@ const SR_LOC_COL = 36; // i32 (4)
|
|
|
417
487
|
const SR_LOC_END_LINE = 40; // i32 (4)
|
|
418
488
|
const SR_LOC_END_COL = 44; // i32 (4)
|
|
419
489
|
|
|
420
|
-
// FffFileItem (
|
|
421
|
-
const
|
|
422
|
-
const
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
const
|
|
428
|
-
const
|
|
429
|
-
const
|
|
430
|
-
const
|
|
431
|
-
const FI_SIZE_OF = 80;
|
|
490
|
+
// FffFileItem (72 bytes)
|
|
491
|
+
const FI_RELPATH = 0; // *mut c_char (8)
|
|
492
|
+
const FI_FNAME = 8; // *mut c_char (8)
|
|
493
|
+
const FI_GIT = 16; // *mut c_char (8)
|
|
494
|
+
const FI_SIZE = 24; // u64 (8)
|
|
495
|
+
const FI_MODIFIED = 32; // u64 (8)
|
|
496
|
+
const FI_ACCESS = 40; // i64 (8)
|
|
497
|
+
const FI_MODFR = 48; // i64 (8)
|
|
498
|
+
const FI_TOTAL_FR = 56; // i64 (8)
|
|
499
|
+
const _FI_BINARY = 64; // bool (1 + 7 pad)
|
|
500
|
+
const FI_SIZE_OF = 72;
|
|
432
501
|
|
|
433
502
|
// FffScore (48 bytes)
|
|
434
503
|
const SC_TOTAL = 0; // i32 (4)
|
|
@@ -454,7 +523,6 @@ function asPtr(n: number): Pointer {
|
|
|
454
523
|
function readFileItemStruct(p: number): FileItem {
|
|
455
524
|
const pp = asPtr(p);
|
|
456
525
|
return {
|
|
457
|
-
path: readCString(read.ptr(pp, FI_PATH)) ?? "",
|
|
458
526
|
relativePath: readCString(read.ptr(pp, FI_RELPATH)) ?? "",
|
|
459
527
|
fileName: readCString(read.ptr(pp, FI_FNAME)) ?? "",
|
|
460
528
|
gitStatus: readCString(read.ptr(pp, FI_GIT)) ?? "",
|
|
@@ -548,44 +616,251 @@ function parseSearchResult(resultPtr: Pointer | null): Result<SearchResult> {
|
|
|
548
616
|
return { ok: true, value: result };
|
|
549
617
|
}
|
|
550
618
|
|
|
619
|
+
// ---------------------------------------------------------------------------
|
|
620
|
+
// FffDirSearchResult byte offsets (must match #[repr(C)] layout on 64-bit)
|
|
621
|
+
// { items: *mut, scores: *mut, count: u32, total_matched: u32, total_dirs: u32 }
|
|
622
|
+
// ---------------------------------------------------------------------------
|
|
623
|
+
const DSR_ITEMS = 0; // *mut FffDirItem (8)
|
|
624
|
+
const DSR_SCORES = 8; // *mut FffScore (8)
|
|
625
|
+
const DSR_COUNT = 16; // u32 (4)
|
|
626
|
+
const DSR_MATCHED = 20; // u32 (4)
|
|
627
|
+
const DSR_TOTAL_DIRS = 24; // u32 (4)
|
|
628
|
+
|
|
629
|
+
// FffDirItem (24 bytes: 8 + 8 + 4 + 4pad)
|
|
630
|
+
const DI_RELPATH = 0; // *mut c_char (8)
|
|
631
|
+
const DI_DIRNAME = 8; // *mut c_char (8)
|
|
632
|
+
const DI_MAX_FRECENCY = 16; // i32 (4)
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Read an FffDirItem struct at the given raw address.
|
|
636
|
+
*/
|
|
637
|
+
function readDirItemStruct(p: number): DirItem {
|
|
638
|
+
const pp = asPtr(p);
|
|
639
|
+
return {
|
|
640
|
+
relativePath: readCString(read.ptr(pp, DI_RELPATH)) ?? "",
|
|
641
|
+
dirName: readCString(read.ptr(pp, DI_DIRNAME)) ?? "",
|
|
642
|
+
maxAccessFrecency: read.i32(pp, DI_MAX_FRECENCY),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Parse an FffDirSearchResult from a raw FffResult pointer, then free native memory.
|
|
648
|
+
*/
|
|
649
|
+
function parseDirSearchResult(resultPtr: Pointer | null): Result<DirSearchResult> {
|
|
650
|
+
if (resultPtr === null) {
|
|
651
|
+
return err("FFI returned null pointer");
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const envelope = readResultEnvelope(resultPtr);
|
|
655
|
+
if (!("success" in envelope)) return envelope;
|
|
656
|
+
|
|
657
|
+
if (envelope.handlePtr === 0) {
|
|
658
|
+
return err("fff_search_directories returned null search result");
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const hp = asPtr(envelope.handlePtr);
|
|
662
|
+
const count = read.u32(hp, DSR_COUNT);
|
|
663
|
+
const totalMatched = read.u32(hp, DSR_MATCHED);
|
|
664
|
+
const totalDirs = read.u32(hp, DSR_TOTAL_DIRS);
|
|
665
|
+
|
|
666
|
+
const library = loadLibrary();
|
|
667
|
+
|
|
668
|
+
const items: DirItem[] = [];
|
|
669
|
+
const scores: Score[] = [];
|
|
670
|
+
|
|
671
|
+
for (let i = 0; i < count; i++) {
|
|
672
|
+
const itemPtr = library.symbols.fff_dir_search_result_get_item(hp, i);
|
|
673
|
+
if (itemPtr !== null && (itemPtr as unknown as number) !== 0) {
|
|
674
|
+
items.push(readDirItemStruct(itemPtr as unknown as number));
|
|
675
|
+
}
|
|
676
|
+
const scorePtr = library.symbols.fff_dir_search_result_get_score(hp, i);
|
|
677
|
+
if (scorePtr !== null && (scorePtr as unknown as number) !== 0) {
|
|
678
|
+
scores.push(readScoreStruct(scorePtr as unknown as number));
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Free native dir search result
|
|
683
|
+
library.symbols.fff_free_dir_search_result(hp);
|
|
684
|
+
|
|
685
|
+
return { ok: true, value: { items, scores, totalMatched, totalDirs } };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ---------------------------------------------------------------------------
|
|
689
|
+
// FffMixedSearchResult byte offsets (must match #[repr(C)] layout on 64-bit)
|
|
690
|
+
// { items: *mut, scores: *mut, count: u32, total_matched: u32, total_files: u32, total_dirs: u32, location: FffLocation }
|
|
691
|
+
// ---------------------------------------------------------------------------
|
|
692
|
+
const MSR_ITEMS = 0; // *mut FffMixedItem (8)
|
|
693
|
+
const MSR_SCORES = 8; // *mut FffScore (8)
|
|
694
|
+
const MSR_COUNT = 16; // u32 (4)
|
|
695
|
+
const MSR_MATCHED = 20; // u32 (4)
|
|
696
|
+
const MSR_TOTAL_FILES = 24; // u32 (4)
|
|
697
|
+
const MSR_TOTAL_DIRS = 28; // u32 (4)
|
|
698
|
+
// FffLocation is inlined at offset 32
|
|
699
|
+
const MSR_LOC_TAG = 32; // u8 (1 + 3 padding)
|
|
700
|
+
const MSR_LOC_LINE = 36; // i32 (4)
|
|
701
|
+
const MSR_LOC_COL = 40; // i32 (4)
|
|
702
|
+
const MSR_LOC_END_LINE = 44; // i32 (4)
|
|
703
|
+
const MSR_LOC_END_COL = 48; // i32 (4)
|
|
704
|
+
|
|
705
|
+
// FffMixedItem (80 bytes)
|
|
706
|
+
const MI_TYPE = 0; // u8 (1 + 7 pad)
|
|
707
|
+
const MI_RELPATH = 8; // *mut c_char (8)
|
|
708
|
+
const MI_DISPLAY = 16; // *mut c_char (8)
|
|
709
|
+
const MI_GIT = 24; // *mut c_char (8)
|
|
710
|
+
const MI_SIZE = 32; // u64 (8)
|
|
711
|
+
const MI_MODIFIED = 40; // u64 (8)
|
|
712
|
+
const MI_ACCESS = 48; // i64 (8)
|
|
713
|
+
const MI_MODFR = 56; // i64 (8)
|
|
714
|
+
const MI_TOTAL_FR = 64; // i64 (8)
|
|
715
|
+
const MI_BINARY = 72; // bool (1 + 7 pad)
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Read an FffMixedItem struct at the given raw address and return a MixedItem.
|
|
719
|
+
*/
|
|
720
|
+
function readMixedItemStruct(p: number): MixedItem {
|
|
721
|
+
const pp = asPtr(p);
|
|
722
|
+
const itemType = read.u8(pp, MI_TYPE);
|
|
723
|
+
|
|
724
|
+
if (itemType === 1) {
|
|
725
|
+
// Directory
|
|
726
|
+
return {
|
|
727
|
+
type: "directory",
|
|
728
|
+
item: {
|
|
729
|
+
relativePath: readCString(read.ptr(pp, MI_RELPATH)) ?? "",
|
|
730
|
+
dirName: readCString(read.ptr(pp, MI_DISPLAY)) ?? "",
|
|
731
|
+
maxAccessFrecency: Number(read.i64(pp, MI_ACCESS)),
|
|
732
|
+
},
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// File (itemType === 0)
|
|
737
|
+
return {
|
|
738
|
+
type: "file",
|
|
739
|
+
item: {
|
|
740
|
+
relativePath: readCString(read.ptr(pp, MI_RELPATH)) ?? "",
|
|
741
|
+
fileName: readCString(read.ptr(pp, MI_DISPLAY)) ?? "",
|
|
742
|
+
gitStatus: readCString(read.ptr(pp, MI_GIT)) ?? "",
|
|
743
|
+
size: Number(read.u64(pp, MI_SIZE)),
|
|
744
|
+
modified: Number(read.u64(pp, MI_MODIFIED)),
|
|
745
|
+
accessFrecencyScore: Number(read.i64(pp, MI_ACCESS)),
|
|
746
|
+
modificationFrecencyScore: Number(read.i64(pp, MI_MODFR)),
|
|
747
|
+
totalFrecencyScore: Number(read.i64(pp, MI_TOTAL_FR)),
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Parse an FffMixedSearchResult from a raw FffResult pointer, then free native memory.
|
|
754
|
+
*/
|
|
755
|
+
function parseMixedSearchResult(resultPtr: Pointer | null): Result<MixedSearchResult> {
|
|
756
|
+
if (resultPtr === null) {
|
|
757
|
+
return err("FFI returned null pointer");
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const envelope = readResultEnvelope(resultPtr);
|
|
761
|
+
if (!("success" in envelope)) return envelope;
|
|
762
|
+
|
|
763
|
+
if (envelope.handlePtr === 0) {
|
|
764
|
+
return err("fff_search_mixed returned null search result");
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const hp = asPtr(envelope.handlePtr);
|
|
768
|
+
const count = read.u32(hp, MSR_COUNT);
|
|
769
|
+
const totalMatched = read.u32(hp, MSR_MATCHED);
|
|
770
|
+
const totalFiles = read.u32(hp, MSR_TOTAL_FILES);
|
|
771
|
+
const totalDirs = read.u32(hp, MSR_TOTAL_DIRS);
|
|
772
|
+
|
|
773
|
+
// Read location
|
|
774
|
+
const locTag = read.u8(hp, MSR_LOC_TAG);
|
|
775
|
+
let location: Location | undefined;
|
|
776
|
+
if (locTag === 1) {
|
|
777
|
+
location = { type: "line", line: read.i32(hp, MSR_LOC_LINE) };
|
|
778
|
+
} else if (locTag === 2) {
|
|
779
|
+
location = {
|
|
780
|
+
type: "position",
|
|
781
|
+
line: read.i32(hp, MSR_LOC_LINE),
|
|
782
|
+
col: read.i32(hp, MSR_LOC_COL),
|
|
783
|
+
};
|
|
784
|
+
} else if (locTag === 3) {
|
|
785
|
+
location = {
|
|
786
|
+
type: "range",
|
|
787
|
+
start: { line: read.i32(hp, MSR_LOC_LINE), col: read.i32(hp, MSR_LOC_COL) },
|
|
788
|
+
end: {
|
|
789
|
+
line: read.i32(hp, MSR_LOC_END_LINE),
|
|
790
|
+
col: read.i32(hp, MSR_LOC_END_COL),
|
|
791
|
+
},
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const library = loadLibrary();
|
|
796
|
+
|
|
797
|
+
const items: MixedItem[] = [];
|
|
798
|
+
const scores: Score[] = [];
|
|
799
|
+
|
|
800
|
+
for (let i = 0; i < count; i++) {
|
|
801
|
+
const itemPtr = library.symbols.fff_mixed_search_result_get_item(hp, i);
|
|
802
|
+
if (itemPtr !== null && (itemPtr as unknown as number) !== 0) {
|
|
803
|
+
items.push(readMixedItemStruct(itemPtr as unknown as number));
|
|
804
|
+
}
|
|
805
|
+
const scorePtr = library.symbols.fff_mixed_search_result_get_score(hp, i);
|
|
806
|
+
if (scorePtr !== null && (scorePtr as unknown as number) !== 0) {
|
|
807
|
+
scores.push(readScoreStruct(scorePtr as unknown as number));
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Free native mixed search result
|
|
812
|
+
library.symbols.fff_free_mixed_search_result(hp);
|
|
813
|
+
|
|
814
|
+
const result: MixedSearchResult = {
|
|
815
|
+
items,
|
|
816
|
+
scores,
|
|
817
|
+
totalMatched,
|
|
818
|
+
totalFiles,
|
|
819
|
+
totalDirs,
|
|
820
|
+
};
|
|
821
|
+
if (location) {
|
|
822
|
+
result.location = location;
|
|
823
|
+
}
|
|
824
|
+
return { ok: true, value: result };
|
|
825
|
+
}
|
|
826
|
+
|
|
551
827
|
// ---------------------------------------------------------------------------
|
|
552
828
|
// FffGrepMatch byte offsets (must match #[repr(C)] layout on 64-bit)
|
|
553
829
|
// ---------------------------------------------------------------------------
|
|
554
830
|
|
|
555
831
|
// Pointers (8 bytes each)
|
|
556
|
-
const
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
const
|
|
560
|
-
const
|
|
561
|
-
const
|
|
562
|
-
const
|
|
563
|
-
const GM_CTX_AFTER = 56;
|
|
832
|
+
const GM_RELPATH = 0;
|
|
833
|
+
const GM_FNAME = 8;
|
|
834
|
+
const GM_GIT = 16;
|
|
835
|
+
const GM_LINE_CONTENT = 24;
|
|
836
|
+
const GM_MATCH_RANGES = 32;
|
|
837
|
+
const GM_CTX_BEFORE = 40;
|
|
838
|
+
const GM_CTX_AFTER = 48;
|
|
564
839
|
|
|
565
840
|
// 8-byte numeric fields
|
|
566
|
-
const GM_SIZE =
|
|
567
|
-
const GM_MODIFIED =
|
|
568
|
-
const GM_TOTAL_FR =
|
|
569
|
-
const GM_ACCESS_FR =
|
|
570
|
-
const GM_MOD_FR =
|
|
571
|
-
const GM_LINE_NUM =
|
|
572
|
-
const GM_BYTE_OFF =
|
|
841
|
+
const GM_SIZE = 56;
|
|
842
|
+
const GM_MODIFIED = 64;
|
|
843
|
+
const GM_TOTAL_FR = 72;
|
|
844
|
+
const GM_ACCESS_FR = 80;
|
|
845
|
+
const GM_MOD_FR = 88;
|
|
846
|
+
const GM_LINE_NUM = 96;
|
|
847
|
+
const GM_BYTE_OFF = 104;
|
|
573
848
|
|
|
574
849
|
// 4-byte fields
|
|
575
|
-
const GM_COL =
|
|
576
|
-
const GM_MR_COUNT =
|
|
577
|
-
const GM_CTX_B_COUNT =
|
|
578
|
-
const GM_CTX_A_COUNT =
|
|
850
|
+
const GM_COL = 112;
|
|
851
|
+
const GM_MR_COUNT = 116;
|
|
852
|
+
const GM_CTX_B_COUNT = 120;
|
|
853
|
+
const GM_CTX_A_COUNT = 124;
|
|
579
854
|
|
|
580
855
|
// 2-byte
|
|
581
|
-
const GM_FUZZY_SCORE =
|
|
856
|
+
const GM_FUZZY_SCORE = 128;
|
|
582
857
|
// 1-byte
|
|
583
|
-
const GM_HAS_FUZZY =
|
|
584
|
-
const GM_IS_BINARY =
|
|
585
|
-
const _GM_IS_DEF =
|
|
858
|
+
const GM_HAS_FUZZY = 130;
|
|
859
|
+
const GM_IS_BINARY = 131;
|
|
860
|
+
const _GM_IS_DEF = 132;
|
|
586
861
|
|
|
587
|
-
// struct size: pad to 8-byte alignment →
|
|
588
|
-
const GM_SIZE_OF =
|
|
862
|
+
// struct size: pad to 8-byte alignment → 136
|
|
863
|
+
const GM_SIZE_OF = 136;
|
|
589
864
|
|
|
590
865
|
// FffGrepResult
|
|
591
866
|
const GR_ITEMS = 0; // *mut FffGrepMatch (8)
|
|
@@ -635,7 +910,6 @@ function readGrepMatchStruct(p: number): GrepMatch {
|
|
|
635
910
|
const ctxAfterCount = read.u32(pp, GM_CTX_A_COUNT);
|
|
636
911
|
|
|
637
912
|
const match: GrepMatch = {
|
|
638
|
-
path: readCString(read.ptr(pp, GM_PATH)) ?? "",
|
|
639
913
|
relativePath: readCString(read.ptr(pp, GM_RELPATH)) ?? "",
|
|
640
914
|
fileName: readCString(read.ptr(pp, GM_FNAME)) ?? "",
|
|
641
915
|
gitStatus: readCString(read.ptr(pp, GM_GIT)) ?? "",
|
|
@@ -735,6 +1009,56 @@ export function ffiSearch(
|
|
|
735
1009
|
return parseSearchResult(resultPtr);
|
|
736
1010
|
}
|
|
737
1011
|
|
|
1012
|
+
/**
|
|
1013
|
+
* Perform fuzzy directory search.
|
|
1014
|
+
*/
|
|
1015
|
+
export function ffiSearchDirectories(
|
|
1016
|
+
handle: NativeHandle,
|
|
1017
|
+
query: string,
|
|
1018
|
+
currentFile: string | null,
|
|
1019
|
+
maxThreads: number,
|
|
1020
|
+
pageIndex: number,
|
|
1021
|
+
pageSize: number,
|
|
1022
|
+
): Result<DirSearchResult> {
|
|
1023
|
+
const library = loadLibrary();
|
|
1024
|
+
const resultPtr = library.symbols.fff_search_directories(
|
|
1025
|
+
handle,
|
|
1026
|
+
ptr(encodeString(query)),
|
|
1027
|
+
ptr(encodeString(currentFile ?? "")),
|
|
1028
|
+
maxThreads,
|
|
1029
|
+
pageIndex,
|
|
1030
|
+
pageSize,
|
|
1031
|
+
);
|
|
1032
|
+
return parseDirSearchResult(resultPtr);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Perform mixed (files + directories) fuzzy search.
|
|
1037
|
+
*/
|
|
1038
|
+
export function ffiSearchMixed(
|
|
1039
|
+
handle: NativeHandle,
|
|
1040
|
+
query: string,
|
|
1041
|
+
currentFile: string,
|
|
1042
|
+
maxThreads: number,
|
|
1043
|
+
pageIndex: number,
|
|
1044
|
+
pageSize: number,
|
|
1045
|
+
comboBoostMultiplier: number,
|
|
1046
|
+
minComboCount: number,
|
|
1047
|
+
): Result<MixedSearchResult> {
|
|
1048
|
+
const library = loadLibrary();
|
|
1049
|
+
const resultPtr = library.symbols.fff_search_mixed(
|
|
1050
|
+
handle,
|
|
1051
|
+
ptr(encodeString(query)),
|
|
1052
|
+
ptr(encodeString(currentFile)),
|
|
1053
|
+
maxThreads,
|
|
1054
|
+
pageIndex,
|
|
1055
|
+
pageSize,
|
|
1056
|
+
comboBoostMultiplier,
|
|
1057
|
+
minComboCount,
|
|
1058
|
+
);
|
|
1059
|
+
return parseMixedSearchResult(resultPtr);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
738
1062
|
/**
|
|
739
1063
|
* Live grep - search file contents.
|
|
740
1064
|
*/
|
|
@@ -822,6 +1146,15 @@ export function ffiIsScanning(handle: NativeHandle): boolean {
|
|
|
822
1146
|
return library.symbols.fff_is_scanning(handle) as boolean;
|
|
823
1147
|
}
|
|
824
1148
|
|
|
1149
|
+
/**
|
|
1150
|
+
* Get the base path of the file picker.
|
|
1151
|
+
*/
|
|
1152
|
+
export function ffiGetBasePath(handle: NativeHandle): Result<string | null> {
|
|
1153
|
+
const library = loadLibrary();
|
|
1154
|
+
const resultPtr = library.symbols.fff_get_base_path(handle);
|
|
1155
|
+
return parseStringResult(resultPtr);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
825
1158
|
// FffScanProgress { scanned_files_count: u64(8), is_scanning: bool(1), is_watcher_ready: bool(1), is_warmup_complete: bool(1) + pad }
|
|
826
1159
|
const SP_COUNT = 0; // u64 (8)
|
|
827
1160
|
const SP_SCANNING = 8; // bool (1)
|
package/src/finder.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
ensureLoaded,
|
|
13
13
|
ffiCreate,
|
|
14
14
|
ffiDestroy,
|
|
15
|
+
ffiGetBasePath,
|
|
15
16
|
ffiGetHistoricalQuery,
|
|
16
17
|
ffiGetScanProgress,
|
|
17
18
|
ffiHealthCheck,
|
|
@@ -22,6 +23,8 @@ import {
|
|
|
22
23
|
ffiRestartIndex,
|
|
23
24
|
ffiScanFiles,
|
|
24
25
|
ffiSearch,
|
|
26
|
+
ffiSearchDirectories,
|
|
27
|
+
ffiSearchMixed,
|
|
25
28
|
ffiTrackQuery,
|
|
26
29
|
ffiWaitForScan,
|
|
27
30
|
ffiWaitForWatcher,
|
|
@@ -30,10 +33,13 @@ import {
|
|
|
30
33
|
} from "./ffi";
|
|
31
34
|
|
|
32
35
|
import type {
|
|
36
|
+
DirSearchOptions,
|
|
37
|
+
DirSearchResult,
|
|
33
38
|
GrepOptions,
|
|
34
39
|
GrepResult,
|
|
35
40
|
HealthCheck,
|
|
36
|
-
InitOptions,
|
|
41
|
+
InitOptions as FFFInitOptions,
|
|
42
|
+
MixedSearchResult,
|
|
37
43
|
MultiGrepOptions,
|
|
38
44
|
Result,
|
|
39
45
|
ScanProgress,
|
|
@@ -101,13 +107,15 @@ export class FileFinder {
|
|
|
101
107
|
* });
|
|
102
108
|
* ```
|
|
103
109
|
*/
|
|
104
|
-
static create(options:
|
|
110
|
+
static create(options: FFFInitOptions): Result<FileFinder> {
|
|
105
111
|
const result = ffiCreate(
|
|
106
112
|
options.basePath,
|
|
107
113
|
options.frecencyDbPath ?? "",
|
|
108
114
|
options.historyDbPath ?? "",
|
|
109
115
|
options.useUnsafeNoLock ?? false,
|
|
110
|
-
options.
|
|
116
|
+
!(options.disableMmapCache ?? false),
|
|
117
|
+
!(options.disableContentIndexing ?? options.disableMmapCache ?? false),
|
|
118
|
+
!(options.disableWatch ?? false),
|
|
111
119
|
options.aiMode ?? false,
|
|
112
120
|
);
|
|
113
121
|
|
|
@@ -189,6 +197,77 @@ export class FileFinder {
|
|
|
189
197
|
);
|
|
190
198
|
}
|
|
191
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Search for directories matching the query.
|
|
202
|
+
*
|
|
203
|
+
* @param query - Search query string
|
|
204
|
+
* @param options - Directory search options
|
|
205
|
+
* @returns Search results with matched directories and scores
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* const result = finder.directorySearch("components", { pageSize: 10 });
|
|
210
|
+
* if (result.ok) {
|
|
211
|
+
* console.log(`Found ${result.value.totalMatched} directories`);
|
|
212
|
+
* for (const item of result.value.items) {
|
|
213
|
+
* console.log(item.relativePath);
|
|
214
|
+
* }
|
|
215
|
+
* }
|
|
216
|
+
* ```
|
|
217
|
+
*/
|
|
218
|
+
directorySearch(query: string, options?: DirSearchOptions): Result<DirSearchResult> {
|
|
219
|
+
const guard = this.ensureAlive();
|
|
220
|
+
if (!guard.ok) return guard;
|
|
221
|
+
|
|
222
|
+
return ffiSearchDirectories(
|
|
223
|
+
guard.value,
|
|
224
|
+
query,
|
|
225
|
+
options?.currentFile ?? null,
|
|
226
|
+
options?.maxThreads ?? 0,
|
|
227
|
+
options?.pageIndex ?? 0,
|
|
228
|
+
options?.pageSize ?? 0,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Search for files and directories together (mixed search).
|
|
234
|
+
*
|
|
235
|
+
* Results are interleaved by total score in descending order.
|
|
236
|
+
*
|
|
237
|
+
* @param query - Search query string
|
|
238
|
+
* @param options - Search options
|
|
239
|
+
* @returns Mixed search results with files and directories interleaved by score
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```typescript
|
|
243
|
+
* const result = finder.mixedSearch("main", { pageSize: 20 });
|
|
244
|
+
* if (result.ok) {
|
|
245
|
+
* for (const entry of result.value.items) {
|
|
246
|
+
* if (entry.type === "file") {
|
|
247
|
+
* console.log(`File: ${entry.item.relativePath}`);
|
|
248
|
+
* } else {
|
|
249
|
+
* console.log(`Dir: ${entry.item.relativePath}`);
|
|
250
|
+
* }
|
|
251
|
+
* }
|
|
252
|
+
* }
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
mixedSearch(query: string, options?: SearchOptions): Result<MixedSearchResult> {
|
|
256
|
+
const guard = this.ensureAlive();
|
|
257
|
+
if (!guard.ok) return guard;
|
|
258
|
+
|
|
259
|
+
return ffiSearchMixed(
|
|
260
|
+
guard.value,
|
|
261
|
+
query,
|
|
262
|
+
options?.currentFile ?? "",
|
|
263
|
+
options?.maxThreads ?? 0,
|
|
264
|
+
options?.pageIndex ?? 0,
|
|
265
|
+
options?.pageSize ?? 0,
|
|
266
|
+
options?.comboBoostMultiplier ?? 0,
|
|
267
|
+
options?.minComboCount ?? 0,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
192
271
|
/**
|
|
193
272
|
* Search file contents (live grep).
|
|
194
273
|
*
|
|
@@ -314,6 +393,15 @@ export class FileFinder {
|
|
|
314
393
|
return ffiIsScanning(this.handle);
|
|
315
394
|
}
|
|
316
395
|
|
|
396
|
+
/**
|
|
397
|
+
* Get the base path of the file picker (the root directory being indexed).
|
|
398
|
+
*/
|
|
399
|
+
getBasePath(): Result<string | null> {
|
|
400
|
+
const guard = this.ensureAlive();
|
|
401
|
+
if (!guard.ok) return guard;
|
|
402
|
+
return ffiGetBasePath(guard.value);
|
|
403
|
+
}
|
|
404
|
+
|
|
317
405
|
/**
|
|
318
406
|
* Get the current scan progress.
|
|
319
407
|
*/
|
package/src/index.ts
CHANGED
|
@@ -52,6 +52,9 @@ export {
|
|
|
52
52
|
|
|
53
53
|
export type {
|
|
54
54
|
DbHealth,
|
|
55
|
+
DirItem,
|
|
56
|
+
DirSearchOptions,
|
|
57
|
+
DirSearchResult,
|
|
55
58
|
FileItem,
|
|
56
59
|
GrepCursor,
|
|
57
60
|
GrepMatch,
|
|
@@ -61,6 +64,8 @@ export type {
|
|
|
61
64
|
HealthCheck,
|
|
62
65
|
InitOptions,
|
|
63
66
|
Location,
|
|
67
|
+
MixedItem,
|
|
68
|
+
MixedSearchResult,
|
|
64
69
|
MultiGrepOptions,
|
|
65
70
|
Result,
|
|
66
71
|
ScanProgress,
|
package/src/types.ts
CHANGED
|
@@ -30,12 +30,24 @@ export interface InitOptions {
|
|
|
30
30
|
/** Use unsafe no-lock mode for databases (optional, defaults to false) */
|
|
31
31
|
useUnsafeNoLock?: boolean;
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* at the cost of
|
|
33
|
+
* Disable mmap cache warmup after the initial scan. When mmap cache is
|
|
34
|
+
* enabled (the default), the first grep search is as fast as subsequent
|
|
35
|
+
* ones at the cost of background resources spent on awarming up the cache
|
|
36
|
+
*/
|
|
37
|
+
disableMmapCache?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Disable the content index built after the initial scan.
|
|
40
|
+
* Content indexing enables faster content-aware filtering during grep.
|
|
41
|
+
* When omitted, follows `disableMmapCache` for backward compatibility.
|
|
42
|
+
* (default: follows `disableMmapCache`)
|
|
43
|
+
*/
|
|
44
|
+
disableContentIndexing?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Disable the background file-system watcher. When the watcher is
|
|
47
|
+
* disabled, files are scanned once but not monitored for changes.
|
|
36
48
|
* (default: false)
|
|
37
49
|
*/
|
|
38
|
-
|
|
50
|
+
disableWatch?: boolean;
|
|
39
51
|
/** enables optimizations for AI agent assistants. Provide as true if running via mcp/agent */
|
|
40
52
|
aiMode?: boolean;
|
|
41
53
|
}
|
|
@@ -62,8 +74,6 @@ export interface SearchOptions {
|
|
|
62
74
|
* A file item in search results
|
|
63
75
|
*/
|
|
64
76
|
export interface FileItem {
|
|
65
|
-
/** Absolute path to the file */
|
|
66
|
-
path: string;
|
|
67
77
|
/** Path relative to the indexed directory */
|
|
68
78
|
relativePath: string;
|
|
69
79
|
/** File name only */
|
|
@@ -136,6 +146,72 @@ export interface SearchResult {
|
|
|
136
146
|
location?: Location;
|
|
137
147
|
}
|
|
138
148
|
|
|
149
|
+
/**
|
|
150
|
+
* A directory item in search results
|
|
151
|
+
*/
|
|
152
|
+
export interface DirItem {
|
|
153
|
+
/** Path relative to the indexed directory (e.g., "src/components/") */
|
|
154
|
+
relativePath: string;
|
|
155
|
+
/** Last path segment (e.g., "components/" for "src/components/") */
|
|
156
|
+
dirName: string;
|
|
157
|
+
/** Maximum access frecency score among direct child files */
|
|
158
|
+
maxAccessFrecency: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Search options for directory search (subset of SearchOptions)
|
|
163
|
+
*/
|
|
164
|
+
export interface DirSearchOptions {
|
|
165
|
+
/** Maximum threads for parallel search (0 = auto) */
|
|
166
|
+
maxThreads?: number;
|
|
167
|
+
/** Current file path (for distance scoring) */
|
|
168
|
+
currentFile?: string;
|
|
169
|
+
/** Page index for pagination (default: 0) */
|
|
170
|
+
pageIndex?: number;
|
|
171
|
+
/** Page size for pagination (default: 100) */
|
|
172
|
+
pageSize?: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Search result from fuzzy directory search
|
|
177
|
+
*/
|
|
178
|
+
export interface DirSearchResult {
|
|
179
|
+
/** Matched directory items */
|
|
180
|
+
items: DirItem[];
|
|
181
|
+
/** Corresponding scores for each item */
|
|
182
|
+
scores: Score[];
|
|
183
|
+
/** Total number of directories that matched */
|
|
184
|
+
totalMatched: number;
|
|
185
|
+
/** Total number of indexed directories */
|
|
186
|
+
totalDirs: number;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* A single item in a mixed (files + directories) search result
|
|
191
|
+
*/
|
|
192
|
+
export type MixedItem =
|
|
193
|
+
| { type: "file"; item: FileItem }
|
|
194
|
+
| { type: "directory"; item: DirItem };
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Search result from mixed (files + directories) fuzzy search.
|
|
198
|
+
* Items are interleaved by total score in descending order.
|
|
199
|
+
*/
|
|
200
|
+
export interface MixedSearchResult {
|
|
201
|
+
/** Matched items (files and directories interleaved by score) */
|
|
202
|
+
items: MixedItem[];
|
|
203
|
+
/** Corresponding scores for each item */
|
|
204
|
+
scores: Score[];
|
|
205
|
+
/** Total number of items (files + dirs) that matched */
|
|
206
|
+
totalMatched: number;
|
|
207
|
+
/** Total number of indexed files */
|
|
208
|
+
totalFiles: number;
|
|
209
|
+
/** Total number of indexed directories */
|
|
210
|
+
totalDirs: number;
|
|
211
|
+
/** Location parsed from query */
|
|
212
|
+
location?: Location;
|
|
213
|
+
}
|
|
214
|
+
|
|
139
215
|
/**
|
|
140
216
|
* Scan progress information
|
|
141
217
|
*/
|
|
@@ -271,8 +347,6 @@ export interface GrepOptions {
|
|
|
271
347
|
* A single grep match with file and line information
|
|
272
348
|
*/
|
|
273
349
|
export interface GrepMatch {
|
|
274
|
-
/** Absolute path to the file */
|
|
275
|
-
path: string;
|
|
276
350
|
/** Path relative to the indexed directory */
|
|
277
351
|
relativePath: string;
|
|
278
352
|
/** File name only */
|