@ff-labs/fff-bun 0.2.4-dev.233679d → 0.2.4-dev.8d9ef38

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.
Files changed (3) hide show
  1. package/package.json +9 -9
  2. package/src/ffi.ts +376 -143
  3. package/src/finder.ts +3 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ff-labs/fff-bun",
3
- "version": "0.2.4-dev.233679d",
3
+ "version": "0.2.4-dev.8d9ef38",
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.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"
65
+ "@ff-labs/fff-bin-darwin-arm64": "0.2.4-dev.8d9ef38",
66
+ "@ff-labs/fff-bin-darwin-x64": "0.2.4-dev.8d9ef38",
67
+ "@ff-labs/fff-bin-linux-x64-gnu": "0.2.4-dev.8d9ef38",
68
+ "@ff-labs/fff-bin-linux-arm64-gnu": "0.2.4-dev.8d9ef38",
69
+ "@ff-labs/fff-bin-linux-x64-musl": "0.2.4-dev.8d9ef38",
70
+ "@ff-labs/fff-bin-linux-arm64-musl": "0.2.4-dev.8d9ef38",
71
+ "@ff-labs/fff-bin-win32-x64": "0.2.4-dev.8d9ef38",
72
+ "@ff-labs/fff-bin-win32-arm64": "0.2.4-dev.8d9ef38"
73
73
  },
