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