Compare commits

...

1 Commits

Author SHA1 Message Date
Dmitriy Kovalenko 8b32971cb7 feat: Cancellation signal for live grep
docs / docs (push) Has been cancelled
2026-04-01 14:59:07 -07:00
23 changed files with 817 additions and 641 deletions
+38 -49
View File
@@ -24,9 +24,10 @@
use std::ffi::{CStr, CString, c_char, c_void};
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use fff::shared::SharedQueryTracker;
mod ffi_types;
use fff::file_picker::FilePicker;
@@ -45,7 +46,7 @@ use ffi_types::{
struct FffInstance {
picker: SharedPicker,
frecency: SharedFrecency,
query_tracker: Arc<RwLock<Option<QueryTracker>>>,
query_tracker: SharedQueryTracker,
}
/// Helper to convert C string to Rust &str.
@@ -135,9 +136,9 @@ pub unsafe extern "C" fn fff_create_instance(
let history_path = unsafe { optional_cstr(history_db_path) }.map(|s| s.to_string());
// Create shared state that background threads will write into.
let shared_picker: SharedPicker = Arc::new(RwLock::new(None));
let shared_frecency: SharedFrecency = Arc::new(RwLock::new(None));
let query_tracker: Arc<RwLock<Option<QueryTracker>>> = Arc::new(RwLock::new(None));
let shared_picker = SharedPicker::default();
let shared_frecency = SharedFrecency::default();
let query_tracker = SharedQueryTracker::default();
// Initialize frecency tracker if path is provided
if let Some(ref frecency_path) = frecency_path {
@@ -147,19 +148,10 @@ pub unsafe extern "C" fn fff_create_instance(
match FrecencyTracker::new(frecency_path, use_unsafe_no_lock) {
Ok(tracker) => {
let mut guard = match shared_frecency.write() {
Ok(g) => g,
Err(e) => {
return FffResult::err(&format!("Failed to acquire frecency lock: {}", e));
}
};
*guard = Some(tracker);
drop(guard);
let _ = FrecencyTracker::spawn_gc(
Arc::clone(&shared_frecency),
frecency_path.clone(),
use_unsafe_no_lock,
);
if let Err(e) = shared_frecency.init(tracker) {
return FffResult::err(&format!("Failed to acquire frecency lock: {}", e));
}
let _ = shared_frecency.spawn_gc(frecency_path.clone(), use_unsafe_no_lock);
}
Err(e) => return FffResult::err(&format!("Failed to init frecency db: {}", e)),
}
@@ -173,16 +165,9 @@ pub unsafe extern "C" fn fff_create_instance(
match QueryTracker::new(history_path, use_unsafe_no_lock) {
Ok(tracker) => {
let mut guard = match query_tracker.write() {
Ok(g) => g,
Err(e) => {
return FffResult::err(&format!(
"Failed to acquire query tracker lock: {}",
e
));
}
};
*guard = Some(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)),
}
@@ -196,11 +181,15 @@ pub unsafe extern "C" fn fff_create_instance(
// Initialize file picker (writes directly into shared_picker)
if let Err(e) = FilePicker::new_with_shared_state(
base_path_str,
warmup_mmap_cache,
mode,
Arc::clone(&shared_picker),
Arc::clone(&shared_frecency),
shared_picker.clone(),
shared_frecency.clone(),
fff::FilePickerOptions {
base_path: base_path_str,
warmup_mmap_cache,
mode,
cache_budget: None,
..Default::default()
},
) {
return FffResult::err(&format!("Failed to init file picker: {}", e));
}
@@ -403,14 +392,7 @@ pub unsafe extern "C" fn fff_live_grep(
classify_definitions,
};
let result = fff::grep::grep_search(
picker.get_files(),
&parsed,
&options,
picker.cache_budget(),
None,
None,
);
let result = picker.grep(&parsed, &options);
let grep_result = FffGrepResult::from_core(&result);
FffResult::ok_handle(grep_result as *mut c_void)
}
@@ -517,6 +499,7 @@ pub unsafe extern "C" fn fff_multi_grep(
constraint_refs,
&options,
picker.cache_budget(),
None,
);
let grep_result = FffGrepResult::from_core(&result);
FffResult::ok_handle(grep_result as *mut c_void)
@@ -606,7 +589,7 @@ pub unsafe extern "C" fn fff_wait_for_scan(
Err(e) => return e,
};
let completed = FilePicker::wait_for_scan(picker, Duration::from_millis(timeout_ms));
let completed = picker.wait_for_scan(Duration::from_millis(timeout_ms));
FffResult::ok_int(completed as i64)
}
@@ -624,7 +607,9 @@ pub unsafe extern "C" fn fff_wait_for_watcher(
Err(e) => return e,
};
let completed = FilePicker::wait_for_watcher(&inst.picker, Duration::from_millis(timeout_ms));
let completed = inst
.picker
.wait_for_watcher(Duration::from_millis(timeout_ms));
FffResult::ok_int(completed as i64)
}
@@ -675,11 +660,15 @@ pub unsafe extern "C" fn fff_restart_index(
drop(guard);
match FilePicker::new_with_shared_state(
canonical_path.to_string_lossy().to_string(),
warmup_caches,
mode,
Arc::clone(&inst.picker),
Arc::clone(&inst.frecency),
inst.picker.clone(),
inst.frecency.clone(),
fff::FilePickerOptions {
base_path: canonical_path.to_string_lossy().to_string(),
warmup_mmap_cache: warmup_caches,
mode,
cache_budget: None,
..Default::default()
},
) {
Ok(()) => FffResult::ok_empty(),
Err(e) => FffResult::err(&format!("Failed to init file picker: {}", e)),
@@ -697,7 +686,7 @@ pub unsafe extern "C" fn fff_refresh_git_status(fff_handle: *mut c_void) -> *mut
Err(e) => return e,
};
match FilePicker::refresh_git_status(&inst.picker, &inst.frecency) {
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)),
}
+2 -2
View File
@@ -1,8 +1,8 @@
use crate::error::Error;
use crate::file_picker::{FFFMode, FilePicker};
use crate::git::GitStatusCache;
use crate::shared::{SharedFrecency, SharedPicker};
use crate::sort_buffer::sort_with_buffer;
use crate::{SharedFrecency, SharedPicker};
use git2::Repository;
use notify::event::{AccessKind, AccessMode};
use notify::{Config, EventKind, RecursiveMode};
@@ -416,7 +416,7 @@ fn handle_debounced_events(
if need_full_git_rescan {
info!("Triggering full git rescan");
let result = FilePicker::refresh_git_status(shared_picker, shared_frecency);
let result = shared_picker.refresh_git_status(shared_frecency);
if let Err(e) = result {
error!("Failed to refresh git status: {:?}", e);
}
+233 -186
View File
@@ -37,11 +37,11 @@ use crate::git::GitStatusCache;
use crate::grep::{GrepResult, GrepSearchOptions, grep_search};
use crate::query_tracker::QueryTracker;
use crate::score::match_and_score_files;
use crate::shared::{SharedFrecency, SharedPicker};
use crate::types::{
BigramFilter, BigramIndexBuilder, BigramOverlay, ContentCacheBudget, FileItem, PaginationArgs,
ScoringContext, SearchResult,
};
use crate::{SharedFrecency, SharedPicker};
use fff_query_parser::FFFQuery;
use git2::{Repository, Status, StatusOptions};
use rayon::prelude::*;
@@ -51,7 +51,7 @@ use std::sync::{
Arc,
atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
};
use std::time::{Duration, SystemTime};
use std::time::SystemTime;
use tracing::{Level, debug, error, info, warn};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -258,27 +258,46 @@ impl FileItem {
}
}
/// The main file picker engine storage
///
/// It maintains an in memory index of all the files that are resent in the file system
/// and borrows them to perform the search
/// Options for creating a [`FilePicker`].
pub struct FilePickerOptions {
pub base_path: String,
pub warmup_mmap_cache: bool,
pub mode: FFFMode,
/// Explicit cache budget. When `None`, the budget is auto-computed from
/// the repo size after the initial scan completes.
pub cache_budget: Option<ContentCacheBudget>,
/// When `false`, `new_with_shared_state` skips the background file watcher.
/// Files are still scanned, warmed up, and bigram-indexed.
pub watch: bool,
}
impl Default for FilePickerOptions {
fn default() -> Self {
Self {
base_path: ".".into(),
warmup_mmap_cache: false,
mode: FFFMode::default(),
cache_budget: None,
watch: true,
}
}
}
pub struct FilePicker {
base_path: PathBuf,
pub mode: FFFMode,
pub base_path: PathBuf,
pub is_scanning: Arc<AtomicBool>,
sync_data: FileSync,
is_scanning: Arc<AtomicBool>,
cache_budget: Arc<ContentCacheBudget>,
has_explicit_cache_budget: bool,
watcher_ready: Arc<AtomicBool>,
scanned_files_count: Arc<AtomicUsize>,
background_watcher: Option<BackgroundWatcher>,
warmup_mmap_cache: bool,
watch: bool,
cancelled: Arc<AtomicBool>,
mode: FFFMode,
pub cache_budget: Arc<ContentCacheBudget>,
/// Inverted bigram index for O(K × N/64) grep prefiltering.
/// Built during warmup phase; `None` until warmup completes.
pub bigram_index: Option<Arc<BigramFilter>>,
/// Incremental overlay tracking file changes since the base bigram index
/// was built. Updated by the background watcher on every file event.
pub bigram_overlay: Option<Arc<parking_lot::RwLock<BigramOverlay>>>,
bigram_index: Option<Arc<BigramFilter>>,
bigram_overlay: Option<Arc<parking_lot::RwLock<BigramOverlay>>>,
}
impl std::fmt::Debug for FilePicker {
@@ -312,6 +331,23 @@ impl FilePicker {
&self.cache_budget
}
pub fn bigram_index(&self) -> Option<&BigramFilter> {
self.bigram_index.as_deref()
}
pub fn bigram_overlay(&self) -> Option<&parking_lot::RwLock<BigramOverlay>> {
self.bigram_overlay.as_deref()
}
pub fn get_file_mut(&mut self, index: usize) -> Option<&mut FileItem> {
self.sync_data.get_file_mut(index)
}
pub fn set_bigram_index(&mut self, index: BigramFilter, overlay: BigramOverlay) {
self.bigram_index = Some(Arc::new(index));
self.bigram_overlay = Some(Arc::new(parking_lot::RwLock::new(overlay)));
}
pub fn git_root(&self) -> Option<&Path> {
self.sync_data.git_workdir.as_deref()
}
@@ -327,66 +363,77 @@ impl FilePicker {
self.sync_data.overflow_files()
}
/// Create a new FilePicker and place it into the provided shared handle.
///
/// The background scan thread and file-system watcher write into the
/// provided `SharedPicker` and read frecency data from the provided
/// `SharedFrecency`.
///
/// Multiple independent instances can coexist in the same process.
pub fn new_with_shared_state(
base_path: String,
warmup_mmap_cache: bool,
mode: FFFMode,
shared_picker: SharedPicker,
shared_frecency: SharedFrecency,
) -> Result<(), Error> {
info!(
"Initializing FilePicker with base_path: {}, warmup: {}, mode: {:?}",
base_path, warmup_mmap_cache, mode
);
let path = PathBuf::from(&base_path);
/// Create a new FilePicker from options.
/// Always prefer new_with_shared_state for the consumer application, use this only if you know
/// what you are doing. This won't spawn the backgraound watcher and won't walk the file tree.
pub fn new(options: FilePickerOptions) -> Result<Self, Error> {
let path = PathBuf::from(&options.base_path);
if !path.exists() {
error!("Base path does not exist: {}", base_path);
error!("Base path does not exist: {}", options.base_path);
return Err(Error::InvalidPath(path));
}
// Initialize scan_signal to `true` so that any `wait_for_scan` call
// that races with the background thread sees "scanning in progress"
// rather than a stale `false` (the thread hasn't started yet).
let scan_signal = Arc::new(AtomicBool::new(true));
let watcher_ready = Arc::new(AtomicBool::new(false));
let synced_files_count = Arc::new(AtomicUsize::new(0));
let cancelled = Arc::new(AtomicBool::new(false));
let has_explicit_budget = options.cache_budget.is_some();
let initial_budget = options.cache_budget.unwrap_or_default();
let picker = FilePicker {
base_path: path.clone(),
Ok(FilePicker {
base_path: path,
sync_data: FileSync::new(),
is_scanning: Arc::clone(&scan_signal),
watcher_ready: Arc::clone(&watcher_ready),
scanned_files_count: Arc::clone(&synced_files_count),
is_scanning: Arc::new(AtomicBool::new(false)),
watcher_ready: Arc::new(AtomicBool::new(false)),
scanned_files_count: Arc::new(AtomicUsize::new(0)),
background_watcher: None,
warmup_mmap_cache,
cancelled: Arc::clone(&cancelled),
mode,
cache_budget: Arc::new(ContentCacheBudget::default()),
warmup_mmap_cache: options.warmup_mmap_cache,
watch: options.watch,
cancelled: Arc::new(AtomicBool::new(false)),
mode: options.mode,
cache_budget: Arc::new(initial_budget),
bigram_index: None,
bigram_overlay: None,
};
has_explicit_cache_budget: has_explicit_budget,
})
}
/// Create a picker, place it into the shared handle, and spawn background
/// indexing + file-system watcher. This is the default entry point.
pub fn new_with_shared_state(
shared_picker: SharedPicker,
shared_frecency: SharedFrecency,
options: FilePickerOptions,
) -> Result<(), Error> {
let picker = Self::new(options)?;
info!(
"Spawning background threads: base_path={}, warmup={}, mode={:?}",
picker.base_path.display(),
picker.warmup_mmap_cache,
picker.mode,
);
let warmup = picker.warmup_mmap_cache;
let watch = picker.watch;
let mode = picker.mode;
picker.is_scanning.store(true, Ordering::Release);
let scan_signal = Arc::clone(&picker.is_scanning);
let watcher_ready = Arc::clone(&picker.watcher_ready);
let synced_files_count = Arc::clone(&picker.scanned_files_count);
let cancelled = Arc::clone(&picker.cancelled);
let path = picker.base_path.clone();
// Place the picker into the shared handle before spawning the
// background thread so the thread can find it immediately.
{
let mut guard = shared_picker.write().map_err(|_| Error::AcquireItemLock)?;
let mut guard = shared_picker.write()?;
*guard = Some(picker);
}
spawn_scan_and_watcher(
path.clone(),
Arc::clone(&scan_signal),
Arc::clone(&watcher_ready),
Arc::clone(&synced_files_count),
warmup_mmap_cache,
path,
scan_signal,
watcher_ready,
synced_files_count,
warmup,
watch,
mode,
shared_picker,
shared_frecency,
@@ -396,6 +443,71 @@ impl FilePicker {
Ok(())
}
/// Synchronous filesystem scan — populates `self` with indexed files.
///
/// Use this when you need direct access to the picker without shared state:
/// ```ignore
/// let mut picker = FilePicker::new(options)?;
/// picker.collect_files()?;
/// // picker.get_files() is now populated
/// ```
pub fn collect_files(&mut self) -> Result<(), Error> {
self.is_scanning.store(true, Ordering::Relaxed);
self.scanned_files_count.store(0, Ordering::Relaxed);
let empty_frecency = SharedFrecency::default();
let walk = walk_filesystem(
&self.base_path,
&self.scanned_files_count,
&empty_frecency,
self.mode,
)?;
self.sync_data = walk.sync;
// Recalculate cache budget based on actual file count (unless
// the caller provided an explicit budget via FilePickerOptions).
if !self.has_explicit_cache_budget {
let file_count = self.sync_data.files().len();
self.cache_budget = Arc::new(ContentCacheBudget::new_for_repo(file_count));
} else {
self.cache_budget.reset();
}
// Apply git status synchronously.
if let Ok(Some(git_cache)) = walk.git_handle.join() {
for file in self.sync_data.files.iter_mut() {
file.git_status = git_cache.lookup_status(&file.path);
}
}
self.is_scanning.store(false, Ordering::Relaxed);
Ok(())
}
/// Start the background file-system watcher.
///
/// The picker must already be placed into `shared_picker` (the watcher
/// needs the shared handle to apply live updates). Call after
/// [`collect_files`](Self::collect_files) or after an initial scan.
pub fn spawn_background_watcher(
&mut self,
shared_picker: &SharedPicker,
shared_frecency: &SharedFrecency,
) -> Result<(), Error> {
let git_workdir = self.sync_data.git_workdir.clone();
let watcher = BackgroundWatcher::new(
self.base_path.clone(),
git_workdir,
shared_picker.clone(),
shared_frecency.clone(),
self.mode,
)?;
self.background_watcher = Some(watcher);
self.watcher_ready.store(true, Ordering::Release);
Ok(())
}
/// Perform fuzzy search on files with a pre-parsed query.
///
/// The query should be parsed using [`FFFQuery`]::parse() before calling
@@ -486,13 +598,35 @@ impl FilePicker {
}
/// Perform a live grep search across indexed files with a pre-parsed query.
pub fn grep<'a>(
files: &'a [FileItem],
pub fn grep(&self, query: &FFFQuery<'_>, options: &GrepSearchOptions) -> GrepResult<'_> {
let overlay_guard = self.bigram_overlay.as_ref().map(|o| o.read());
grep_search(
self.get_files(),
query,
options,
self.cache_budget(),
self.bigram_index.as_deref(),
overlay_guard.as_deref(),
Some(&self.cancelled),
)
}
/// Like [`grep`](Self::grep) but ignores the bigram overlay.
/// Useful for testing that the overlay is actually contributing results.
pub fn grep_without_overlay(
&self,
query: &FFFQuery<'_>,
options: &GrepSearchOptions,
budget: &ContentCacheBudget,
) -> GrepResult<'a> {
grep_search(files, query, options, budget, None, None)
) -> GrepResult<'_> {
grep_search(
self.get_files(),
query,
options,
self.cache_budget(),
self.bigram_index.as_deref(),
None,
Some(&self.cancelled),
)
}
// Returns an ongoing or finisshed scan progress
@@ -519,9 +653,7 @@ impl FilePicker {
);
let mode = self.mode;
let frecency = shared_frecency
.read()
.map_err(|_| Error::AcquireFrecencyLock)?;
let frecency = shared_frecency.read()?;
status_cache
.into_iter()
.try_for_each(|(path, status)| -> Result<(), Error> {
@@ -539,46 +671,6 @@ impl FilePicker {
Ok(())
}
/// Refreshes git statuses using the provided shared picker and frecency handles.
pub fn refresh_git_status(
shared_picker: &SharedPicker,
shared_frecency: &SharedFrecency,
) -> Result<usize, Error> {
let git_status = {
let guard = shared_picker.read().map_err(|_| Error::AcquireItemLock)?;
let Some(ref picker) = *guard else {
return Err(Error::FilePickerMissing);
};
debug!(
"Refreshing git statuses for picker: {:?}",
picker.git_root()
);
GitStatusCache::read_git_status(
picker.git_root(),
StatusOptions::new()
.include_untracked(true)
.recurse_untracked_dirs(true)
.include_unmodified(true)
.exclude_submodules(true),
)
};
let mut guard = shared_picker.write().map_err(|_| Error::AcquireItemLock)?;
let picker = guard.as_mut().ok_or(Error::FilePickerMissing)?;
let statuses_count = if let Some(git_status) = git_status {
let count = git_status.statuses_len();
picker.update_git_statuses(git_status, shared_frecency)?;
count
} else {
0
};
Ok(statuses_count)
}
pub fn update_single_file_frecency(
&mut self,
file_path: impl AsRef<Path>,
@@ -800,6 +892,7 @@ impl FilePicker {
shared_frecency,
self.mode,
);
match walk_result {
Ok(walk) => {
info!(
@@ -853,50 +946,6 @@ impl FilePicker {
pub fn watcher_signal(&self) -> Arc<AtomicBool> {
Arc::clone(&self.watcher_ready)
}
/// Block the current thread until the background filesystem scan finishes.
/// Returns `true` if scan completed, `false` on timeout.
/// Use with CAUTION — blocking. Prefer `scan_signal` + async polling.
pub fn wait_for_scan(shared_picker: &SharedPicker, timeout: Duration) -> bool {
let signal = {
let guard = shared_picker.read().expect("shared picker lock poisoned");
match guard.as_ref() {
Some(picker) => picker.scan_signal(),
None => return true,
}
};
let start = std::time::Instant::now();
while signal.load(Ordering::Acquire) {
if start.elapsed() >= timeout {
return false;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
true
}
/// Block the current thread until the background file watcher is ready.
/// Returns `true` if watcher is ready, `false` on timeout.
/// Use with CAUTION — blocking. Prefer `watcher_signal` + async polling.
pub fn wait_for_watcher(shared_picker: &SharedPicker, timeout: Duration) -> bool {
let signal = {
let guard = shared_picker.read().expect("shared picker lock poisoned");
match guard.as_ref() {
Some(picker) => picker.watcher_signal(),
None => return true,
}
};
let start = std::time::Instant::now();
while !signal.load(Ordering::Acquire) {
if start.elapsed() >= timeout {
return false;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
true
}
}
/// A point-in-time snapshot of the file-scanning progress.
@@ -921,6 +970,7 @@ fn spawn_scan_and_watcher(
watcher_ready: Arc<AtomicBool>,
synced_files_count: Arc<AtomicUsize>,
warmup_mmap_cache: bool,
watch: bool,
mode: FFFMode,
shared_picker: SharedPicker,
shared_frecency: SharedFrecency,
@@ -979,41 +1029,37 @@ fn spawn_scan_and_watcher(
}
}
if cancelled.load(Ordering::Acquire) {
info!("Picker was replaced, skipping background watcher creation");
watcher_ready.store(true, Ordering::Release);
return;
}
if watch && !cancelled.load(Ordering::Acquire) {
match BackgroundWatcher::new(
base_path,
git_workdir,
shared_picker.clone(),
shared_frecency.clone(),
mode,
) {
Ok(watcher) => {
info!("Background file watcher initialized successfully");
match BackgroundWatcher::new(
base_path,
git_workdir,
shared_picker.clone(),
shared_frecency.clone(),
mode,
) {
Ok(watcher) => {
info!("Background file watcher initialized successfully");
if cancelled.load(Ordering::Acquire) {
info!("Picker was replaced, dropping orphaned watcher");
drop(watcher);
watcher_ready.store(true, Ordering::Release);
return;
}
let write_result = shared_picker.write().ok().map(|mut guard| {
if let Some(ref mut picker) = *guard {
picker.background_watcher = Some(watcher);
if cancelled.load(Ordering::Acquire) {
info!("Picker was replaced, dropping orphaned watcher");
drop(watcher);
watcher_ready.store(true, Ordering::Release);
return;
}
});
if write_result.is_none() {
error!("Failed to store background watcher in picker");
let write_result = shared_picker.write().ok().map(|mut guard| {
if let Some(ref mut picker) = *guard {
picker.background_watcher = Some(watcher);
}
});
if write_result.is_none() {
error!("Failed to store background watcher in picker");
}
}
Err(e) => {
error!("Failed to initialize background file watcher: {:?}", e);
}
}
Err(e) => {
error!("Failed to initialize background file watcher: {:?}", e);
}
}
@@ -1022,9 +1068,10 @@ fn spawn_scan_and_watcher(
if warmup_mmap_cache && !cancelled.load(Ordering::Acquire) {
let phase_start = std::time::Instant::now();
// Scale cache limits based on repo size.
// Scale cache limits based on repo size (skip if caller provided an explicit budget).
if let Ok(mut guard) = shared_picker.write()
&& let Some(ref mut picker) = *guard
&& !picker.has_explicit_cache_budget
{
let file_count = picker.sync_data.files().len();
picker.cache_budget = Arc::new(ContentCacheBudget::new_for_repo(file_count));
@@ -1108,7 +1155,7 @@ fn spawn_scan_and_watcher(
/// Files beyond the budget are still available via temporary mmaps on first
/// grep access, so correctness is unaffected.
#[tracing::instrument(skip(files), name = "warmup_mmaps", level = Level::DEBUG)]
fn warmup_mmaps(files: &[FileItem], budget: &ContentCacheBudget) {
pub fn warmup_mmaps(files: &[FileItem], budget: &ContentCacheBudget) {
let max_files = budget.max_files;
let max_bytes = budget.max_bytes;
let max_file_size = budget.max_file_size;
+2 -1
View File
@@ -1,7 +1,8 @@
use crate::db_healthcheck::DbHealthChecker;
use crate::error::{Error, Result};
use crate::file_picker::FFFMode;
use crate::{SharedFrecency, git::is_modified_status};
use crate::git::is_modified_status;
use crate::shared::SharedFrecency;
use heed::{Database, Env, EnvOpenOptions};
use heed::{
EnvFlags,
+67 -28
View File
@@ -322,6 +322,16 @@ pub struct GrepSearchOptions {
pub classify_definitions: bool,
}
#[derive(Clone, Copy)]
struct GrepContext<'a, 'b> {
total_files: usize,
filtered_file_count: usize,
budget: &'a ContentCacheBudget,
prefilter: Option<&'a memchr::memmem::Finder<'b>>,
prefilter_case_insensitive: bool,
is_cancelled: Option<&'a AtomicBool>,
}
/// Lightweight wrapper around `regex::bytes::Regex` implementing the
/// `grep_matcher::Matcher` trait required by `grep-searcher`.
///
@@ -863,6 +873,7 @@ pub fn multi_grep_search<'a>(
constraints: &[fff_query_parser::Constraint<'_>],
options: &GrepSearchOptions,
budget: &ContentCacheBudget,
is_cancelled: Option<&AtomicBool>,
) -> GrepResult<'a> {
let total_files = files.len();
@@ -918,11 +929,14 @@ pub fn multi_grep_search<'a>(
perform_grep(
&files_to_search,
options,
total_files,
filtered_file_count,
budget,
None, // no memmem prefilter for multi-pattern search
false,
&GrepContext {
total_files,
filtered_file_count,
budget,
prefilter: None, // no memmem prefilter for multi-pattern search
prefilter_case_insensitive: false,
is_cancelled,
},
|file_bytes: &[u8], max_matches: usize| {
let state = SinkState {
file_index: 0,
@@ -1039,11 +1053,7 @@ const PAGINATED_CHUNK_SIZE: usize = 512;
fn perform_grep<'a, F>(
files_to_search: &[&'a FileItem],
options: &GrepSearchOptions,
total_files: usize,
filtered_file_count: usize,
budget: &ContentCacheBudget,
prefilter: Option<&memchr::memmem::Finder<'_>>,
prefilter_case_insensitive: bool,
ctx: &GrepContext<'_, '_>,
search_file: F,
) -> GrepResult<'a>
where
@@ -1090,6 +1100,13 @@ where
.par_iter()
.enumerate()
.filter_map(|(local_idx, file)| {
if let Some(flag) = ctx.is_cancelled
&& flag.load(Ordering::Relaxed)
{
budget_exceeded.store(true, Ordering::Relaxed);
return None;
}
if let Some(budget) = time_budget
&& search_start.elapsed() > budget
{
@@ -1097,12 +1114,13 @@ where
return None;
}
let content = file.get_content_for_search(budget)?;
let content = file.get_content_for_search(ctx.budget)?;
// A very important prefilter that skips line splitting and terminates early
// if nothing is foiund. Should be as fast as possible and can be false positive
if let Some(pf) = prefilter {
let found = if prefilter_case_insensitive {
// Fast whole-file memmem check before entering the
// grep-searcher machinery. Skips Vec alloc, Searcher
// setup, and line-splitting for files that can't match.
if let Some(pf) = ctx.prefilter {
let found = if ctx.prefilter_case_insensitive {
case_insensitive_memmem::search_packed_pair(&content, pf.needle())
} else {
pf.find(&content).is_some()
@@ -1168,8 +1186,8 @@ where
files_with_matches: result_files.len(),
files: result_files,
total_files_searched: files_consumed,
total_files,
filtered_file_count,
total_files: ctx.total_files,
filtered_file_count: ctx.filtered_file_count,
next_file_offset,
regex_fallback_error: None,
}
@@ -1339,6 +1357,7 @@ fn prepare_files_to_search<'a>(
/// 2. Batch all lines through `match_list` (SIMD smith-waterman)
/// 3. Filter results by `min_score`
/// 4. Call `match_indices` only on passing lines to get character highlight offsets
#[allow(clippy::too_many_arguments)]
fn fuzzy_grep_search<'a>(
grep_text: &str,
files_to_search: &[&'a FileItem],
@@ -1347,6 +1366,7 @@ fn fuzzy_grep_search<'a>(
filtered_file_count: usize,
case_insensitive: bool,
budget: &ContentCacheBudget,
is_cancelled: Option<&AtomicBool>,
) -> GrepResult<'a> {
// max_typos controls how many *needle* characters can be unmatched.
// A transposition (e.g. "shcema" → "schema") costs ~1 typo with
@@ -1439,6 +1459,13 @@ fn fuzzy_grep_search<'a>(
.map_init(
|| matcher.clone(),
|matcher, (idx, file)| {
if let Some(flag) = is_cancelled
&& flag.load(Ordering::Relaxed)
{
budget_exceeded.store(true, Ordering::Relaxed);
return None;
}
if let Some(budget) = time_budget
&& search_start.elapsed() > budget
{
@@ -1636,14 +1663,15 @@ fn fuzzy_grep_search<'a>(
///
/// When `query` is empty, returns git-modified/untracked files sorted by
/// frecency for the "welcome state" UI.
#[tracing::instrument(skip(files, options, budget, bigram_index, bigram_overlay), fields(file_count = files.len()))]
#[tracing::instrument(skip(files, options, budget, bigram_index, bigram_overlay, is_cancelled), fields(file_count = files.len()))]
pub fn grep_search<'a>(
files: &'a [FileItem],
query: &FFFQuery<'_>,
options: &GrepSearchOptions,
budget: &ContentCacheBudget,
bigram_index: Option<&BigramFilter>,
bigram_overlay: Option<&parking_lot::RwLock<BigramOverlay>>,
bigram_overlay: Option<&BigramOverlay>,
is_cancelled: Option<&AtomicBool>,
) -> GrepResult<'a> {
let total_files = files.len();
@@ -1719,6 +1747,7 @@ pub fn grep_search<'a>(
filtered_file_count,
case_insensitive,
budget,
is_cancelled,
);
}
GrepMode::Regex => build_regex(&grep_text, options.smart_case)
@@ -1754,8 +1783,7 @@ pub fn grep_search<'a>(
&& idx.is_ready()
&& let Some(mut candidates) = idx.query(effective_pattern.as_bytes())
{
if let Some(overlay_lock) = bigram_overlay {
let overlay = overlay_lock.read();
if let Some(overlay) = bigram_overlay {
let pattern_bigrams = extract_bigrams(effective_pattern.as_bytes());
for (r, t) in candidates.iter_mut().zip(overlay.tombstones().iter()) {
*r &= !t;
@@ -1868,11 +1896,14 @@ pub fn grep_search<'a>(
let mut result = perform_grep(
&files_to_search,
options,
total_files,
filtered_file_count,
budget,
should_prefilter.then_some(&finder),
case_insensitive,
&GrepContext {
total_files,
filtered_file_count,
budget,
prefilter: should_prefilter.then_some(&finder),
prefilter_case_insensitive: case_insensitive,
is_cancelled,
},
|file_bytes: &[u8], max_matches: usize| {
let state = SinkState {
file_index: 0,
@@ -2122,6 +2153,7 @@ mod tests {
&[],
&options,
&ContentCacheBudget::unlimited(),
None,
);
// Should find matches from file1 (GrepMode, GrepMatch) and file2 (PlainTextMatcher)
@@ -2159,6 +2191,7 @@ mod tests {
&[],
&options,
&ContentCacheBudget::unlimited(),
None,
);
assert_eq!(
result2.matches.len(),
@@ -2167,8 +2200,14 @@ mod tests {
);
// Test with empty patterns
let result3 =
super::multi_grep_search(&files, &[], &[], &options, &ContentCacheBudget::unlimited());
let result3 = super::multi_grep_search(
&files,
&[],
&[],
&options,
&ContentCacheBudget::unlimited(),
None,
);
assert_eq!(
result3.matches.len(),
0,
+25 -30
View File
@@ -21,10 +21,9 @@
//! ## Shared State
//!
//! [`SharedPicker`], [`SharedFrecency`], and [`SharedQueryTracker`] are
//! `Arc<RwLock<Option<T>>>` type aliases for thread-safe shared access. FFF
//! is designed for long-running processes that keep the file index in global
//! state, so these wrappers let background threads (scanner, watcher) share
//! data with the calling code safely.
//! newtype wrappers around `Arc<RwLock<Option<T>>>` for thread-safe shared
//! access. They provide `read()` / `write()` methods with built-in error
//! conversion and convenience helpers like `wait_for_scan()`.
//!
//! ## Quick Start
//!
@@ -33,49 +32,51 @@
//! use fff_search::frecency::FrecencyTracker;
//! use fff_search::query_tracker::QueryTracker;
//! use fff_search::{
//! FFFMode, FuzzySearchOptions, PaginationArgs, QueryParser,
//! FFFMode, FilePickerOptions, FuzzySearchOptions, PaginationArgs, QueryParser,
//! SharedFrecency, SharedPicker, SharedQueryTracker,
//! };
//!
//! let shared_picker: SharedPicker = Default::default();
//! let shared_frecency: SharedFrecency = Default::default();
//! let shared_query_tracker: SharedQueryTracker = Default::default();
//! let shared_picker = SharedPicker::default();
//! let shared_frecency = SharedFrecency::default();
//! let shared_query_tracker = SharedQueryTracker::default();
//!
//! let tmp = std::env::temp_dir().join("fff-doctest");
//! std::fs::create_dir_all(&tmp).unwrap();
//!
//! // 1. Optionally initialize frecency and query tracker databases
//! let frecency = FrecencyTracker::new(tmp.join("frecency"), false)?;
//! *shared_frecency.write().unwrap() = Some(frecency);
//! shared_frecency.init(frecency)?;
//!
//! let query_tracker = QueryTracker::new(tmp.join("queries"), false)?;
//! *shared_query_tracker.write().unwrap() = Some(query_tracker);
//! shared_query_tracker.init(query_tracker)?;
//!
//! // 2. Init the file picker (spawns background scan + watcher)
//! FilePicker::new_with_shared_state(
//! ".".into(),
//! /* warmup memap caches = */ false,
//! FFFMode::Ai, // use AI for ai agents, and Neovim for editors
//! shared_picker.clone(),
//! shared_frecency.clone(),
//! FilePickerOptions {
//! base_path: ".".into(),
//! mode: FFFMode::Ai,
//! ..Default::default()
//! },
//! )?;
//!
//! // 3. Wait for scan (in real app you would like to add some tokio flavor here)
//! let _ = FilePicker::wait_for_scan(&shared_picker, std::time::Duration::from_secs(10));
//! // 3. Wait for scan
//! shared_picker.wait_for_scan(std::time::Duration::from_secs(10));
//!
//! // 4. Search: lock the picker and query tracker
//! let picker_lock_guard = shared_picker.read().unwrap();
//! let picker = picker_lock_guard.as_ref().unwrap();
//! let query_tracker_lock_guard = shared_query_tracker.read().unwrap();
//! let picker_guard = shared_picker.read()?;
//! let picker = picker_guard.as_ref().unwrap();
//! let qt_guard = shared_query_tracker.read()?;
//!
//! // 5. Parse the query and perform fuzzy search with frecency and combo-boost scoring
//! // 5. Parse the query and perform fuzzy search
//! let parser = QueryParser::default();
//! let query = parser.parse("lib.rs");
//!
//! let results = FilePicker::fuzzy_search(
//! picker.get_files(),
//! &query,
//! query_tracker_lock_guard.as_ref(),
//! qt_guard.as_ref(),
//! FuzzySearchOptions {
//! max_threads: 0,
//! current_file: None,
@@ -134,16 +135,9 @@ pub mod query_tracker;
/// Core data types shared across the crate.
pub mod types;
use std::sync::{Arc, RwLock};
/// Thread-safe shared handle to the [`FilePicker`] instance.
pub type SharedPicker = Arc<RwLock<Option<FilePicker>>>;
/// Thread-safe shared handle to the [`FrecencyTracker`] instance.
pub type SharedFrecency = Arc<RwLock<Option<FrecencyTracker>>>;
/// Thread-safe shared handle to the [`QueryTracker`] instance.
pub type SharedQueryTracker = Arc<RwLock<Option<QueryTracker>>>;
/// Thread-safe shared handles for [`FilePicker`], [`FrecencyTracker`],
/// and [`QueryTracker`].
pub mod shared;
pub use db_healthcheck::{DbHealth, DbHealthChecker};
pub use error::{Error, Result};
@@ -152,4 +146,5 @@ pub use file_picker::*;
pub use frecency::*;
pub use grep::*;
pub use query_tracker::*;
pub use shared::*;
pub use types::*;
+181
View File
@@ -0,0 +1,181 @@
use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
use std::time::Duration;
use crate::error::Error;
use crate::file_picker::FilePicker;
use crate::frecency::FrecencyTracker;
use crate::git::GitStatusCache;
use crate::query_tracker::QueryTracker;
/// Thread-safe shared handle to the [`FilePicker`] instance.
///
/// Wraps `Arc<RwLock<Option<FilePicker>>>` with convenience methods.
/// `Clone` gives a new handle to the same picker (Arc clone).
/// `Default` creates an empty handle suitable for `Lazy::new(SharedPicker::default)`.
#[derive(Clone, Default)]
pub struct SharedPicker(pub(crate) Arc<RwLock<Option<FilePicker>>>);
impl std::fmt::Debug for SharedPicker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("SharedPicker").field(&"..").finish()
}
}
impl SharedPicker {
pub fn read(&self) -> Result<RwLockReadGuard<'_, Option<FilePicker>>, Error> {
self.0.read().map_err(|_| Error::AcquireItemLock)
}
pub fn write(&self) -> Result<RwLockWriteGuard<'_, Option<FilePicker>>, Error> {
self.0.write().map_err(|_| Error::AcquireItemLock)
}
/// Block until the background filesystem scan finishes.
/// Returns `true` if scan completed, `false` on timeout.
pub fn wait_for_scan(&self, timeout: Duration) -> bool {
let signal = {
let guard = self.0.read().expect("shared picker lock poisoned");
match guard.as_ref() {
Some(picker) => picker.scan_signal(),
None => return true,
}
};
let start = std::time::Instant::now();
while signal.load(std::sync::atomic::Ordering::Acquire) {
if start.elapsed() >= timeout {
return false;
}
std::thread::sleep(Duration::from_millis(10));
}
true
}
/// Block until the background file watcher is ready.
/// Returns `true` if watcher ready, `false` on timeout.
pub fn wait_for_watcher(&self, timeout: Duration) -> bool {
let signal = {
let guard = self.0.read().expect("shared picker lock poisoned");
match guard.as_ref() {
Some(picker) => picker.watcher_signal(),
None => return true,
}
};
let start = std::time::Instant::now();
while !signal.load(std::sync::atomic::Ordering::Acquire) {
if start.elapsed() >= timeout {
return false;
}
std::thread::sleep(Duration::from_millis(10));
}
true
}
/// Refresh git statuses for all indexed files.
pub fn refresh_git_status(&self, shared_frecency: &SharedFrecency) -> Result<usize, Error> {
use git2::StatusOptions;
use tracing::debug;
let git_status = {
let guard = self.read()?;
let Some(ref picker) = *guard else {
return Err(Error::FilePickerMissing);
};
debug!(
"Refreshing git statuses for picker: {:?}",
picker.git_root()
);
GitStatusCache::read_git_status(
picker.git_root(),
StatusOptions::new()
.include_untracked(true)
.recurse_untracked_dirs(true)
.include_unmodified(true)
.exclude_submodules(true),
)
};
let mut guard = self.write()?;
let picker = guard.as_mut().ok_or(Error::FilePickerMissing)?;
let statuses_count = if let Some(git_status) = git_status {
let count = git_status.statuses_len();
picker.update_git_statuses(git_status, shared_frecency)?;
count
} else {
0
};
Ok(statuses_count)
}
}
// ── SharedFrecency ─────────────────────────────────────────────────────
/// Thread-safe shared handle to the [`FrecencyTracker`] instance.
#[derive(Clone, Default)]
pub struct SharedFrecency(pub(crate) Arc<RwLock<Option<FrecencyTracker>>>);
impl std::fmt::Debug for SharedFrecency {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("SharedFrecency").field(&"..").finish()
}
}
impl SharedFrecency {
pub fn read(&self) -> Result<RwLockReadGuard<'_, Option<FrecencyTracker>>, Error> {
self.0.read().map_err(|_| Error::AcquireFrecencyLock)
}
pub fn write(&self) -> Result<RwLockWriteGuard<'_, Option<FrecencyTracker>>, Error> {
self.0.write().map_err(|_| Error::AcquireFrecencyLock)
}
/// Initialize the frecency tracker, replacing any existing one.
pub fn init(&self, tracker: FrecencyTracker) -> Result<(), Error> {
let mut guard = self.write()?;
*guard = Some(tracker);
Ok(())
}
/// Spawn a background GC thread for this frecency tracker.
pub fn spawn_gc(
&self,
db_path: String,
use_unsafe_no_lock: bool,
) -> crate::Result<std::thread::JoinHandle<()>> {
FrecencyTracker::spawn_gc(self.clone(), db_path, use_unsafe_no_lock)
}
}
// ── SharedQueryTracker ─────────────────────────────────────────────────
/// Thread-safe shared handle to the [`QueryTracker`] instance.
#[derive(Clone, Default)]
pub struct SharedQueryTracker(pub(crate) Arc<RwLock<Option<QueryTracker>>>);
impl std::fmt::Debug for SharedQueryTracker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("SharedQueryTracker").field(&"..").finish()
}
}
impl SharedQueryTracker {
pub fn read(&self) -> Result<RwLockReadGuard<'_, Option<QueryTracker>>, Error> {
self.0.read().map_err(|_| Error::AcquireFrecencyLock)
}
pub fn write(&self) -> Result<RwLockWriteGuard<'_, Option<QueryTracker>>, Error> {
self.0.write().map_err(|_| Error::AcquireFrecencyLock)
}
/// Initialize the query tracker, replacing any existing one.
pub fn init(&self, tracker: QueryTracker) -> Result<(), Error> {
let mut guard = self.write()?;
*guard = Some(tracker);
Ok(())
}
}
@@ -2,14 +2,12 @@
//! still makes the new content findable via grep (through the overlay layer).
use std::fs;
use std::sync::Arc;
use std::time::Duration;
use tempfile::TempDir;
use fff_search::file_picker::{FFFMode, FilePicker};
use fff_search::grep::{GrepMode, GrepSearchOptions, grep_search, parse_grep_query};
use fff_search::types::ContentCacheBudget;
use fff_search::{SharedFrecency, SharedPicker};
use fff_search::grep::{GrepMode, GrepSearchOptions, parse_grep_query};
use fff_search::{FilePickerOptions, SharedFrecency, SharedPicker};
/// Create a temp directory with some initial files, run the full picker lifecycle,
/// then modify a file and verify grep finds the new content.
@@ -28,15 +26,18 @@ fn modified_file_findable_via_overlay() {
fs::write(base.join("gamma.txt"), "yet another file\nmore lines\n").unwrap();
// ── Phase 1: Initialize picker ──────────────────────────────────────
let shared_picker: SharedPicker = Arc::new(std::sync::RwLock::new(None));
let shared_frecency: SharedFrecency = Arc::new(std::sync::RwLock::new(None));
let shared_picker = SharedPicker::default();
let shared_frecency = SharedFrecency::default();
FilePicker::new_with_shared_state(
base.to_string_lossy().to_string(),
true, // warmup (builds bigram index)
FFFMode::Neovim,
Arc::clone(&shared_picker),
Arc::clone(&shared_frecency),
shared_picker.clone(),
shared_frecency.clone(),
FilePickerOptions {
base_path: base.to_string_lossy().to_string(),
warmup_mmap_cache: true,
mode: FFFMode::Neovim,
..Default::default()
},
)
.expect("Failed to create FilePicker");
@@ -51,7 +52,7 @@ fn modified_file_findable_via_overlay() {
.map(|guard| {
guard
.as_ref()
.map_or(false, |p| !p.is_scan_active() && p.bigram_index.is_some())
.map_or(false, |p| !p.is_scan_active() && p.bigram_index().is_some())
})
.unwrap_or(false);
@@ -70,11 +71,11 @@ fn modified_file_findable_via_overlay() {
let picker = guard.as_ref().unwrap();
assert_eq!(picker.get_files().len(), 3, "Expected 3 files after scan");
assert!(
picker.bigram_index.is_some(),
picker.bigram_index().is_some(),
"Bigram index should be built"
);
assert!(
picker.bigram_overlay.is_some(),
picker.bigram_overlay().is_some(),
"Overlay should be initialized"
);
}
@@ -86,14 +87,7 @@ fn modified_file_findable_via_overlay() {
let picker = guard.as_ref().unwrap();
let parsed = parse_grep_query("UNIQUE_NEEDLE");
let opts = grep_opts();
let result = grep_search(
picker.get_files(),
&parsed,
&opts,
&ContentCacheBudget::unlimited(),
picker.bigram_index.as_deref(),
picker.bigram_overlay.as_deref(),
);
let result = picker.grep(&parsed, &opts);
assert_eq!(
result.matches.len(),
0,
@@ -136,14 +130,7 @@ fn modified_file_findable_via_overlay() {
let picker = guard.as_ref().unwrap();
let parsed = parse_grep_query("UNIQUE_NEEDLE");
let opts = grep_opts();
let result = grep_search(
picker.get_files(),
&parsed,
&opts,
&ContentCacheBudget::unlimited(),
picker.bigram_index.as_deref(),
picker.bigram_overlay.as_deref(),
);
let result = picker.grep(&parsed, &opts);
assert!(
!result.matches.is_empty(),
"UNIQUE_NEEDLE should be findable after modification (overlay adds the candidate back)"
@@ -160,14 +147,7 @@ fn modified_file_findable_via_overlay() {
let picker = guard.as_ref().unwrap();
let parsed = parse_grep_query("UNIQUE_NEEDLE");
let opts = grep_opts();
let result = grep_search(
picker.get_files(),
&parsed,
&opts,
&ContentCacheBudget::unlimited(),
picker.bigram_index.as_deref(),
None, // no overlay!
);
let result = picker.grep_without_overlay(&parsed, &opts);
assert_eq!(
result.matches.len(),
0,
@@ -192,15 +172,18 @@ fn deleted_file_excluded_via_overlay() {
fs::write(base.join("keep.txt"), "keep this content\n").unwrap();
fs::write(base.join("remove.txt"), "DELETEME_TOKEN is here\n").unwrap();
let shared_picker: SharedPicker = Arc::new(std::sync::RwLock::new(None));
let shared_frecency: SharedFrecency = Arc::new(std::sync::RwLock::new(None));
let shared_picker = SharedPicker::default();
let shared_frecency = SharedFrecency::default();
FilePicker::new_with_shared_state(
base.to_string_lossy().to_string(),
true,
FFFMode::Neovim,
Arc::clone(&shared_picker),
Arc::clone(&shared_frecency),
shared_picker.clone(),
shared_frecency.clone(),
FilePickerOptions {
base_path: base.to_string_lossy().to_string(),
warmup_mmap_cache: true,
mode: FFFMode::Neovim,
..Default::default()
},
)
.unwrap();
@@ -257,15 +240,18 @@ fn new_file_findable_after_add() {
fs::write(base.join("existing.txt"), "original content\n").unwrap();
let shared_picker: SharedPicker = Arc::new(std::sync::RwLock::new(None));
let shared_frecency: SharedFrecency = Arc::new(std::sync::RwLock::new(None));
let shared_picker = SharedPicker::default();
let shared_frecency = SharedFrecency::default();
FilePicker::new_with_shared_state(
base.to_string_lossy().to_string(),
true,
FFFMode::Neovim,
Arc::clone(&shared_picker),
Arc::clone(&shared_frecency),
shared_picker.clone(),
shared_frecency.clone(),
FilePickerOptions {
base_path: base.to_string_lossy().to_string(),
warmup_mmap_cache: true,
mode: FFFMode::Neovim,
..Default::default()
},
)
.unwrap();
@@ -326,14 +312,7 @@ fn grep_opts() -> GrepSearchOptions {
fn grep_for<'a>(picker: &'a FilePicker, query: &str) -> fff_search::grep::GrepResult<'a> {
let parsed = parse_grep_query(query);
grep_search(
picker.get_files(),
&parsed,
&grep_opts(),
&ContentCacheBudget::unlimited(),
picker.bigram_index.as_deref(),
picker.bigram_overlay.as_deref(),
)
picker.grep(&parsed, &grep_opts())
}
fn wait_for_bigram(shared_picker: &SharedPicker) {
@@ -346,7 +325,7 @@ fn wait_for_bigram(shared_picker: &SharedPicker) {
.map(|guard| {
guard
.as_ref()
.map_or(false, |p| !p.is_scan_active() && p.bigram_index.is_some())
.map_or(false, |p| !p.is_scan_active() && p.bigram_index().is_some())
})
.unwrap_or(false);
if ready {
+67
View File
@@ -81,6 +81,7 @@ fn plain_text_finds_exact_literal() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 1);
@@ -106,6 +107,7 @@ fn plain_text_smart_case_insensitive() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
@@ -133,6 +135,7 @@ fn plain_text_smart_case_sensitive_with_uppercase() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
@@ -161,6 +164,7 @@ fn plain_text_regex_metacharacters_are_literal() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 1);
@@ -175,6 +179,7 @@ fn plain_text_regex_metacharacters_are_literal() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result2.matches.len(), 1);
assert_eq!(result2.matches[0].line_number, 2);
@@ -198,6 +203,7 @@ fn plain_text_dot_is_literal() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
@@ -225,6 +231,7 @@ fn plain_text_asterisk_is_literal() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 1);
assert_eq!(result.matches[0].line_number, 1);
@@ -247,6 +254,7 @@ fn plain_text_backslash_is_literal() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 1);
}
@@ -268,6 +276,7 @@ fn plain_text_across_multiple_files() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 3);
@@ -288,6 +297,7 @@ fn plain_text_highlight_offsets_are_correct() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 1);
@@ -313,6 +323,7 @@ fn plain_text_empty_query_returns_no_content_matches() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
// Empty query in grep returns git-modified welcome state (no content matches)
@@ -353,6 +364,7 @@ fn plain_text_binary_files_are_skipped() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
// Only the text file should be searched, not the binary one
@@ -380,6 +392,7 @@ fn plain_text_max_matches_per_file() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
@@ -409,6 +422,7 @@ fn plain_text_page_limit() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
// page_limit is a soft minimum: we always finish the current file, so we
@@ -459,6 +473,7 @@ fn plain_text_file_offset_pagination() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
for m in &result.matches {
@@ -517,6 +532,7 @@ fn plain_text_line_numbers_are_correct() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 4);
@@ -544,6 +560,7 @@ fn plain_text_max_file_size_filter() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 0, "large file should be filtered out");
@@ -569,6 +586,7 @@ fn regex_basic_pattern() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 1);
@@ -590,6 +608,7 @@ fn regex_capture_group_matching() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 2);
@@ -620,6 +639,7 @@ fn regex_dot_matches_any_char() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
@@ -646,6 +666,7 @@ fn regex_alternation() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 2);
@@ -671,6 +692,7 @@ fn regex_character_class() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 3);
@@ -701,6 +723,7 @@ fn regex_quantifiers() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 3, "should match foo, fooo, foooo");
@@ -723,6 +746,7 @@ fn regex_anchors() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 1);
@@ -747,6 +771,7 @@ fn regex_anchors_multiword() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
@@ -771,6 +796,7 @@ fn regex_highlight_offsets_variable_length() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 1);
@@ -801,6 +827,7 @@ fn regex_invalid_pattern_falls_back_to_literal() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
// Fallback to literal: finds "name(" in "call name(arg)"
@@ -824,6 +851,7 @@ fn regex_invalid_pattern_falls_back_to_literal() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result2.matches.len(), 0);
assert!(result2.regex_fallback_error.is_some());
@@ -847,6 +875,7 @@ fn regex_smart_case() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result_lower.matches.len(), 3);
@@ -859,6 +888,7 @@ fn regex_smart_case() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result_upper.matches.len(), 1);
}
@@ -888,6 +918,7 @@ fn regex_across_multiple_files() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
// Should match: fn main(), fn helper(), fn test_one(), fn test_two()
@@ -914,6 +945,7 @@ fn plain_text_and_regex_agree_on_simple_literal() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
let regex_result = grep_search(
&files,
@@ -922,6 +954,7 @@ fn plain_text_and_regex_agree_on_simple_literal() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(plain_result.matches.len(), regex_result.matches.len());
@@ -949,6 +982,7 @@ fn plain_text_escapes_what_regex_does_not() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
let parsed_regex = parse_grep_query("\\$100");
let regex_result = grep_search(
@@ -958,6 +992,7 @@ fn plain_text_escapes_what_regex_does_not() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
// Plain text should find "$100" literally
@@ -987,6 +1022,7 @@ fn grep_with_extension_constraint() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
// Should only search .rs files
@@ -1022,6 +1058,7 @@ fn plain_text_bracket_is_literal() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
@@ -1051,6 +1088,7 @@ fn grep_backslash_escapes_extension_filter() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
result_filter.files.len(),
@@ -1067,6 +1105,7 @@ fn grep_backslash_escapes_extension_filter() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
result_literal.matches.len(),
@@ -1092,6 +1131,7 @@ fn grep_backslash_escapes_path_segment() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
result.matches.len(),
@@ -1118,6 +1158,7 @@ fn grep_backslash_escapes_negation() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 1);
assert!(result.matches[0].line_content.contains("!test"));
@@ -1140,6 +1181,7 @@ fn grep_with_path_constraint() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 1);
@@ -1166,6 +1208,7 @@ fn grep_with_negated_extension_constraint() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
@@ -1199,6 +1242,7 @@ fn grep_with_negated_path_constraint() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
@@ -1232,6 +1276,7 @@ fn grep_with_negated_text_constraint() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
// "tests/helper.rs" contains "test" in path, should be excluded
@@ -1270,6 +1315,7 @@ fn grep_empty_file_is_skipped() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 1);
@@ -1288,6 +1334,7 @@ fn grep_single_line_no_trailing_newline() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 1);
@@ -1311,6 +1358,7 @@ fn grep_unicode_content() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 1);
assert_eq!(result.matches[0].line_number, 2);
@@ -1323,6 +1371,7 @@ fn grep_unicode_content() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result2.matches.len(), 1);
assert_eq!(result2.matches[0].line_number, 3);
@@ -1342,6 +1391,7 @@ fn grep_long_line_is_truncated() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 1);
@@ -1370,6 +1420,7 @@ fn regex_word_boundary() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
@@ -1397,6 +1448,7 @@ fn plain_text_question_mark_is_literal() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
@@ -1423,6 +1475,7 @@ fn plain_text_query_with_question_mark_in_word() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
@@ -1446,6 +1499,7 @@ fn regex_question_mark_is_quantifier() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
@@ -1474,6 +1528,7 @@ fn fuzzy_finds_exact_substring() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
@@ -1503,6 +1558,7 @@ fn fuzzy_finds_scattered_characters() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert!(
@@ -1525,6 +1581,7 @@ fn fuzzy_highlight_offsets_correct() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 1);
@@ -1557,6 +1614,7 @@ fn fuzzy_unicode_char_indices() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
// Should fuzzy match "régulière" (with multi-byte é and è)
@@ -1578,6 +1636,7 @@ fn fuzzy_empty_query_returns_empty() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
// Empty query returns git-modified files, not fuzzy matches
@@ -1601,6 +1660,7 @@ fn fuzzy_with_extension_constraint() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
// Should only search .rs files
@@ -1634,6 +1694,7 @@ fn fuzzy_respects_page_limit() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
// page_limit is a soft minimum: we always finish the current file, so we
@@ -1677,6 +1738,7 @@ fn fuzzy_respects_max_matches_per_file() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
@@ -1705,6 +1767,7 @@ fn fuzzy_filters_low_quality_matches() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
// Should only get high-quality matches
@@ -1742,6 +1805,7 @@ fn fuzzy_exact_match_always_passes() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
@@ -1769,6 +1833,7 @@ fn fuzzy_score_is_captured() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 1);
@@ -1798,6 +1863,7 @@ fn fuzzy_score_is_none_in_plain_mode() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(result.matches.len(), 1);
@@ -1830,6 +1896,7 @@ fn plain_text_smart_case_finds_uppercase_content_with_lowercase_query() {
&ContentCacheBudget::unlimited(),
None,
None,
None,
);
assert_eq!(
+16 -24
View File
@@ -12,8 +12,6 @@ mod output;
mod server;
mod update_check;
use std::sync::{Arc, RwLock};
use clap::Parser;
use fff::file_picker::FilePicker;
use fff::frecency::FrecencyTracker;
@@ -254,16 +252,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let frecency_db_path = args.frecency_db_path.unwrap_or_default();
let shared_picker: SharedPicker = Arc::new(RwLock::new(None));
let shared_frecency: SharedFrecency = Arc::new(RwLock::new(None));
let shared_picker = SharedPicker::default();
let shared_frecency = SharedFrecency::default();
match FrecencyTracker::new(&frecency_db_path, false) {
Ok(tracker) => {
if let Ok(mut guard) = shared_frecency.write() {
*guard = Some(tracker);
}
let _ =
FrecencyTracker::spawn_gc(Arc::clone(&shared_frecency), frecency_db_path, false);
let _ = shared_frecency.init(tracker);
let _ = shared_frecency.spawn_gc(frecency_db_path, false);
}
Err(e) => {
eprintln!("Warning: Failed to init frecency db: {}", e);
@@ -272,22 +266,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize file picker (spawns background scan + watcher)
FilePicker::new_with_shared_state(
base_path,
!args.no_warmup, // warmup_mmap_cache
FFFMode::Ai,
Arc::clone(&shared_picker),
Arc::clone(&shared_frecency),
shared_picker.clone(),
shared_frecency.clone(),
fff::FilePickerOptions {
base_path,
warmup_mmap_cache: !args.no_warmup,
mode: FFFMode::Ai,
cache_budget: args
.max_cached_files
.map(fff::ContentCacheBudget::new_for_repo),
..Default::default()
},
)
.map_err(|e| format!("Failed to init file picker: {}", e))?;
// Apply user-configured cache limit after picker creation.
if let Some(limit) = args.max_cached_files
&& let Ok(mut guard) = shared_picker.write()
&& let Some(ref mut picker) = *guard
{
picker.cache_budget = std::sync::Arc::new(fff::ContentCacheBudget::new_for_repo(limit));
}
if !args.no_update_check {
update_check::spawn_update_check();
}
@@ -296,7 +288,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let server = FffServer::new(shared_picker.clone(), shared_frecency.clone());
// Wait for initial scan in background — don't block server startup
let picker_clone_for_scan = Arc::clone(&shared_picker);
let picker_clone_for_scan = shared_picker.clone();
tokio::task::spawn_blocking(move || {
let start = std::time::Instant::now();
loop {
+8 -20
View File
@@ -249,15 +249,9 @@ impl FffServer {
.as_ref()
.ok_or_else(|| ErrorData::internal_error("File picker not initialized", None))?;
let files = picker.get_files();
let budget = picker.cache_budget();
let bigram_idx = picker.bigram_index.as_deref();
let bigram_overlay = picker.bigram_overlay.as_deref();
let parser = QueryParser::new(AiGrepConfig);
let parsed = parser.parse(query);
let result =
grep::grep_search(files, &parsed, &options, budget, bigram_idx, bigram_overlay);
let result = picker.grep(&parsed, &options);
if result.matches.is_empty() && file_offset == 0 {
// Auto-retry: try broadening multi-word queries by dropping first non-constraint word
@@ -280,14 +274,7 @@ impl FffServer {
};
let (retry_options, _) = make_grep_options(output_mode, retry_mode, 0, context);
let retry_result = grep::grep_search(
files,
&rest_parsed,
&retry_options,
budget,
bigram_idx,
bigram_overlay,
);
let retry_result = picker.grep(&rest_parsed, &retry_options);
if !retry_result.matches.is_empty() && retry_result.matches.len() <= 10 {
let mut cs = self.lock_cursors()?;
@@ -315,8 +302,7 @@ impl FffServer {
let fuzzy_query = cleanup_fuzzy_query(query);
let (fuzzy_options, _) = make_grep_options(output_mode, GrepMode::Fuzzy, 0, Some(0));
let fuzzy_parsed = parser.parse(&fuzzy_query);
let fuzzy_result =
grep::grep_search(files, &fuzzy_parsed, &fuzzy_options, budget, None, None);
let fuzzy_result = picker.grep(&fuzzy_parsed, &fuzzy_options);
if !fuzzy_result.matches.is_empty() {
let mut lines: Vec<String> = Vec::new();
@@ -353,7 +339,8 @@ impl FffServer {
limit: 1,
},
};
let file_result = FilePicker::fuzzy_search(files, &file_query, None, file_opts);
let file_result =
FilePicker::fuzzy_search(picker.get_files(), &file_query, None, file_opts);
if let (Some(top), Some(score)) =
(file_result.items.first(), file_result.scores.first())
{
@@ -603,7 +590,8 @@ impl FffServer {
let files = picker.get_files();
let budget = picker.cache_budget();
let result = grep::multi_grep_search(files, &patterns_refs, constraints, &options, budget);
let result =
grep::multi_grep_search(files, &patterns_refs, constraints, &options, budget, None);
let file_refs: Vec<&FileItem> = result.files.to_vec();
if result.matches.is_empty() && file_offset == 0 {
@@ -626,7 +614,7 @@ impl FffServer {
let parsed = parser.parse(&full_query);
let fb_result =
grep::grep_search(files, &parsed, &fallback_options, budget, None, None);
grep::grep_search(files, &parsed, &fallback_options, budget, None, None, None);
if !fb_result.matches.is_empty() {
let fb_file_refs: Vec<&FileItem> = fb_result.files.to_vec();
+19 -16
View File
@@ -2,11 +2,10 @@ use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_ma
use fff::file_picker::{FFFMode, FilePicker};
use fff::types::{ContentCacheBudget, FileItem, PaginationArgs};
use fff::{
FuzzySearchOptions, GrepMode, GrepSearchOptions, QueryParser, SharedFrecency, SharedPicker,
build_bigram_index, grep,
FilePickerOptions, FuzzySearchOptions, GrepMode, GrepSearchOptions, QueryParser,
SharedFrecency, SharedPicker, build_bigram_index, grep,
};
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use std::time::Duration;
/// Initialize tracing to output to console
@@ -30,11 +29,14 @@ fn init_file_picker_internal(
shared_frecency: &SharedFrecency,
) -> Result<(), String> {
FilePicker::new_with_shared_state(
path.to_string(),
false,
FFFMode::Neovim,
Arc::clone(shared_picker),
Arc::clone(shared_frecency),
shared_picker.clone(),
shared_frecency.clone(),
FilePickerOptions {
base_path: path.to_string(),
warmup_mmap_cache: false,
mode: FFFMode::Neovim,
..Default::default()
},
)
.map_err(|e| format!("Failed to create FilePicker: {:?}", e))
}
@@ -136,8 +138,8 @@ fn setup_once() -> Result<(Vec<FileItem>, SharedPicker, SharedFrecency), String>
.map_err(|e| format!("Failed to canonicalize path: {}", e))?;
eprintln!(" Path: {:?}", canonical_path);
let shared_picker: SharedPicker = Arc::new(RwLock::new(None));
let shared_frecency: SharedFrecency = Arc::new(RwLock::new(None));
let shared_picker = SharedPicker::default();
let shared_frecency = SharedFrecency::default();
init_file_picker_internal(
&canonical_path.to_string_lossy(),
@@ -182,8 +184,8 @@ fn bench_indexing(c: &mut Criterion) {
group.bench_function("index_big_repo", |b| {
b.iter(|| {
let sp: SharedPicker = Arc::new(RwLock::new(None));
let sf: SharedFrecency = Arc::new(RwLock::new(None));
let sp = SharedPicker::default();
let sf = SharedFrecency::default();
let start = std::time::Instant::now();
init_file_picker_internal(black_box(&canonical_path.to_string_lossy()), &sp, &sf)
@@ -684,13 +686,12 @@ fn bench_grep_search(c: &mut Criterion) {
eprintln!(" Building bigram index for {} files...", files.len());
let start = std::time::Instant::now();
let bigram_index = build_bigram_index(&files, &budget);
let (bigram_filter, _overflow_indices) = build_bigram_index(&files, &budget);
eprintln!(
" Bigram index built in {:.2}s ({} columns)",
start.elapsed().as_secs_f64(),
bigram_index.columns_used(),
bigram_filter.columns_used(),
);
let bigram_index = std::sync::Arc::new(bigram_index);
let mut group = c.benchmark_group("grep");
group.sample_size(50);
@@ -727,7 +728,8 @@ fn bench_grep_search(c: &mut Criterion) {
black_box(&parsed),
black_box(&options),
&budget,
Some(&bigram_index),
Some(&bigram_filter),
None,
None,
);
result.matches.len()
@@ -744,6 +746,7 @@ fn bench_grep_search(c: &mut Criterion) {
&budget,
None,
None,
None,
);
result.matches.len()
});
+1 -1
View File
@@ -21,7 +21,7 @@ fn fmt_dur(us: u128) -> String {
}
}
fn stats(times_us: &mut Vec<u128>) -> (u128, u128, u128, u128) {
fn stats(times_us: &mut [u128]) -> (u128, u128, u128, u128) {
times_us.sort();
let sum: u128 = times_us.iter().sum();
let mean = sum / times_us.len() as u128;
+1 -1
View File
@@ -99,7 +99,7 @@ fn run_grep(
for i in 0..iters {
let t = Instant::now();
let result = grep_search(files, &parsed, &options, &budget, index, None);
let result = grep_search(files, &parsed, &options, &budget, index, None, None);
let us = t.elapsed().as_micros();
times_us.push(us);
@@ -85,6 +85,7 @@ fn run_fuzzy_query(files: &[FileItem], query: &str, label: &str) {
&fff::ContentCacheBudget::zero(),
None,
None,
None,
);
let elapsed = start.elapsed();
+2
View File
@@ -159,6 +159,7 @@ impl<'a> GrepBench<'a> {
&ContentCacheBudget::default(),
self.bigram_index,
None,
None,
);
let elapsed = start.elapsed();
(elapsed, result.matches.len(), result.total_files_searched)
@@ -512,6 +513,7 @@ fn main() {
&fff::ContentCacheBudget::unlimited(),
None,
None,
None,
);
let elapsed = start.elapsed();
eprintln!(
+3
View File
@@ -217,6 +217,7 @@ fn run_fff_full(files: &[FileItem], query: &str) -> (usize, Duration) {
&fff::ContentCacheBudget::zero(),
None,
None,
None,
);
let elapsed = start.elapsed();
(result.matches.len(), elapsed)
@@ -244,6 +245,7 @@ fn benchmark_fff_smart_case(files: &[FileItem], parsed: &FFFQuery<'_>) -> (usize
&fff::ContentCacheBudget::unlimited(),
None,
None,
None,
);
let elapsed = start.elapsed();
(result.matches.len(), elapsed)
@@ -272,6 +274,7 @@ fn run_fff_page(files: &[FileItem], query: &str) -> (usize, Duration) {
&fff::ContentCacheBudget::unlimited(),
None,
None,
None,
);
let elapsed = start.elapsed();
(result.matches.len(), elapsed)
+10 -8
View File
@@ -1,7 +1,6 @@
use fff::file_picker::{FFFMode, FilePicker};
use fff::{FuzzySearchOptions, PaginationArgs, QueryParser, SharedFrecency, SharedPicker};
use std::env;
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;
@@ -179,17 +178,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!();
// Create shared state
let shared_picker: SharedPicker = Arc::new(RwLock::new(None));
let shared_frecency: SharedFrecency = Arc::new(RwLock::new(None));
let shared_picker = SharedPicker::default();
let shared_frecency = SharedFrecency::default();
// Initialize FilePicker
println!("Initializing FilePicker...");
FilePicker::new_with_shared_state(
base_path.clone(),
false,
FFFMode::Neovim,
Arc::clone(&shared_picker),
Arc::clone(&shared_frecency),
shared_picker.clone(),
shared_frecency.clone(),
fff::FilePickerOptions {
base_path: base_path.clone(),
warmup_mmap_cache: false,
mode: FFFMode::Neovim,
..Default::default()
},
)?;
// Wait for initial scan
+22 -46
View File
@@ -2,49 +2,22 @@ use fff::file_picker::{FFFMode, FilePicker};
use fff::{
FileItem, FuzzySearchOptions, PaginationArgs, QueryParser, SharedFrecency, SharedPicker,
};
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
/// Wait for background scan to complete
fn wait_for_scan(shared_picker: &SharedPicker, timeout_secs: u64) -> Result<usize, String> {
let start = Instant::now();
let timeout = Duration::from_secs(timeout_secs);
let mut iteration = 0;
if !shared_picker.wait_for_scan(timeout) {
return Err(format!("Scan timed out after {} seconds", timeout_secs));
}
loop {
iteration += 1;
let picker_guard = shared_picker
.read()
.map_err(|_| "Failed to acquire read lock")?;
if let Some(ref picker) = *picker_guard {
let is_scanning = picker.is_scan_active();
let file_count = picker.get_files().len();
if iteration % 20 == 0 {
eprintln!(
" [{:.1}s] Scanning: {}, Files: {}",
start.elapsed().as_secs_f64(),
is_scanning,
file_count
);
}
if !is_scanning && file_count > 0 {
return Ok(file_count);
}
} else if iteration % 20 == 0 {
eprintln!(
" [{:.1}s] FilePicker is None",
start.elapsed().as_secs_f64()
);
}
if start.elapsed() > timeout {
return Err(format!("Scan timed out after {} seconds", timeout_secs));
}
std::thread::sleep(Duration::from_millis(100));
let picker_guard = shared_picker
.read()
.map_err(|e| format!("Failed to acquire read lock: {}", e))?;
if let Some(ref picker) = *picker_guard {
Ok(picker.get_files().len())
} else {
Err("FilePicker not initialized".to_string())
}
}
@@ -52,7 +25,7 @@ fn wait_for_scan(shared_picker: &SharedPicker, timeout_secs: u64) -> Result<usiz
fn get_files(shared_picker: &SharedPicker) -> Result<Vec<FileItem>, String> {
let picker_guard = shared_picker
.read()
.map_err(|_| "Failed to acquire read lock")?;
.map_err(|e| format!("Failed to acquire read lock: {}", e))?;
if let Some(ref picker) = *picker_guard {
Ok(picker.get_files().to_vec())
} else {
@@ -74,18 +47,21 @@ fn main() {
fff::path_utils::canonicalize(&big_repo_path).expect("Failed to canonicalize path");
// Create shared state
let shared_picker: SharedPicker = Arc::new(RwLock::new(None));
let shared_frecency: SharedFrecency = Arc::new(RwLock::new(None));
let shared_picker = SharedPicker::default();
let shared_frecency = SharedFrecency::default();
eprintln!("Initializing FilePicker for: {:?}", canonical_path);
FilePicker::new_with_shared_state(
canonical_path.to_string_lossy().to_string(),
false,
FFFMode::Neovim,
Arc::clone(&shared_picker),
Arc::clone(&shared_frecency),
shared_picker.clone(),
shared_frecency.clone(),
fff::FilePickerOptions {
base_path: canonical_path.to_string_lossy().to_string(),
warmup_mmap_cache: false,
mode: FFFMode::Neovim,
..Default::default()
},
)
.expect("Failed to init FilePicker");
.expect("Failed to init FilePicker with shared state");
// Give background thread time to start
std::thread::sleep(Duration::from_millis(200));
+10 -8
View File
@@ -2,7 +2,6 @@ use fff::file_picker::{FFFMode, FilePicker};
use fff::{FuzzySearchOptions, PaginationArgs, QueryParser, SharedFrecency, SharedPicker};
use std::env;
use std::io::{self, Write};
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::{Duration, Instant};
@@ -79,17 +78,20 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!();
// Create shared state
let shared_picker: SharedPicker = Arc::new(RwLock::new(None));
let shared_frecency: SharedFrecency = Arc::new(RwLock::new(None));
let shared_picker = SharedPicker::default();
let shared_frecency = SharedFrecency::default();
// Initialize the file picker
println!("📁 Initializing FilePicker...");
FilePicker::new_with_shared_state(
base_path.clone(),
false,
FFFMode::Neovim,
Arc::clone(&shared_picker),
Arc::clone(&shared_frecency),
shared_picker.clone(),
shared_frecency.clone(),
fff::FilePickerOptions {
base_path: base_path.clone(),
warmup_mmap_cache: false,
mode: FFFMode::Neovim,
..Default::default()
},
)?;
// Wait for initial scan to complete
+13 -9
View File
@@ -7,8 +7,8 @@ use fff::git::format_git_status;
use fff::{FFFMode, FuzzySearchOptions, PaginationArgs, QueryParser, SharedFrecency, SharedPicker};
use std::env;
use std::io::{self, Write};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;
@@ -25,11 +25,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let r = running.clone();
// Create shared state
let shared_picker: SharedPicker = Arc::new(RwLock::new(None));
let shared_frecency: SharedFrecency = Arc::new(RwLock::new(None));
let shared_picker = SharedPicker::default();
let shared_frecency = SharedFrecency::default();
// Clone for signal handler
let picker_for_cleanup = Arc::clone(&shared_picker);
let picker_for_cleanup = shared_picker.clone();
ctrlc::set_handler(move || {
println!("\n🛑 Received interrupt signal, shutting down...");
if let Ok(mut guard) = picker_for_cleanup.write() {
@@ -46,11 +46,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize the file picker using shared state
FilePicker::new_with_shared_state(
base_path.clone(),
false,
FFFMode::default(),
Arc::clone(&shared_picker),
Arc::clone(&shared_frecency),
shared_picker.clone(),
shared_frecency.clone(),
fff::FilePickerOptions {
base_path: base_path.clone(),
warmup_mmap_cache: false,
mode: FFFMode::default(),
..Default::default()
},
)?;
// Get initial file count from shared state
@@ -198,5 +201,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
picker.stop_background_monitor();
}
}
Ok(())
}
-11
View File
@@ -24,14 +24,3 @@ impl<T> IntoLuaResult<T> for Result<T, CoreError> {
self.map_err(to_lua_error)
}
}
/// Extension trait for Result<T, PoisonError> to convert to Result<T, CoreError>
pub trait IntoCoreError<T> {
fn with_lock_error(self, err: CoreError) -> Result<T, CoreError>;
}
impl<T, G> IntoCoreError<T> for Result<T, std::sync::PoisonError<G>> {
fn with_lock_error(self, err: CoreError) -> Result<T, CoreError> {
self.map_err(|_| err)
}
}
+56 -140
View File
@@ -1,5 +1,5 @@
use crate::path_shortening::shorten_path_with_cache;
use error::{IntoCoreError, IntoLuaResult};
use error::IntoLuaResult;
use fff::file_picker::FilePicker;
use fff::frecency::FrecencyTracker;
use fff::path_utils::expand_tilde;
@@ -13,7 +13,6 @@ use mlua::prelude::*;
use once_cell::sync::Lazy;
use path_shortening::PathShortenStrategy;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::time::Duration;
mod error;
@@ -27,18 +26,15 @@ static GLOBAL: MiMalloc = MiMalloc;
// the global state for neovim lives here for efficiency
// lua ffi is pretty bad with the overhead of converting raw pointer into tables
pub static FILE_PICKER: Lazy<SharedPicker> = Lazy::new(|| Arc::new(RwLock::new(None)));
pub static FRECENCY: Lazy<SharedFrecency> = Lazy::new(|| Arc::new(RwLock::new(None)));
pub static QUERY_TRACKER: Lazy<SharedQueryTracker> = Lazy::new(|| Arc::new(RwLock::new(None)));
pub static FILE_PICKER: Lazy<SharedPicker> = Lazy::new(SharedPicker::default);
pub static FRECENCY: Lazy<SharedFrecency> = Lazy::new(SharedFrecency::default);
pub static QUERY_TRACKER: Lazy<SharedQueryTracker> = Lazy::new(SharedQueryTracker::default);
pub fn init_db(
_: &Lua,
(frecency_db_path, history_db_path, use_unsafe_no_lock): (String, String, bool),
) -> LuaResult<bool> {
let mut frecency = FRECENCY
.write()
.with_lock_error(Error::AcquireFrecencyLock)
.into_lua_result()?;
let mut frecency = FRECENCY.write().into_lua_result()?;
if frecency.is_some() {
*frecency = None;
}
@@ -48,12 +44,9 @@ pub fn init_db(
drop(frecency);
// Spawn background GC to purge stale entries without blocking startup
let _ = FrecencyTracker::spawn_gc(Arc::clone(&FRECENCY), frecency_db_path, use_unsafe_no_lock);
let _ = FRECENCY.spawn_gc(frecency_db_path, use_unsafe_no_lock);
let mut query_tracker = QUERY_TRACKER
.write()
.with_lock_error(Error::AcquireFrecencyLock)
.into_lua_result()?;
let mut query_tracker = QUERY_TRACKER.write().into_lua_result()?;
if query_tracker.is_some() {
*query_tracker = None;
}
@@ -66,40 +59,34 @@ pub fn init_db(
}
pub fn destroy_frecency_db(_: &Lua, _: ()) -> LuaResult<bool> {
let mut frecency = FRECENCY
.write()
.with_lock_error(Error::AcquireFrecencyLock)
.into_lua_result()?;
let mut frecency = FRECENCY.write().into_lua_result()?;
*frecency = None;
Ok(true)
}
pub fn destroy_query_db(_: &Lua, _: ()) -> LuaResult<bool> {
let mut query_tracker = QUERY_TRACKER
.write()
.with_lock_error(Error::AcquireFrecencyLock)
.into_lua_result()?;
let mut query_tracker = QUERY_TRACKER.write().into_lua_result()?;
*query_tracker = None;
Ok(true)
}
pub fn init_file_picker(_: &Lua, base_path: String) -> LuaResult<bool> {
{
let guard = FILE_PICKER
.read()
.with_lock_error(Error::AcquireItemLock)
.into_lua_result()?;
let guard = FILE_PICKER.read().into_lua_result()?;
if guard.is_some() {
return Ok(false);
}
}
FilePicker::new_with_shared_state(
base_path,
true,
FFFMode::Neovim,
Arc::clone(&FILE_PICKER),
Arc::clone(&FRECENCY),
FILE_PICKER.clone(),
FRECENCY.clone(),
fff::FilePickerOptions {
base_path,
warmup_mmap_cache: true,
mode: FFFMode::Neovim,
..Default::default()
},
)
.into_lua_result()?;
@@ -111,9 +98,7 @@ fn reinit_file_picker_internal(path: &Path) -> Result<(), Error> {
// a window where FILE_PICKER is None (which causes FilePickerMissing
// errors if the UI is searching concurrently).
{
let mut guard = FILE_PICKER
.write()
.with_lock_error(Error::AcquireItemLock)?;
let mut guard = FILE_PICKER.write()?;
if let Some(ref mut picker) = *guard {
// Signal cancellation BEFORE stopping — this tells any orphaned
// scan threads from this picker to discard their results.
@@ -126,11 +111,14 @@ fn reinit_file_picker_internal(path: &Path) -> Result<(), Error> {
// Create new picker — this atomically replaces the old one via write lock
FilePicker::new_with_shared_state(
path.to_string_lossy().to_string(),
true,
FFFMode::Neovim,
Arc::clone(&FILE_PICKER),
Arc::clone(&FRECENCY),
FILE_PICKER.clone(),
FRECENCY.clone(),
fff::FilePickerOptions {
base_path: path.to_string_lossy().to_string(),
warmup_mmap_cache: true,
mode: FFFMode::Neovim,
..Default::default()
},
)?;
Ok(())
@@ -172,10 +160,7 @@ pub fn restart_index_in_path(_: &Lua, new_path: String) -> LuaResult<()> {
}
pub fn scan_files(_: &Lua, _: ()) -> LuaResult<()> {
let mut file_picker = FILE_PICKER
.write()
.with_lock_error(Error::AcquireItemLock)
.into_lua_result()?;
let mut file_picker = FILE_PICKER.write().into_lua_result()?;
let picker = file_picker
.as_mut()
.ok_or(Error::FilePickerMissing)
@@ -207,10 +192,7 @@ pub fn fuzzy_search_files(
Option<usize>,
),
) -> LuaResult<LuaValue> {
let file_picker_guard = FILE_PICKER
.read()
.with_lock_error(Error::AcquireItemLock)
.into_lua_result()?;
let file_picker_guard = FILE_PICKER.read().into_lua_result()?;
let Some(ref picker) = *file_picker_guard else {
return Err(error::to_lua_error(Error::FilePickerMissing));
};
@@ -218,10 +200,7 @@ pub fn fuzzy_search_files(
let base_path = picker.base_path();
let min_combo_count = min_combo_count.unwrap_or(3);
let query_tracker_guard = QUERY_TRACKER
.read()
.with_lock_error(Error::AcquireFrecencyLock)
.into_lua_result()?;
let query_tracker_guard = QUERY_TRACKER.read().into_lua_result()?;
if query_tracker_guard.as_ref().is_none() {
tracing::warn!("Query tracker not initialized");
@@ -311,16 +290,12 @@ pub fn live_grep(
Option<u64>,
),
) -> LuaResult<LuaValue> {
let file_picker_guard = FILE_PICKER
.read()
.with_lock_error(Error::AcquireItemLock)
.into_lua_result()?;
let file_picker_guard = FILE_PICKER.read().into_lua_result()?;
let Some(ref picker) = *file_picker_guard else {
return Err(error::to_lua_error(Error::FilePickerMissing));
};
let parsed = fff::grep::parse_grep_query(&query);
let mode = match grep_mode.as_deref() {
Some("regex") => fff::GrepMode::Regex,
Some("fuzzy") => fff::GrepMode::Fuzzy,
@@ -340,17 +315,7 @@ pub fn live_grep(
classify_definitions: false,
};
let bigram_idx = picker.bigram_index.as_deref();
let bigram_overlay = picker.bigram_overlay.as_deref();
let result = fff::grep::grep_search(
picker.get_files(),
&parsed,
&options,
picker.cache_budget(),
bigram_idx,
bigram_overlay,
);
let result = picker.grep(&parsed, &options);
lua_types::GrepResultLua::from(result).into_lua(lua)
}
@@ -409,10 +374,7 @@ pub fn track_access(_: &Lua, file_path: String) -> LuaResult<bool> {
// Track access in frecency DB (expensive LMDB write, ~100-200ms)
// Do this WITHOUT holding FILE_PICKER lock to avoid blocking searches
let frecency_guard = FRECENCY
.read()
.with_lock_error(Error::AcquireFrecencyLock)
.into_lua_result()?;
let frecency_guard = FRECENCY.read().into_lua_result()?;
let Some(ref frecency) = *frecency_guard else {
return Ok(false);
};
@@ -422,18 +384,12 @@ pub fn track_access(_: &Lua, file_path: String) -> LuaResult<bool> {
drop(frecency_guard);
// Quick lock to update single file's frecency score in picker
let mut file_picker = FILE_PICKER
.write()
.with_lock_error(Error::AcquireItemLock)
.into_lua_result()?;
let mut file_picker = FILE_PICKER.write().into_lua_result()?;
let Some(ref mut picker) = *file_picker else {
return Err(error::to_lua_error(Error::FilePickerMissing));
};
let frecency_guard = FRECENCY
.read()
.with_lock_error(Error::AcquireFrecencyLock)
.into_lua_result()?;
let frecency_guard = FRECENCY.read().into_lua_result()?;
let Some(ref frecency) = *frecency_guard else {
return Ok(false);
};
@@ -445,10 +401,7 @@ pub fn track_access(_: &Lua, file_path: String) -> LuaResult<bool> {
}
pub fn get_scan_progress(lua: &Lua, _: ()) -> LuaResult<LuaValue> {
let file_picker = FILE_PICKER
.read()
.with_lock_error(Error::AcquireItemLock)
.into_lua_result()?;
let file_picker = FILE_PICKER.read().into_lua_result()?;
let picker = file_picker
.as_ref()
.ok_or(Error::FilePickerMissing)
@@ -462,10 +415,7 @@ pub fn get_scan_progress(lua: &Lua, _: ()) -> LuaResult<LuaValue> {
}
pub fn is_scanning(_: &Lua, _: ()) -> LuaResult<bool> {
let file_picker = FILE_PICKER
.read()
.with_lock_error(Error::AcquireItemLock)
.into_lua_result()?;
let file_picker = FILE_PICKER.read().into_lua_result()?;
let picker = file_picker
.as_ref()
.ok_or(Error::FilePickerMissing)
@@ -474,10 +424,7 @@ pub fn is_scanning(_: &Lua, _: ()) -> LuaResult<bool> {
}
pub fn get_git_root(_: &Lua, _: ()) -> LuaResult<Option<String>> {
let file_picker = FILE_PICKER
.read()
.with_lock_error(Error::AcquireItemLock)
.into_lua_result()?;
let file_picker = FILE_PICKER.read().into_lua_result()?;
let Some(ref picker) = *file_picker else {
return Ok(None);
};
@@ -486,22 +433,16 @@ pub fn get_git_root(_: &Lua, _: ()) -> LuaResult<Option<String>> {
}
pub fn refresh_git_status(_: &Lua, _: ()) -> LuaResult<usize> {
FilePicker::refresh_git_status(&FILE_PICKER, &FRECENCY).into_lua_result()
FILE_PICKER.refresh_git_status(&FRECENCY).into_lua_result()
}
pub fn update_single_file_frecency(_: &Lua, file_path: String) -> LuaResult<bool> {
let frecency_guard = FRECENCY
.read()
.with_lock_error(Error::AcquireFrecencyLock)
.into_lua_result()?;
let frecency_guard = FRECENCY.read().into_lua_result()?;
let Some(ref frecency) = *frecency_guard else {
return Ok(false);
};
let mut file_picker = FILE_PICKER
.write()
.with_lock_error(Error::AcquireItemLock)
.into_lua_result()?;
let mut file_picker = FILE_PICKER.write().into_lua_result()?;
let Some(ref mut picker) = *file_picker else {
return Err(error::to_lua_error(Error::FilePickerMissing));
};
@@ -513,10 +454,7 @@ pub fn update_single_file_frecency(_: &Lua, file_path: String) -> LuaResult<bool
}
pub fn stop_background_monitor(_: &Lua, _: ()) -> LuaResult<bool> {
let mut file_picker = FILE_PICKER
.write()
.with_lock_error(Error::AcquireItemLock)
.into_lua_result()?;
let mut file_picker = FILE_PICKER.write().into_lua_result()?;
let Some(ref mut picker) = *file_picker else {
return Err(error::to_lua_error(Error::FilePickerMissing));
};
@@ -527,10 +465,7 @@ pub fn stop_background_monitor(_: &Lua, _: ()) -> LuaResult<bool> {
}
pub fn cleanup_file_picker(_: &Lua, _: ()) -> LuaResult<bool> {
let mut file_picker = FILE_PICKER
.write()
.with_lock_error(Error::AcquireItemLock)
.into_lua_result()?;
let mut file_picker = FILE_PICKER.write().into_lua_result()?;
if let Some(picker) = file_picker.take() {
drop(picker);
::tracing::info!("FilePicker cleanup completed");
@@ -548,10 +483,7 @@ pub fn cancel_scan(_: &Lua, _: ()) -> LuaResult<bool> {
pub fn track_query_completion(_: &Lua, (query, file_path): (String, String)) -> LuaResult<bool> {
// Get the project path before spawning thread
let project_path = {
let file_picker = FILE_PICKER
.read()
.with_lock_error(Error::AcquireItemLock)
.into_lua_result()?;
let file_picker = FILE_PICKER.read().into_lua_result()?;
let Some(ref picker) = *file_picker else {
return Ok(false);
};
@@ -568,9 +500,10 @@ pub fn track_query_completion(_: &Lua, (query, file_path): (String, String)) ->
};
// Spawn background thread to do the actual tracking (expensive DB write)
let query_tracker = Arc::clone(&QUERY_TRACKER);
let query_tracker = QUERY_TRACKER.clone();
std::thread::spawn(move || {
if let Ok(Some(tracker)) = query_tracker.write().as_deref_mut()
if let Ok(mut guard) = query_tracker.write()
&& let Some(tracker) = guard.as_mut()
&& let Err(e) = tracker.track_query_completion(&query, &project_path, &file_path)
{
tracing::error!(
@@ -587,20 +520,14 @@ pub fn track_query_completion(_: &Lua, (query, file_path): (String, String)) ->
pub fn get_historical_query(_: &Lua, offset: usize) -> LuaResult<Option<String>> {
let project_path = {
let file_picker = FILE_PICKER
.read()
.with_lock_error(Error::AcquireItemLock)
.into_lua_result()?;
let file_picker = FILE_PICKER.read().into_lua_result()?;
let Some(ref picker) = *file_picker else {
return Ok(None);
};
picker.base_path().to_path_buf()
};
let query_tracker = QUERY_TRACKER
.read()
.with_lock_error(Error::AcquireFrecencyLock)
.into_lua_result()?;
let query_tracker = QUERY_TRACKER.read().into_lua_result()?;
let Some(ref tracker) = *query_tracker else {
return Ok(None);
};
@@ -612,19 +539,17 @@ pub fn get_historical_query(_: &Lua, offset: usize) -> LuaResult<Option<String>>
pub fn track_grep_query(_: &Lua, query: String) -> LuaResult<bool> {
let project_path = {
let file_picker = FILE_PICKER
.read()
.with_lock_error(Error::AcquireItemLock)
.into_lua_result()?;
let file_picker = FILE_PICKER.read().into_lua_result()?;
let Some(ref picker) = *file_picker else {
return Ok(false);
};
picker.base_path().to_path_buf()
};
let query_tracker = Arc::clone(&QUERY_TRACKER);
let query_tracker = QUERY_TRACKER.clone();
std::thread::spawn(move || {
if let Ok(Some(tracker)) = query_tracker.write().as_deref_mut()
if let Ok(mut guard) = query_tracker.write()
&& let Some(ref mut tracker) = *guard
&& let Err(e) = tracker.track_grep_query(&query, &project_path)
{
tracing::error!(
@@ -640,20 +565,14 @@ pub fn track_grep_query(_: &Lua, query: String) -> LuaResult<bool> {
pub fn get_historical_grep_query(_: &Lua, offset: usize) -> LuaResult<Option<String>> {
let project_path = {
let file_picker = FILE_PICKER
.read()
.with_lock_error(Error::AcquireItemLock)
.into_lua_result()?;
let file_picker = FILE_PICKER.read().into_lua_result()?;
let Some(ref picker) = *file_picker else {
return Ok(None);
};
picker.base_path().to_path_buf()
};
let query_tracker = QUERY_TRACKER
.read()
.with_lock_error(Error::AcquireFrecencyLock)
.into_lua_result()?;
let query_tracker = QUERY_TRACKER.read().into_lua_result()?;
let Some(ref tracker) = *query_tracker else {
return Ok(None);
};
@@ -669,10 +588,7 @@ pub fn wait_for_initial_scan(_: &Lua, timeout_ms: Option<u64>) -> LuaResult<bool
// Holding a read lock while polling would deadlock: the scan thread
// needs a write lock to finish, but can't acquire it while we hold the read lock.
let scan_signal = {
let file_picker = FILE_PICKER
.read()
.with_lock_error(Error::AcquireItemLock)
.into_lua_result()?;
let file_picker = FILE_PICKER.read().into_lua_result()?;
let picker = file_picker
.as_ref()
.ok_or(Error::FilePickerMissing)