74
74
  "devDependencies": {
75
75
  "@types/bun": "^1.3.8",
package/src/ffi.ts CHANGED
@@ -10,8 +10,16 @@
10
10
 
11
11
  import { CString, dlopen, FFIType, type Pointer, ptr, read } from "bun:ffi";
12
12
  import { findBinary } from "./download";
13
- import type { FileItem, Location, Result, Score, SearchResult } from "./types";
14
- import { err } from "./types";
13
+ import type {
14
+ FileItem,
15
+ GrepMatch,
16
+ GrepResult,
17
+ Location,
18
+ Result,
19
+ Score,
20
+ SearchResult,
21
+ } from "./types";
22
+ import { createGrepCursor, err } from "./types";
15
23
 
16
24
  /** Grep mode constants matching the C API (u8). */
17
25
  const GREP_MODE_PLAIN = 0;
@@ -144,7 +152,7 @@ const ffiDefinition = {
144
152
  returns: FFIType.ptr,
145
153
  },
146
154
 
147
- // Search result accessors
155
+ // Search result accessors / free
148
156
  fff_free_search_result: {
149
157
  args: [FFIType.ptr],
150
158
  returns: FFIType.void,
@@ -158,6 +166,16 @@ const ffiDefinition = {
158
166
  returns: FFIType.ptr,
159
167
  },
160
168
 
169
+ // Grep result accessors / free
170
+ fff_free_grep_result: {
171
+ args: [FFIType.ptr],
172
+ returns: FFIType.void,
173
+ },
174
+ fff_grep_result_get_match: {
175
+ args: [FFIType.ptr, FFIType.u32],
176
+ returns: FFIType.ptr,
177
+ },
178
+
161
179
  // Memory management
162
180
  fff_free_result: {
163
181
  args: [FFIType.ptr],
@@ -167,6 +185,10 @@ const ffiDefinition = {
167
185
  args: [FFIType.ptr],
168
186
  returns: FFIType.void,
169
187
  },
188
+ fff_free_scan_progress: {
189
+ args: [FFIType.ptr],
190
+ returns: FFIType.void,
191
+ },
170
192
  } as const;
171
193
 
172
194
  type FFFLibrary = ReturnType<typeof dlopen<typeof ffiDefinition>>;
@@ -219,59 +241,100 @@ function snakeToCamel(obj: unknown): unknown {
219
241
 
220
242
  const result: Record<string, unknown> = {};
221
243
  for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
222
- const camelKey = key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
244
+ const camelKey = key.replace(/_([a-z])/g, (_, letter) =>
245
+ letter.toUpperCase(),
246
+ );
223
247
  result[camelKey] = snakeToCamel(value);
224
248
  }
225
249
  return result;
226
250
  }
227
251
 
252
+ // ---------------------------------------------------------------------------
253
+ // FffResult byte offsets (must match #[repr(C)] layout on 64-bit)
254
+ // { success: bool(1+7pad), error: *char(8), handle: *void(8), int_value: i64(8) }
255
+ // ---------------------------------------------------------------------------
256
+ const RES_SUCCESS = 0; // bool (1 + 7 padding)
257
+ const RES_ERROR = 8; // *mut c_char (8)
258
+ const RES_HANDLE = 16; // *mut c_void (8)
259
+ const RES_INT_VALUE = 24; // i64 (8)
260
+
228
261
  /**
229
- * Parse a FffResult from the FFI return value.
230
- *
231
- * The result is a pointer to a struct:
232
- * { success: bool, data: *char, error: *char, handle: *void }
233
- *
234
- * Layout (with alignment padding):
235
- * offset 0: success (bool, 1 byte + 7 padding)
236
- * offset 8: data pointer (8 bytes)
237
- * offset 16: error pointer (8 bytes)
238
- * offset 24: handle pointer (8 bytes)
262
+ * Read the FffResult envelope: check success, extract payload, free envelope.
263
+ * On error returns a Result<never>. On success returns the raw handle pointer and int_value.
239
264
  */
240
- function parseResult<T>(resultPtr: Pointer | null): Result<T> {
265
+ function readResultEnvelope(resultPtr: Pointer | null): { success: true; handlePtr: number; intValue: number } | Result<never> {
241
266
  if (resultPtr === null) {
242
267
  return err("FFI returned null pointer");
243
268
  }
244
269
 
245
- const success = read.u8(resultPtr, 0) !== 0;
246
- const dataPtr = read.ptr(resultPtr, 8);
247
- const errorPtr = read.ptr(resultPtr, 16);
248
-
270
+ const success = read.u8(resultPtr, RES_SUCCESS) !== 0;
249
271
  const library = loadLibrary();
250
272
 
251
- if (success) {
252
- const data = readCString(dataPtr);
253
- // Free the result
254
- library.symbols.fff_free_result(resultPtr);
255
-
256
- if (data === null || data === "") {
257
- return { ok: true, value: undefined as T };
258
- }
259
-
260
- try {
261
- const parsed = JSON.parse(data);
262
- // Convert snake_case to camelCase for TypeScript consumers
263
- const transformed = snakeToCamel(parsed) as T;
264
- return { ok: true, value: transformed };
265
- } catch {
266
- // For simple values like "true" or numbers
267
- return { ok: true, value: data as T };
268
- }
269
- } else {
273
+ if (!success) {
274
+ const errorPtr = read.ptr(resultPtr, RES_ERROR);
270
275
  const errorMsg = readCString(errorPtr) || "Unknown error";
271
- // Free the result
272
276
  library.symbols.fff_free_result(resultPtr);
273
277
  return err(errorMsg);
274
278
  }
279
+
280
+ const handlePtr = read.ptr(resultPtr, RES_HANDLE);
281
+ const intValue = Number(read.i64(resultPtr, RES_INT_VALUE));
282
+ library.symbols.fff_free_result(resultPtr);
283
+ return { success: true, handlePtr, intValue };
284
+ }
285
+
286
+ /** Parse a FffResult that carries a bool in int_value (0 = false, nonzero = true). */
287
+ function parseBoolResult(resultPtr: Pointer | null): Result<boolean> {
288
+ const envelope = readResultEnvelope(resultPtr);
289
+ if (!("success" in envelope)) return envelope;
290
+ return { ok: true, value: envelope.intValue !== 0 };
291
+ }
292
+
293
+ /** Parse a FffResult that carries an integer in int_value. */
294
+ function parseIntResult(resultPtr: Pointer | null): Result<number> {
295
+ const envelope = readResultEnvelope(resultPtr);
296
+ if (!("success" in envelope)) return envelope;
297
+ return { ok: true, value: envelope.intValue };
298
+ }
299
+
300
+ /** Parse a FffResult that carries a string in handle (freed with fff_free_string). */
301
+ function parseStringResult(resultPtr: Pointer | null): Result<string | null> {
302
+ const envelope = readResultEnvelope(resultPtr);
303
+ if (!("success" in envelope)) return envelope;
304
+
305
+ if (envelope.handlePtr === 0) return { ok: true, value: null };
306
+
307
+ const library = loadLibrary();
308
+ const str = readCString(envelope.handlePtr);
309
+ library.symbols.fff_free_string(asPtr(envelope.handlePtr));
310
+ return { ok: true, value: str };
311
+ }
312
+
313
+ /** Parse a FffResult that carries a JSON string in handle. */
314
+ function parseJsonResult<T>(resultPtr: Pointer | null): Result<T> {
315
+ const envelope = readResultEnvelope(resultPtr);
316
+ if (!("success" in envelope)) return envelope;
317
+
318
+ if (envelope.handlePtr === 0) return { ok: true, value: undefined as T };
319
+
320
+ const library = loadLibrary();
321
+ const jsonStr = readCString(envelope.handlePtr);
322
+ library.symbols.fff_free_string(asPtr(envelope.handlePtr));
323
+
324
+ if (jsonStr === null || jsonStr === "") return { ok: true, value: undefined as T };
325
+
326
+ try {
327
+ return { ok: true, value: snakeToCamel(JSON.parse(jsonStr)) as T };
328
+ } catch {
329
+ return { ok: true, value: jsonStr as T };
330
+ }
331
+ }
332
+
333
+ /** Parse a FffResult with no payload (void, success/error only). */
334
+ function parseVoidResult(resultPtr: Pointer | null): Result<void> {
335
+ const envelope = readResultEnvelope(resultPtr);
336
+ if (!("success" in envelope)) return envelope;
337
+ return { ok: true, value: undefined };
275
338
  }
276
339
 
277
340
  /**
@@ -304,9 +367,9 @@ export function ffiCreate(
304
367
  return err("FFI returned null pointer");
305
368
  }
306
369
 
307
- const success = read.u8(resultPtr, 0) !== 0;
308
- const errorPtr = read.ptr(resultPtr, 16);
309
- const handlePtr = read.ptr(resultPtr, 24);
370
+ const success = read.u8(resultPtr, RES_SUCCESS) !== 0;
371
+ const errorPtr = read.ptr(resultPtr, RES_ERROR);
372
+ const handlePtr = read.ptr(resultPtr, RES_HANDLE);
310
373
 
311
374
  if (success) {
312
375
  const handle = handlePtr as unknown as Pointer;
@@ -337,43 +400,43 @@ export function ffiDestroy(handle: NativeHandle): void {
337
400
  // ---------------------------------------------------------------------------
338
401
 
339
402
  // 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)
403
+ const SR_ITEMS = 0; // *mut FffFileItem (8)
404
+ const SR_SCORES = 8; // *mut FffScore (8)
405
+ const SR_COUNT = 16; // u32 (4)
406
+ const SR_MATCHED = 20; // u32 (4)
407
+ const SR_TOTAL = 24; // u32 (4)
345
408
  // 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)
409
+ const SR_LOC_TAG = 28; // u8 (1 + 3 padding)
410
+ const SR_LOC_LINE = 32; // i32 (4)
411
+ const SR_LOC_COL = 36; // i32 (4)
349
412
  const SR_LOC_END_LINE = 40; // i32 (4)
350
- const SR_LOC_END_COL = 44; // i32 (4)
413
+ const SR_LOC_END_COL = 44; // i32 (4)
351
414
 
352
415
  // 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;
416
+ const FI_PATH = 0; // *mut c_char (8)
417
+ const FI_RELPATH = 8; // *mut c_char (8)
418
+ const FI_FNAME = 16; // *mut c_char (8)
419
+ const FI_GIT = 24; // *mut c_char (8)
420
+ const FI_SIZE = 32; // u64 (8)
421
+ const FI_MODIFIED = 40; // u64 (8)
422
+ const FI_ACCESS = 48; // i64 (8)
423
+ const FI_MODFR = 56; // i64 (8)
424
+ const FI_TOTAL_FR = 64; // i64 (8)
425
+ const _FI_BINARY = 72; // bool (1 + 7 pad)
426
+ const FI_SIZE_OF = 80;
364
427
 
365
428
  // 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;
429
+ const SC_TOTAL = 0; // i32 (4)
430
+ const SC_BASE = 4; // i32 (4)
431
+ const SC_FNAME = 8; // i32 (4)
432
+ const SC_SPECIAL = 12; // i32 (4)
433
+ const SC_FREC = 16; // i32 (4)
434
+ const SC_DIST = 20; // i32 (4)
435
+ const SC_CURFILE = 24; // i32 (4)
436
+ const SC_COMBO = 28; // i32 (4)
437
+ const SC_EXACT = 32; // bool (1 + 7 pad)
438
+ const SC_MTYPE = 40; // *mut c_char (8)
439
+ const SC_SIZE_OF = 48;
377
440
 
378
441
  /** Cast a number (raw address from pointer math) to Pointer for read.*. */
379
442
  function asPtr(n: number): Pointer {
@@ -386,15 +449,15 @@ function asPtr(n: number): Pointer {
386
449
  function readFileItemStruct(p: number): FileItem {
387
450
  const pp = asPtr(p);
388
451
  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)),
452
+ path: readCString(read.ptr(pp, FI_PATH)) ?? "",
453
+ relativePath: readCString(read.ptr(pp, FI_RELPATH)) ?? "",
454
+ fileName: readCString(read.ptr(pp, FI_FNAME)) ?? "",
455
+ gitStatus: readCString(read.ptr(pp, FI_GIT)) ?? "",
456
+ size: Number(read.u64(pp, FI_SIZE)),
457
+ modified: Number(read.u64(pp, FI_MODIFIED)),
458
+ accessFrecencyScore: Number(read.i64(pp, FI_ACCESS)),
396
459
  modificationFrecencyScore: Number(read.i64(pp, FI_MODFR)),
397
- totalFrecencyScore: Number(read.i64(pp, FI_TOTAL_FR)),
460
+ totalFrecencyScore: Number(read.i64(pp, FI_TOTAL_FR)),
398
461
  };
399
462
  }
400
463
 
@@ -404,16 +467,16 @@ function readFileItemStruct(p: number): FileItem {
404
467
  function readScoreStruct(p: number): Score {
405
468
  const pp = asPtr(p);
406
469
  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)) ?? "",
470
+ total: read.i32(pp, SC_TOTAL),
471
+ baseScore: read.i32(pp, SC_BASE),
472
+ filenameBonus: read.i32(pp, SC_FNAME),
473
+ specialFilenameBonus: read.i32(pp, SC_SPECIAL),
474
+ frecencyBoost: read.i32(pp, SC_FREC),
475
+ distancePenalty: read.i32(pp, SC_DIST),
476
+ currentFilePenalty: read.i32(pp, SC_CURFILE),
477
+ comboMatchBoost: read.i32(pp, SC_COMBO),
478
+ exactMatch: read.u8(pp, SC_EXACT) !== 0,
479
+ matchType: readCString(read.ptr(pp, SC_MTYPE)) ?? "",
417
480
  };
418
481
  }
