Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b32971cb7 |
+38
-49
@@ -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)),
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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::*;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user