1b279d4d10
Lua CI / lua-language-server type check (push) Failing after 1s
Lua CI / luacheck lint (push) Failing after 1s
Python CI / Python bindings (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Neovim aarch64-linux-android (push) Failing after 3s
Build & Publish / Build Neovim aarch64-unknown-linux-gnu (push) Failing after 0s
Build & Publish / Build Neovim aarch64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build Neovim x86_64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build Neovim x86_64-unknown-linux-musl (push) Failing after 1s
Build & Publish / Build C FFI aarch64-linux-android (push) Failing after 1s
Build & Publish / Build C FFI aarch64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build C FFI aarch64-unknown-linux-musl (push) Failing after 0s
Build & Publish / Build C FFI x86_64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build C FFI x86_64-unknown-linux-musl (push) Failing after 3s
Build & Publish / Build MCP aarch64-unknown-linux-gnu (push) Failing after 1s
Build & Publish / Build MCP aarch64-unknown-linux-musl (push) Failing after 0s
Build & Publish / Build MCP x86_64-unknown-linux-gnu (push) Failing after 2s
Rust CI / Fuzz Tests (ubuntu-latest) (push) Failing after 1s
Rust CI / Build i686-unknown-linux-gnu (push) Failing after 1s
Rust CI / cargo fmt (push) Failing after 1s
Build & Publish / Build MCP x86_64-unknown-linux-musl (push) Failing after 4s
Build & Publish / Build Python wheels aarch64 (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Python wheels x86_64 (ubuntu-latest) (push) Failing after 1s
Build & Publish / Build Python sdist (push) Failing after 0s
Rust CI / Test (ubuntu-latest) (push) Failing after 1s
Rust CI / cargo clippy (push) Failing after 1s
Spelling / Spell Check with Typos (push) Failing after 1s
Stylua / Check lua files using Stylua (push) Failing after 1s
e2e Tests / e2e (ubuntu-latest) (push) Failing after 4s
e2e Tests / e2e (alpine-musl) (push) Successful in 58m54s
Build & Publish / Build C FFI aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build C FFI x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build C FFI x86_64-pc-windows-msvc (push) Has been cancelled
e2e Tests / e2e (macos-latest) (push) Has been cancelled
e2e Tests / e2e (windows-latest) (push) Has been cancelled
Nix CI / check (push) Has been cancelled
Python CI / Python bindings (macos-latest) (push) Has been cancelled
Python CI / Python bindings (windows-latest) (push) Has been cancelled
Build & Publish / Build Neovim aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Neovim x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build Neovim x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP aarch64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP aarch64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build MCP x86_64-apple-darwin (push) Has been cancelled
Build & Publish / Build MCP x86_64-pc-windows-msvc (push) Has been cancelled
Build & Publish / Build Python wheels aarch64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (macos-latest) (push) Has been cancelled
Build & Publish / Build Python wheels x86_64 (windows-latest) (push) Has been cancelled
Build & Publish / Release (push) Has been cancelled
Build & Publish / Publish Python wheels to PyPI (push) Has been cancelled
Build & Publish / Publish Rust crates (push) Has been cancelled
Build & Publish / Publish npm packages (push) Has been cancelled
Rust CI / Test (macos-latest) (push) Has been cancelled
Rust CI / Test (windows-latest) (push) Has been cancelled
Rust CI / Fuzz Tests (macos-latest) (push) Has been cancelled
Rust CI / Fuzz Tests (windows-latest) (push) Has been cancelled
1700 lines
58 KiB
Rust
1700 lines
58 KiB
Rust
//! C FFI bindings for fff-core
|
||
//!
|
||
//! This crate provides C-compatible FFI exports that can be used from any language
|
||
//! with C FFI support (Bun, Node.js, Python, Ruby, etc.).
|
||
//!
|
||
//! # Instance-based API
|
||
//!
|
||
//! All state is owned by an opaque `FffInstance` fff_handle. Callers create an instance
|
||
//! with `fff_create_instance`, pass the fff_handle to every subsequent call, and free it with
|
||
//! `fff_destroy`. Multiple independent instances can coexist in the same process.
|
||
//!
|
||
//! # Memory management
|
||
//!
|
||
//! * Every `fff_*` function that returns `*mut FffResult` requires the caller to
|
||
//! free the result with `fff_free_result`.
|
||
//! * The instance itself must be freed with `fff_destroy`.
|
||
//!
|
||
//! # Parameter conventions
|
||
//!
|
||
//! * Optional `*const c_char` parameters: pass NULL or an empty string to omit.
|
||
//! * Numeric parameters: 0 means "use default" unless documented otherwise.
|
||
//! * Grep mode (`u8`): 0 = plain text, 1 = regex, 2 = fuzzy.
|
||
//! * Multi-grep patterns are passed as a single newline-separated (`\n`) string.
|
||
|
||
use std::ffi::{CStr, CString, c_char, c_void};
|
||
use std::path::PathBuf;
|
||
use std::time::Duration;
|
||
|
||
use fff::shared::SharedQueryTracker;
|
||
|
||
mod accessors;
|
||
mod ffi_types;
|
||
|
||
use fff::file_picker::FilePicker;
|
||
use fff::frecency::FrecencyTracker;
|
||
use fff::query_tracker::QueryTracker;
|
||
use fff::{DbHealthChecker, FFFMode, FuzzySearchOptions, PaginationArgs, QueryParser};
|
||
use fff::{SharedFilePicker, SharedFrecency};
|
||
use ffi_types::{
|
||
FFF_CREATE_OPTIONS_VERSION, FffCreateOptions, FffDirItem, FffDirSearchResult, FffFileItem,
|
||
FffGrepMatch, FffGrepResult, FffMixedItem, FffMixedSearchResult, FffResult, FffScanProgress,
|
||
FffScore, FffSearchResult,
|
||
};
|
||
|
||
/// Opaque fff_handle holding all per-instance state.
|
||
///
|
||
/// The caller receives this as `*mut c_void` and must pass it to every FFI call.
|
||
/// The fff_handle is freed by `fff_destroy`.
|
||
struct FffInstance {
|
||
picker: SharedFilePicker,
|
||
frecency: SharedFrecency,
|
||
query_tracker: SharedQueryTracker,
|
||
}
|
||
|
||
/// Helper to convert C string to Rust &str.
|
||
///
|
||
/// Returns `None` if the pointer is null or the string is not valid UTF-8.
|
||
unsafe fn cstr_to_str<'a>(s: *const c_char) -> Option<&'a str> {
|
||
if s.is_null() {
|
||
None
|
||
} else {
|
||
unsafe { CStr::from_ptr(s).to_str().ok() }
|
||
}
|
||
}
|
||
|
||
/// Helper to convert an optional C string parameter.
|
||
///
|
||
/// Returns `None` if the pointer is null, empty, or not valid UTF-8.
|
||
unsafe fn optional_cstr<'a>(s: *const c_char) -> Option<&'a str> {
|
||
unsafe { cstr_to_str(s) }.filter(|s| !s.is_empty())
|
||
}
|
||
|
||
/// Recover a `&FffInstance` from the opaque pointer.
|
||
///
|
||
/// Returns an error `FffResult` if the pointer is null.
|
||
unsafe fn instance_ref<'a>(fff_handle: *mut c_void) -> Result<&'a FffInstance, *mut FffResult> {
|
||
if fff_handle.is_null() {
|
||
Err(FffResult::err(
|
||
"Instance handle is null. Create one with fff_create_instance first.",
|
||
))
|
||
} else {
|
||
Ok(unsafe { &*(fff_handle as *const FffInstance) })
|
||
}
|
||
}
|
||
|
||
/// Decode a `u8` grep mode into the core enum.
|
||
fn grep_mode_from_u8(mode: u8) -> fff::GrepMode {
|
||
match mode {
|
||
1 => fff::GrepMode::Regex,
|
||
2 => fff::GrepMode::Fuzzy,
|
||
_ => fff::GrepMode::PlainText,
|
||
}
|
||
}
|
||
|
||
/// Apply "0 means default" convention.
|
||
fn default_u32(val: u32, default: u32) -> u32 {
|
||
if val == 0 { default } else { val }
|
||
}
|
||
|
||
fn default_u64(val: u64, default: u64) -> u64 {
|
||
if val == 0 { default } else { val }
|
||
}
|
||
|
||
fn default_i32(val: i32, default: i32) -> i32 {
|
||
if val == 0 { default } else { val }
|
||
}
|
||
|
||
/// Create a new file finder instance (legacy 8-arg positional signature).
|
||
///
|
||
/// @deprecated Use [`fff_create_instance_with`] (or
|
||
/// [`fff_create_instance_with_value`] for FFI bindings) — both take the
|
||
/// versioned [`FffCreateOptions`] struct that evolves without ABI breaks.
|
||
/// This function delegates to `fff_create_instance_with` internally; the
|
||
/// `use_unsafe_no_lock` parameter is deprecated and ignored.
|
||
///
|
||
/// ## Safety
|
||
/// See `fff_create_instance_with`.
|
||
#[deprecated(
|
||
since = "0.8.5",
|
||
note = "Use fff_create_instance_with (by pointer) or fff_create_instance_with_value (by value) with FffCreateOptions instead. The struct evolves without ABI breaks."
|
||
)]
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_create_instance(
|
||
base_path: *const c_char,
|
||
frecency_db_path: *const c_char,
|
||
history_db_path: *const c_char,
|
||
_use_unsafe_no_lock: bool,
|
||
enable_mmap_cache: bool,
|
||
enable_content_indexing: bool,
|
||
watch: bool,
|
||
ai_mode: bool,
|
||
) -> *mut FffResult {
|
||
let mut opts = FffCreateOptions::defaults();
|
||
opts.base_path = base_path;
|
||
opts.frecency_db_path = frecency_db_path;
|
||
opts.history_db_path = history_db_path;
|
||
opts.enable_mmap_cache = enable_mmap_cache;
|
||
opts.enable_content_indexing = enable_content_indexing;
|
||
opts.watch = watch;
|
||
opts.ai_mode = ai_mode;
|
||
unsafe { fff_create_instance_with(&opts as *const FffCreateOptions) }
|
||
}
|
||
|
||
/// Create a new file finder instance (legacy 13-arg positional signature).
|
||
///
|
||
/// @deprecated Use [`fff_create_instance_with`] (or
|
||
/// [`fff_create_instance_with_value`] for FFI bindings) — both take the
|
||
/// versioned [`FffCreateOptions`] struct that evolves without ABI breaks.
|
||
/// The `use_unsafe_no_lock` parameter is deprecated and ignored.
|
||
///
|
||
/// ## Safety
|
||
/// See `fff_create_instance_with`.
|
||
#[deprecated(
|
||
since = "0.8.5",
|
||
note = "Use fff_create_instance_with (by pointer) or fff_create_instance_with_value (by value) with FffCreateOptions instead. The struct evolves without ABI breaks."
|
||
)]
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_create_instance2(
|
||
base_path: *const c_char,
|
||
frecency_db_path: *const c_char,
|
||
history_db_path: *const c_char,
|
||
_use_unsafe_no_lock: bool,
|
||
enable_mmap_cache: bool,
|
||
enable_content_indexing: bool,
|
||
watch: bool,
|
||
ai_mode: bool,
|
||
log_file_path: *const c_char,
|
||
log_level: *const c_char,
|
||
cache_budget_max_files: u64,
|
||
cache_budget_max_bytes: u64,
|
||
cache_budget_max_file_size: u64,
|
||
) -> *mut FffResult {
|
||
let mut opts = FffCreateOptions::defaults();
|
||
opts.base_path = base_path;
|
||
opts.frecency_db_path = frecency_db_path;
|
||
opts.history_db_path = history_db_path;
|
||
opts.enable_mmap_cache = enable_mmap_cache;
|
||
opts.enable_content_indexing = enable_content_indexing;
|
||
opts.watch = watch;
|
||
opts.ai_mode = ai_mode;
|
||
opts.log_file_path = log_file_path;
|
||
opts.log_level = log_level;
|
||
opts.cache_budget_max_files = cache_budget_max_files;
|
||
opts.cache_budget_max_bytes = cache_budget_max_bytes;
|
||
opts.cache_budget_max_file_size = cache_budget_max_file_size;
|
||
unsafe { fff_create_instance_with(&opts as *const FffCreateOptions) }
|
||
}
|
||
|
||
/// Create a new file finder instance from an [`FffCreateOptions`] struct.
|
||
///
|
||
/// **Direct C consumers** populate the struct (designated initializers
|
||
/// recommended), set `version` to [`FFF_CREATE_OPTIONS_VERSION`], and pass
|
||
/// it by pointer. New fields are appended in future versions; old callers
|
||
/// passing `version = 1` keep working forever.
|
||
///
|
||
/// **FFI consumers** that prefer struct-by-value semantics (e.g. ffi-rs's
|
||
/// `paramsType: [structDef]`) should use [`fff_create_instance_with_value`]
|
||
/// instead — it's a thin calling-convention adapter that delegates here.
|
||
///
|
||
/// Required: `opts.base_path` must be non-NULL and non-empty.
|
||
///
|
||
/// When all three `cache_budget_*` values are 0 the budget is auto-computed
|
||
/// from repo size after the initial scan. Otherwise an explicit budget is
|
||
/// used: any field left at 0 falls back to its `unlimited()` default.
|
||
///
|
||
/// ## Safety
|
||
/// * `opts` must be a valid pointer to an `FffCreateOptions` whose `version`
|
||
/// is in the range `1..=FFF_CREATE_OPTIONS_VERSION`.
|
||
/// * All string pointers inside `opts` must be valid null-terminated UTF-8
|
||
/// or NULL.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_create_instance_with(opts: *const FffCreateOptions) -> *mut FffResult {
|
||
if opts.is_null() {
|
||
return FffResult::err("opts is null");
|
||
}
|
||
let opts = unsafe { &*opts };
|
||
if opts.version == 0 || opts.version > FFF_CREATE_OPTIONS_VERSION {
|
||
return FffResult::err(&format!(
|
||
"Unsupported FffCreateOptions version {} (library understands up to {})",
|
||
opts.version, FFF_CREATE_OPTIONS_VERSION
|
||
));
|
||
}
|
||
|
||
let base_path_str = match unsafe { cstr_to_str(opts.base_path) } {
|
||
Some(s) if !s.is_empty() => s.to_string(),
|
||
_ => return FffResult::err("opts.base_path is null or empty"),
|
||
};
|
||
|
||
if let Some(log_path) = unsafe { optional_cstr(opts.log_file_path) } {
|
||
let level = unsafe { optional_cstr(opts.log_level) };
|
||
if let Err(e) = fff::log::init_tracing(log_path, level, None) {
|
||
return FffResult::err(&format!("Failed to init tracing: {}", e));
|
||
}
|
||
}
|
||
|
||
let frecency_path = unsafe { optional_cstr(opts.frecency_db_path) }.map(|s| s.to_string());
|
||
let history_path = unsafe { optional_cstr(opts.history_db_path) }.map(|s| s.to_string());
|
||
|
||
let shared_picker = SharedFilePicker::default();
|
||
let shared_frecency = SharedFrecency::default();
|
||
let query_tracker = SharedQueryTracker::default();
|
||
|
||
if let Some(ref frecency_path) = frecency_path {
|
||
if let Some(parent) = PathBuf::from(frecency_path).parent() {
|
||
let _ = std::fs::create_dir_all(parent);
|
||
}
|
||
|
||
match FrecencyTracker::open(frecency_path) {
|
||
Ok(tracker) => {
|
||
if let Err(e) = shared_frecency.init(tracker) {
|
||
return FffResult::err(&format!("Failed to acquire frecency lock: {}", e));
|
||
}
|
||
}
|
||
Err(e) => return FffResult::err(&format!("Failed to init frecency db: {}", e)),
|
||
}
|
||
}
|
||
|
||
if let Some(ref history_path) = history_path {
|
||
if let Some(parent) = PathBuf::from(history_path).parent() {
|
||
let _ = std::fs::create_dir_all(parent);
|
||
}
|
||
|
||
match QueryTracker::open(history_path) {
|
||
Ok(tracker) => {
|
||
if let Err(e) = query_tracker.init(tracker) {
|
||
return FffResult::err(&format!("Failed to acquire query tracker lock: {}", e));
|
||
}
|
||
}
|
||
Err(e) => return FffResult::err(&format!("Failed to init query tracker db: {}", e)),
|
||
}
|
||
}
|
||
|
||
let mode = if opts.ai_mode {
|
||
FFFMode::Ai
|
||
} else {
|
||
FFFMode::Neovim
|
||
};
|
||
|
||
let cache_budget = fff::ContentCacheBudget::from_overrides(
|
||
opts.cache_budget_max_files as usize,
|
||
opts.cache_budget_max_bytes,
|
||
opts.cache_budget_max_file_size,
|
||
);
|
||
|
||
if let Err(e) = FilePicker::new_with_shared_state(
|
||
shared_picker.clone(),
|
||
shared_frecency.clone(),
|
||
fff::FilePickerOptions {
|
||
base_path: base_path_str,
|
||
enable_mmap_cache: opts.enable_mmap_cache,
|
||
enable_content_indexing: opts.enable_content_indexing,
|
||
watch: opts.watch,
|
||
mode,
|
||
cache_budget,
|
||
follow_symlinks: opts.version >= 2 && opts.follow_symlinks,
|
||
enable_fs_root_scanning: opts.enable_fs_root_scanning,
|
||
enable_home_dir_scanning: opts.enable_home_dir_scanning,
|
||
},
|
||
) {
|
||
return FffResult::err(&format!("Failed to init file picker: {}", e));
|
||
}
|
||
|
||
let instance = Box::new(FffInstance {
|
||
picker: shared_picker,
|
||
frecency: shared_frecency,
|
||
query_tracker,
|
||
});
|
||
|
||
let fff_handle = Box::into_raw(instance) as *mut c_void;
|
||
FffResult::ok_handle(fff_handle)
|
||
}
|
||
|
||
/// Calling-convention adapter for [`fff_create_instance_with`].
|
||
///
|
||
/// Same logic, but takes the [`FffCreateOptions`] struct **by value**. This
|
||
/// makes the function callable from FFI libraries whose native struct
|
||
/// support passes structs by value on the wire (e.g. Node's `ffi-rs` with
|
||
/// `paramsType: [structDef]`).
|
||
///
|
||
/// This is **not** a versioned wrapper — when new fields are appended to
|
||
/// `FffCreateOptions`, both this function and `fff_create_instance_with`
|
||
/// pick them up automatically with no signature change.
|
||
///
|
||
/// ## Safety
|
||
/// All `*const c_char` fields inside `opts` must be valid null-terminated
|
||
/// UTF-8 or NULL. The struct itself is consumed by value.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_create_instance_with_value(opts: FffCreateOptions) -> *mut FffResult {
|
||
unsafe { fff_create_instance_with(&opts as *const FffCreateOptions) }
|
||
}
|
||
|
||
/// Destroy a file finder instance and free all its resources.
|
||
///
|
||
/// ## Safety
|
||
/// `fff_handle` must be a valid pointer returned by `fff_create_instance`, or null (no-op).
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_destroy(fff_handle: *mut c_void) {
|
||
if fff_handle.is_null() {
|
||
return;
|
||
}
|
||
|
||
let instance = unsafe { Box::from_raw(fff_handle as *mut FffInstance) };
|
||
|
||
if let Ok(mut guard) = instance.picker.write()
|
||
&& let Some(picker) = guard.take()
|
||
{
|
||
drop(picker);
|
||
}
|
||
|
||
if let Ok(mut guard) = instance.frecency.write() {
|
||
*guard = None;
|
||
}
|
||
if let Ok(mut guard) = instance.query_tracker.write() {
|
||
*guard = None;
|
||
}
|
||
}
|
||
|
||
/// Perform fuzzy search on indexed files.
|
||
///
|
||
/// # Parameters
|
||
///
|
||
/// * `fff_handle` – instance from `fff_create_instance`
|
||
/// * `query` – search query string
|
||
/// * `current_file` – path of the currently open file for deprioritization (NULL/empty to skip)
|
||
/// * `max_threads` – maximum worker threads (0 = auto-detect)
|
||
/// * `page_index` – pagination offset (0 = first page)
|
||
/// * `page_size` – results per page (0 = default 100)
|
||
/// * `combo_boost_multiplier` – score multiplier for combo matches (0 = default 100)
|
||
/// * `min_combo_count` – minimum combo count before boost applies (0 = default 3)
|
||
///
|
||
/// ## Safety
|
||
/// * `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||
/// * `query` and `current_file` must be valid null-terminated UTF-8 strings or NULL.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_search(
|
||
fff_handle: *mut c_void,
|
||
query: *const c_char,
|
||
current_file: *const c_char,
|
||
max_threads: u32,
|
||
page_index: u32,
|
||
page_size: u32,
|
||
combo_boost_multiplier: i32,
|
||
min_combo_count: u32,
|
||
) -> *mut FffResult {
|
||
let inst = match unsafe { instance_ref(fff_handle) } {
|
||
Ok(i) => i,
|
||
Err(e) => return e,
|
||
};
|
||
|
||
let query_str = match unsafe { cstr_to_str(query) } {
|
||
Some(s) => s,
|
||
None => return FffResult::err("Query is null or invalid UTF-8"),
|
||
};
|
||
|
||
let current_file_str = unsafe { optional_cstr(current_file) };
|
||
let page_size = default_u32(page_size, 100) as usize;
|
||
let min_combo_count = default_u32(min_combo_count, 3);
|
||
let combo_boost_multiplier = default_i32(combo_boost_multiplier, 100);
|
||
|
||
let picker_guard = match inst.picker.read() {
|
||
Ok(g) => g,
|
||
Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)),
|
||
};
|
||
|
||
let picker = match picker_guard.as_ref() {
|
||
Some(p) => p,
|
||
None => {
|
||
return FffResult::err("File picker not initialized. Call fff_create_instance first.");
|
||
}
|
||
};
|
||
|
||
// Get query tracker ref for combo matching
|
||
let qt_guard = match inst.query_tracker.read() {
|
||
Ok(q) => q,
|
||
Err(_) => return FffResult::err("Failed to acquire query tracker lock"),
|
||
};
|
||
let query_tracker_ref = qt_guard.as_ref();
|
||
|
||
let parser = QueryParser::default();
|
||
let parsed = parser.parse(query_str);
|
||
|
||
let results = picker.fuzzy_search(
|
||
&parsed,
|
||
query_tracker_ref,
|
||
FuzzySearchOptions {
|
||
max_threads: max_threads as usize,
|
||
current_file: current_file_str,
|
||
project_path: Some(picker.base_path()),
|
||
combo_boost_score_multiplier: combo_boost_multiplier,
|
||
min_combo_count,
|
||
pagination: PaginationArgs {
|
||
offset: page_index as usize,
|
||
limit: page_size,
|
||
},
|
||
},
|
||
);
|
||
|
||
let search_result = FffSearchResult::from_core(&results, picker);
|
||
FffResult::ok_handle(search_result as *mut c_void)
|
||
}
|
||
|
||
/// Glob-only search: filter indexed files by a single glob pattern, rank by
|
||
/// frecency, and paginate. Bypasses the regular query parser entirely.
|
||
///
|
||
/// Use this when you already have a literal glob pattern (e.g. `*.rs`, a
|
||
/// recursive `**` match, or `src/components` prefix) and want neither fuzzy
|
||
/// matching nor multi-token constraint parsing. Ranking falls back to
|
||
/// frecency because there is no fuzzy score to combine with.
|
||
///
|
||
/// # Parameters
|
||
///
|
||
/// * `fff_handle` - instance from `fff_create_instance`
|
||
/// * `pattern` - glob pattern (required, no parsing - passed through verbatim)
|
||
/// * `current_file` - path of the currently open file for deprioritization (NULL/empty to skip)
|
||
/// * `max_threads` - maximum worker threads (0 = auto-detect)
|
||
/// * `page_index` - pagination offset (0 = first page)
|
||
/// * `page_size` - results per page (0 = default 100)
|
||
///
|
||
/// ## Safety
|
||
/// * `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||
/// * `pattern` and `current_file` must be valid null-terminated UTF-8 strings or NULL.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_glob(
|
||
fff_handle: *mut c_void,
|
||
pattern: *const c_char,
|
||
current_file: *const c_char,
|
||
max_threads: u32,
|
||
page_index: u32,
|
||
page_size: u32,
|
||
) -> *mut FffResult {
|
||
let inst = match unsafe { instance_ref(fff_handle) } {
|
||
Ok(i) => i,
|
||
Err(e) => return e,
|
||
};
|
||
|
||
let pattern_str = match unsafe { cstr_to_str(pattern) } {
|
||
Some(s) if !s.is_empty() => s,
|
||
_ => return FffResult::err("Pattern is null, empty, or invalid UTF-8"),
|
||
};
|
||
|
||
let current_file_str = unsafe { optional_cstr(current_file) };
|
||
let page_size = default_u32(page_size, 100) as usize;
|
||
|
||
let picker_guard = match inst.picker.read() {
|
||
Ok(g) => g,
|
||
Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)),
|
||
};
|
||
|
||
let picker = match picker_guard.as_ref() {
|
||
Some(p) => p,
|
||
None => {
|
||
return FffResult::err("File picker not initialized. Call fff_create_instance first.");
|
||
}
|
||
};
|
||
|
||
let results = picker.glob(
|
||
pattern_str,
|
||
FuzzySearchOptions {
|
||
max_threads: max_threads as usize,
|
||
current_file: current_file_str,
|
||
project_path: Some(picker.base_path()),
|
||
combo_boost_score_multiplier: 0,
|
||
min_combo_count: 0,
|
||
pagination: PaginationArgs {
|
||
offset: page_index as usize,
|
||
limit: page_size,
|
||
},
|
||
},
|
||
);
|
||
|
||
let search_result = FffSearchResult::from_core(&results, picker);
|
||
FffResult::ok_handle(search_result as *mut c_void)
|
||
}
|
||
|
||
/// Perform fuzzy search on indexed directories.
|
||
///
|
||
/// # Parameters
|
||
///
|
||
/// * `fff_handle` – instance from `fff_create_instance`
|
||
/// * `query` – search query string
|
||
/// * `current_file` – path of the currently open file for distance scoring (NULL/empty to skip)
|
||
/// * `max_threads` – maximum worker threads (0 = auto-detect)
|
||
/// * `page_index` – pagination offset (0 = first page)
|
||
/// * `page_size` – results per page (0 = default 100)
|
||
///
|
||
/// ## Safety
|
||
/// * `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||
/// * `query` and `current_file` must be valid null-terminated UTF-8 strings or NULL.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_search_directories(
|
||
fff_handle: *mut c_void,
|
||
query: *const c_char,
|
||
current_file: *const c_char,
|
||
max_threads: u32,
|
||
page_index: u32,
|
||
page_size: u32,
|
||
) -> *mut FffResult {
|
||
let inst = match unsafe { instance_ref(fff_handle) } {
|
||
Ok(i) => i,
|
||
Err(e) => return e,
|
||
};
|
||
|
||
let query_str = match unsafe { cstr_to_str(query) } {
|
||
Some(s) => s,
|
||
None => return FffResult::err("Query is null or invalid UTF-8"),
|
||
};
|
||
|
||
let current_file_str = unsafe { optional_cstr(current_file) };
|
||
let page_size = default_u32(page_size, 100) as usize;
|
||
|
||
let picker_guard = match inst.picker.read() {
|
||
Ok(g) => g,
|
||
Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)),
|
||
};
|
||
|
||
let picker = match picker_guard.as_ref() {
|
||
Some(p) => p,
|
||
None => {
|
||
return FffResult::err("File picker not initialized. Call fff_create_instance first.");
|
||
}
|
||
};
|
||
|
||
let parser = QueryParser::new(fff_query_parser::DirSearchConfig);
|
||
let parsed = parser.parse(query_str);
|
||
|
||
let results = picker.fuzzy_search_directories(
|
||
&parsed,
|
||
FuzzySearchOptions {
|
||
max_threads: max_threads as usize,
|
||
current_file: current_file_str,
|
||
project_path: Some(picker.base_path()),
|
||
combo_boost_score_multiplier: 0,
|
||
min_combo_count: 0,
|
||
pagination: PaginationArgs {
|
||
offset: page_index as usize,
|
||
limit: page_size,
|
||
},
|
||
},
|
||
);
|
||
|
||
let dir_result = FffDirSearchResult::from_core(&results, picker);
|
||
FffResult::ok_handle(dir_result as *mut c_void)
|
||
}
|
||
|
||
/// Perform a mixed fuzzy search across both files and directories.
|
||
///
|
||
/// Returns a single flat list where files and directories are interleaved
|
||
/// by total score in descending order. Each item has an `item_type` field
|
||
/// (0 = file, 1 = directory).
|
||
///
|
||
/// # Parameters
|
||
///
|
||
/// * `fff_handle` – instance from `fff_create_instance`
|
||
/// * `query` – search query string
|
||
/// * `current_file` – path of the currently open file (NULL/empty to skip)
|
||
/// * `max_threads` – maximum worker threads (0 = auto-detect)
|
||
/// * `page_index` – pagination offset (0 = first page)
|
||
/// * `page_size` – results per page (0 = default 100)
|
||
/// * `combo_boost_multiplier` – score multiplier for combo matches (0 = default 100)
|
||
/// * `min_combo_count` – minimum combo count before boost applies (0 = default 3)
|
||
///
|
||
/// ## Safety
|
||
/// * `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||
/// * `query` and `current_file` must be valid null-terminated UTF-8 strings or NULL.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_search_mixed(
|
||
fff_handle: *mut c_void,
|
||
query: *const c_char,
|
||
current_file: *const c_char,
|
||
max_threads: u32,
|
||
page_index: u32,
|
||
page_size: u32,
|
||
combo_boost_multiplier: i32,
|
||
min_combo_count: u32,
|
||
) -> *mut FffResult {
|
||
let inst = match unsafe { instance_ref(fff_handle) } {
|
||
Ok(i) => i,
|
||
Err(e) => return e,
|
||
};
|
||
|
||
let query_str = match unsafe { cstr_to_str(query) } {
|
||
Some(s) => s,
|
||
None => return FffResult::err("Query is null or invalid UTF-8"),
|
||
};
|
||
|
||
let current_file_str = unsafe { optional_cstr(current_file) };
|
||
let page_size = default_u32(page_size, 100) as usize;
|
||
let min_combo_count = default_u32(min_combo_count, 3);
|
||
let combo_boost_multiplier = default_i32(combo_boost_multiplier, 100);
|
||
|
||
let picker_guard = match inst.picker.read() {
|
||
Ok(g) => g,
|
||
Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)),
|
||
};
|
||
|
||
let picker = match picker_guard.as_ref() {
|
||
Some(p) => p,
|
||
None => {
|
||
return FffResult::err("File picker not initialized. Call fff_create_instance first.");
|
||
}
|
||
};
|
||
|
||
let qt_guard = match inst.query_tracker.read() {
|
||
Ok(q) => q,
|
||
Err(_) => return FffResult::err("Failed to acquire query tracker lock"),
|
||
};
|
||
let query_tracker_ref = qt_guard.as_ref();
|
||
|
||
let parser = QueryParser::new(fff_query_parser::MixedSearchConfig);
|
||
let parsed = parser.parse(query_str);
|
||
|
||
let results = picker.fuzzy_search_mixed(
|
||
&parsed,
|
||
query_tracker_ref,
|
||
FuzzySearchOptions {
|
||
max_threads: max_threads as usize,
|
||
current_file: current_file_str,
|
||
project_path: Some(picker.base_path()),
|
||
combo_boost_score_multiplier: combo_boost_multiplier,
|
||
min_combo_count,
|
||
pagination: PaginationArgs {
|
||
offset: page_index as usize,
|
||
limit: page_size,
|
||
},
|
||
},
|
||
);
|
||
|
||
let mixed_result = FffMixedSearchResult::from_core(&results, picker);
|
||
FffResult::ok_handle(mixed_result as *mut c_void)
|
||
}
|
||
|
||
/// Perform content search (grep) across indexed files.
|
||
///
|
||
/// # Parameters
|
||
///
|
||
/// * `fff_handle` – instance from `fff_create_instance`
|
||
/// * `query` – search query (supports constraint syntax like `*.rs pattern`)
|
||
/// * `mode` – 0 = plain text (SIMD), 1 = regex, 2 = fuzzy
|
||
/// * `max_file_size` – skip files larger than this in bytes (0 = default 10 MB)
|
||
/// * `max_matches_per_file` – max matches per file (0 = unlimited)
|
||
/// * `smart_case` – case-insensitive when query is all lowercase
|
||
/// * `file_offset` – file-based pagination offset (0 = start)
|
||
/// * `page_limit` – max matches to return (0 = default 50)
|
||
/// * `time_budget_ms` – wall-clock budget in ms (0 = unlimited)
|
||
/// * `before_context` – context lines before each match
|
||
/// * `after_context` – context lines after each match
|
||
/// * `classify_definitions` – tag matches that are code definitions
|
||
///
|
||
/// ## Safety
|
||
/// * `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||
/// * `query` must be a valid null-terminated UTF-8 string.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_live_grep(
|
||
fff_handle: *mut c_void,
|
||
query: *const c_char,
|
||
mode: u8,
|
||
max_file_size: u64,
|
||
max_matches_per_file: u32,
|
||
smart_case: bool,
|
||
file_offset: u32,
|
||
page_limit: u32,
|
||
time_budget_ms: u64,
|
||
before_context: u32,
|
||
after_context: u32,
|
||
classify_definitions: bool,
|
||
) -> *mut FffResult {
|
||
let inst = match unsafe { instance_ref(fff_handle) } {
|
||
Ok(i) => i,
|
||
Err(e) => return e,
|
||
};
|
||
|
||
let query_str = match unsafe { cstr_to_str(query) } {
|
||
Some(s) => s,
|
||
None => return FffResult::err("Query is null or invalid UTF-8"),
|
||
};
|
||
|
||
let picker_guard = match inst.picker.read() {
|
||
Ok(g) => g,
|
||
Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)),
|
||
};
|
||
|
||
let picker = match picker_guard.as_ref() {
|
||
Some(p) => p,
|
||
None => {
|
||
return FffResult::err("File picker not initialized. Call fff_create_instance first.");
|
||
}
|
||
};
|
||
|
||
let is_ai = picker.mode().is_ai();
|
||
let parsed = if is_ai {
|
||
fff::QueryParser::new(fff_query_parser::AiGrepConfig).parse(query_str)
|
||
} else {
|
||
fff::grep::parse_grep_query(query_str)
|
||
};
|
||
|
||
let options = fff::GrepSearchOptions {
|
||
max_file_size: default_u64(max_file_size, 10 * 1024 * 1024),
|
||
max_matches_per_file: max_matches_per_file as usize,
|
||
smart_case,
|
||
file_offset: file_offset as usize,
|
||
page_limit: default_u32(page_limit, 50) as usize,
|
||
mode: grep_mode_from_u8(mode),
|
||
time_budget_ms,
|
||
before_context: before_context as usize,
|
||
after_context: after_context as usize,
|
||
classify_definitions,
|
||
trim_whitespace: false,
|
||
abort_signal: None,
|
||
};
|
||
|
||
let result = picker.grep(&parsed, &options);
|
||
let grep_result = FffGrepResult::from_core(&result, picker);
|
||
FffResult::ok_handle(grep_result as *mut c_void)
|
||
}
|
||
|
||
/// Perform multi-pattern OR search (Aho-Corasick) across indexed files.
|
||
///
|
||
/// Searches for lines matching ANY of the provided patterns using
|
||
/// SIMD-accelerated multi-needle matching.
|
||
///
|
||
/// # Parameters
|
||
///
|
||
/// * `fff_handle` – instance from `fff_create_instance`
|
||
/// * `patterns_joined` – patterns separated by `\n` (e.g. `"foo\nbar\nbaz"`)
|
||
/// * `constraints` – file filter like `"*.rs"` or `"/src/"` (NULL/empty to skip)
|
||
/// * `max_file_size` – skip files larger than this in bytes (0 = default 10 MB)
|
||
/// * `max_matches_per_file` – max matches per file (0 = unlimited)
|
||
/// * `smart_case` – case-insensitive when all patterns are lowercase
|
||
/// * `file_offset` – file-based pagination offset (0 = start)
|
||
/// * `page_limit` – max matches to return (0 = default 50)
|
||
/// * `time_budget_ms` – wall-clock budget in ms (0 = unlimited)
|
||
/// * `before_context` – context lines before each match
|
||
/// * `after_context` – context lines after each match
|
||
/// * `classify_definitions` – tag matches that are code definitions
|
||
///
|
||
/// ## Safety
|
||
/// * `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||
/// * `patterns_joined` and `constraints` must be valid null-terminated UTF-8 or NULL.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_multi_grep(
|
||
fff_handle: *mut c_void,
|
||
patterns_joined: *const c_char,
|
||
constraints: *const c_char,
|
||
max_file_size: u64,
|
||
max_matches_per_file: u32,
|
||
smart_case: bool,
|
||
file_offset: u32,
|
||
page_limit: u32,
|
||
time_budget_ms: u64,
|
||
before_context: u32,
|
||
after_context: u32,
|
||
classify_definitions: bool,
|
||
) -> *mut FffResult {
|
||
let inst = match unsafe { instance_ref(fff_handle) } {
|
||
Ok(i) => i,
|
||
Err(e) => return e,
|
||
};
|
||
|
||
let patterns_str = match unsafe { cstr_to_str(patterns_joined) } {
|
||
Some(s) if !s.is_empty() => s,
|
||
_ => return FffResult::err("patterns_joined is null or empty"),
|
||
};
|
||
|
||
let patterns: Vec<&str> = patterns_str.split('\n').collect();
|
||
if patterns.is_empty() || patterns.iter().all(|p| p.is_empty()) {
|
||
return FffResult::err("patterns must not be empty");
|
||
}
|
||
|
||
let constraints_str = unsafe { optional_cstr(constraints) };
|
||
|
||
let picker_guard = match inst.picker.read() {
|
||
Ok(g) => g,
|
||
Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)),
|
||
};
|
||
|
||
let picker = match picker_guard.as_ref() {
|
||
Some(p) => p,
|
||
None => {
|
||
return FffResult::err("File picker not initialized. Call fff_create_instance first.");
|
||
}
|
||
};
|
||
|
||
let is_ai = picker.mode().is_ai();
|
||
|
||
// Parse constraints from the optional string (e.g. "*.rs /src/")
|
||
let parsed_constraints = constraints_str.map(|c| {
|
||
if is_ai {
|
||
fff::QueryParser::new(fff_query_parser::AiGrepConfig).parse(c)
|
||
} else {
|
||
fff::grep::parse_grep_query(c)
|
||
}
|
||
});
|
||
|
||
let constraint_refs: &[fff::Constraint<'_>] = match &parsed_constraints {
|
||
Some(q) => &q.constraints,
|
||
None => &[],
|
||
};
|
||
|
||
let options = fff::GrepSearchOptions {
|
||
max_file_size: default_u64(max_file_size, 10 * 1024 * 1024),
|
||
max_matches_per_file: max_matches_per_file as usize,
|
||
smart_case,
|
||
file_offset: file_offset as usize,
|
||
page_limit: default_u32(page_limit, 50) as usize,
|
||
mode: fff::GrepMode::PlainText, // ignored by multi_grep_search
|
||
time_budget_ms,
|
||
before_context: before_context as usize,
|
||
after_context: after_context as usize,
|
||
classify_definitions,
|
||
trim_whitespace: false,
|
||
abort_signal: None,
|
||
};
|
||
|
||
let result = picker.multi_grep(&patterns, constraint_refs, &options);
|
||
let grep_result = FffGrepResult::from_core(&result, picker);
|
||
FffResult::ok_handle(grep_result as *mut c_void)
|
||
}
|
||
|
||
/// Trigger a rescan of the file index.
|
||
///
|
||
/// ## Safety
|
||
/// `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_scan_files(fff_handle: *mut c_void) -> *mut FffResult {
|
||
let inst = match unsafe { instance_ref(fff_handle) } {
|
||
Ok(i) => i,
|
||
Err(e) => return e,
|
||
};
|
||
|
||
// Async: rescan runs on a BG thread, caller returns immediately.
|
||
// Use `fff_is_scanning` / `fff_wait_for_scan` to observe progress.
|
||
match inst.picker.trigger_full_rescan_async(&inst.frecency) {
|
||
Ok(()) => FffResult::ok_empty(),
|
||
Err(e) => FffResult::err(&format!("Failed to trigger rescan: {}", e)),
|
||
}
|
||
}
|
||
|
||
/// Check if a scan is currently in progress.
|
||
///
|
||
/// ## Safety
|
||
/// `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_is_scanning(fff_handle: *mut c_void) -> bool {
|
||
let inst = match unsafe { instance_ref(fff_handle) } {
|
||
Ok(i) => i,
|
||
Err(_) => return false,
|
||
};
|
||
|
||
inst.picker
|
||
.read()
|
||
.ok()
|
||
.and_then(|guard| guard.as_ref().map(|p| p.is_scan_active()))
|
||
.unwrap_or(false)
|
||
}
|
||
|
||
/// Get the base path of the file picker.
|
||
///
|
||
/// Returns an `FffResult` with a heap-allocated C string in the `handle`
|
||
/// field. Free the string with `fff_free_string` after reading it.
|
||
///
|
||
/// ## Safety
|
||
/// `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_get_base_path(fff_handle: *mut c_void) -> *mut FffResult {
|
||
let inst = match unsafe { instance_ref(fff_handle) } {
|
||
Ok(i) => i,
|
||
Err(e) => return e,
|
||
};
|
||
|
||
let guard = match inst.picker.read() {
|
||
Ok(g) => g,
|
||
Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)),
|
||
};
|
||
|
||
let picker = match guard.as_ref() {
|
||
Some(p) => p,
|
||
None => return FffResult::err("File picker not initialized"),
|
||
};
|
||
|
||
FffResult::ok_string(&picker.base_path().to_string_lossy())
|
||
}
|
||
|
||
/// Get scan progress information.
|
||
///
|
||
/// ## Safety
|
||
/// `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_get_scan_progress(fff_handle: *mut c_void) -> *mut FffResult {
|
||
let inst = match unsafe { instance_ref(fff_handle) } {
|
||
Ok(i) => i,
|
||
Err(e) => return e,
|
||
};
|
||
|
||
let guard = match inst.picker.read() {
|
||
Ok(g) => g,
|
||
Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)),
|
||
};
|
||
|
||
let picker = match guard.as_ref() {
|
||
Some(p) => p,
|
||
None => return FffResult::err("File picker not initialized"),
|
||
};
|
||
|
||
let result = Box::into_raw(Box::new(FffScanProgress::from(picker.get_scan_progress())));
|
||
FffResult::ok_handle(result as *mut c_void)
|
||
}
|
||
|
||
/// Wait for initial scan to complete.
|
||
///
|
||
/// ## Safety
|
||
/// `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_wait_for_scan(
|
||
fff_handle: *mut c_void,
|
||
timeout_ms: u64,
|
||
) -> *mut FffResult {
|
||
let FffInstance { picker, .. } = match unsafe { instance_ref(fff_handle) } {
|
||
Ok(i) => i,
|
||
Err(e) => return e,
|
||
};
|
||
|
||
let completed = picker.wait_for_scan(Duration::from_millis(timeout_ms));
|
||
FffResult::ok_int(completed as i64)
|
||
}
|
||
|
||
/// Wait for the background file watcher to be ready.
|
||
///
|
||
/// ## Safety
|
||
/// `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_wait_for_watcher(
|
||
fff_handle: *mut c_void,
|
||
timeout_ms: u64,
|
||
) -> *mut FffResult {
|
||
let inst = match unsafe { instance_ref(fff_handle) } {
|
||
Ok(i) => i,
|
||
Err(e) => return e,
|
||
};
|
||
|
||
let completed = inst
|
||
.picker
|
||
.wait_for_watcher(Duration::from_millis(timeout_ms));
|
||
FffResult::ok_int(completed as i64)
|
||
}
|
||
|
||
/// Restart indexing in a new directory.
|
||
///
|
||
/// ## Safety
|
||
/// * `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||
/// * `new_path` must be a valid null-terminated UTF-8 string.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_restart_index(
|
||
fff_handle: *mut c_void,
|
||
new_path: *const c_char,
|
||
) -> *mut FffResult {
|
||
let inst = match unsafe { instance_ref(fff_handle) } {
|
||
Ok(i) => i,
|
||
Err(e) => return e,
|
||
};
|
||
|
||
let path_str = match unsafe { cstr_to_str(new_path) } {
|
||
Some(s) => s,
|
||
None => return FffResult::err("Path is null or invalid UTF-8"),
|
||
};
|
||
|
||
let path = PathBuf::from(&path_str);
|
||
if !path.exists() {
|
||
return FffResult::err(&format!("Path does not exist: {}", path_str));
|
||
}
|
||
|
||
let canonical_path = match fff::path_utils::canonicalize(&path) {
|
||
Ok(p) => p,
|
||
Err(e) => return FffResult::err(&format!("Failed to canonicalize path: {}", e)),
|
||
};
|
||
|
||
let guard = match inst.picker.write() {
|
||
Ok(g) => g,
|
||
Err(e) => return FffResult::err(&format!("Failed to acquire file picker lock: {}", e)),
|
||
};
|
||
|
||
let (warmup_caches, content_indexing, watch, mode, fs_root, home_dir, follow_symlinks) =
|
||
if let Some(ref picker) = *guard {
|
||
(
|
||
picker.has_mmap_cache(),
|
||
picker.has_content_indexing(),
|
||
picker.has_watcher(),
|
||
picker.mode(),
|
||
picker.fs_root_scanning_enabled(),
|
||
picker.home_dir_scanning_enabled(),
|
||
picker.follows_symlinks(),
|
||
)
|
||
} else {
|
||
(false, true, true, FFFMode::default(), false, false, false)
|
||
};
|
||
|
||
drop(guard);
|
||
|
||
match FilePicker::new_with_shared_state(
|
||
inst.picker.clone(),
|
||
inst.frecency.clone(),
|
||
fff::FilePickerOptions {
|
||
base_path: canonical_path.to_string_lossy().to_string(),
|
||
enable_mmap_cache: warmup_caches,
|
||
enable_content_indexing: content_indexing,
|
||
watch,
|
||
mode,
|
||
cache_budget: None,
|
||
follow_symlinks,
|
||
enable_fs_root_scanning: fs_root,
|
||
enable_home_dir_scanning: home_dir,
|
||
},
|
||
) {
|
||
Ok(()) => FffResult::ok_empty(),
|
||
Err(e) => FffResult::err(&format!("Failed to init file picker: {}", e)),
|
||
}
|
||
}
|
||
|
||
/// Refresh git status cache.
|
||
///
|
||
/// ## Safety
|
||
/// `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_refresh_git_status(fff_handle: *mut c_void) -> *mut FffResult {
|
||
let inst = match unsafe { instance_ref(fff_handle) } {
|
||
Ok(i) => i,
|
||
Err(e) => return e,
|
||
};
|
||
|
||
match inst.picker.refresh_git_status(&inst.frecency) {
|
||
Ok(count) => FffResult::ok_int(count as i64),
|
||
Err(e) => FffResult::err(&format!("Failed to refresh git status: {}", e)),
|
||
}
|
||
}
|
||
|
||
/// Track query completion for smart suggestions.
|
||
///
|
||
/// ## Safety
|
||
/// * `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||
/// * `query` and `file_path` must be valid null-terminated UTF-8 strings.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_track_query(
|
||
fff_handle: *mut c_void,
|
||
query: *const c_char,
|
||
file_path: *const c_char,
|
||
) -> *mut FffResult {
|
||
let inst = match unsafe { instance_ref(fff_handle) } {
|
||
Ok(i) => i,
|
||
Err(e) => return e,
|
||
};
|
||
|
||
let query_str = match unsafe { cstr_to_str(query) } {
|
||
Some(s) => s,
|
||
None => return FffResult::err("Query is null or invalid UTF-8"),
|
||
};
|
||
|
||
let path_str = match unsafe { cstr_to_str(file_path) } {
|
||
Some(s) => s,
|
||
None => return FffResult::err("File path is null or invalid UTF-8"),
|
||
};
|
||
|
||
let file_path = match fff::path_utils::canonicalize(path_str) {
|
||
Ok(p) => p,
|
||
Err(e) => return FffResult::err(&format!("Failed to canonicalize path: {}", e)),
|
||
};
|
||
|
||
let project_path = {
|
||
let guard = match inst.picker.read() {
|
||
Ok(g) => g,
|
||
Err(_) => return FffResult::ok_int(0),
|
||
};
|
||
match guard.as_ref() {
|
||
Some(p) => p.base_path().to_path_buf(),
|
||
None => return FffResult::ok_int(0),
|
||
}
|
||
};
|
||
|
||
let mut qt_guard = match inst.query_tracker.write() {
|
||
Ok(q) => q,
|
||
Err(_) => return FffResult::ok_int(0),
|
||
};
|
||
|
||
if let Some(ref mut tracker) = *qt_guard
|
||
&& let Err(e) = tracker.track_query_completion(query_str, &project_path, &file_path)
|
||
{
|
||
return FffResult::err(&format!("Failed to track query: {}", e));
|
||
}
|
||
|
||
FffResult::ok_int(1)
|
||
}
|
||
|
||
/// Get historical query by offset (0 = most recent).
|
||
///
|
||
/// ## Safety
|
||
/// `fff_handle` must be a valid instance pointer from `fff_create_instance`.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_get_historical_query(
|
||
fff_handle: *mut c_void,
|
||
offset: u64,
|
||
) -> *mut FffResult {
|
||
let inst = match unsafe { instance_ref(fff_handle) } {
|
||
Ok(i) => i,
|
||
Err(e) => return e,
|
||
};
|
||
|
||
let project_path = {
|
||
let guard = match inst.picker.read() {
|
||
Ok(g) => g,
|
||
Err(_) => return FffResult::ok_empty(),
|
||
};
|
||
match guard.as_ref() {
|
||
Some(p) => p.base_path().to_path_buf(),
|
||
None => return FffResult::ok_empty(),
|
||
}
|
||
};
|
||
|
||
let qt_guard = match inst.query_tracker.read() {
|
||
Ok(q) => q,
|
||
Err(_) => return FffResult::ok_empty(),
|
||
};
|
||
|
||
let tracker = match qt_guard.as_ref() {
|
||
Some(t) => t,
|
||
None => return FffResult::ok_empty(),
|
||
};
|
||
|
||
match tracker.get_historical_query(&project_path, offset as usize) {
|
||
Ok(Some(query)) => FffResult::ok_string(&query),
|
||
Ok(None) => FffResult::ok_empty(),
|
||
Err(e) => FffResult::err(&format!("Failed to get historical query: {}", e)),
|
||
}
|
||
}
|
||
|
||
/// Get health check information.
|
||
///
|
||
/// ## Safety
|
||
/// * `fff_handle` must be a valid instance pointer from `fff_create_instance`, or null for
|
||
/// a limited health check (version + git only).
|
||
/// * `test_path` can be null or a valid null-terminated UTF-8 string.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_health_check(
|
||
fff_handle: *mut c_void,
|
||
test_path: *const c_char,
|
||
) -> *mut FffResult {
|
||
let test_path = unsafe { optional_cstr(test_path) }
|
||
.map(PathBuf::from)
|
||
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
|
||
|
||
let mut health = serde_json::Map::new();
|
||
health.insert(
|
||
"version".to_string(),
|
||
serde_json::Value::String(env!("CARGO_PKG_VERSION").to_string()),
|
||
);
|
||
|
||
// Git info
|
||
let mut git_info = serde_json::Map::new();
|
||
let git_version = git2::Version::get();
|
||
let (major, minor, rev) = git_version.libgit2_version();
|
||
git_info.insert(
|
||
"libgit2_version".to_string(),
|
||
serde_json::Value::String(format!("{}.{}.{}", major, minor, rev)),
|
||
);
|
||
|
||
match git2::Repository::discover(&test_path) {
|
||
Ok(repo) => {
|
||
git_info.insert("available".to_string(), serde_json::Value::Bool(true));
|
||
git_info.insert(
|
||
"repository_found".to_string(),
|
||
serde_json::Value::Bool(true),
|
||
);
|
||
if let Some(workdir) = repo.workdir() {
|
||
git_info.insert(
|
||
"workdir".to_string(),
|
||
serde_json::Value::String(workdir.to_string_lossy().to_string()),
|
||
);
|
||
}
|
||
}
|
||
Err(e) => {
|
||
git_info.insert("available".to_string(), serde_json::Value::Bool(true));
|
||
git_info.insert(
|
||
"repository_found".to_string(),
|
||
serde_json::Value::Bool(false),
|
||
);
|
||
git_info.insert(
|
||
"error".to_string(),
|
||
serde_json::Value::String(e.message().to_string()),
|
||
);
|
||
}
|
||
}
|
||
health.insert("git".to_string(), serde_json::Value::Object(git_info));
|
||
|
||
let inst: Option<&FffInstance> = if fff_handle.is_null() {
|
||
None
|
||
} else {
|
||
Some(unsafe { &*(fff_handle as *const FffInstance) })
|
||
};
|
||
|
||
// File picker info
|
||
let mut picker_info = serde_json::Map::new();
|
||
if let Some(inst) = inst {
|
||
match inst.picker.read() {
|
||
Ok(guard) => {
|
||
if let Some(ref picker) = *guard {
|
||
picker_info.insert("initialized".to_string(), serde_json::Value::Bool(true));
|
||
picker_info.insert(
|
||
"base_path".to_string(),
|
||
serde_json::Value::String(picker.base_path().to_string_lossy().to_string()),
|
||
);
|
||
picker_info.insert(
|
||
"is_scanning".to_string(),
|
||
serde_json::Value::Bool(picker.is_scan_active()),
|
||
);
|
||
let progress = picker.get_scan_progress();
|
||
picker_info.insert(
|
||
"indexed_files".to_string(),
|
||
serde_json::Value::Number(progress.scanned_files_count.into()),
|
||
);
|
||
} else {
|
||
picker_info.insert("initialized".to_string(), serde_json::Value::Bool(false));
|
||
}
|
||
}
|
||
Err(_) => {
|
||
picker_info.insert("initialized".to_string(), serde_json::Value::Bool(false));
|
||
picker_info.insert(
|
||
"error".to_string(),
|
||
serde_json::Value::String("Failed to acquire lock".to_string()),
|
||
);
|
||
}
|
||
}
|
||
} else {
|
||
picker_info.insert("initialized".to_string(), serde_json::Value::Bool(false));
|
||
}
|
||
health.insert(
|
||
"file_picker".to_string(),
|
||
serde_json::Value::Object(picker_info),
|
||
);
|
||
|
||
// Frecency info
|
||
let mut frecency_info = serde_json::Map::new();
|
||
if let Some(inst) = inst {
|
||
match inst.frecency.read() {
|
||
Ok(guard) => {
|
||
frecency_info.insert(
|
||
"initialized".to_string(),
|
||
serde_json::Value::Bool(guard.is_some()),
|
||
);
|
||
if let Some(ref frecency) = *guard
|
||
&& let Ok(health_data) = frecency.get_health()
|
||
{
|
||
let mut db_health = serde_json::Map::new();
|
||
db_health.insert(
|
||
"path".to_string(),
|
||
serde_json::Value::String(health_data.path),
|
||
);
|
||
db_health.insert(
|
||
"disk_size".to_string(),
|
||
serde_json::Value::Number(health_data.disk_size.into()),
|
||
);
|
||
frecency_info.insert(
|
||
"db_healthcheck".to_string(),
|
||
serde_json::Value::Object(db_health),
|
||
);
|
||
}
|
||
}
|
||
Err(_) => {
|
||
frecency_info.insert("initialized".to_string(), serde_json::Value::Bool(false));
|
||
}
|
||
}
|
||
} else {
|
||
frecency_info.insert("initialized".to_string(), serde_json::Value::Bool(false));
|
||
}
|
||
health.insert(
|
||
"frecency".to_string(),
|
||
serde_json::Value::Object(frecency_info),
|
||
);
|
||
|
||
// Query tracker info
|
||
let mut query_info = serde_json::Map::new();
|
||
if let Some(inst) = inst {
|
||
match inst.query_tracker.read() {
|
||
Ok(guard) => {
|
||
query_info.insert(
|
||
"initialized".to_string(),
|
||
serde_json::Value::Bool(guard.is_some()),
|
||
);
|
||
if let Some(ref tracker) = *guard
|
||
&& let Ok(health_data) = tracker.get_health()
|
||
{
|
||
let mut db_health = serde_json::Map::new();
|
||
db_health.insert(
|
||
"path".to_string(),
|
||
serde_json::Value::String(health_data.path),
|
||
);
|
||
db_health.insert(
|
||
"disk_size".to_string(),
|
||
serde_json::Value::Number(health_data.disk_size.into()),
|
||
);
|
||
query_info.insert(
|
||
"db_healthcheck".to_string(),
|
||
serde_json::Value::Object(db_health),
|
||
);
|
||
}
|
||
}
|
||
Err(_) => {
|
||
query_info.insert("initialized".to_string(), serde_json::Value::Bool(false));
|
||
}
|
||
}
|
||
} else {
|
||
query_info.insert("initialized".to_string(), serde_json::Value::Bool(false));
|
||
}
|
||
health.insert(
|
||
"query_tracker".to_string(),
|
||
serde_json::Value::Object(query_info),
|
||
);
|
||
|
||
match serde_json::to_string(&health) {
|
||
Ok(json) => FffResult::ok_string(&json),
|
||
Err(e) => FffResult::err(&format!("Failed to serialize health check: {}", e)),
|
||
}
|
||
}
|
||
|
||
/// Free a search result returned by `fff_search`.
|
||
///
|
||
/// This frees the `FffSearchResult` struct, its `items` and `scores` arrays,
|
||
/// and all heap-allocated strings within each item and score.
|
||
///
|
||
/// ## Safety
|
||
/// `result` must be a valid pointer previously returned via `FffResult.handle`
|
||
/// from `fff_search`, or null (no-op).
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_free_search_result(result: *mut FffSearchResult) {
|
||
if result.is_null() {
|
||
return;
|
||
}
|
||
|
||
unsafe {
|
||
let result = Box::from_raw(result);
|
||
let count = result.count as usize;
|
||
|
||
if !result.items.is_null() {
|
||
let mut items = Vec::from_raw_parts(result.items, count, count);
|
||
for item in &mut items {
|
||
item.free_strings();
|
||
}
|
||
}
|
||
if !result.scores.is_null() {
|
||
let mut scores = Vec::from_raw_parts(result.scores, count, count);
|
||
for score in &mut scores {
|
||
score.free_strings();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Get a pointer to the `index`-th `FffFileItem` in a search result.
|
||
///
|
||
/// Returns null if `result` is null or `index >= result->count`.
|
||
/// The returned pointer is valid until the search result is freed.
|
||
///
|
||
/// ## Safety
|
||
/// `result` must be a valid `FffSearchResult` pointer from `fff_search`.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_search_result_get_item(
|
||
result: *const FffSearchResult,
|
||
index: u32,
|
||
) -> *const FffFileItem {
|
||
if result.is_null() {
|
||
return std::ptr::null();
|
||
}
|
||
let result = unsafe { &*result };
|
||
if index >= result.count || result.items.is_null() {
|
||
return std::ptr::null();
|
||
}
|
||
unsafe { result.items.add(index as usize) }
|
||
}
|
||
|
||
/// Get a pointer to the `index`-th `FffScore` in a search result.
|
||
///
|
||
/// Returns null if `result` is null or `index >= result->count`.
|
||
/// The returned pointer is valid until the search result is freed.
|
||
///
|
||
/// ## Safety
|
||
/// `result` must be a valid `FffSearchResult` pointer from `fff_search`.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_search_result_get_score(
|
||
result: *const FffSearchResult,
|
||
index: u32,
|
||
) -> *const FffScore {
|
||
if result.is_null() {
|
||
return std::ptr::null();
|
||
}
|
||
let result = unsafe { &*result };
|
||
if index >= result.count || result.scores.is_null() {
|
||
return std::ptr::null();
|
||
}
|
||
unsafe { result.scores.add(index as usize) }
|
||
}
|
||
|
||
/// Free a grep result returned by `fff_live_grep` or `fff_multi_grep`.
|
||
///
|
||
/// This frees the `FffGrepResult` struct, its `items` array, and all
|
||
/// heap-allocated strings, match ranges, and context arrays within each match.
|
||
///
|
||
/// ## Safety
|
||
/// `result` must be a valid pointer previously returned via `FffResult.handle`
|
||
/// from `fff_live_grep` or `fff_multi_grep`, or null (no-op).
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_free_grep_result(result: *mut FffGrepResult) {
|
||
if result.is_null() {
|
||
return;
|
||
}
|
||
|
||
unsafe {
|
||
let result = Box::from_raw(result);
|
||
let count = result.count as usize;
|
||
|
||
if !result.items.is_null() {
|
||
let mut items = Vec::from_raw_parts(result.items, count, count);
|
||
for item in &mut items {
|
||
item.free_fields();
|
||
}
|
||
}
|
||
if !result.regex_fallback_error.is_null() {
|
||
drop(CString::from_raw(result.regex_fallback_error));
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Get a pointer to the `index`-th `FffGrepMatch` in a grep result.
|
||
///
|
||
/// Returns null if `result` is null or `index >= result->count`.
|
||
/// The returned pointer is valid until the grep result is freed.
|
||
///
|
||
/// ## Safety
|
||
/// `result` must be a valid `FffGrepResult` pointer from `fff_live_grep` or `fff_multi_grep`.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_grep_result_get_match(
|
||
result: *const FffGrepResult,
|
||
index: u32,
|
||
) -> *const FffGrepMatch {
|
||
if result.is_null() {
|
||
return std::ptr::null();
|
||
}
|
||
let result = unsafe { &*result };
|
||
if index >= result.count || result.items.is_null() {
|
||
return std::ptr::null();
|
||
}
|
||
unsafe { result.items.add(index as usize) }
|
||
}
|
||
|
||
/// Free a scan progress result returned by `fff_get_scan_progress`.
|
||
///
|
||
/// ## Safety
|
||
/// `result` must be a valid pointer previously returned via `FffResult.handle`
|
||
/// from `fff_get_scan_progress`, or null (no-op).
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_free_scan_progress(result: *mut FffScanProgress) {
|
||
if !result.is_null() {
|
||
unsafe { drop(Box::from_raw(result)) };
|
||
}
|
||
}
|
||
|
||
/// Offset a pointer by `byte_offset` bytes.
|
||
///
|
||
/// General-purpose utility for FFI consumers that need pointer arithmetic
|
||
/// (e.g. iterating over arrays). Returns null if `base` is null.
|
||
///
|
||
/// ## Safety
|
||
/// The resulting pointer must be within the bounds of the original allocation.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_ptr_offset(base: *const c_void, byte_offset: usize) -> *const c_void {
|
||
if base.is_null() {
|
||
return std::ptr::null();
|
||
}
|
||
unsafe { (base as *const u8).add(byte_offset) as *const c_void }
|
||
}
|
||
|
||
/// Free a result returned by any `fff_*` function.
|
||
/// **IMPORTANT:** this doesn't clean the the internal handle, so it is safe to call right after
|
||
/// you handle the error case.
|
||
///
|
||
/// Note: Many non-libffi implementations are not supporting struct-by-value returns, so it's more
|
||
/// convenient to have pointer returned at most of the time, though allocating result for every call
|
||
/// is annoying, so we just rely on the fact that our allocator is good enough.
|
||
///
|
||
/// ## Safety
|
||
/// `result_ptr` must be a valid pointer returned by a `fff_*` function.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_free_result(result_ptr: *mut FffResult) {
|
||
if result_ptr.is_null() {
|
||
return;
|
||
}
|
||
|
||
unsafe {
|
||
let result = Box::from_raw(result_ptr);
|
||
if !result.error.is_null() {
|
||
drop(CString::from_raw(result.error));
|
||
}
|
||
// Note: `handle` is NOT freed here — the caller must free it
|
||
// with the appropriate function (fff_destroy, fff_free_search_result,
|
||
// fff_free_grep_result, fff_free_string, fff_free_scan_progress, etc.).
|
||
}
|
||
}
|
||
|
||
/// Free a string returned by `fff_*` functions.
|
||
///
|
||
/// ## Safety
|
||
/// `s` must be a valid C string allocated by this library.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_free_string(s: *mut c_char) {
|
||
unsafe {
|
||
if !s.is_null() {
|
||
drop(CString::from_raw(s));
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Directory search: free and accessor functions
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Free a directory search result returned by `fff_search_directories`.
|
||
///
|
||
/// ## Safety
|
||
/// `result` must be a valid pointer previously returned via `FffResult.handle`
|
||
/// from `fff_search_directories`, or null (no-op).
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_free_dir_search_result(result: *mut FffDirSearchResult) {
|
||
if result.is_null() {
|
||
return;
|
||
}
|
||
|
||
unsafe {
|
||
let result = Box::from_raw(result);
|
||
let count = result.count as usize;
|
||
|
||
if !result.items.is_null() {
|
||
let mut items = Vec::from_raw_parts(result.items, count, count);
|
||
for item in &mut items {
|
||
item.free_strings();
|
||
}
|
||
}
|
||
if !result.scores.is_null() {
|
||
let mut scores = Vec::from_raw_parts(result.scores, count, count);
|
||
for score in &mut scores {
|
||
score.free_strings();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Get a pointer to the `index`-th `FffDirItem` in a directory search result.
|
||
///
|
||
/// ## Safety
|
||
/// `result` must be a valid `FffDirSearchResult` pointer from `fff_search_directories`.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_dir_search_result_get_item(
|
||
result: *const FffDirSearchResult,
|
||
index: u32,
|
||
) -> *const FffDirItem {
|
||
if result.is_null() {
|
||
return std::ptr::null();
|
||
}
|
||
let result = unsafe { &*result };
|
||
if index >= result.count || result.items.is_null() {
|
||
return std::ptr::null();
|
||
}
|
||
unsafe { result.items.add(index as usize) }
|
||
}
|
||
|
||
/// Get a pointer to the `index`-th `FffScore` in a directory search result.
|
||
///
|
||
/// ## Safety
|
||
/// `result` must be a valid `FffDirSearchResult` pointer from `fff_search_directories`.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_dir_search_result_get_score(
|
||
result: *const FffDirSearchResult,
|
||
index: u32,
|
||
) -> *const FffScore {
|
||
if result.is_null() {
|
||
return std::ptr::null();
|
||
}
|
||
let result = unsafe { &*result };
|
||
if index >= result.count || result.scores.is_null() {
|
||
return std::ptr::null();
|
||
}
|
||
unsafe { result.scores.add(index as usize) }
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Mixed search: free and accessor functions
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Free a mixed search result returned by `fff_search_mixed`.
|
||
///
|
||
/// ## Safety
|
||
/// `result` must be a valid pointer previously returned via `FffResult.handle`
|
||
/// from `fff_search_mixed`, or null (no-op).
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_free_mixed_search_result(result: *mut FffMixedSearchResult) {
|
||
if result.is_null() {
|
||
return;
|
||
}
|
||
|
||
unsafe {
|
||
let result = Box::from_raw(result);
|
||
let count = result.count as usize;
|
||
|
||
if !result.items.is_null() {
|
||
let mut items = Vec::from_raw_parts(result.items, count, count);
|
||
for item in &mut items {
|
||
item.free_strings();
|
||
}
|
||
}
|
||
if !result.scores.is_null() {
|
||
let mut scores = Vec::from_raw_parts(result.scores, count, count);
|
||
for score in &mut scores {
|
||
score.free_strings();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Get a pointer to the `index`-th `FffMixedItem` in a mixed search result.
|
||
///
|
||
/// ## Safety
|
||
/// `result` must be a valid `FffMixedSearchResult` pointer from `fff_search_mixed`.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_mixed_search_result_get_item(
|
||
result: *const FffMixedSearchResult,
|
||
index: u32,
|
||
) -> *const FffMixedItem {
|
||
if result.is_null() {
|
||
return std::ptr::null();
|
||
}
|
||
let result = unsafe { &*result };
|
||
if index >= result.count || result.items.is_null() {
|
||
return std::ptr::null();
|
||
}
|
||
unsafe { result.items.add(index as usize) }
|
||
}
|
||
|
||
/// Get a pointer to the `index`-th `FffScore` in a mixed search result.
|
||
///
|
||
/// ## Safety
|
||
/// `result` must be a valid `FffMixedSearchResult` pointer from `fff_search_mixed`.
|
||
#[unsafe(no_mangle)]
|
||
pub unsafe extern "C" fn fff_mixed_search_result_get_score(
|
||
result: *const FffMixedSearchResult,
|
||
index: u32,
|
||
) -> *const FffScore {
|
||
if result.is_null() {
|
||
return std::ptr::null();
|
||
}
|
||
let result = unsafe { &*result };
|
||
if index >= result.count || result.scores.is_null() {
|
||
return std::ptr::null();
|
||
}
|
||
unsafe { result.scores.add(index as usize) }
|
||
}
|