@ff-labs/fff-node 0.2.4-nightly.d1d5d86 → 0.2.5-dev.df8b3c4

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/dist/src/ffi.js CHANGED
@@ -35,8 +35,23 @@
35
35
  */
36
36
  import { close, DataType, isNullPointer, load, open, restorePointer, wrapPointer, } from "ffi-rs";
37
37
  import { findBinary } from "./binary.js";
38
- import { err } from "./types.js";
38
+ import { createGrepCursor, err } from "./types.js";
39
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
+ }
40
55
  // Track whether the library is loaded
41
56
  let isLoaded = false;
42
57
  /**
@@ -48,9 +63,9 @@ let isLoaded = false;
48
63
  */
49
64
  const FFF_RESULT_STRUCT = {
50
65
  success: DataType.U8,
51
- data: DataType.External,
52
66
  error: DataType.External,
53
67
  handle: DataType.External,
68
+ int_value: DataType.I64,
54
69
  };
55
70
  /**
56
71
  * Load the native library using ffi-rs
@@ -145,60 +160,112 @@ function freeResult(resultPtr) {
145
160
  }
146
161
  }
147
162
  /**
148
- * Call a fff C function that returns *mut FffResult, parse the result,
149
- * free the native memory, and return a typed Result<T>.
150
- *
151
- * Strategy:
152
- * 1. Call the C function with External retType to get the raw pointer
153
- * 2. Read struct fields via restorePointer
154
- * 3. Based on success flag, read the data or error string
155
- * 4. Call fff_free_result with the original raw pointer to free Rust memory
156
- * 5. Parse JSON data and convert snake_case to camelCase
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.
157
165
  */
158
- function callFfiResult(funcName, paramsType, paramsValue) {
166
+ function readResultEnvelope(funcName, paramsType, paramsValue) {
159
167
  loadLibrary();
160
168
  const { rawPtr, struct: structData } = callRaw(funcName, paramsType, paramsValue);
161
- const success = structData.success !== 0;
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 };
162
228
  try {
163
- if (success) {
164
- const dataStr = readCString(structData.data);
165
- if (dataStr === null || dataStr === "") {
166
- return { ok: true, value: undefined };
167
- }
168
- try {
169
- const parsed = JSON.parse(dataStr);
170
- const transformed = snakeToCamel(parsed);
171
- return { ok: true, value: transformed };
172
- }
173
- catch {
174
- // For simple values like "true" or numbers
175
- return { ok: true, value: dataStr };
176
- }
177
- }
178
- else {
179
- const errorStr = readCString(structData.error);
180
- return err(errorStr || "Unknown error");
181
- }
229
+ return { ok: true, value: snakeToCamel(JSON.parse(jsonStr)) };
182
230
  }
183
- finally {
184
- freeResult(rawPtr);
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
185
248
  }
186
249
  }
187
250
  /**
188
251
  * Create a new file finder instance.
189
- *
190
- * Returns the opaque native handle on success. The handle must be passed to
191
- * all subsequent FFI calls and freed with `ffiDestroy`.
192
252
  */
