@edxeth/fff-node 0.7.2-edxeth.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.
@@ -0,0 +1,1141 @@
1
+ /**
2
+ * Node.js FFI bindings for the fff-c native library using ffi-rs
3
+ *
4
+ * This module uses ffi-rs to call into the Rust C library.
5
+ * All functions follow the Result pattern for error handling.
6
+ *
7
+ * The API is instance-based: `ffiCreate` returns an opaque handle that must
8
+ * be passed to all subsequent calls and freed with `ffiDestroy`.
9
+ *
10
+ * ## Memory management
11
+ *
12
+ * Every `fff_*` function returning `*mut FffResult` allocates with Rust's Box.
13
+ * We MUST call `fff_free_result` to properly deallocate (not libc::free).
14
+ *
15
+ * ## FffResult struct reading
16
+ *
17
+ * The FffResult struct layout (#[repr(C)]):
18
+ * offset 0: success (bool, 1 byte + 7 padding)
19
+ * offset 8: data pointer (8 bytes) - *mut c_char (JSON string or null)
20
+ * offset 16: error pointer (8 bytes) - *mut c_char (error message or null)
21
+ * offset 24: handle pointer (8 bytes) - *mut c_void (instance handle or null)
22
+ *
23
+ * ## Two-step approach for reading + freeing
24
+ *
25
+ * ffi-rs auto-dereferences struct retType pointers, losing the original pointer.
26
+ * We solve this by:
27
+ * 1. Calling the C function with `retType: DataType.External` to get the raw pointer
28
+ * 2. Using `restorePointer` to read the struct fields from the raw pointer
29
+ * 3. Calling `fff_free_result` with the original raw pointer
30
+ *
31
+ * ## Null pointer detection
32
+ *
33
+ * `isNullPointer` from ffi-rs correctly detects null C pointers wrapped as
34
+ * V8 External objects. We use this instead of truthy checks.
35
+ */
36
+ import { close, DataType, isNullPointer, load, open, restorePointer, wrapPointer, } from "ffi-rs";
37
+ import { findBinary } from "./binary.js";
38
+ import { createGrepCursor, err } from "./types.js";
39
+ const LIBRARY_KEY = "fff_c";
40
+ /** Grep mode constants matching the C API (u8). */
41
+ const GREP_MODE_PLAIN = 0;
42
+ const GREP_MODE_REGEX = 1;
43
+ const GREP_MODE_FUZZY = 2;
44
+ /** Map string mode to u8 */
45
+ function grepModeToU8(mode) {
46
+ switch (mode) {
47
+ case "regex":
48
+ return GREP_MODE_REGEX;
49
+ case "fuzzy":
50
+ return GREP_MODE_FUZZY;
51
+ default:
52
+ return GREP_MODE_PLAIN;
53
+ }
54
+ }
55
+ // Track whether the library is loaded
56
+ let isLoaded = false;
57
+ /**
58
+ * Struct type definition for FffResult used with restorePointer.
59
+ *
60
+ * Uses U8 for the bool success field (correct alignment with ffi-rs).
61
+ * Uses External for ALL pointer fields to avoid hangs on null char* pointers
62
+ * (ffi-rs hangs when trying to read DataType.String from null char*).
63
+ */
64
+ const FFF_RESULT_STRUCT = {
65
+ success: DataType.U8,
66
+ error: DataType.External,
67
+ handle: DataType.External,
68
+ int_value: DataType.I64,
69
+ };
70
+ /**
71
+ * Load the native library using ffi-rs
72
+ */
73
+ function loadLibrary() {
74
+ if (isLoaded)
75
+ return;
76
+ const binaryPath = findBinary();
77
+ if (!binaryPath) {
78
+ throw new Error("fff native library not found. Run `npx @ff-labs/fff-node download` or build from source with `cargo build --release -p fff-c`");
79
+ }
80
+ open({ library: LIBRARY_KEY, path: binaryPath });
81
+ isLoaded = true;
82
+ }
83
+ /**
84
+ * Convert snake_case keys to camelCase recursively
85
+ */
86
+ function snakeToCamel(obj) {
87
+ if (obj === null || obj === undefined)
88
+ return obj;
89
+ if (typeof obj !== "object")
90
+ return obj;
91
+ if (Array.isArray(obj))
92
+ return obj.map(snakeToCamel);
93
+ const result = {};
94
+ for (const [key, value] of Object.entries(obj)) {
95
+ const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
96
+ result[camelKey] = snakeToCamel(value);
97
+ }
98
+ return result;
99
+ }
100
+ /**
101
+ * Read a C string (char*) from an ffi-rs External pointer.
102
+ *
103
+ * Uses restorePointer + wrapPointer to dereference the char* and read the
104
+ * null-terminated string. Returns null if the pointer is null.
105
+ */
106
+ function readCString(ptr) {
107
+ if (isNullPointer(ptr))
108
+ return null;
109
+ try {
110
+ const [str] = restorePointer({
111
+ retType: [DataType.String],
112
+ paramsValue: wrapPointer([ptr]),
113
+ });
114
+ return str;
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ }
120
+ /**
121
+ * Call a C function that returns `*mut FffResult` and get both the raw pointer
122
+ * (for freeing) and the parsed struct fields.
123
+ *
124
+ * Step 1: Call function with `DataType.External` retType → raw pointer
125
+ * Step 2: Use `restorePointer` to read struct fields from the raw pointer
126
+ */
127
+ function callRaw(funcName, paramsType, paramsValue) {
128
+ const rawPtr = load({
129
+ library: LIBRARY_KEY,
130
+ funcName,
131
+ retType: DataType.External,
132
+ paramsType,
133
+ paramsValue,
134
+ freeResultMemory: false,
135
+ });
136
+ const [structData] = restorePointer({
137
+ retType: [FFF_RESULT_STRUCT],
138
+ paramsValue: wrapPointer([rawPtr]),
139
+ });
140
+ return { rawPtr, struct: structData };
141
+ }
142
+ /**
143
+ * Free a FffResult pointer by calling fff_free_result.
144
+ *
145
+ * This frees the FffResult struct and its data/error strings using Rust's
146
+ * Box::from_raw and CString::from_raw. The handle field is NOT freed.
147
+ */
148
+ function freeResult(resultPtr) {
149
+ try {
150
+ load({
151
+ library: LIBRARY_KEY,
152
+ funcName: "fff_free_result",
153
+ retType: DataType.Void,
154
+ paramsType: [DataType.External],
155
+ paramsValue: [resultPtr],
156
+ });
157
+ }
158
+ catch {
159
+ // Ignore cleanup errors
160
+ }
161
+ }
162
+ /**
163
+ * Read the FffResult envelope from a raw call. Returns the parsed struct + raw pointer.
164
+ * On error, frees the result and returns a Result error.
165
+ */
166
+ function readResultEnvelope(funcName, paramsType, paramsValue) {
167
+ loadLibrary();
168
+ const { rawPtr, struct: structData } = callRaw(funcName, paramsType, paramsValue);
169
+ if (structData.success === 0) {
170
+ const errorStr = readCString(structData.error);
171
+ freeResult(rawPtr);
172
+ return err(errorStr || "Unknown error");
173
+ }
174
+ return { rawPtr, struct: structData };
175
+ }
176
+ /** Call a function returning FffResult with void payload. */
177
+ function callVoidResult(funcName, paramsType, paramsValue) {
178
+ const res = readResultEnvelope(funcName, paramsType, paramsValue);
179
+ if ("ok" in res)
180
+ return res;
181
+ freeResult(res.rawPtr);
182
+ return { ok: true, value: undefined };
183
+ }
184
+ /** Call a function returning FffResult with int_value payload. */
185
+ function callIntResult(funcName, paramsType, paramsValue) {
186
+ const res = readResultEnvelope(funcName, paramsType, paramsValue);
187
+ if ("ok" in res)
188
+ return res;
189
+ const value = Number(res.struct.int_value);
190
+ freeResult(res.rawPtr);
191
+ return { ok: true, value };
192
+ }
193
+ /** Call a function returning FffResult with bool in int_value. */
194
+ function callBoolResult(funcName, paramsType, paramsValue) {
195
+ const res = readResultEnvelope(funcName, paramsType, paramsValue);
196
+ if ("ok" in res)
197
+ return res;
198
+ const value = Number(res.struct.int_value) !== 0;
199
+ freeResult(res.rawPtr);
200
+ return { ok: true, value };
201
+ }
202
+ /** Call a function returning FffResult with a C string in handle. */
203
+ function callStringResult(funcName, paramsType, paramsValue) {
204
+ const res = readResultEnvelope(funcName, paramsType, paramsValue);
205
+ if ("ok" in res)
206
+ return res;
207
+ const handlePtr = res.struct.handle;
208
+ freeResult(res.rawPtr);
209
+ if (isNullPointer(handlePtr))
210
+ return { ok: true, value: null };
211
+ const str = readCString(handlePtr);
212
+ freeString(handlePtr);
213
+ return { ok: true, value: str };
214
+ }
215
+ /** Call a function returning FffResult with a JSON string in handle. */
216
+ function callJsonResult(funcName, paramsType, paramsValue) {
217
+ const res = readResultEnvelope(funcName, paramsType, paramsValue);
218
+ if ("ok" in res)
219
+ return res;
220
+ const handlePtr = res.struct.handle;
221
+ freeResult(res.rawPtr);
222
+ if (isNullPointer(handlePtr))
223
+ return { ok: true, value: undefined };
224
+ const jsonStr = readCString(handlePtr);
225
+ freeString(handlePtr);
226
+ if (jsonStr === null || jsonStr === "")
227
+ return { ok: true, value: undefined };
228
+ try {
229
+ return { ok: true, value: snakeToCamel(JSON.parse(jsonStr)) };
230
+ }
231
+ catch {
232
+ return { ok: true, value: jsonStr };
233
+ }
234
+ }
235
+ /** Free a C string via fff_free_string. */
236
+ function freeString(ptr) {
237
+ try {
238
+ load({
239
+ library: LIBRARY_KEY,
240
+ funcName: "fff_free_string",
241
+ retType: DataType.Void,
242
+ paramsType: [DataType.External],
243
+ paramsValue: [ptr],
244
+ });
245
+ }
246
+ catch {
247
+ // Ignore
248
+ }
249
+ }
250
+ /**
251
+ * Create a new file finder instance.
252
+ */
253
+ export function ffiCreate(basePath, frecencyDbPath, historyDbPath, useUnsafeNoLock, enableMmapCache, enableContentIndexing, watch, aiMode, logFilePath, logLevel, cacheBudgetMaxFiles, cacheBudgetMaxBytes, cacheBudgetMaxFileSize) {
254
+ loadLibrary();
255
+ const { rawPtr, struct: structData } = callRaw("fff_create_instance2", [
256
+ DataType.String, // base_path
257
+ DataType.String, // frecency_db_path
258
+ DataType.String, // history_db_path
259
+ DataType.Boolean, // use_unsafe_no_lock
260
+ DataType.Boolean, // enable_mmap_cache
261
+ DataType.Boolean, // enable_content_indexing
262
+ DataType.Boolean, // watch
263
+ DataType.Boolean, // ai_mode
264
+ DataType.String, // log_file_path
265
+ DataType.String, // log_level
266
+ DataType.U64, // cache_budget_max_files
267
+ DataType.U64, // cache_budget_max_bytes
268
+ DataType.U64, // cache_budget_max_file_size
269
+ ], [
270
+ basePath,
271
+ frecencyDbPath,
272
+ historyDbPath,
273
+ useUnsafeNoLock,
274
+ enableMmapCache,
275
+ enableContentIndexing,
276
+ watch,
277
+ aiMode,
278
+ logFilePath,
279
+ logLevel,
280
+ cacheBudgetMaxFiles,
281
+ cacheBudgetMaxBytes,
282
+ cacheBudgetMaxFileSize,
283
+ ]);
284
+ const success = structData.success !== 0;
285
+ try {
286
+ if (success) {
287
+ const handle = structData.handle;
288
+ if (isNullPointer(handle)) {
289
+ return err("fff_create_instance2 returned null handle");
290
+ }
291
+ return { ok: true, value: handle };
292
+ }
293
+ else {
294
+ const errorStr = readCString(structData.error);
295
+ return err(errorStr || "Unknown error");
296
+ }
297
+ }
298
+ finally {
299
+ freeResult(rawPtr);
300
+ }
301
+ }
302
+ /**
303
+ * Destroy and clean up an instance.
304
+ */
305
+ export function ffiDestroy(handle) {
306
+ loadLibrary();
307
+ load({
308
+ library: LIBRARY_KEY,
309
+ funcName: "fff_destroy",
310
+ retType: DataType.Void,
311
+ paramsType: [DataType.External],
312
+ paramsValue: [handle],
313
+ });
314
+ }
315
+ // ---------------------------------------------------------------------------
316
+ // Struct type definitions for restorePointer (must match #[repr(C)] layout)
317
+ // ---------------------------------------------------------------------------
318
+ const FFF_FILE_ITEM_STRUCT = {
319
+ relative_path: DataType.External,
320
+ file_name: DataType.External,
321
+ git_status: DataType.External,
322
+ size: DataType.U64,
323
+ modified: DataType.U64,
324
+ access_frecency_score: DataType.I64,
325
+ modification_frecency_score: DataType.I64,
326
+ total_frecency_score: DataType.I64,
327
+ is_binary: DataType.U8,
328
+ };
329
+ const FFF_SCORE_STRUCT = {
330
+ total: DataType.I32,
331
+ base_score: DataType.I32,
332
+ filename_bonus: DataType.I32,
333
+ special_filename_bonus: DataType.I32,
334
+ frecency_boost: DataType.I32,
335
+ distance_penalty: DataType.I32,
336
+ current_file_penalty: DataType.I32,
337
+ combo_match_boost: DataType.I32,
338
+ exact_match: DataType.U8,
339
+ match_type: DataType.External,
340
+ };
341
+ const FFF_SEARCH_RESULT_STRUCT = {
342
+ items: DataType.External,
343
+ scores: DataType.External,
344
+ count: DataType.U32,
345
+ total_matched: DataType.U32,
346
+ total_files: DataType.U32,
347
+ // FffLocation inlined (flattened)
348
+ location_tag: DataType.U8,
349
+ location_line: DataType.I32,
350
+ location_col: DataType.I32,
351
+ location_end_line: DataType.I32,
352
+ location_end_col: DataType.I32,
353
+ };
354
+ // FffDirItem struct (#[repr(C)]): char* (8) + char* (8) + i32 (4) + 4 padding = 24 bytes
355
+ const FFF_DIR_ITEM_STRUCT = {
356
+ relative_path: DataType.External,
357
+ dir_name: DataType.External,
358
+ max_access_frecency: DataType.I32,
359
+ };
360
+ const FFF_DIR_SEARCH_RESULT_STRUCT = {
361
+ items: DataType.External,
362
+ scores: DataType.External,
363
+ count: DataType.U32,
364
+ total_matched: DataType.U32,
365
+ total_dirs: DataType.U32,
366
+ };
367
+ // FffMixedItem struct (#[repr(C)]): u8 (1) + 7 padding + char* (8) + char* (8) + char* (8)
368
+ // + u64 (8) + u64 (8) + i64 (8) + i64 (8) + i64 (8) + bool (1) + 7 padding = 80 bytes
369
+ const FFF_MIXED_ITEM_STRUCT = {
370
+ item_type: DataType.U8,
371
+ relative_path: DataType.External,
372
+ display_name: DataType.External,
373
+ git_status: DataType.External,
374
+ size: DataType.U64,
375
+ modified: DataType.U64,
376
+ access_frecency_score: DataType.I64,
377
+ modification_frecency_score: DataType.I64,
378
+ total_frecency_score: DataType.I64,
379
+ is_binary: DataType.U8,
380
+ };
381
+ const FFF_MIXED_SEARCH_RESULT_STRUCT = {
382
+ items: DataType.External,
383
+ scores: DataType.External,
384
+ count: DataType.U32,
385
+ total_matched: DataType.U32,
386
+ total_files: DataType.U32,
387
+ total_dirs: DataType.U32,
388
+ // FffLocation inlined (flattened)
389
+ location_tag: DataType.U8,
390
+ location_line: DataType.I32,
391
+ location_col: DataType.I32,
392
+ location_end_line: DataType.I32,
393
+ location_end_col: DataType.I32,
394
+ };
395
+ // FffGrepMatch (144 bytes) — ordered by alignment: ptrs, u64s, u32s, u16, bools
396
+ const FFF_GREP_MATCH_STRUCT = {
397
+ relative_path: DataType.External,
398
+ file_name: DataType.External,
399
+ git_status: DataType.External,
400
+ line_content: DataType.External,
401
+ match_ranges: DataType.External,
402
+ context_before: DataType.External,
403
+ context_after: DataType.External,
404
+ size: DataType.U64,
405
+ modified: DataType.U64,
406
+ total_frecency_score: DataType.I64,
407
+ access_frecency_score: DataType.I64,
408
+ modification_frecency_score: DataType.I64,
409
+ line_number: DataType.U64,
410
+ byte_offset: DataType.U64,
411
+ col: DataType.U32,
412
+ match_ranges_count: DataType.U32,
413
+ context_before_count: DataType.U32,
414
+ context_after_count: DataType.U32,
415
+ fuzzy_score: DataType.U32, // actually u16 in C, but ffi-rs doesn't have U16 — reads as u32 with padding
416
+ has_fuzzy_score: DataType.U8,
417
+ is_binary: DataType.U8,
418
+ is_definition: DataType.U8,
419
+ };
420
+ const FFF_GREP_RESULT_STRUCT = {
421
+ items: DataType.External,
422
+ count: DataType.U32,
423
+ total_matched: DataType.U32,
424
+ total_files_searched: DataType.U32,
425
+ total_files: DataType.U32,
426
+ filtered_file_count: DataType.U32,
427
+ next_file_offset: DataType.U32,
428
+ regex_fallback_error: DataType.External,
429
+ };
430
+ const FFF_MATCH_RANGE_STRUCT = {
431
+ start: DataType.U32,
432
+ end: DataType.U32,
433
+ };
434
+ // ---------------------------------------------------------------------------
435
+ // Struct reading helpers
436
+ // ---------------------------------------------------------------------------
437
+ function readFileItemFromRaw(raw) {
438
+ return {
439
+ relativePath: readCString(raw.relative_path) ?? "",
440
+ fileName: readCString(raw.file_name) ?? "",
441
+ gitStatus: readCString(raw.git_status) ?? "",
442
+ size: Number(raw.size),
443
+ modified: Number(raw.modified),
444
+ accessFrecencyScore: Number(raw.access_frecency_score),
445
+ modificationFrecencyScore: Number(raw.modification_frecency_score),
446
+ totalFrecencyScore: Number(raw.total_frecency_score),
447
+ };
448
+ }
449
+ function readScoreFromRaw(raw) {
450
+ return {
451
+ total: raw.total,
452
+ baseScore: raw.base_score,
453
+ filenameBonus: raw.filename_bonus,
454
+ specialFilenameBonus: raw.special_filename_bonus,
455
+ frecencyBoost: raw.frecency_boost,
456
+ distancePenalty: raw.distance_penalty,
457
+ currentFilePenalty: raw.current_file_penalty,
458
+ comboMatchBoost: raw.combo_match_boost,
459
+ exactMatch: raw.exact_match !== 0,
460
+ matchType: readCString(raw.match_type) ?? "",
461
+ };
462
+ }
463
+ function readDirItemFromRaw(raw) {
464
+ return {
465
+ relativePath: readCString(raw.relative_path) ?? "",
466
+ dirName: readCString(raw.dir_name) ?? "",
467
+ maxAccessFrecency: raw.max_access_frecency,
468
+ };
469
+ }
470
+ function readMixedItemFromRaw(raw) {
471
+ if (raw.item_type === 1) {
472
+ // Directory
473
+ return {
474
+ type: "directory",
475
+ item: {
476
+ relativePath: readCString(raw.relative_path) ?? "",
477
+ dirName: readCString(raw.display_name) ?? "",
478
+ maxAccessFrecency: Number(raw.access_frecency_score),
479
+ },
480
+ };
481
+ }
482
+ // File (item_type === 0)
483
+ return {
484
+ type: "file",
485
+ item: {
486
+ relativePath: readCString(raw.relative_path) ?? "",
487
+ fileName: readCString(raw.display_name) ?? "",
488
+ gitStatus: readCString(raw.git_status) ?? "",
489
+ size: Number(raw.size),
490
+ modified: Number(raw.modified),
491
+ accessFrecencyScore: Number(raw.access_frecency_score),
492
+ modificationFrecencyScore: Number(raw.modification_frecency_score),
493
+ totalFrecencyScore: Number(raw.total_frecency_score),
494
+ },
495
+ };
496
+ }
497
+ /**
498
+ * Call an accessor function that returns a pointer to a struct element,
499
+ * then read the struct from that pointer.
500
+ */
501
+ function callAccessor(funcName, resultPtr, index, structDef) {
502
+ loadLibrary();
503
+ const elemPtr = load({
504
+ library: LIBRARY_KEY,
505
+ funcName,
506
+ retType: DataType.External,
507
+ paramsType: [DataType.External, DataType.U32],
508
+ paramsValue: [resultPtr, index],
509
+ });
510
+ const [raw] = restorePointer({
511
+ retType: [structDef],
512
+ paramsValue: wrapPointer([elemPtr]),
513
+ });
514
+ return raw;
515
+ }
516
+ /**
517
+ * Offset a pointer by `bytes` using the C API helper.
518
+ */
519
+ function ptrOffset(base, bytes) {
520
+ return load({
521
+ library: LIBRARY_KEY,
522
+ funcName: "fff_ptr_offset",
523
+ retType: DataType.External,
524
+ paramsType: [DataType.External, DataType.U64],
525
+ paramsValue: [base, bytes],
526
+ });
527
+ }
528
+ /**
529
+ * Read a C string array (char**) of `count` elements.
530
+ */
531
+ function readCStringArray(ptrArray, count) {
532
+ if (count === 0 || isNullPointer(ptrArray))
533
+ return [];
534
+ const result = [];
535
+ for (let i = 0; i < count; i++) {
536
+ const elemPtr = ptrOffset(ptrArray, i * 8);
537
+ const [charPtr] = restorePointer({
538
+ retType: [DataType.External],
539
+ paramsValue: [elemPtr],
540
+ });
541
+ result.push(readCString(charPtr) ?? "");
542
+ }
543
+ return result;
544
+ }
545
+ function readGrepMatchFromRaw(raw) {
546
+ // Read match_ranges array via pointer offsets
547
+ const matchRanges = [];
548
+ for (let i = 0; i < raw.match_ranges_count; i++) {
549
+ const rangePtr = ptrOffset(raw.match_ranges, i * 8); // FffMatchRange is 8 bytes
550
+ const [rangeRaw] = restorePointer({
551
+ retType: [FFF_MATCH_RANGE_STRUCT],
552
+ paramsValue: wrapPointer([rangePtr]),
553
+ });
554
+ matchRanges.push([rangeRaw.start, rangeRaw.end]);
555
+ }
556
+ const match = {
557
+ relativePath: readCString(raw.relative_path) ?? "",
558
+ fileName: readCString(raw.file_name) ?? "",
559
+ gitStatus: readCString(raw.git_status) ?? "",
560
+ lineContent: readCString(raw.line_content) ?? "",
561
+ size: Number(raw.size),
562
+ modified: Number(raw.modified),
563
+ totalFrecencyScore: Number(raw.total_frecency_score),
564
+ accessFrecencyScore: Number(raw.access_frecency_score),
565
+ modificationFrecencyScore: Number(raw.modification_frecency_score),
566
+ isBinary: raw.is_binary !== 0,
567
+ lineNumber: Number(raw.line_number),
568
+ col: raw.col,
569
+ byteOffset: Number(raw.byte_offset),
570
+ matchRanges,
571
+ };
572
+ if (raw.has_fuzzy_score !== 0) {
573
+ match.fuzzyScore = raw.fuzzy_score;
574
+ }
575
+ if (raw.context_before_count > 0) {
576
+ match.contextBefore = readCStringArray(raw.context_before, raw.context_before_count);
577
+ }
578
+ if (raw.context_after_count > 0) {
579
+ match.contextAfter = readCStringArray(raw.context_after, raw.context_after_count);
580
+ }
581
+ if (raw.is_definition !== 0) {
582
+ match.isDefinition = true;
583
+ }
584
+ return match;
585
+ }
586
+ /**
587
+ * Parse an FffGrepResult from `FffResult.handle`, then free native memory.
588
+ */
589
+ function parseGrepResult(rawPtr) {
590
+ loadLibrary();
591
+ const [envelope] = restorePointer({
592
+ retType: [FFF_RESULT_STRUCT],
593
+ paramsValue: wrapPointer([rawPtr]),
594
+ });
595
+ const success = envelope.success !== 0;
596
+ if (!success) {
597
+ const errorMsg = readCString(envelope.error) || "Unknown error";
598
+ freeResult(rawPtr);
599
+ return err(errorMsg);
600
+ }
601
+ const handlePtr = envelope.handle;
602
+ freeResult(rawPtr);
603
+ if (isNullPointer(handlePtr)) {
604
+ return err("grep returned null result");
605
+ }
606
+ const [gr] = restorePointer({
607
+ retType: [FFF_GREP_RESULT_STRUCT],
608
+ paramsValue: wrapPointer([handlePtr]),
609
+ });
610
+ const count = gr.count;
611
+ const regexFallbackError = readCString(gr.regex_fallback_error) ?? undefined;
612
+ const items = [];
613
+ for (let i = 0; i < count; i++) {
614
+ const rawMatch = callAccessor("fff_grep_result_get_match", handlePtr, i, FFF_GREP_MATCH_STRUCT);
615
+ items.push(readGrepMatchFromRaw(rawMatch));
616
+ }
617
+ // Free native grep result
618
+ load({
619
+ library: LIBRARY_KEY,
620
+ funcName: "fff_free_grep_result",
621
+ retType: DataType.Void,
622
+ paramsType: [DataType.External],
623
+ paramsValue: [handlePtr],
624
+ });
625
+ const grepResult = {
626
+ items,
627
+ totalMatched: gr.total_matched,
628
+ totalFilesSearched: gr.total_files_searched,
629
+ totalFiles: gr.total_files,
630
+ filteredFileCount: gr.filtered_file_count,
631
+ nextCursor: gr.next_file_offset > 0 ? createGrepCursor(gr.next_file_offset) : null,
632
+ };
633
+ if (regexFallbackError) {
634
+ grepResult.regexFallbackError = regexFallbackError;
635
+ }
636
+ return { ok: true, value: grepResult };
637
+ }
638
+ /**
639
+ * Parse an FffSearchResult from `FffResult.handle`, then free native memory.
640
+ */
641
+ function parseSearchResult(rawPtr) {
642
+ loadLibrary();
643
+ // Read FffResult envelope
644
+ const [envelope] = restorePointer({
645
+ retType: [FFF_RESULT_STRUCT],
646
+ paramsValue: wrapPointer([rawPtr]),
647
+ });
648
+ const success = envelope.success !== 0;
649
+ if (!success) {
650
+ const errorMsg = readCString(envelope.error) || "Unknown error";
651
+ freeResult(rawPtr);
652
+ return err(errorMsg);
653
+ }
654
+ const handlePtr = envelope.handle;
655
+ // Free the FffResult envelope (does NOT free handle)
656
+ freeResult(rawPtr);
657
+ if (isNullPointer(handlePtr)) {
658
+ return err("fff_search returned null search result");
659
+ }
660
+ // Read FffSearchResult struct
661
+ const [sr] = restorePointer({
662
+ retType: [FFF_SEARCH_RESULT_STRUCT],
663
+ paramsValue: wrapPointer([handlePtr]),
664
+ });
665
+ const count = sr.count;
666
+ // Read location
667
+ let location;
668
+ if (sr.location_tag === 1) {
669
+ location = { type: "line", line: sr.location_line };
670
+ }
671
+ else if (sr.location_tag === 2) {
672
+ location = { type: "position", line: sr.location_line, col: sr.location_col };
673
+ }
674
+ else if (sr.location_tag === 3) {
675
+ location = {
676
+ type: "range",
677
+ start: { line: sr.location_line, col: sr.location_col },
678
+ end: { line: sr.location_end_line, col: sr.location_end_col },
679
+ };
680
+ }
681
+ // Read items and scores via accessor functions
682
+ const items = [];
683
+ const scores = [];
684
+ for (let i = 0; i < count; i++) {
685
+ const rawItem = callAccessor("fff_search_result_get_item", handlePtr, i, FFF_FILE_ITEM_STRUCT);
686
+ items.push(readFileItemFromRaw(rawItem));
687
+ const rawScore = callAccessor("fff_search_result_get_score", handlePtr, i, FFF_SCORE_STRUCT);
688
+ scores.push(readScoreFromRaw(rawScore));
689
+ }
690
+ // Free native search result
691
+ load({
692
+ library: LIBRARY_KEY,
693
+ funcName: "fff_free_search_result",
694
+ retType: DataType.Void,
695
+ paramsType: [DataType.External],
696
+ paramsValue: [handlePtr],
697
+ });
698
+ const result = {
699
+ items,
700
+ scores,
701
+ totalMatched: sr.total_matched,
702
+ totalFiles: sr.total_files,
703
+ };
704
+ if (location) {
705
+ result.location = location;
706
+ }
707
+ return { ok: true, value: result };
708
+ }
709
+ /**
710
+ * Parse an FffDirSearchResult from `FffResult.handle`, then free native memory.
711
+ */
712
+ function parseDirSearchResult(rawPtr) {
713
+ loadLibrary();
714
+ // Read FffResult envelope
715
+ const [envelope] = restorePointer({
716
+ retType: [FFF_RESULT_STRUCT],
717
+ paramsValue: wrapPointer([rawPtr]),
718
+ });
719
+ const success = envelope.success !== 0;
720
+ if (!success) {
721
+ const errorMsg = readCString(envelope.error) || "Unknown error";
722
+ freeResult(rawPtr);
723
+ return err(errorMsg);
724
+ }
725
+ const handlePtr = envelope.handle;
726
+ // Free the FffResult envelope (does NOT free handle)
727
+ freeResult(rawPtr);
728
+ if (isNullPointer(handlePtr)) {
729
+ return err("fff_search_directories returned null search result");
730
+ }
731
+ // Read FffDirSearchResult struct
732
+ const [sr] = restorePointer({
733
+ retType: [FFF_DIR_SEARCH_RESULT_STRUCT],
734
+ paramsValue: wrapPointer([handlePtr]),
735
+ });
736
+ const count = sr.count;
737
+ // Read items and scores via accessor functions
738
+ const items = [];
739
+ const scores = [];
740
+ for (let i = 0; i < count; i++) {
741
+ const rawItem = callAccessor("fff_dir_search_result_get_item", handlePtr, i, FFF_DIR_ITEM_STRUCT);
742
+ items.push(readDirItemFromRaw(rawItem));
743
+ const rawScore = callAccessor("fff_dir_search_result_get_score", handlePtr, i, FFF_SCORE_STRUCT);
744
+ scores.push(readScoreFromRaw(rawScore));
745
+ }
746
+ // Free native dir search result
747
+ load({
748
+ library: LIBRARY_KEY,
749
+ funcName: "fff_free_dir_search_result",
750
+ retType: DataType.Void,
751
+ paramsType: [DataType.External],
752
+ paramsValue: [handlePtr],
753
+ });
754
+ return {
755
+ ok: true,
756
+ value: {
757
+ items,
758
+ scores,
759
+ totalMatched: sr.total_matched,
760
+ totalDirs: sr.total_dirs,
761
+ },
762
+ };
763
+ }
764
+ /**
765
+ * Parse an FffMixedSearchResult from `FffResult.handle`, then free native memory.
766
+ */
767
+ function parseMixedSearchResult(rawPtr) {
768
+ loadLibrary();
769
+ // Read FffResult envelope
770
+ const [envelope] = restorePointer({
771
+ retType: [FFF_RESULT_STRUCT],
772
+ paramsValue: wrapPointer([rawPtr]),
773
+ });
774
+ const success = envelope.success !== 0;
775
+ if (!success) {
776
+ const errorMsg = readCString(envelope.error) || "Unknown error";
777
+ freeResult(rawPtr);
778
+ return err(errorMsg);
779
+ }
780
+ const handlePtr = envelope.handle;
781
+ // Free the FffResult envelope (does NOT free handle)
782
+ freeResult(rawPtr);
783
+ if (isNullPointer(handlePtr)) {
784
+ return err("fff_search_mixed returned null search result");
785
+ }
786
+ // Read FffMixedSearchResult struct
787
+ const [sr] = restorePointer({
788
+ retType: [FFF_MIXED_SEARCH_RESULT_STRUCT],
789
+ paramsValue: wrapPointer([handlePtr]),
790
+ });
791
+ const count = sr.count;
792
+ // Read location
793
+ let location;
794
+ if (sr.location_tag === 1) {
795
+ location = { type: "line", line: sr.location_line };
796
+ }
797
+ else if (sr.location_tag === 2) {
798
+ location = { type: "position", line: sr.location_line, col: sr.location_col };
799
+ }
800
+ else if (sr.location_tag === 3) {
801
+ location = {
802
+ type: "range",
803
+ start: { line: sr.location_line, col: sr.location_col },
804
+ end: { line: sr.location_end_line, col: sr.location_end_col },
805
+ };
806
+ }
807
+ // Read items and scores via accessor functions
808
+ const items = [];
809
+ const scores = [];
810
+ for (let i = 0; i < count; i++) {
811
+ const rawItem = callAccessor("fff_mixed_search_result_get_item", handlePtr, i, FFF_MIXED_ITEM_STRUCT);
812
+ items.push(readMixedItemFromRaw(rawItem));
813
+ const rawScore = callAccessor("fff_mixed_search_result_get_score", handlePtr, i, FFF_SCORE_STRUCT);
814
+ scores.push(readScoreFromRaw(rawScore));
815
+ }
816
+ // Free native mixed search result
817
+ load({
818
+ library: LIBRARY_KEY,
819
+ funcName: "fff_free_mixed_search_result",
820
+ retType: DataType.Void,
821
+ paramsType: [DataType.External],
822
+ paramsValue: [handlePtr],
823
+ });
824
+ const result = {
825
+ items,
826
+ scores,
827
+ totalMatched: sr.total_matched,
828
+ totalFiles: sr.total_files,
829
+ totalDirs: sr.total_dirs,
830
+ };
831
+ if (location) {
832
+ result.location = location;
833
+ }
834
+ return { ok: true, value: result };
835
+ }
836
+ /**
837
+ * Perform fuzzy search.
838
+ */
839
+ export function ffiSearch(handle, query, currentFile, maxThreads, pageIndex, pageSize, comboBoostMultiplier, minComboCount) {
840
+ loadLibrary();
841
+ const rawPtr = load({
842
+ library: LIBRARY_KEY,
843
+ funcName: "fff_search",
844
+ retType: DataType.External,
845
+ paramsType: [
846
+ DataType.External, // handle
847
+ DataType.String, // query
848
+ DataType.String, // current_file
849
+ DataType.U32, // max_threads
850
+ DataType.U32, // page_index
851
+ DataType.U32, // page_size
852
+ DataType.I32, // combo_boost_multiplier
853
+ DataType.U32, // min_combo_count
854
+ ],
855
+ paramsValue: [
856
+ handle,
857
+ query,
858
+ currentFile,
859
+ maxThreads,
860
+ pageIndex,
861
+ pageSize,
862
+ comboBoostMultiplier,
863
+ minComboCount,
864
+ ],
865
+ freeResultMemory: false,
866
+ });
867
+ return parseSearchResult(rawPtr);
868
+ }
869
+ /**
870
+ * Perform fuzzy directory search.
871
+ */
872
+ export function ffiSearchDirectories(handle, query, currentFile, maxThreads, pageIndex, pageSize) {
873
+ loadLibrary();
874
+ const rawPtr = load({
875
+ library: LIBRARY_KEY,
876
+ funcName: "fff_search_directories",
877
+ retType: DataType.External,
878
+ paramsType: [
879
+ DataType.External, // handle
880
+ DataType.String, // query
881
+ DataType.String, // current_file
882
+ DataType.U32, // max_threads
883
+ DataType.U32, // page_index
884
+ DataType.U32, // page_size
885
+ ],
886
+ paramsValue: [handle, query, currentFile ?? "", maxThreads, pageIndex, pageSize],
887
+ freeResultMemory: false,
888
+ });
889
+ return parseDirSearchResult(rawPtr);
890
+ }
891
+ /**
892
+ * Perform mixed (files + directories) fuzzy search.
893
+ */
894
+ export function ffiSearchMixed(handle, query, currentFile, maxThreads, pageIndex, pageSize, comboBoostMultiplier, minComboCount) {
895
+ loadLibrary();
896
+ const rawPtr = load({
897
+ library: LIBRARY_KEY,
898
+ funcName: "fff_search_mixed",
899
+ retType: DataType.External,
900
+ paramsType: [
901
+ DataType.External, // handle
902
+ DataType.String, // query
903
+ DataType.String, // current_file
904
+ DataType.U32, // max_threads
905
+ DataType.U32, // page_index
906
+ DataType.U32, // page_size
907
+ DataType.I32, // combo_boost_multiplier
908
+ DataType.U32, // min_combo_count
909
+ ],
910
+ paramsValue: [
911
+ handle,
912
+ query,
913
+ currentFile,
914
+ maxThreads,
915
+ pageIndex,
916
+ pageSize,
917
+ comboBoostMultiplier,
918
+ minComboCount,
919
+ ],
920
+ freeResultMemory: false,
921
+ });
922
+ return parseMixedSearchResult(rawPtr);
923
+ }
924
+ /**
925
+ * Live grep - search file contents.
926
+ */
927
+ export function ffiLiveGrep(handle, query, mode, maxFileSize, maxMatchesPerFile, smartCase, fileOffset, pageLimit, timeBudgetMs, beforeContext, afterContext, classifyDefinitions) {
928
+ loadLibrary();
929
+ const rawPtr = load({
930
+ library: LIBRARY_KEY,
931
+ funcName: "fff_live_grep",
932
+ retType: DataType.External,
933
+ paramsType: [
934
+ DataType.External, // handle
935
+ DataType.String, // query
936
+ DataType.U8, // mode
937
+ DataType.U64, // max_file_size
938
+ DataType.U32, // max_matches_per_file
939
+ DataType.Boolean, // smart_case
940
+ DataType.U32, // file_offset
941
+ DataType.U32, // page_limit
942
+ DataType.U64, // time_budget_ms
943
+ DataType.U32, // before_context
944
+ DataType.U32, // after_context
945
+ DataType.Boolean, // classify_definitions
946
+ ],
947
+ paramsValue: [
948
+ handle,
949
+ query,
950
+ grepModeToU8(mode),
951
+ maxFileSize,
952
+ maxMatchesPerFile,
953
+ smartCase,
954
+ fileOffset,
955
+ pageLimit,
956
+ timeBudgetMs,
957
+ beforeContext,
958
+ afterContext,
959
+ classifyDefinitions,
960
+ ],
961
+ freeResultMemory: false,
962
+ });
963
+ return parseGrepResult(rawPtr);
964
+ }
965
+ /**
966
+ * Multi-pattern grep - Aho-Corasick multi-needle search.
967
+ */
968
+ export function ffiMultiGrep(handle, patternsJoined, constraints, maxFileSize, maxMatchesPerFile, smartCase, fileOffset, pageLimit, timeBudgetMs, beforeContext, afterContext, classifyDefinitions) {
969
+ loadLibrary();
970
+ const rawPtr = load({
971
+ library: LIBRARY_KEY,
972
+ funcName: "fff_multi_grep",
973
+ retType: DataType.External,
974
+ paramsType: [
975
+ DataType.External, // handle
976
+ DataType.String, // patterns_joined
977
+ DataType.String, // constraints
978
+ DataType.U64, // max_file_size
979
+ DataType.U32, // max_matches_per_file
980
+ DataType.Boolean, // smart_case
981
+ DataType.U32, // file_offset
982
+ DataType.U32, // page_limit
983
+ DataType.U64, // time_budget_ms
984
+ DataType.U32, // before_context
985
+ DataType.U32, // after_context
986
+ DataType.Boolean, // classify_definitions
987
+ ],
988
+ paramsValue: [
989
+ handle,
990
+ patternsJoined,
991
+ constraints,
992
+ maxFileSize,
993
+ maxMatchesPerFile,
994
+ smartCase,
995
+ fileOffset,
996
+ pageLimit,
997
+ timeBudgetMs,
998
+ beforeContext,
999
+ afterContext,
1000
+ classifyDefinitions,
1001
+ ],
1002
+ freeResultMemory: false,
1003
+ });
1004
+ return parseGrepResult(rawPtr);
1005
+ }
1006
+ /**
1007
+ * Trigger file scan.
1008
+ */
1009
+ export function ffiScanFiles(handle) {
1010
+ return callVoidResult("fff_scan_files", [DataType.External], [handle]);
1011
+ }
1012
+ /**
1013
+ * Check if scanning.
1014
+ */
1015
+ export function ffiIsScanning(handle) {
1016
+ loadLibrary();
1017
+ return load({
1018
+ library: LIBRARY_KEY,
1019
+ funcName: "fff_is_scanning",
1020
+ retType: DataType.Boolean,
1021
+ paramsType: [DataType.External],
1022
+ paramsValue: [handle],
1023
+ });
1024
+ }
1025
+ /**
1026
+ * Get the base path of the file picker.
1027
+ */
1028
+ export function ffiGetBasePath(handle) {
1029
+ return callStringResult("fff_get_base_path", [DataType.External], [handle]);
1030
+ }
1031
+ // FffScanProgress struct definition
1032
+ const FFF_SCAN_PROGRESS_STRUCT = {
1033
+ scanned_files_count: DataType.U64,
1034
+ is_scanning: DataType.U8,
1035
+ };
1036
+ /**
1037
+ * Get scan progress.
1038
+ */
1039
+ export function ffiGetScanProgress(handle) {
1040
+ loadLibrary();
1041
+ const res = readResultEnvelope("fff_get_scan_progress", [DataType.External], [handle]);
1042
+ if ("ok" in res)
1043
+ return res;
1044
+ const handlePtr = res.struct.handle;
1045
+ freeResult(res.rawPtr);
1046
+ if (isNullPointer(handlePtr))
1047
+ return err("scan progress returned null");
1048
+ const [sp] = restorePointer({
1049
+ retType: [FFF_SCAN_PROGRESS_STRUCT],
1050
+ paramsValue: wrapPointer([handlePtr]),
1051
+ });
1052
+ const result = {
1053
+ scannedFilesCount: Number(sp.scanned_files_count),
1054
+ isScanning: sp.is_scanning !== 0,
1055
+ };
1056
+ // Free native scan progress
1057
+ load({
1058
+ library: LIBRARY_KEY,
1059
+ funcName: "fff_free_scan_progress",
1060
+ retType: DataType.Void,
1061
+ paramsType: [DataType.External],
1062
+ paramsValue: [handlePtr],
1063
+ });
1064
+ return { ok: true, value: result };
1065
+ }
1066
+ /**
1067
+ * Wait for a tree scan to complete.
1068
+ */
1069
+ export function ffiWaitForScan(handle, timeoutMs) {
1070
+ return callBoolResult("fff_wait_for_scan", [DataType.External, DataType.U64], [handle, timeoutMs]);
1071
+ }
1072
+ /**
1073
+ * Restart index in new path.
1074
+ */
1075
+ export function ffiRestartIndex(handle, newPath) {
1076
+ return callVoidResult("fff_restart_index", [DataType.External, DataType.String], [handle, newPath]);
1077
+ }
1078
+ /**
1079
+ * Refresh git status.
1080
+ */
1081
+ export function ffiRefreshGitStatus(handle) {
1082
+ return callIntResult("fff_refresh_git_status", [DataType.External], [handle]);
1083
+ }
1084
+ /**
1085
+ * Track query completion.
1086
+ */
1087
+ export function ffiTrackQuery(handle, query, filePath) {
1088
+ return callBoolResult("fff_track_query", [DataType.External, DataType.String, DataType.String], [handle, query, filePath]);
1089
+ }
1090
+ /**
1091
+ * Get historical query.
1092
+ */
1093
+ export function ffiGetHistoricalQuery(handle, offset) {
1094
+ return callStringResult("fff_get_historical_query", [DataType.External, DataType.U64], [handle, offset]);
1095
+ }
1096
+ /**
1097
+ * Health check.
1098
+ *
1099
+ * `handle` can be null for a limited check (version + git only).
1100
+ * When null, we pass DataType.U64 with value 0 as a null pointer workaround
1101
+ * since ffi-rs does not accept `null` for External parameters.
1102
+ */
1103
+ export function ffiHealthCheck(handle, testPath) {
1104
+ if (handle === null) {
1105
+ // Use U64(0) as a null pointer since ffi-rs rejects null for External params
1106
+ return callJsonResult("fff_health_check", [DataType.U64, DataType.String], [0, testPath]);
1107
+ }
1108
+ return callJsonResult("fff_health_check", [DataType.External, DataType.String], [handle, testPath]);
1109
+ }
1110
+ /**
1111
+ * Ensure the library is loaded.
1112
+ *
1113
+ * Loads the native library from the platform-specific npm package
1114
+ * or a local dev build. Throws if the binary is not found.
1115
+ */
1116
+ export function ensureLoaded() {
1117
+ loadLibrary();
1118
+ }
1119
+ /**
1120
+ * Check if the library is available.
1121
+ */
1122
+ export function isAvailable() {
1123
+ try {
1124
+ loadLibrary();
1125
+ return true;
1126
+ }
1127
+ catch {
1128
+ return false;
1129
+ }
1130
+ }
1131
+ /**
1132
+ * Close the library and release ffi-rs resources.
1133
+ * Call this when completely done with the library.
1134
+ */
1135
+ export function closeLibrary() {
1136
+ if (isLoaded) {
1137
+ close(LIBRARY_KEY);
1138
+ isLoaded = false;
1139
+ }
1140
+ }
1141
+ //# sourceMappingURL=ffi.js.map