@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.
- package/package.json +9 -9
- package/src/ffi.ts +376 -143
- 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.
|
|
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.
|
|
66
|
-
"@ff-labs/fff-bin-darwin-x64": "0.2.4-dev.
|
|
67
|
-
"@ff-labs/fff-bin-linux-x64-gnu": "0.2.4-dev.
|
|
68
|
-
"@ff-labs/fff-bin-linux-arm64-gnu": "0.2.4-dev.
|
|
69
|
-
"@ff-labs/fff-bin-linux-x64-musl": "0.2.4-dev.
|
|
70
|
-
"@ff-labs/fff-bin-linux-arm64-musl": "0.2.4-dev.
|
|
71
|
-
"@ff-labs/fff-bin-win32-x64": "0.2.4-dev.
|
|
72
|
-
"@ff-labs/fff-bin-win32-arm64": "0.2.4-dev.
|
|
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 {
|
|
14
|
-
|
|
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) =>
|
|
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
|
-
*
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
308
|
-
const errorPtr = read.ptr(resultPtr,
|
|
309
|
-
const handlePtr = read.ptr(resultPtr,
|
|
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
|
|
341
|
-
const SR_SCORES
|
|
342
|
-
const SR_COUNT
|
|
343
|
-
const SR_MATCHED
|
|
344
|
-
const SR_TOTAL
|
|
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
|
|
347
|
-
const SR_LOC_LINE = 32;
|
|
348
|
-
const SR_LOC_COL
|
|
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
|
|
413
|
+
const SR_LOC_END_COL = 44; // i32 (4)
|
|
351
414
|
|
|
352
415
|
// FffFileItem (80 bytes)
|
|
353
|
-
const FI_PATH
|
|
354
|
-
const FI_RELPATH
|
|
355
|
-
const FI_FNAME
|
|
356
|
-
const FI_GIT
|
|
357
|
-
const FI_SIZE
|
|
358
|
-
const FI_MODIFIED = 40;
|
|
359
|
-
const FI_ACCESS
|
|
360
|
-
const FI_MODFR
|
|
361
|
-
const FI_TOTAL_FR = 64;
|
|
362
|
-
const
|
|
363
|
-
const FI_SIZE_OF
|
|
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
|
|
367
|
-
const SC_BASE
|
|
368
|
-
const SC_FNAME
|
|
369
|
-
const SC_SPECIAL
|
|
370
|
-
const SC_FREC
|
|
371
|
-
const SC_DIST
|
|
372
|
-
const SC_CURFILE
|
|
373
|
-
const SC_COMBO
|
|
374
|
-
const SC_EXACT
|
|
375
|
-
const SC_MTYPE
|
|
376
|
-
const SC_SIZE_OF
|
|
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:
|
|
390
|
-
relativePath:
|
|
391
|
-
fileName:
|
|
392
|
-
gitStatus:
|
|
393
|
-
size:
|
|
394
|
-
modified:
|
|
395
|
-
accessFrecencyScore:
|
|
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:
|
|
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:
|
|
408
|
-
baseScore:
|
|
409
|
-
filenameBonus:
|
|
410
|
-
specialFilenameBonus:read.i32(pp, SC_SPECIAL),
|
|
411
|
-
frecencyBoost:
|
|
412
|
-
distancePenalty:
|
|
413
|
-
currentFilePenalty:
|
|
414
|
-
comboMatchBoost:
|
|
415
|
-
exactMatch:
|
|
416
|
-
matchType:
|
|
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
|
|
429
|
-
|
|
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
|
-
|
|
448
|
-
|
|
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
|
|
453
|
-
const itemsBase
|
|
454
|
-
const scoresBase
|
|
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 = {
|
|
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: {
|
|
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
|
-
|
|
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<
|
|
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
|
|
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<
|
|
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
|
|
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
|
|
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<
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|