419
482
 
@@ -425,33 +488,19 @@ function parseSearchResult(resultPtr: Pointer | null): Result<SearchResult> {
425
488
  return err("FFI returned null pointer");
426
489
  }
427
490
 
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);
491
+ const envelope = readResultEnvelope(resultPtr);
492
+ if (!("success" in envelope)) return envelope;
442
493
 
443
- if (handlePtr === 0) {
494
+ if (envelope.handlePtr === 0) {
444
495
  return err("fff_search returned null search result");
445
496
  }
446
497
 
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);
498
+ const hp = asPtr(envelope.handlePtr);
499
+ const count = read.u32(hp, SR_COUNT);
451
500
  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);
501
+ const totalFiles = read.u32(hp, SR_TOTAL);
502
+ const itemsBase = read.ptr(hp, SR_ITEMS);
503
+ const scoresBase = read.ptr(hp, SR_SCORES);
455
504
 
456
505
  // Read location
457
506
  const locTag = read.u8(hp, SR_LOC_TAG);
@@ -459,12 +508,19 @@ function parseSearchResult(resultPtr: Pointer | null): Result<SearchResult> {
459
508
  if (locTag === 1) {
460
509
  location = { type: "line", line: read.i32(hp, SR_LOC_LINE) };
461
510
  } else if (locTag === 2) {
462
- location = { type: "position", line: read.i32(hp, SR_LOC_LINE), col: read.i32(hp, SR_LOC_COL) };
511
+ location = {
512
+ type: "position",
513
+ line: read.i32(hp, SR_LOC_LINE),
514
+ col: read.i32(hp, SR_LOC_COL),
515
+ };
463
516
  } else if (locTag === 3) {
464
517
  location = {
465
518
  type: "range",
466
519
  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) },
520
+ end: {
521
+ line: read.i32(hp, SR_LOC_END_LINE),
522
+ col: read.i32(hp, SR_LOC_END_COL),
523
+ },
468
524
  };
