@ff-labs/fff-bun 0.1.0-nightly.f69dda4 → 0.2.4-dev.233679d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/examples/grep.ts +1 -1
- package/package.json +9 -9
- package/src/ffi.ts +334 -55
- package/src/finder.ts +50 -25
- package/src/git-lifecycle.test.ts +3 -3
- package/src/index.test.ts +4 -4
- package/src/types.ts +0 -131
package/README.md
CHANGED
|
@@ -113,7 +113,7 @@ Track file access for frecency scoring.
|
|
|
113
113
|
FileFinder.trackAccess("/path/to/file.ts");
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
### `FileFinder.
|
|
116
|
+
### `FileFinder.grep(query, options?)`
|
|
117
117
|
|
|
118
118
|
Search file contents with SIMD-accelerated matching.
|
|
119
119
|
|
|
@@ -129,7 +129,7 @@ interface GrepOptions {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
// Plain text search
|
|
132
|
-
const result = FileFinder.
|
|
132
|
+
const result = FileFinder.grep("TODO", { pageLimit: 20 });
|
|
133
133
|
if (result.ok) {
|
|
134
134
|
for (const match of result.value.items) {
|
|
135
135
|
console.log(`${match.relativePath}:${match.lineNumber}: ${match.lineContent}`);
|
|
@@ -137,22 +137,22 @@ if (result.ok) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// Regex search
|
|
140
|
-
const regexResult = FileFinder.
|
|
140
|
+
const regexResult = FileFinder.grep("fn\\s+\\w+", { mode: "regex" });
|
|
141
141
|
|
|
142
142
|
// Fuzzy search
|
|
143
|
-
const fuzzyResult = FileFinder.
|
|
143
|
+
const fuzzyResult = FileFinder.grep("imprt recat", { mode: "fuzzy" });
|
|
144
144
|
|
|
145
145
|
// Pagination
|
|
146
|
-
const page1 = FileFinder.
|
|
146
|
+
const page1 = FileFinder.grep("error");
|
|
147
147
|
if (page1.ok && page1.value.nextCursor) {
|
|
148
|
-
const page2 = FileFinder.
|
|
148
|
+
const page2 = FileFinder.grep("error", {
|
|
149
149
|
cursor: page1.value.nextCursor,
|
|
150
150
|
});
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
// With file constraints
|
|
154
|
-
const tsOnly = FileFinder.
|
|
155
|
-
const srcOnly = FileFinder.
|
|
154
|
+
const tsOnly = FileFinder.grep("*.ts useState");
|
|
155
|
+
const srcOnly = FileFinder.grep("src/ handleClick");
|
|
156
156
|
```
|
|
157
157
|
|
|
158
158
|
### `FileFinder.trackQuery(query, selectedFile)`
|
|
@@ -178,7 +178,7 @@ if (health.ok) {
|
|
|
178
178
|
|
|
179
179
|
### Other Methods
|
|
180
180
|
|
|
181
|
-
- `FileFinder.
|
|
181
|
+
- `FileFinder.grep(query, options?)` - Search file contents
|
|
182
182
|
- `FileFinder.scanFiles()` - Trigger rescan
|
|
183
183
|
- `FileFinder.isScanning()` - Check scan status
|
|
184
184
|
- `FileFinder.getScanProgress()` - Get scan progress
|
package/examples/grep.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ff-labs/fff-bun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.4-dev.233679d",
|
|
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.2.4-dev.233679d",
|
|
66
|
+
"@ff-labs/fff-bin-darwin-x64": "0.2.4-dev.233679d",
|
|
67
|
+
"@ff-labs/fff-bin-linux-x64-gnu": "0.2.4-dev.233679d",
|
|
68
|
+
"@ff-labs/fff-bin-linux-arm64-gnu": "0.2.4-dev.233679d",
|
|
69
|
+
"@ff-labs/fff-bin-linux-x64-musl": "0.2.4-dev.233679d",
|
|
70
|
+
"@ff-labs/fff-bin-linux-arm64-musl": "0.2.4-dev.233679d",
|
|
71
|
+
"@ff-labs/fff-bin-win32-x64": "0.2.4-dev.233679d",
|
|
72
|
+
"@ff-labs/fff-bin-win32-arm64": "0.2.4-dev.233679d"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@types/bun": "^1.3.8",
|
package/src/ffi.ts
CHANGED
|
@@ -10,14 +10,36 @@
|
|
|
10
10
|
|
|
11
11
|
import { CString, dlopen, FFIType, type Pointer, ptr, read } from "bun:ffi";
|
|
12
12
|
import { findBinary } from "./download";
|
|
13
|
-
import type { Result } from "./types";
|
|
13
|
+
import type { FileItem, Location, Result, Score, SearchResult } from "./types";
|
|
14
14
|
import { err } from "./types";
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/** Grep mode constants matching the C API (u8). */
|
|
17
|
+
const GREP_MODE_PLAIN = 0;
|
|
18
|
+
const GREP_MODE_REGEX = 1;
|
|
19
|
+
const GREP_MODE_FUZZY = 2;
|
|
20
|
+
|
|
21
|
+
/** Map string mode to u8 */
|
|
22
|
+
function grepModeToU8(mode?: string): number {
|
|
23
|
+
switch (mode) {
|
|
24
|
+
case "regex":
|
|
25
|
+
return GREP_MODE_REGEX;
|
|
26
|
+
case "fuzzy":
|
|
27
|
+
return GREP_MODE_FUZZY;
|
|
28
|
+
default:
|
|
29
|
+
return GREP_MODE_PLAIN;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
17
33
|
const ffiDefinition = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
34
|
+
fff_create_instance: {
|
|
35
|
+
args: [
|
|
36
|
+
FFIType.cstring, // base_path
|
|
37
|
+
FFIType.cstring, // frecency_db_path
|
|
38
|
+
FFIType.cstring, // history_db_path
|
|
39
|
+
FFIType.bool, // use_unsafe_no_lock
|
|
40
|
+
FFIType.bool, // warmup_mmap_cache
|
|
41
|
+
FFIType.bool, // ai_mode
|
|
42
|
+
],
|
|
21
43
|
returns: FFIType.ptr,
|
|
22
44
|
},
|
|
23
45
|
fff_destroy: {
|
|
@@ -27,19 +49,54 @@ const ffiDefinition = {
|
|
|
27
49
|
|
|
28
50
|
// Search
|
|
29
51
|
fff_search: {
|
|
30
|
-
args: [
|
|
52
|
+
args: [
|
|
53
|
+
FFIType.ptr, // handle
|
|
54
|
+
FFIType.cstring, // query
|
|
55
|
+
FFIType.cstring, // current_file
|
|
56
|
+
FFIType.u32, // max_threads
|
|
57
|
+
FFIType.u32, // page_index
|
|
58
|
+
FFIType.u32, // page_size
|
|
59
|
+
FFIType.i32, // combo_boost_multiplier
|
|
60
|
+
FFIType.u32, // min_combo_count
|
|
61
|
+
],
|
|
31
62
|
returns: FFIType.ptr,
|
|
32
63
|
},
|
|
33
64
|
|
|
34
65
|
// Live grep (content search)
|
|
35
66
|
fff_live_grep: {
|
|
36
|
-
args: [
|
|
67
|
+
args: [
|
|
68
|
+
FFIType.ptr, // handle
|
|
69
|
+
FFIType.cstring, // query
|
|
70
|
+
FFIType.u8, // mode
|
|
71
|
+
FFIType.u64, // max_file_size
|
|
72
|
+
FFIType.u32, // max_matches_per_file
|
|
73
|
+
FFIType.bool, // smart_case
|
|
74
|
+
FFIType.u32, // file_offset
|
|
75
|
+
FFIType.u32, // page_limit
|
|
76
|
+
FFIType.u64, // time_budget_ms
|
|
77
|
+
FFIType.u32, // before_context
|
|
78
|
+
FFIType.u32, // after_context
|
|
79
|
+
FFIType.bool, // classify_definitions
|
|
80
|
+
],
|
|
37
81
|
returns: FFIType.ptr,
|
|
38
82
|
},
|
|
39
83
|
|
|
40
84
|
// Multi-pattern grep (Aho-Corasick)
|
|
41
85
|
fff_multi_grep: {
|
|
42
|
-
args: [
|
|
86
|
+
args: [
|
|
87
|
+
FFIType.ptr, // handle
|
|
88
|
+
FFIType.cstring, // patterns_joined (\n-separated)
|
|
89
|
+
FFIType.cstring, // constraints
|
|
90
|
+
FFIType.u64, // max_file_size
|
|
91
|
+
FFIType.u32, // max_matches_per_file
|
|
92
|
+
FFIType.bool, // smart_case
|
|
93
|
+
FFIType.u32, // file_offset
|
|
94
|
+
FFIType.u32, // page_limit
|
|
95
|
+
FFIType.u64, // time_budget_ms
|
|
96
|
+
FFIType.u32, // before_context
|
|
97
|
+
FFIType.u32, // after_context
|
|
98
|
+
FFIType.bool, // classify_definitions
|
|
99
|
+
],
|
|
43
100
|
returns: FFIType.ptr,
|
|
44
101
|
},
|
|
45
102
|
|
|
@@ -87,6 +144,20 @@ const ffiDefinition = {
|
|
|
87
144
|
returns: FFIType.ptr,
|
|
88
145
|
},
|
|
89
146
|
|
|
147
|
+
// Search result accessors
|
|
148
|
+
fff_free_search_result: {
|
|
149
|
+
args: [FFIType.ptr],
|
|
150
|
+
returns: FFIType.void,
|
|
151
|
+
},
|
|
152
|
+
fff_search_result_get_item: {
|
|
153
|
+
args: [FFIType.ptr, FFIType.u32],
|
|
154
|
+
returns: FFIType.ptr,
|
|
155
|
+
},
|
|
156
|
+
fff_search_result_get_score: {
|
|
157
|
+
args: [FFIType.ptr, FFIType.u32],
|
|
158
|
+
returns: FFIType.ptr,
|
|
159
|
+
},
|
|
160
|
+
|
|
90
161
|
// Memory management
|
|
91
162
|
fff_free_result: {
|
|
92
163
|
args: [FFIType.ptr],
|
|
@@ -210,13 +281,24 @@ export type NativeHandle = Pointer;
|
|
|
210
281
|
|
|
211
282
|
/**
|
|
212
283
|
* Create a new file finder instance.
|
|
213
|
-
*
|
|
214
|
-
* Returns the opaque native handle on success. The handle must be passed to
|
|
215
|
-
* all subsequent FFI calls and freed with `ffiDestroy`.
|
|
216
284
|
*/
|
|
217
|
-
export function ffiCreate(
|
|
285
|
+
export function ffiCreate(
|
|
286
|
+
basePath: string,
|
|
287
|
+
frecencyDbPath: string,
|
|
288
|
+
historyDbPath: string,
|
|
289
|
+
useUnsafeNoLock: boolean,
|
|
290
|
+
warmupMmapCache: boolean,
|
|
291
|
+
aiMode: boolean,
|
|
292
|
+
): Result<NativeHandle> {
|
|
218
293
|
const library = loadLibrary();
|
|
219
|
-
const resultPtr = library.symbols.
|
|
294
|
+
const resultPtr = library.symbols.fff_create_instance(
|
|
295
|
+
ptr(encodeString(basePath)),
|
|
296
|
+
ptr(encodeString(frecencyDbPath)),
|
|
297
|
+
ptr(encodeString(historyDbPath)),
|
|
298
|
+
useUnsafeNoLock,
|
|
299
|
+
warmupMmapCache,
|
|
300
|
+
aiMode,
|
|
301
|
+
);
|
|
220
302
|
|
|
221
303
|
if (resultPtr === null) {
|
|
222
304
|
return err("FFI returned null pointer");
|
|
@@ -231,7 +313,7 @@ export function ffiCreate(optsJson: string): Result<NativeHandle> {
|
|
|
231
313
|
library.symbols.fff_free_result(resultPtr);
|
|
232
314
|
|
|
233
315
|
if (!handle || handle === (0 as unknown as Pointer)) {
|
|
234
|
-
return err("
|
|
316
|
+
return err("fff_create_instance returned null handle");
|
|
235
317
|
}
|
|
236
318
|
|
|
237
319
|
return { ok: true, value: handle };
|
|
@@ -250,19 +332,254 @@ export function ffiDestroy(handle: NativeHandle): void {
|
|
|
250
332
|
library.symbols.fff_destroy(handle);
|
|
251
333
|
}
|
|
252
334
|
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// Struct byte offsets (must match #[repr(C)] layout on 64-bit)
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
// FffSearchResult { items: *mut, scores: *mut, count: u32, total_matched: u32, total_files: u32, location: FffLocation }
|
|
340
|
+
const SR_ITEMS = 0; // *mut FffFileItem (8)
|
|
341
|
+
const SR_SCORES = 8; // *mut FffScore (8)
|
|
342
|
+
const SR_COUNT = 16; // u32 (4)
|
|
343
|
+
const SR_MATCHED = 20; // u32 (4)
|
|
344
|
+
const SR_TOTAL = 24; // u32 (4)
|
|
345
|
+
// FffLocation is inlined at offset 28
|
|
346
|
+
const SR_LOC_TAG = 28; // u8 (1 + 3 padding)
|
|
347
|
+
const SR_LOC_LINE = 32; // i32 (4)
|
|
348
|
+
const SR_LOC_COL = 36; // i32 (4)
|
|
349
|
+
const SR_LOC_END_LINE = 40; // i32 (4)
|
|
350
|
+
const SR_LOC_END_COL = 44; // i32 (4)
|
|
351
|
+
|
|
352
|
+
// FffFileItem (80 bytes)
|
|
353
|
+
const FI_PATH = 0; // *mut c_char (8)
|
|
354
|
+
const FI_RELPATH = 8; // *mut c_char (8)
|
|
355
|
+
const FI_FNAME = 16; // *mut c_char (8)
|
|
356
|
+
const FI_GIT = 24; // *mut c_char (8)
|
|
357
|
+
const FI_SIZE = 32; // u64 (8)
|
|
358
|
+
const FI_MODIFIED = 40; // u64 (8)
|
|
359
|
+
const FI_ACCESS = 48; // i64 (8)
|
|
360
|
+
const FI_MODFR = 56; // i64 (8)
|
|
361
|
+
const FI_TOTAL_FR = 64; // i64 (8)
|
|
362
|
+
const FI_BINARY = 72; // bool (1 + 7 pad)
|
|
363
|
+
const FI_SIZE_OF = 80;
|
|
364
|
+
|
|
365
|
+
// FffScore (48 bytes)
|
|
366
|
+
const SC_TOTAL = 0; // i32 (4)
|
|
367
|
+
const SC_BASE = 4; // i32 (4)
|
|
368
|
+
const SC_FNAME = 8; // i32 (4)
|
|
369
|
+
const SC_SPECIAL = 12; // i32 (4)
|
|
370
|
+
const SC_FREC = 16; // i32 (4)
|
|
371
|
+
const SC_DIST = 20; // i32 (4)
|
|
372
|
+
const SC_CURFILE = 24; // i32 (4)
|
|
373
|
+
const SC_COMBO = 28; // i32 (4)
|
|
374
|
+
const SC_EXACT = 32; // bool (1 + 7 pad)
|
|
375
|
+
const SC_MTYPE = 40; // *mut c_char (8)
|
|
376
|
+
const SC_SIZE_OF = 48;
|
|
377
|
+
|
|
378
|
+
/** Cast a number (raw address from pointer math) to Pointer for read.*. */
|
|
379
|
+
function asPtr(n: number): Pointer {
|
|
380
|
+
return n as unknown as Pointer;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Read an FffFileItem struct at the given raw address.
|
|
385
|
+
*/
|
|
386
|
+
function readFileItemStruct(p: number): FileItem {
|
|
387
|
+
const pp = asPtr(p);
|
|
388
|
+
return {
|
|
389
|
+
path: readCString(read.ptr(pp, FI_PATH)) ?? "",
|
|
390
|
+
relativePath: readCString(read.ptr(pp, FI_RELPATH)) ?? "",
|
|
391
|
+
fileName: readCString(read.ptr(pp, FI_FNAME)) ?? "",
|
|
392
|
+
gitStatus: readCString(read.ptr(pp, FI_GIT)) ?? "",
|
|
393
|
+
size: Number(read.u64(pp, FI_SIZE)),
|
|
394
|
+
modified: Number(read.u64(pp, FI_MODIFIED)),
|
|
395
|
+
accessFrecencyScore: Number(read.i64(pp, FI_ACCESS)),
|
|
396
|
+
modificationFrecencyScore: Number(read.i64(pp, FI_MODFR)),
|
|
397
|
+
totalFrecencyScore: Number(read.i64(pp, FI_TOTAL_FR)),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Read an FffScore struct at the given raw address.
|
|
403
|
+
*/
|
|
404
|
+
function readScoreStruct(p: number): Score {
|
|
405
|
+
const pp = asPtr(p);
|
|
406
|
+
return {
|
|
407
|
+
total: read.i32(pp, SC_TOTAL),
|
|
408
|
+
baseScore: read.i32(pp, SC_BASE),
|
|
409
|
+
filenameBonus: read.i32(pp, SC_FNAME),
|
|
410
|
+
specialFilenameBonus:read.i32(pp, SC_SPECIAL),
|
|
411
|
+
frecencyBoost: read.i32(pp, SC_FREC),
|
|
412
|
+
distancePenalty: read.i32(pp, SC_DIST),
|
|
413
|
+
currentFilePenalty: read.i32(pp, SC_CURFILE),
|
|
414
|
+
comboMatchBoost: read.i32(pp, SC_COMBO),
|
|
415
|
+
exactMatch: read.u8(pp, SC_EXACT) !== 0,
|
|
416
|
+
matchType: readCString(read.ptr(pp, SC_MTYPE)) ?? "",
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Parse an FffSearchResult from a raw pointer, then free native memory.
|
|
422
|
+
*/
|
|
423
|
+
function parseSearchResult(resultPtr: Pointer | null): Result<SearchResult> {
|
|
424
|
+
if (resultPtr === null) {
|
|
425
|
+
return err("FFI returned null pointer");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const success = read.u8(resultPtr, 0) !== 0;
|
|
429
|
+
const errorPtr = read.ptr(resultPtr, 16);
|
|
430
|
+
const handlePtr = read.ptr(resultPtr, 24);
|
|
431
|
+
|
|
432
|
+
const library = loadLibrary();
|
|
433
|
+
|
|
434
|
+
if (!success) {
|
|
435
|
+
const errorMsg = readCString(errorPtr) || "Unknown error";
|
|
436
|
+
library.symbols.fff_free_result(resultPtr);
|
|
437
|
+
return err(errorMsg);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Free the FffResult envelope (does NOT free handle)
|
|
441
|
+
library.symbols.fff_free_result(resultPtr);
|
|
442
|
+
|
|
443
|
+
if (handlePtr === 0) {
|
|
444
|
+
return err("fff_search returned null search result");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Read FffSearchResult struct from handlePtr
|
|
448
|
+
// Cast number to Pointer for Bun's read.* type requirements
|
|
449
|
+
const hp = handlePtr as unknown as Pointer;
|
|
450
|
+
const count = read.u32(hp, SR_COUNT);
|
|
451
|
+
const totalMatched = read.u32(hp, SR_MATCHED);
|
|
452
|
+
const totalFiles = read.u32(hp, SR_TOTAL);
|
|
453
|
+
const itemsBase = read.ptr(hp, SR_ITEMS);
|
|
454
|
+
const scoresBase = read.ptr(hp, SR_SCORES);
|
|
455
|
+
|
|
456
|
+
// Read location
|
|
457
|
+
const locTag = read.u8(hp, SR_LOC_TAG);
|
|
458
|
+
let location: Location | undefined;
|
|
459
|
+
if (locTag === 1) {
|
|
460
|
+
location = { type: "line", line: read.i32(hp, SR_LOC_LINE) };
|
|
461
|
+
} else if (locTag === 2) {
|
|
462
|
+
location = { type: "position", line: read.i32(hp, SR_LOC_LINE), col: read.i32(hp, SR_LOC_COL) };
|
|
463
|
+
} else if (locTag === 3) {
|
|
464
|
+
location = {
|
|
465
|
+
type: "range",
|
|
466
|
+
start: { line: read.i32(hp, SR_LOC_LINE), col: read.i32(hp, SR_LOC_COL) },
|
|
467
|
+
end: { line: read.i32(hp, SR_LOC_END_LINE), col: read.i32(hp, SR_LOC_END_COL) },
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Read items and scores arrays using pointer arithmetic
|
|
472
|
+
const items: FileItem[] = [];
|
|
473
|
+
const scores: Score[] = [];
|
|
474
|
+
|
|
475
|
+
for (let i = 0; i < count; i++) {
|
|
476
|
+
items.push(readFileItemStruct(itemsBase + i * FI_SIZE_OF));
|
|
477
|
+
scores.push(readScoreStruct(scoresBase + i * SC_SIZE_OF));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Free native search result
|
|
481
|
+
library.symbols.fff_free_search_result(hp);
|
|
482
|
+
|
|
483
|
+
const result: SearchResult = { items, scores, totalMatched, totalFiles };
|
|
484
|
+
if (location) {
|
|
485
|
+
result.location = location;
|
|
486
|
+
}
|
|
487
|
+
return { ok: true, value: result };
|
|
488
|
+
}
|
|
489
|
+
|
|
253
490
|
/**
|
|
254
491
|
* Perform fuzzy search.
|
|
255
492
|
*/
|
|
256
493
|
export function ffiSearch(
|
|
257
494
|
handle: NativeHandle,
|
|
258
495
|
query: string,
|
|
259
|
-
|
|
260
|
-
|
|
496
|
+
currentFile: string,
|
|
497
|
+
maxThreads: number,
|
|
498
|
+
pageIndex: number,
|
|
499
|
+
pageSize: number,
|
|
500
|
+
comboBoostMultiplier: number,
|
|
501
|
+
minComboCount: number,
|
|
502
|
+
): Result<SearchResult> {
|
|
261
503
|
const library = loadLibrary();
|
|
262
504
|
const resultPtr = library.symbols.fff_search(
|
|
263
505
|
handle,
|
|
264
506
|
ptr(encodeString(query)),
|
|
265
|
-
ptr(encodeString(
|
|
507
|
+
ptr(encodeString(currentFile)),
|
|
508
|
+
maxThreads,
|
|
509
|
+
pageIndex,
|
|
510
|
+
pageSize,
|
|
511
|
+
comboBoostMultiplier,
|
|
512
|
+
minComboCount,
|
|
513
|
+
);
|
|
514
|
+
return parseSearchResult(resultPtr);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Live grep - search file contents.
|
|
519
|
+
*/
|
|
520
|
+
export function ffiLiveGrep(
|
|
521
|
+
handle: NativeHandle,
|
|
522
|
+
query: string,
|
|
523
|
+
mode: string,
|
|
524
|
+
maxFileSize: number,
|
|
525
|
+
maxMatchesPerFile: number,
|
|
526
|
+
smartCase: boolean,
|
|
527
|
+
fileOffset: number,
|
|
528
|
+
pageLimit: number,
|
|
529
|
+
timeBudgetMs: number,
|
|
530
|
+
beforeContext: number,
|
|
531
|
+
afterContext: number,
|
|
532
|
+
classifyDefinitions: boolean,
|
|
533
|
+
): Result<unknown> {
|
|
534
|
+
const library = loadLibrary();
|
|
535
|
+
const resultPtr = library.symbols.fff_live_grep(
|
|
536
|
+
handle,
|
|
537
|
+
ptr(encodeString(query)),
|
|
538
|
+
grepModeToU8(mode),
|
|
539
|
+
BigInt(maxFileSize),
|
|
540
|
+
maxMatchesPerFile,
|
|
541
|
+
smartCase,
|
|
542
|
+
fileOffset,
|
|
543
|
+
pageLimit,
|
|
544
|
+
BigInt(timeBudgetMs),
|
|
545
|
+
beforeContext,
|
|
546
|
+
afterContext,
|
|
547
|
+
classifyDefinitions,
|
|
548
|
+
);
|
|
549
|
+
return parseResult<unknown>(resultPtr);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Multi-pattern grep - Aho-Corasick multi-needle search.
|
|
554
|
+
*/
|
|
555
|
+
export function ffiMultiGrep(
|
|
556
|
+
handle: NativeHandle,
|
|
557
|
+
patternsJoined: string,
|
|
558
|
+
constraints: string,
|
|
559
|
+
maxFileSize: number,
|
|
560
|
+
maxMatchesPerFile: number,
|
|
561
|
+
smartCase: boolean,
|
|
562
|
+
fileOffset: number,
|
|
563
|
+
pageLimit: number,
|
|
564
|
+
timeBudgetMs: number,
|
|
565
|
+
beforeContext: number,
|
|
566
|
+
afterContext: number,
|
|
567
|
+
classifyDefinitions: boolean,
|
|
568
|
+
): Result<unknown> {
|
|
569
|
+
const library = loadLibrary();
|
|
570
|
+
const resultPtr = library.symbols.fff_multi_grep(
|
|
571
|
+
handle,
|
|
572
|
+
ptr(encodeString(patternsJoined)),
|
|
573
|
+
ptr(encodeString(constraints)),
|
|
574
|
+
BigInt(maxFileSize),
|
|
575
|
+
maxMatchesPerFile,
|
|
576
|
+
smartCase,
|
|
577
|
+
fileOffset,
|
|
578
|
+
pageLimit,
|
|
579
|
+
BigInt(timeBudgetMs),
|
|
580
|
+
beforeContext,
|
|
581
|
+
afterContext,
|
|
582
|
+
classifyDefinitions,
|
|
266
583
|
);
|
|
267
584
|
return parseResult<unknown>(resultPtr);
|
|
268
585
|
}
|
|
@@ -301,8 +618,6 @@ export function ffiWaitForScan(handle: NativeHandle, timeoutMs: number): Result<
|
|
|
301
618
|
const resultPtr = library.symbols.fff_wait_for_scan(handle, BigInt(timeoutMs));
|
|
302
619
|
const result = parseResult<boolean | string>(resultPtr);
|
|
303
620
|
if (!result.ok) return result;
|
|
304
|
-
// JSON.parse("true") returns boolean true, but we also handle
|
|
305
|
-
// the string case defensively.
|
|
306
621
|
return { ok: true, value: result.value === true || result.value === "true" };
|
|
307
622
|
}
|
|
308
623
|
|
|
@@ -323,7 +638,6 @@ export function ffiRefreshGitStatus(handle: NativeHandle): Result<number> {
|
|
|
323
638
|
const resultPtr = library.symbols.fff_refresh_git_status(handle);
|
|
324
639
|
const result = parseResult<number | string>(resultPtr);
|
|
325
640
|
if (!result.ok) return result;
|
|
326
|
-
// JSON.parse("3") returns 3 (number), parseInt handles both
|
|
327
641
|
return {
|
|
328
642
|
ok: true,
|
|
329
643
|
value: typeof result.value === "number" ? result.value : parseInt(result.value, 10),
|
|
@@ -381,41 +695,6 @@ export function ffiHealthCheck(
|
|
|
381
695
|
return parseResult<unknown>(resultPtr);
|
|
382
696
|
}
|
|
383
697
|
|
|
384
|
-
/**
|
|
385
|
-
* Detect workspace roots in the indexed directory.
|
|
386
|
-
*/
|
|
387
|
-
export function ffiDetectWorkspaces(handle: NativeHandle): Result<unknown> {
|
|
388
|
-
const library = loadLibrary();
|
|
389
|
-
const resultPtr = library.symbols.fff_detect_workspaces(handle);
|
|
390
|
-
return parseResult<unknown>(resultPtr);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Live grep - search file contents.
|
|
395
|
-
*/
|
|
396
|
-
export function ffiLiveGrep(
|
|
397
|
-
handle: NativeHandle,
|
|
398
|
-
query: string,
|
|
399
|
-
optsJson: string,
|
|
400
|
-
): Result<unknown> {
|
|
401
|
-
const library = loadLibrary();
|
|
402
|
-
const resultPtr = library.symbols.fff_live_grep(
|
|
403
|
-
handle,
|
|
404
|
-
ptr(encodeString(query)),
|
|
405
|
-
ptr(encodeString(optsJson)),
|
|
406
|
-
);
|
|
407
|
-
return parseResult<unknown>(resultPtr);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Multi-pattern grep - Aho-Corasick multi-needle search.
|
|
412
|
-
*/
|
|
413
|
-
export function ffiMultiGrep(handle: NativeHandle, optsJson: string): Result<unknown> {
|
|
414
|
-
const library = loadLibrary();
|
|
415
|
-
const resultPtr = library.symbols.fff_multi_grep(handle, ptr(encodeString(optsJson)));
|
|
416
|
-
return parseResult<unknown>(resultPtr);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
698
|
/**
|
|
420
699
|
* Ensure the library is loaded.
|
|
421
700
|
*
|
package/src/finder.ts
CHANGED
|
@@ -40,14 +40,7 @@ import type {
|
|
|
40
40
|
SearchResult,
|
|
41
41
|
} from "./types";
|
|
42
42
|
|
|
43
|
-
import {
|
|
44
|
-
createGrepCursor,
|
|
45
|
-
err,
|
|
46
|
-
toInternalGrepOptions,
|
|
47
|
-
toInternalInitOptions,
|
|
48
|
-
toInternalMultiGrepOptions,
|
|
49
|
-
toInternalSearchOptions,
|
|
50
|
-
} from "./types";
|
|
43
|
+
import { createGrepCursor, err } from "./types";
|
|
51
44
|
|
|
52
45
|
/**
|
|
53
46
|
* FileFinder - Fast file finder with fuzzy search
|
|
@@ -108,8 +101,14 @@ export class FileFinder {
|
|
|
108
101
|
* ```
|
|
109
102
|
*/
|
|
110
103
|
static create(options: InitOptions): Result<FileFinder> {
|
|
111
|
-
const
|
|
112
|
-
|
|
104
|
+
const result = ffiCreate(
|
|
105
|
+
options.basePath,
|
|
106
|
+
options.frecencyDbPath ?? "",
|
|
107
|
+
options.historyDbPath ?? "",
|
|
108
|
+
options.useUnsafeNoLock ?? false,
|
|
109
|
+
options.warmupMmapCache ?? false,
|
|
110
|
+
options.aiMode ?? false,
|
|
111
|
+
);
|
|
113
112
|
|
|
114
113
|
if (!result.ok) {
|
|
115
114
|
return result;
|
|
@@ -177,14 +176,16 @@ export class FileFinder {
|
|
|
177
176
|
const guard = this.ensureAlive();
|
|
178
177
|
if (!guard.ok) return guard;
|
|
179
178
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
179
|
+
return ffiSearch(
|
|
180
|
+
guard.value,
|
|
181
|
+
query,
|
|
182
|
+
options?.currentFile ?? "",
|
|
183
|
+
options?.maxThreads ?? 0,
|
|
184
|
+
options?.pageIndex ?? 0,
|
|
185
|
+
options?.pageSize ?? 0,
|
|
186
|
+
options?.comboBoostMultiplier ?? 0,
|
|
187
|
+
options?.minComboCount ?? 0,
|
|
188
|
+
);
|
|
188
189
|
}
|
|
189
190
|
|
|
190
191
|
/**
|
|
@@ -209,26 +210,38 @@ export class FileFinder {
|
|
|
209
210
|
* @example
|
|
210
211
|
* ```typescript
|
|
211
212
|
* // First page
|
|
212
|
-
* const result = finder.
|
|
213
|
+
* const result = finder.grep("TODO", { mode: "plain" });
|
|
213
214
|
* if (result.ok) {
|
|
214
215
|
* for (const match of result.value.items) {
|
|
215
216
|
* console.log(`${match.relativePath}:${match.lineNumber}: ${match.lineContent}`);
|
|
216
217
|
* }
|
|
217
218
|
* // Fetch next page
|
|
218
219
|
* if (result.value.nextCursor) {
|
|
219
|
-
* const page2 = finder.
|
|
220
|
+
* const page2 = finder.grep("TODO", {
|
|
220
221
|
* cursor: result.value.nextCursor,
|
|
221
222
|
* });
|
|
222
223
|
* }
|
|
223
224
|
* }
|
|
224
225
|
* ```
|
|
225
226
|
*/
|
|
226
|
-
|
|
227
|
+
grep(query: string, options?: GrepOptions): Result<GrepResult> {
|
|
227
228
|
const guard = this.ensureAlive();
|
|
228
229
|
if (!guard.ok) return guard;
|
|
229
230
|
|
|
230
|
-
const
|
|
231
|
-
|
|
231
|
+
const result = ffiLiveGrep(
|
|
232
|
+
guard.value,
|
|
233
|
+
query,
|
|
234
|
+
options?.mode ?? "plain",
|
|
235
|
+
options?.maxFileSize ?? 0,
|
|
236
|
+
options?.maxMatchesPerFile ?? 0,
|
|
237
|
+
options?.smartCase ?? true,
|
|
238
|
+
options?.cursor?._offset ?? 0,
|
|
239
|
+
0, // page_limit (0 = default 50)
|
|
240
|
+
options?.timeBudgetMs ?? 0,
|
|
241
|
+
options?.beforeContext ?? 0,
|
|
242
|
+
options?.afterContext ?? 0,
|
|
243
|
+
false,
|
|
244
|
+
);
|
|
232
245
|
|
|
233
246
|
return transformGrepResult(result);
|
|
234
247
|
}
|
|
@@ -266,8 +279,20 @@ export class FileFinder {
|
|
|
266
279
|
return err("patterns array must have at least 1 element");
|
|
267
280
|
}
|
|
268
281
|
|
|
269
|
-
const
|
|
270
|
-
|
|
282
|
+
const result = ffiMultiGrep(
|
|
283
|
+
guard.value,
|
|
284
|
+
options.patterns.join("\n"),
|
|
285
|
+
options.constraints ?? "",
|
|
286
|
+
options.maxFileSize ?? 0,
|
|
287
|
+
options.maxMatchesPerFile ?? 0,
|
|
288
|
+
options.smartCase ?? true,
|
|
289
|
+
options.cursor?._offset ?? 0,
|
|
290
|
+
0, // page_limit (0 = default 50)
|
|
291
|
+
options.timeBudgetMs ?? 0,
|
|
292
|
+
options.beforeContext ?? 0,
|
|
293
|
+
options.afterContext ?? 0,
|
|
294
|
+
false,
|
|
295
|
+
);
|
|
271
296
|
|
|
272
297
|
return transformGrepResult(result);
|
|
273
298
|
}
|
|
@@ -107,7 +107,7 @@ async function waitForFileCount(finder: FileFinder, count: number): Promise<numb
|
|
|
107
107
|
return result.ok ? result.value.totalFiles : -1;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
/** Poll
|
|
110
|
+
/** Poll grep until predicate on totalMatched is satisfied, or the timeout is exceeded. */
|
|
111
111
|
async function waitForGrep(
|
|
112
112
|
finder: FileFinder,
|
|
113
113
|
pattern: string,
|
|
@@ -116,11 +116,11 @@ async function waitForGrep(
|
|
|
116
116
|
) {
|
|
117
117
|
const start = Date.now();
|
|
118
118
|
while (Date.now() - start < WATCHER_TIMEOUT_MS) {
|
|
119
|
-
const result = finder.
|
|
119
|
+
const result = finder.grep(pattern, options);
|
|
120
120
|
if (result.ok && predicate(result.value.totalMatched)) return result;
|
|
121
121
|
await sleep(POLL_INTERVAL_MS);
|
|
122
122
|
}
|
|
123
|
-
return finder.
|
|
123
|
+
return finder.grep(pattern, options);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
describe.skipIf(process.platform === "win32")("Git lifecycle integration", () => {
|
package/src/index.test.ts
CHANGED
|
@@ -162,8 +162,8 @@ describe("FileFinder - Full Lifecycle", () => {
|
|
|
162
162
|
}
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
test("
|
|
166
|
-
const result = finder.
|
|
165
|
+
test("grep plain text returns matching lines", () => {
|
|
166
|
+
const result = finder.grep("fff-core", {
|
|
167
167
|
mode: "plain",
|
|
168
168
|
});
|
|
169
169
|
expect(result.ok).toBe(true);
|
|
@@ -197,9 +197,9 @@ describe("FileFinder - Full Lifecycle", () => {
|
|
|
197
197
|
}
|
|
198
198
|
});
|
|
199
199
|
|
|
200
|
-
test("
|
|
200
|
+
test("grep fuzzy mode returns results with scores", () => {
|
|
201
201
|
// Intentional typo: "depdnency" instead of "dependency" to exercise fuzzy matching
|
|
202
|
-
const result = finder.
|
|
202
|
+
const result = finder.grep("depdnency", {
|
|
203
203
|
mode: "fuzzy",
|
|
204
204
|
});
|
|
205
205
|
expect(result.ok).toBe(true);
|
package/src/types.ts
CHANGED
|
@@ -208,62 +208,6 @@ export interface HealthCheck {
|
|
|
208
208
|
};
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
/**
|
|
212
|
-
* Internal: Options format sent to Rust FFI
|
|
213
|
-
* @internal
|
|
214
|
-
*/
|
|
215
|
-
export interface InitOptionsInternal {
|
|
216
|
-
base_path: string;
|
|
217
|
-
frecency_db_path?: string;
|
|
218
|
-
history_db_path?: string;
|
|
219
|
-
use_unsafe_no_lock: boolean;
|
|
220
|
-
warmup_mmap_cache: boolean;
|
|
221
|
-
ai_mode: boolean;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Internal: Search options format sent to Rust FFI
|
|
226
|
-
* @internal
|
|
227
|
-
*/
|
|
228
|
-
export interface SearchOptionsInternal {
|
|
229
|
-
max_threads?: number;
|
|
230
|
-
current_file?: string;
|
|
231
|
-
combo_boost_multiplier?: number;
|
|
232
|
-
min_combo_count?: number;
|
|
233
|
-
page_index?: number;
|
|
234
|
-
page_size?: number;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Convert public InitOptions to internal format
|
|
239
|
-
* @internal
|
|
240
|
-
*/
|
|
241
|
-
export function toInternalInitOptions(opts: InitOptions): InitOptionsInternal {
|
|
242
|
-
return {
|
|
243
|
-
base_path: opts.basePath,
|
|
244
|
-
frecency_db_path: opts.frecencyDbPath,
|
|
245
|
-
history_db_path: opts.historyDbPath,
|
|
246
|
-
use_unsafe_no_lock: opts.useUnsafeNoLock ?? false,
|
|
247
|
-
warmup_mmap_cache: opts.warmupMmapCache ?? false,
|
|
248
|
-
ai_mode: opts.aiMode ?? false,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Convert public SearchOptions to internal format
|
|
254
|
-
* @internal
|
|
255
|
-
*/
|
|
256
|
-
export function toInternalSearchOptions(opts?: SearchOptions): SearchOptionsInternal {
|
|
257
|
-
return {
|
|
258
|
-
max_threads: opts?.maxThreads,
|
|
259
|
-
current_file: opts?.currentFile,
|
|
260
|
-
combo_boost_multiplier: opts?.comboBoostMultiplier,
|
|
261
|
-
min_combo_count: opts?.minComboCount,
|
|
262
|
-
page_index: opts?.pageIndex,
|
|
263
|
-
page_size: opts?.pageSize,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
211
|
/**
|
|
268
212
|
* Grep search mode
|
|
269
213
|
*/
|
|
@@ -417,78 +361,3 @@ export interface MultiGrepOptions {
|
|
|
417
361
|
afterContext?: number;
|
|
418
362
|
}
|
|
419
363
|
|
|
420
|
-
/**
|
|
421
|
-
* Internal: Multi-grep options format sent to Rust FFI
|
|
422
|
-
* @internal
|
|
423
|
-
*/
|
|
424
|
-
export interface MultiGrepOptionsInternal {
|
|
425
|
-
patterns: string[];
|
|
426
|
-
constraints?: string;
|
|
427
|
-
max_file_size?: number;
|
|
428
|
-
max_matches_per_file?: number;
|
|
429
|
-
smart_case?: boolean;
|
|
430
|
-
file_offset?: number;
|
|
431
|
-
page_limit?: number;
|
|
432
|
-
time_budget_ms?: number;
|
|
433
|
-
before_context?: number;
|
|
434
|
-
after_context?: number;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Convert public MultiGrepOptions to internal format
|
|
439
|
-
* @internal
|
|
440
|
-
*/
|
|
441
|
-
export function toInternalMultiGrepOptions(
|
|
442
|
-
opts: MultiGrepOptions,
|
|
443
|
-
pageLimit?: number,
|
|
444
|
-
): MultiGrepOptionsInternal {
|
|
445
|
-
return {
|
|
446
|
-
patterns: opts.patterns,
|
|
447
|
-
constraints: opts.constraints,
|
|
448
|
-
max_file_size: opts.maxFileSize,
|
|
449
|
-
max_matches_per_file: opts.maxMatchesPerFile,
|
|
450
|
-
smart_case: opts.smartCase,
|
|
451
|
-
file_offset: opts.cursor?._offset ?? 0,
|
|
452
|
-
page_limit: pageLimit,
|
|
453
|
-
time_budget_ms: opts.timeBudgetMs,
|
|
454
|
-
before_context: opts.beforeContext,
|
|
455
|
-
after_context: opts.afterContext,
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Internal: Grep options format sent to Rust FFI
|
|
461
|
-
* @internal
|
|
462
|
-
*/
|
|
463
|
-
export interface GrepOptionsInternal {
|
|
464
|
-
max_file_size?: number;
|
|
465
|
-
max_matches_per_file?: number;
|
|
466
|
-
smart_case?: boolean;
|
|
467
|
-
file_offset?: number;
|
|
468
|
-
page_limit?: number;
|
|
469
|
-
mode?: string;
|
|
470
|
-
time_budget_ms?: number;
|
|
471
|
-
before_context?: number;
|
|
472
|
-
after_context?: number;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Convert public GrepOptions to internal format
|
|
477
|
-
* @internal
|
|
478
|
-
*/
|
|
479
|
-
export function toInternalGrepOptions(
|
|
480
|
-
opts?: GrepOptions,
|
|
481
|
-
pageLimit?: number,
|
|
482
|
-
): GrepOptionsInternal {
|
|
483
|
-
return {
|
|
484
|
-
max_file_size: opts?.maxFileSize,
|
|
485
|
-
max_matches_per_file: opts?.maxMatchesPerFile,
|
|
486
|
-
smart_case: opts?.smartCase,
|
|
487
|
-
file_offset: opts?.cursor?._offset ?? 0,
|
|
488
|
-
page_limit: pageLimit,
|
|
489
|
-
mode: opts?.mode,
|
|
490
|
-
time_budget_ms: opts?.timeBudgetMs,
|
|
491
|
-
before_context: opts?.beforeContext,
|
|
492
|
-
after_context: opts?.afterContext,
|
|
493
|
-
};
|
|
494
|
-
}
|