193
- export function ffiCreate(optsJson) {
253
+ export function ffiCreate(basePath, frecencyDbPath, historyDbPath, useUnsafeNoLock, warmupMmapCache, aiMode) {
194
254
  loadLibrary();
195
- const { rawPtr, struct: structData } = callRaw("fff_create", [DataType.String], [optsJson]);
255
+ const { rawPtr, struct: structData } = callRaw("fff_create_instance", [
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, // warmup_mmap_cache
261
+ DataType.Boolean, // ai_mode
262
+ ], [basePath, frecencyDbPath, historyDbPath, useUnsafeNoLock, warmupMmapCache, aiMode]);
196
263
  const success = structData.success !== 0;
197
264
  try {
198
265
  if (success) {
199
266
  const handle = structData.handle;
200
267
  if (isNullPointer(handle)) {
201
- return err("fff_create returned null handle");
268
+ return err("fff_create_instance returned null handle");
202
269
  }
203
270
  return { ok: true, value: handle };
204
271
  }
@@ -224,17 +291,416 @@ export function ffiDestroy(handle) {
224
291
  paramsValue: [handle],
225
292
  });
226
293
  }
294
+ // ---------------------------------------------------------------------------
295
+ // Struct type definitions for restorePointer (must match #[repr(C)] layout)
296
+ // ---------------------------------------------------------------------------
297
+ const FFF_FILE_ITEM_STRUCT = {
298
+ path: DataType.External,
299
+ relative_path: DataType.External,
300
+ file_name: DataType.External,
301
+ git_status: DataType.External,
302
+ size: DataType.U64,
303
+ modified: DataType.U64,
304
+ access_frecency_score: DataType.I64,
305
+ modification_frecency_score: DataType.I64,
306
+ total_frecency_score: DataType.I64,
307
+ is_binary: DataType.U8,
308
+ };
309
+ const FFF_SCORE_STRUCT = {
310
+ total: DataType.I32,
311
+ base_score: DataType.I32,
312
+ filename_bonus: DataType.I32,
313
+ special_filename_bonus: DataType.I32,
314
+ frecency_boost: DataType.I32,
315
+ distance_penalty: DataType.I32,
316
+ current_file_penalty: DataType.I32,
317
+ combo_match_boost: DataType.I32,
318
+ exact_match: DataType.U8,
319
+ match_type: DataType.External,
320
+ };
321
+ const FFF_SEARCH_RESULT_STRUCT = {
322
+ items: DataType.External,
323
+ scores: DataType.External,
324
+ count: DataType.U32,
325
+ total_matched: DataType.U32,
326
+ total_files: DataType.U32,
327
+ // FffLocation inlined (flattened)
328
+ location_tag: DataType.U8,
329
+ location_line: DataType.I32,
330
+ location_col: DataType.I32,
331
+ location_end_line: DataType.I32,
332
+ location_end_col: DataType.I32,
333
+ };
334
+ // FffGrepMatch (144 bytes) — ordered by alignment: ptrs, u64s, u32s, u16, bools
335
+ const FFF_GREP_MATCH_STRUCT = {
336
+ path: DataType.External,
337
+ relative_path: DataType.External,
338
+ file_name: DataType.External,
339
+ git_status: DataType.External,
340
+ line_content: DataType.External,
341
+ match_ranges: DataType.External,
342
+ context_before: DataType.External,
343
+ context_after: DataType.External,
344
+ size: DataType.U64,
345
+ modified: DataType.U64,
346
+ total_frecency_score: DataType.I64,
347
+ access_frecency_score: DataType.I64,
348
+ modification_frecency_score: DataType.I64,
349
+ line_number: DataType.U64,
350
+ byte_offset: DataType.U64,
351
+ col: DataType.U32,
352
+ match_ranges_count: DataType.U32,
353
+ context_before_count: DataType.U32,
354
+ context_after_count: DataType.U32,
355
+ fuzzy_score: DataType.U32, // actually u16 in C, but ffi-rs doesn't have U16 — reads as u32 with padding
356
+ has_fuzzy_score: DataType.U8,
357
+ is_binary: DataType.U8,
358
+ is_definition: DataType.U8,
359
+ };
360
+ const FFF_GREP_RESULT_STRUCT = {
361
+ items: DataType.External,
362
+ count: DataType.U32,
363
+ total_matched: DataType.U32,
364
+ total_files_searched: DataType.U32,
365
+ total_files: DataType.U32,
366
+ filtered_file_count: DataType.U32,
367
+ next_file_offset: DataType.U32,
368
+ regex_fallback_error: DataType.External,
369
+ };
370
+ const FFF_MATCH_RANGE_STRUCT = {
371
+ start: DataType.U32,
372
+ end: DataType.U32,
373
+ };
374
+ // ---------------------------------------------------------------------------
375
+ // Struct reading helpers
376
+ // ---------------------------------------------------------------------------
377
+ function readFileItemFromRaw(raw) {
378
+ return {
379
+ path: readCString(raw.path) ?? "",
380
+ relativePath: readCString(raw.relative_path) ?? "",
381
+ fileName: readCString(raw.file_name) ?? "",
382
+ gitStatus: readCString(raw.git_status) ?? "",
383
+ size: Number(raw.size),
384
+ modified: Number(raw.modified),
385
+ accessFrecencyScore: Number(raw.access_frecency_score),
386
+ modificationFrecencyScore: Number(raw.modification_frecency_score),
387
+ totalFrecencyScore: Number(raw.total_frecency_score),
388
+ };
389
+ }
390
+ function readScoreFromRaw(raw) {
391
+ return {
392
+ total: raw.total,
393
+ baseScore: raw.base_score,
394
+ filenameBonus: raw.filename_bonus,
395
+ specialFilenameBonus: raw.special_filename_bonus,
396
+ frecencyBoost: raw.frecency_boost,
397
+ distancePenalty: raw.distance_penalty,
398
+ currentFilePenalty: raw.current_file_penalty,
399
+ comboMatchBoost: raw.combo_match_boost,
400
+ exactMatch: raw.exact_match !== 0,
401
+ matchType: readCString(raw.match_type) ?? "",
402
+ };
403
+ }
404
+ /**
405
+ * Call an accessor function that returns a pointer to a struct element,
406
+ * then read the struct from that pointer.
407
+ */
408
+ function callAccessor(funcName, resultPtr, index, structDef) {
409
+ loadLibrary();
410
+ const elemPtr = load({
411
+ library: LIBRARY_KEY,
412
+ funcName,
413
+ retType: DataType.External,
414
+ paramsType: [DataType.External, DataType.U32],
415
+ paramsValue: [resultPtr, index],
416
+ });
417
+ const [raw] = restorePointer({
418
+ retType: [structDef],
419
+ paramsValue: wrapPointer([elemPtr]),
420
+ });
421
+ return raw;
422
+ }
423
+ /**
424
+ * Offset a pointer by `bytes` using the C API helper.
425
+ */
426
+ function ptrOffset(base, bytes) {
427
+ return load({
428
+ library: LIBRARY_KEY,
429
+ funcName: "fff_ptr_offset",
430
+ retType: DataType.External,
431
+ paramsType: [DataType.External, DataType.U64],
432
+ paramsValue: [base, bytes],
433
+ });
434
+ }
435
+ /**
436
+ * Read a C string array (char**) of `count` elements.
437
+ */
438
+ function readCStringArray(ptrArray, count) {
439
+ if (count === 0 || isNullPointer(ptrArray))
440
+ return [];
441
+ const result = [];
442
+ for (let i = 0; i < count; i++) {
443
+ const elemPtr = ptrOffset(ptrArray, i * 8);
444
+ const [charPtr] = restorePointer({
445
+ retType: [DataType.External],
446
+ paramsValue: wrapPointer([elemPtr]),
447
+ });
448
+ result.push(readCString(charPtr) ?? "");
449
+ }
450
+ return result;
451
+ }
452
+ function readGrepMatchFromRaw(raw) {
453
+ // Read match_ranges array via pointer offsets
454
+ const matchRanges = [];
455
+ for (let i = 0; i < raw.match_ranges_count; i++) {
456
+ const rangePtr = ptrOffset(raw.match_ranges, i * 8); // FffMatchRange is 8 bytes
457
+ const [rangeRaw] = restorePointer({
458
+ retType: [FFF_MATCH_RANGE_STRUCT],
459
+ paramsValue: wrapPointer([rangePtr]),
460
+ });
461
+ matchRanges.push([rangeRaw.start, rangeRaw.end]);
462
+ }
463
+ const match = {
464
+ path: readCString(raw.path) ?? "",
465
+ relativePath: readCString(raw.relative_path) ?? "",
466
+ fileName: readCString(raw.file_name) ?? "",
467
+ gitStatus: readCString(raw.git_status) ?? "",
468
+ lineContent: readCString(raw.line_content) ?? "",
469
+ size: Number(raw.size),
470
+ modified: Number(raw.modified),
471
+ totalFrecencyScore: Number(raw.total_frecency_score),
472
+ accessFrecencyScore: Number(raw.access_frecency_score),
473
+ modificationFrecencyScore: Number(raw.modification_frecency_score),
474
+ isBinary: raw.is_binary !== 0,
475
+ lineNumber: Number(raw.line_number),
476
+ col: raw.col,
477
+ byteOffset: Number(raw.byte_offset),
478
+ matchRanges,
479
+ };
480
+ if (raw.has_fuzzy_score !== 0) {
481
+ match.fuzzyScore = raw.fuzzy_score;
482
+ }
483
+ if (raw.context_before_count > 0) {
484
+ match.contextBefore = readCStringArray(raw.context_before, raw.context_before_count);
485
+ }
486
+ if (raw.context_after_count > 0) {
487
+ match.contextAfter = readCStringArray(raw.context_after, raw.context_after_count);
488
+ }
489
+ return match;
490
+ }
491
+ /**
492
+ * Parse an FffGrepResult from `FffResult.handle`, then free native memory.
493
+ */
494
+ function parseGrepResult(rawPtr) {
495
+ loadLibrary();
496
+ const [envelope] = restorePointer({
497
+ retType: [FFF_RESULT_STRUCT],
498
+ paramsValue: wrapPointer([rawPtr]),
499
+ });
500
+ const success = envelope.success !== 0;
501
+ if (!success) {
502
+ const errorMsg = readCString(envelope.error) || "Unknown error";
503
+ freeResult(rawPtr);
504
+ return err(errorMsg);
505
+ }
506
+ const handlePtr = envelope.handle;
507
+ freeResult(rawPtr);
508
+ if (isNullPointer(handlePtr)) {
509
+ return err("grep returned null result");
510
+ }
511
+ const [gr] = restorePointer({
512
+ retType: [FFF_GREP_RESULT_STRUCT],
513
+ paramsValue: wrapPointer([handlePtr]),
514
+ });
515
+ const count = gr.count;
516
+ const regexFallbackError = readCString(gr.regex_fallback_error) ?? undefined;
517
+ const items = [];
518
+ for (let i = 0; i < count; i++) {
519
+ const rawMatch = callAccessor("fff_grep_result_get_match", handlePtr, i, FFF_GREP_MATCH_STRUCT);
520
+ items.push(readGrepMatchFromRaw(rawMatch));
521
+ }
522
+ // Free native grep result
523
+ load({
524
+ library: LIBRARY_KEY,
525
+ funcName: "fff_free_grep_result",
526
+ retType: DataType.Void,
527
+ paramsType: [DataType.External],
528
+ paramsValue: [handlePtr],
529
+ });
530
+ const grepResult = {
531
+ items,
532
+ totalMatched: gr.total_matched,
533
+ totalFilesSearched: gr.total_files_searched,
534
+ totalFiles: gr.total_files,
535
+ filteredFileCount: gr.filtered_file_count,
536
+ nextCursor: gr.next_file_offset > 0 ? createGrepCursor(gr.next_file_offset) : null,
537
+ };
538
+ if (regexFallbackError) {
539
+ grepResult.regexFallbackError = regexFallbackError;
540
+ }
541
+ return { ok: true, value: grepResult };
542
+ }
543
+ /**
544
+ * Parse an FffSearchResult from `FffResult.handle`, then free native memory.
545
+ */
546
+ function parseSearchResult(rawPtr) {
547
+ loadLibrary();
548
+ // Read FffResult envelope
549
+ const [envelope] = restorePointer({
550
+ retType: [FFF_RESULT_STRUCT],
551
+ paramsValue: wrapPointer([rawPtr]),
552
+ });
553
+ const success = envelope.success !== 0;
554
+ if (!success) {
555
+ const errorMsg = readCString(envelope.error) || "Unknown error";
556
+ freeResult(rawPtr);
557
+ return err(errorMsg);
558
+ }
559
+ const handlePtr = envelope.handle;
560
+ // Free the FffResult envelope (does NOT free handle)
561
+ freeResult(rawPtr);
562
+ if (isNullPointer(handlePtr)) {
563
+ return err("fff_search returned null search result");
564
+ }
565
+ // Read FffSearchResult struct
566
+ const [sr] = restorePointer({
567
+ retType: [FFF_SEARCH_RESULT_STRUCT],
568
+ paramsValue: wrapPointer([handlePtr]),
569
+ });
570
+ const count = sr.count;
571
+ // Read location
572
+ let location;
573
+ if (sr.location_tag === 1) {
574
+ location = { type: "line", line: sr.location_line };
575
+ }
576
+ else if (sr.location_tag === 2) {
577
+ location = { type: "position", line: sr.location_line, col: sr.location_col };
578
+ }
579
+ else if (sr.location_tag === 3) {
580
+ location = {
581
+ type: "range",
582
+ start: { line: sr.location_line, col: sr.location_col },
583
+ end: { line: sr.location_end_line, col: sr.location_end_col },
584
+ };
585
+ }
586
+ // Read items and scores via accessor functions
587
+ const items = [];
588
+ const scores = [];
589
+ for (let i = 0; i < count; i++) {
590
+ const rawItem = callAccessor("fff_search_result_get_item", handlePtr, i, FFF_FILE_ITEM_STRUCT);
591
+ items.push(readFileItemFromRaw(rawItem));
592
+ const rawScore = callAccessor("fff_search_result_get_score", handlePtr, i, FFF_SCORE_STRUCT);
593
+ scores.push(readScoreFromRaw(rawScore));
594
+ }
595
+ // Free native search result
596
+ load({
597
+ library: LIBRARY_KEY,
598
+ funcName: "fff_free_search_result",
599
+ retType: DataType.Void,
600
+ paramsType: [DataType.External],
601
+ paramsValue: [handlePtr],
602
+ });
603
+ const result = { items, scores, totalMatched: sr.total_matched, totalFiles: sr.total_files };
604
+ if (location) {
605
+ result.location = location;
606
+ }
607
+ return { ok: true, value: result };
608
+ }
227
609
  /**
228
610
  * Perform fuzzy search.
229
611
  */
230
- export function ffiSearch(handle, query, optsJson) {
231
- return callFfiResult("fff_search", [DataType.External, DataType.String, DataType.String], [handle, query, optsJson]);
612
+ export function ffiSearch(handle, query, currentFile, maxThreads, pageIndex, pageSize, comboBoostMultiplier, minComboCount) {
613
+ loadLibrary();
614
+ const rawPtr = load({
615
+ library: LIBRARY_KEY,
616
+ funcName: "fff_search",
617
+ retType: DataType.External,
618
+ paramsType: [
619
+ DataType.External, // handle
620
+ DataType.String, // query
621
+ DataType.String, // current_file
622
+ DataType.U32, // max_threads
623
+ DataType.U32, // page_index
624
+ DataType.U32, // page_size
625
+ DataType.I32, // combo_boost_multiplier
626
+ DataType.U32, // min_combo_count
627
+ ],
628
+ paramsValue: [handle, query, currentFile, maxThreads, pageIndex, pageSize, comboBoostMultiplier, minComboCount],
629
+ freeResultMemory: false,
630
+ });
631
+ return parseSearchResult(rawPtr);
632
+ }
633
+ /**
634
+ * Live grep - search file contents.
635
+ */
636
+ export function ffiLiveGrep(handle, query, mode, maxFileSize, maxMatchesPerFile, smartCase, fileOffset, pageLimit, timeBudgetMs, beforeContext, afterContext, classifyDefinitions) {
637
+ loadLibrary();
638
+ const rawPtr = load({
639
+ library: LIBRARY_KEY,
640
+ funcName: "fff_live_grep",
641
+ retType: DataType.External,
642
+ paramsType: [
643
+ DataType.External, // handle
644
+ DataType.String, // query
645
+ DataType.U8, // mode
646
+ DataType.U64, // max_file_size
647
+ DataType.U32, // max_matches_per_file
648
+ DataType.Boolean, // smart_case
649
+ DataType.U32, // file_offset
650
+ DataType.U32, // page_limit
651
+ DataType.U64, // time_budget_ms
652
+ DataType.U32, // before_context
653
+ DataType.U32, // after_context
654
+ DataType.Boolean, // classify_definitions
655
+ ],
656
+ paramsValue: [
657
+ handle, query, grepModeToU8(mode),
658
+ maxFileSize, maxMatchesPerFile, smartCase,
659
+ fileOffset, pageLimit, timeBudgetMs,
660
+ beforeContext, afterContext, classifyDefinitions,
661
+ ],
662
+ freeResultMemory: false,
663
+ });
664
+ return parseGrepResult(rawPtr);
665
+ }
666
+ /**
667
+ * Multi-pattern grep - Aho-Corasick multi-needle search.
668
+ */
669
+ export function ffiMultiGrep(handle, patternsJoined, constraints, maxFileSize, maxMatchesPerFile, smartCase, fileOffset, pageLimit, timeBudgetMs, beforeContext, afterContext, classifyDefinitions) {
670
+ loadLibrary();
671
+ const rawPtr = load({
672
+ library: LIBRARY_KEY,
673
+ funcName: "fff_multi_grep",
674
+ retType: DataType.External,
675
+ paramsType: [
676
+ DataType.External, // handle
677
+ DataType.String, // patterns_joined
678
+ DataType.String, // constraints
679
+ DataType.U64, // max_file_size
680
+ DataType.U32, // max_matches_per_file
681
+ DataType.Boolean, // smart_case
682
+ DataType.U32, // file_offset
683
+ DataType.U32, // page_limit
684
+ DataType.U64, // time_budget_ms
685
+ DataType.U32, // before_context
686
+ DataType.U32, // after_context
687
+ DataType.Boolean, // classify_definitions
688
+ ],
689
+ paramsValue: [
690
+ handle, patternsJoined, constraints,
691
+ maxFileSize, maxMatchesPerFile, smartCase,
692
+ fileOffset, pageLimit, timeBudgetMs,
693
+ beforeContext, afterContext, classifyDefinitions,
694
+ ],
695
+ freeResultMemory: false,
696
+ });
697
+ return parseGrepResult(rawPtr);
232
698
  }
233
699
  /**
234
700
  * Trigger file scan.
235
701
  */
236
702
  export function ffiScanFiles(handle) {
237
- return callFfiResult("fff_scan_files", [DataType.External], [handle]);
703
+ return callVoidResult("fff_scan_files", [DataType.External], [handle]);
238
704
  }
239
705
  /**
240
706
  * Check if scanning.
@@ -249,60 +715,70 @@ export function ffiIsScanning(handle) {
249
715
  paramsValue: [handle],
250
716
  });
251
717
  }
718
+ // FffScanProgress struct definition
719
+ const FFF_SCAN_PROGRESS_STRUCT = {
720
+ scanned_files_count: DataType.U64,
721
+ is_scanning: DataType.U8,
722
+ };
252
723
  /**
253
724
  * Get scan progress.
254
725
  */
255
726
  export function ffiGetScanProgress(handle) {
256
- return callFfiResult("fff_get_scan_progress", [DataType.External], [handle]);
727
+ loadLibrary();
728
+ const res = readResultEnvelope("fff_get_scan_progress", [DataType.External], [handle]);
729
+ if ("ok" in res)
730
+ return res;
731
+ const handlePtr = res.struct.handle;
732
+ freeResult(res.rawPtr);
733
+ if (isNullPointer(handlePtr))
734
+ return err("scan progress returned null");
735
+ const [sp] = restorePointer({
736
+ retType: [FFF_SCAN_PROGRESS_STRUCT],
737
+ paramsValue: wrapPointer([handlePtr]),
738
+ });
739
+ const result = {
740
+ scannedFilesCount: Number(sp.scanned_files_count),
741
+ isScanning: sp.is_scanning !== 0,
742
+ };
743
+ // Free native scan progress
744
+ load({
745
+ library: LIBRARY_KEY,
746
+ funcName: "fff_free_scan_progress",
747
+ retType: DataType.Void,
748
+ paramsType: [DataType.External],
749
+ paramsValue: [handlePtr],
750
+ });
751
+ return { ok: true, value: result };
257
752
  }
258
753
  /**
259
754
  * Wait for a tree scan to complete.
260
755
  */
261
756
  export function ffiWaitForScan(handle, timeoutMs) {
262
- const result = callFfiResult("fff_wait_for_scan", [DataType.External, DataType.U64], [handle, timeoutMs]);
263
- if (!result.ok)
264
- return result;
265
- return { ok: true, value: result.value === true || result.value === "true" };
757
+ return callBoolResult("fff_wait_for_scan", [DataType.External, DataType.U64], [handle, timeoutMs]);
266
758
  }
267
759
  /**
268
760
  * Restart index in new path.
269
761
  */
270
762
  export function ffiRestartIndex(handle, newPath) {
271
- return callFfiResult("fff_restart_index", [DataType.External, DataType.String], [handle, newPath]);
763
+ return callVoidResult("fff_restart_index", [DataType.External, DataType.String], [handle, newPath]);
272
764
  }
273
765
  /**
274
766
  * Refresh git status.
275
767
  */
276
768
  export function ffiRefreshGitStatus(handle) {
277
- const result = callFfiResult("fff_refresh_git_status", [DataType.External], [handle]);
278
- if (!result.ok)
279
- return result;
280
- return {
281
- ok: true,
282
- value: typeof result.value === "number"
283
- ? result.value
284
- : parseInt(result.value, 10),
285
- };
769
+ return callIntResult("fff_refresh_git_status", [DataType.External], [handle]);
286
770
  }
287
771
  /**
288
772
  * Track query completion.
289
773
  */
290
774
  export function ffiTrackQuery(handle, query, filePath) {
291
- const result = callFfiResult("fff_track_query", [DataType.External, DataType.String, DataType.String], [handle, query, filePath]);
292
- if (!result.ok)
293
- return result;
294
- return { ok: true, value: result.value === true || result.value === "true" };
775
+ return callBoolResult("fff_track_query", [DataType.External, DataType.String, DataType.String], [handle, query, filePath]);
295
776
  }
296
777
  /**
297
778
  * Get historical query.
298
779
  */
299
780
  export function ffiGetHistoricalQuery(handle, offset) {
300
- const result = callFfiResult("fff_get_historical_query", [DataType.External, DataType.U64], [handle, offset]);
301
- if (!result.ok)
302
- return result;
303
- if (result.value === null || result.value === "null")
304
- return { ok: true, value: null };
305
- return result;
781
+ return callStringResult("fff_get_historical_query", [DataType.External, DataType.U64], [handle, offset]);
306
782
  }
307
783
  /**
308
784
  * Health check.
@@ -312,57 +788,11 @@ export function ffiGetHistoricalQuery(handle, offset) {
312
788
  * since ffi-rs does not accept `null` for External parameters.
313
789
  */
314
790
  export function ffiHealthCheck(handle, testPath) {
315
- loadLibrary();
316
791
  if (handle === null) {
317
792
  // Use U64(0) as a null pointer since ffi-rs rejects null for External params
318
- const rawPtr = load({
319
- library: LIBRARY_KEY,
320
- funcName: "fff_health_check",
321
- retType: DataType.External,
322
- paramsType: [DataType.U64, DataType.String],
323
- paramsValue: [0, testPath],
324
- freeResultMemory: false,
325
- });
326
- const [structData] = restorePointer({
327
- retType: [FFF_RESULT_STRUCT],
328
- paramsValue: wrapPointer([rawPtr]),
329
- });
330
- const success = structData.success !== 0;
331
- try {
332
- if (success) {
333
- const dataStr = readCString(structData.data);
334
- if (dataStr === null || dataStr === "") {
335
- return { ok: true, value: undefined };
336
- }
337
- try {
338
- return { ok: true, value: snakeToCamel(JSON.parse(dataStr)) };
339
- }
340
- catch {
341
- return { ok: true, value: dataStr };
342
- }
343
- }
344
- else {
345
- const errorStr = readCString(structData.error);
346
- return err(errorStr || "Unknown error");
347
- }
348
- }
349
- finally {
350
- freeResult(rawPtr);
351
- }
793
+ return callJsonResult("fff_health_check", [DataType.U64, DataType.String], [0, testPath]);
352
794
  }
353
- return callFfiResult("fff_health_check", [DataType.External, DataType.String], [handle, testPath]);
354
- }
355
- /**
356
- * Live grep - search file contents.
357
- */
358
- export function ffiLiveGrep(handle, query, optsJson) {
359
- return callFfiResult("fff_live_grep", [DataType.External, DataType.String, DataType.String], [handle, query, optsJson]);
360
- }
361
- /**
362
- * Multi-pattern grep - Aho-Corasick multi-needle search.
363
- */
364
- export function ffiMultiGrep(handle, optsJson) {
365
- return callFfiResult("fff_multi_grep", [DataType.External, DataType.String], [handle, optsJson]);
795
+ return callJsonResult("fff_health_check", [DataType.External, DataType.String], [handle, testPath]);
366
796
  }
367
797
  /**
368
798
  * Ensure the library is loaded.