469
525
  }
470
526
 
@@ -478,7 +534,7 @@ function parseSearchResult(resultPtr: Pointer | null): Result<SearchResult> {
478
534
  }
479
535
 
480
536
  // Free native search result
481
- library.symbols.fff_free_search_result(hp);
537
+ loadLibrary().symbols.fff_free_search_result(hp);
482
538
 
483
539
  const result: SearchResult = { items, scores, totalMatched, totalFiles };
484
540
  if (location) {
@@ -487,6 +543,172 @@ function parseSearchResult(resultPtr: Pointer | null): Result<SearchResult> {
487
543
  return { ok: true, value: result };
488
544
  }
489
545
 
546
+ // ---------------------------------------------------------------------------
547
+ // FffGrepMatch byte offsets (must match #[repr(C)] layout on 64-bit)
548
+ // ---------------------------------------------------------------------------
549
+
550
+ // Pointers (8 bytes each)
551
+ const GM_PATH = 0;
552
+ const GM_RELPATH = 8;
553
+ const GM_FNAME = 16;
554
+ const GM_GIT = 24;
555
+ const GM_LINE_CONTENT = 32;
556
+ const GM_MATCH_RANGES = 40;
557
+ const GM_CTX_BEFORE = 48;
558
+ const GM_CTX_AFTER = 56;
559
+
560
+ // 8-byte numeric fields
561
+ const GM_SIZE = 64;
562
+ const GM_MODIFIED = 72;
563
+ const GM_TOTAL_FR = 80;
564
+ const GM_ACCESS_FR = 88;
565
+ const GM_MOD_FR = 96;
566
+ const GM_LINE_NUM = 104;
567
+ const GM_BYTE_OFF = 112;
568
+
569
+ // 4-byte fields
570
+ const GM_COL = 120;
571
+ const GM_MR_COUNT = 124;
572
+ const GM_CTX_B_COUNT = 128;
573
+ const GM_CTX_A_COUNT = 132;
574
+
575
+ // 2-byte
576
+ const GM_FUZZY_SCORE = 136;
577
+ // 1-byte
578
+ const GM_HAS_FUZZY = 138;
579
+ const GM_IS_BINARY = 139;
580
+ const _GM_IS_DEF = 140;
581
+
582
+ // struct size: pad to 8-byte alignment → 144
583
+ const GM_SIZE_OF = 144;
584
+
585
+ // FffGrepResult
586
+ const GR_ITEMS = 0; // *mut FffGrepMatch (8)
587
+ const GR_COUNT = 8; // u32 (4)
588
+ const GR_MATCHED = 12; // u32 (4)
589
+ const GR_FILES_SEARCHED = 16; // u32 (4)
590
+ const GR_TOTAL_FILES = 20; // u32 (4)
591
+ const GR_FILTERED = 24; // u32 (4)
592
+ const GR_NEXT_OFFSET = 28; // u32 (4)
593
+ const GR_REGEX_ERR = 32; // *mut c_char (8)
594
+
595
+ // FffMatchRange (8 bytes)
596
+ const MR_START = 0;
597
+ const MR_END = 4;
598
+ const MR_SIZE = 8;
599
+
600
+ /**
601
+ * Read a C string array (char**) at the given pointer, with `count` elements.
602
+ */
603
+ function readCStringArray(base: number, count: number): string[] {
604
+ if (count === 0 || base === 0) return [];
605
+ const result: string[] = [];
606
+ const bp = asPtr(base);
607
+ for (let i = 0; i < count; i++) {
608
+ const strPtr = read.ptr(bp, i * 8); // each pointer is 8 bytes
609
+ result.push(readCString(strPtr) ?? "");
610
+ }
611
+ return result;
612
+ }
613
+
614
+ /**
615
+ * Read an FffGrepMatch struct at the given raw address.
616
+ */
617
+ function readGrepMatchStruct(p: number): GrepMatch {
618
+ const pp = asPtr(p);
619
+ const matchRangesPtr = read.ptr(pp, GM_MATCH_RANGES);
620
+ const matchRangesCount = read.u32(pp, GM_MR_COUNT);
621
+ const matchRanges: [number, number][] = [];
622
+ for (let i = 0; i < matchRangesCount; i++) {
623
+ const base = matchRangesPtr + i * MR_SIZE;
624
+ const bp = asPtr(base);
625
+ matchRanges.push([read.u32(bp, MR_START), read.u32(bp, MR_END)]);
626
+ }
627
+
628
+ const hasFuzzy = read.u8(pp, GM_HAS_FUZZY) !== 0;
629
+ const ctxBeforeCount = read.u32(pp, GM_CTX_B_COUNT);
630
+ const ctxAfterCount = read.u32(pp, GM_CTX_A_COUNT);
631
+
632
+ const match: GrepMatch = {
633
+ path: readCString(read.ptr(pp, GM_PATH)) ?? "",
634
+ relativePath: readCString(read.ptr(pp, GM_RELPATH)) ?? "",
635
+ fileName: readCString(read.ptr(pp, GM_FNAME)) ?? "",
636
+ gitStatus: readCString(read.ptr(pp, GM_GIT)) ?? "",
637
+ lineContent: readCString(read.ptr(pp, GM_LINE_CONTENT)) ?? "",
638
+ size: Number(read.u64(pp, GM_SIZE)),
639
+ modified: Number(read.u64(pp, GM_MODIFIED)),
640
+ totalFrecencyScore: Number(read.i64(pp, GM_TOTAL_FR)),
641
+ accessFrecencyScore: Number(read.i64(pp, GM_ACCESS_FR)),
642
+ modificationFrecencyScore: Number(read.i64(pp, GM_MOD_FR)),
643
+ isBinary: read.u8(pp, GM_IS_BINARY) !== 0,
644
+ lineNumber: Number(read.u64(pp, GM_LINE_NUM)),
645
+ col: read.u32(pp, GM_COL),
646
+ byteOffset: Number(read.u64(pp, GM_BYTE_OFF)),
647
+ matchRanges,
648
+ };
649
+
650
+ if (hasFuzzy) {
651
+ match.fuzzyScore = read.u16(pp, GM_FUZZY_SCORE);
652
+ }
653
+ if (ctxBeforeCount > 0) {
654
+ match.contextBefore = readCStringArray(
655
+ read.ptr(pp, GM_CTX_BEFORE),
656
+ ctxBeforeCount,
657
+ );
658
+ }
659
+ if (ctxAfterCount > 0) {
660
+ match.contextAfter = readCStringArray(
661
+ read.ptr(pp, GM_CTX_AFTER),
662
+ ctxAfterCount,
663
+ );
664
+ }
665
+
666
+ return match;
667
+ }
668
+
669
+ /**
670
+ * Parse an FffGrepResult from a raw FffResult pointer, then free native memory.
671
+ */
672
+ function parseGrepResult(resultPtr: Pointer | null): Result<GrepResult> {
673
+ const envelope = readResultEnvelope(resultPtr);
674
+ if (!("success" in envelope)) return envelope;
675
+
676
+ if (envelope.handlePtr === 0) {
677
+ return err("grep returned null result");
678
+ }
679
+
680
+ const hp = asPtr(envelope.handlePtr);
681
+ const count = read.u32(hp, GR_COUNT);
682
+ const totalMatched = read.u32(hp, GR_MATCHED);
683
+ const totalFilesSearched = read.u32(hp, GR_FILES_SEARCHED);
684
+ const totalFiles = read.u32(hp, GR_TOTAL_FILES);
685
+ const filteredFileCount = read.u32(hp, GR_FILTERED);
686
+ const nextFileOffset = read.u32(hp, GR_NEXT_OFFSET);
687
+ const regexErrPtr = read.ptr(hp, GR_REGEX_ERR);
688
+ const regexFallbackError = readCString(regexErrPtr) ?? undefined;
689
+ const itemsBase = read.ptr(hp, GR_ITEMS);
690
+
691
+ const items: GrepMatch[] = [];
692
+ for (let i = 0; i < count; i++) {
693
+ items.push(readGrepMatchStruct(itemsBase + i * GM_SIZE_OF));
694
+ }
695
+
696
+ loadLibrary().symbols.fff_free_grep_result(hp);
697
+
698
+ const grepResult: GrepResult = {
699
+ items,
700
+ totalMatched,
701
+ totalFilesSearched,
702
+ totalFiles,
703
+ filteredFileCount,
704
+ nextCursor: nextFileOffset > 0 ? createGrepCursor(nextFileOffset) : null,
705
+ };
706
+ if (regexFallbackError) {
707
+ grepResult.regexFallbackError = regexFallbackError;
708
+ }
709
+ return { ok: true, value: grepResult };
710
+ }
711
+
490
712
  /**
491
713
  * Perform fuzzy search.
492
714
  */
@@ -530,7 +752,7 @@ export function ffiLiveGrep(
530
752
  beforeContext: number,
531
753
  afterContext: number,
532
754
  classifyDefinitions: boolean,
533
- ): Result<unknown> {
755
+ ): Result<GrepResult> {
534
756
  const library = loadLibrary();
535
757
  const resultPtr = library.symbols.fff_live_grep(
536
758
  handle,
@@ -546,7 +768,7 @@ export function ffiLiveGrep(
546
768
  afterContext,
547
769
  classifyDefinitions,
548
770
  );
549
- return parseResult<unknown>(resultPtr);
771
+ return parseGrepResult(resultPtr);
550
772
  }
551
773
 
552
774
  /**
@@ -565,7 +787,7 @@ export function ffiMultiGrep(
565
787
  beforeContext: number,
566
788
  afterContext: number,
567
789
  classifyDefinitions: boolean,
568
- ): Result<unknown> {
790
+ ): Result<GrepResult> {
569
791
  const library = loadLibrary();
570
792
  const resultPtr = library.symbols.fff_multi_grep(
571
793
  handle,
@@ -581,7 +803,7 @@ export function ffiMultiGrep(
581
803
  afterContext,
582
804
  classifyDefinitions,
583
805
  );
584
- return parseResult<unknown>(resultPtr);
806
+ return parseGrepResult(resultPtr);
585
807
  }
586
808
 
587
809
  /**
@@ -590,7 +812,7 @@ export function ffiMultiGrep(
590
812
  export function ffiScanFiles(handle: NativeHandle): Result<void> {
591
813
  const library = loadLibrary();
592
814
  const resultPtr = library.symbols.fff_scan_files(handle);
593
- return parseResult<void>(resultPtr);
815
+ return parseVoidResult(resultPtr);
594
816
  }
595
817
 
596
818
  /**
@@ -601,33 +823,54 @@ export function ffiIsScanning(handle: NativeHandle): boolean {
601
823
  return library.symbols.fff_is_scanning(handle) as boolean;
602
824
  }
603
825
 
826
+ // FffScanProgress { scanned_files_count: u64(8), is_scanning: bool(1+7pad) }
827
+ const SP_COUNT = 0; // u64 (8)
828
+ const SP_SCANNING = 8; // bool (1 + 7 pad)
829
+
604
830
  /**
605
831
  * Get scan progress.
606
832
  */
607
- export function ffiGetScanProgress(handle: NativeHandle): Result<unknown> {
833
+ export function ffiGetScanProgress(handle: NativeHandle): Result<{ scannedFilesCount: number; isScanning: boolean }> {
608
834
  const library = loadLibrary();
609
835
  const resultPtr = library.symbols.fff_get_scan_progress(handle);
610
- return parseResult<unknown>(resultPtr);
836
+ const envelope = readResultEnvelope(resultPtr);
837
+ if (!("success" in envelope)) return envelope;
838
+
839
+ if (envelope.handlePtr === 0) {
840
+ return err("scan progress returned null");
841
+ }
842
+
843
+ const hp = asPtr(envelope.handlePtr);
844
+ const result = {
845
+ scannedFilesCount: Number(read.u64(hp, SP_COUNT)),
846
+ isScanning: read.u8(hp, SP_SCANNING) !== 0,
847
+ };
848
+ library.symbols.fff_free_scan_progress(hp);
849
+ return { ok: true, value: result };
611
850
  }
612
851
 
613
852
  /**
614
853
  * Wait for scan to complete.
615
854
  */
616
- export function ffiWaitForScan(handle: NativeHandle, timeoutMs: number): Result<boolean> {
855
+ export function ffiWaitForScan(
856
+ handle: NativeHandle,
857
+ timeoutMs: number,
858
+ ): Result<boolean> {
617
859
  const library = loadLibrary();
618
860
  const resultPtr = library.symbols.fff_wait_for_scan(handle, BigInt(timeoutMs));
619
- const result = parseResult<boolean | string>(resultPtr);
620
- if (!result.ok) return result;
621
- return { ok: true, value: result.value === true || result.value === "true" };
861
+ return parseBoolResult(resultPtr);
622
862
  }
623
863
 
624
864
  /**
625
865
  * Restart index in new path.
626
866
  */
627
- export function ffiRestartIndex(handle: NativeHandle, newPath: string): Result<void> {
867
+ export function ffiRestartIndex(
868
+ handle: NativeHandle,
869
+ newPath: string,
870
+ ): Result<void> {
628
871
  const library = loadLibrary();
629
872
  const resultPtr = library.symbols.fff_restart_index(handle, ptr(encodeString(newPath)));
630
- return parseResult<void>(resultPtr);
873
+ return parseVoidResult(resultPtr);
631
874
  }
632
875
 
633
876
  /**
@@ -636,12 +879,7 @@ export function ffiRestartIndex(handle: NativeHandle, newPath: string): Result<v
636
879
  export function ffiRefreshGitStatus(handle: NativeHandle): Result<number> {
637
880
  const library = loadLibrary();
638
881
  const resultPtr = library.symbols.fff_refresh_git_status(handle);
639
- const result = parseResult<number | string>(resultPtr);
640
- if (!result.ok) return result;
641
- return {
642
- ok: true,
643
- value: typeof result.value === "number" ? result.value : parseInt(result.value, 10),
644
- };
882
+ return parseIntResult(resultPtr);
645
883
  }
646
884
 
647
885
  /**
@@ -658,9 +896,7 @@ export function ffiTrackQuery(
658
896
  ptr(encodeString(query)),
659
897
  ptr(encodeString(filePath)),
660
898
  );
661
- const result = parseResult<boolean | string>(resultPtr);
662
- if (!result.ok) return result;
663
- return { ok: true, value: result.value === true || result.value === "true" };
899
+ return parseBoolResult(resultPtr);
664
900
  }
665
901
 
666
902
  /**
@@ -672,10 +908,7 @@ export function ffiGetHistoricalQuery(
672
908
  ): Result<string | null> {
673
909
  const library = loadLibrary();
674
910
  const resultPtr = library.symbols.fff_get_historical_query(handle, BigInt(offset));
675
- const result = parseResult<string | null>(resultPtr);
676
- if (!result.ok) return result;
677
- if (result.value === null || result.value === "null") return { ok: true, value: null };
678
- return result as Result<string>;
911
+ return parseStringResult(resultPtr);
679
912
  }
680
913
 
681
914
  /**
@@ -692,7 +925,7 @@ export function ffiHealthCheck(
692
925
  handle ?? (0 as unknown as Pointer),
693
926
  ptr(encodeString(testPath)),
694
927
  );
695
- return parseResult<unknown>(resultPtr);
928
+ return parseJsonResult<unknown>(resultPtr);
696
929
  }
697
930
 
698
931
  /**
package/src/finder.ts CHANGED
@@ -40,7 +40,7 @@ import type {
40
40
  SearchResult,
41
41
  } from "./types";
42
42
 
43
- import { createGrepCursor, err } from "./types";
43
+ import { err } from "./types";
44
44
 
45
45
  /**
46
46
  * FileFinder - Fast file finder with fuzzy search
@@ -228,7 +228,7 @@ export class FileFinder {
228
228
  const guard = this.ensureAlive();
229
229
  if (!guard.ok) return guard;
230
230
 
231
- const result = ffiLiveGrep(
231
+ return ffiLiveGrep(
232
232
  guard.value,
233
233
  query,
234
234
  options?.mode ?? "plain",
@@ -242,8 +242,6 @@ export class FileFinder {
242
242
  options?.afterContext ?? 0,
243
243
  false,
244
244
  );
245
-
246
- return transformGrepResult(result);
247
245
  }
248
246
 
249
247
  /**
@@ -279,7 +277,7 @@ export class FileFinder {
279
277
  return err("patterns array must have at least 1 element");
280
278
  }
281
279
 
282
- const result = ffiMultiGrep(
280
+ return ffiMultiGrep(
283
281
  guard.value,
284
282
  options.patterns.join("\n"),
285
283
  options.constraints ?? "",
@@ -293,8 +291,6 @@ export class FileFinder {
293
291
  options.afterContext ?? 0,
294
292
  false,
295
293
  );
296
-
297
- return transformGrepResult(result);
298
294
  }
299
295
 
300
296
  /**
@@ -435,20 +431,3 @@ export class FileFinder {
435
431
  }
436
432
  }
437
433
 
438
- function transformGrepResult(result: Result<unknown>): Result<GrepResult> {
439
- if (!result.ok) {
440
- return result;
441
- }
442
- const raw = result.value as Record<string, unknown>;
443
- const nextFileOffset = raw.nextFileOffset as number;
444
- const grepResult: GrepResult = {
445
- items: raw.items as GrepResult["items"],
446
- totalMatched: raw.totalMatched as number,
447
- totalFilesSearched: raw.totalFilesSearched as number,
448
- totalFiles: raw.totalFiles as number,
449
- filteredFileCount: raw.filteredFileCount as number,
450
- nextCursor: nextFileOffset > 0 ? createGrepCursor(nextFileOffset) : null,
451
- regexFallbackError: raw.regexFallbackError as string | undefined,
452
- };
453
- return { ok: true, value: grepResult };
454
- }