Skip to main content

fff_search/
shared.rs

1use std::path::PathBuf;
2use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
3use std::time::Duration;
4
5use crate::error::Error;
6use crate::file_picker::FilePicker;
7use crate::frecency::FrecencyTracker;
8use crate::git::GitStatusCache;
9use crate::query_tracker::QueryTracker;
10
11/// Thread-safe shared handle to the [`FilePicker`] instance.
12///
13/// Uses `parking_lot::RwLock` which is reader-fair — new readers are not
14/// blocked when a writer is waiting, preventing search query stalls during
15/// background bigram builds or watcher writes.
16///
17/// `Clone` gives a new handle to the same picker (Arc clone).
18/// `Default` creates an empty handle suitable for `Lazy::new(SharedPicker::default)`.
19#[derive(Clone, Default)]
20pub struct SharedPicker(pub(crate) Arc<parking_lot::RwLock<Option<FilePicker>>>);
21
22impl std::fmt::Debug for SharedPicker {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        f.debug_tuple("SharedPicker").field(&"..").finish()
25    }
26}
27
28impl SharedPicker {
29    pub fn read(&self) -> Result<parking_lot::RwLockReadGuard<'_, Option<FilePicker>>, Error> {
30        Ok(self.0.read())
31    }
32
33    pub fn write(&self) -> Result<parking_lot::RwLockWriteGuard<'_, Option<FilePicker>>, Error> {
34        Ok(self.0.write())
35    }
36
37    /// Return `true` if this is an instance of the picker that requires a complicated post-scan
38    /// indexing/cache warmup job. The indexing is not crazy but it takes time.
39    pub fn need_complex_rebuild(&self) -> bool {
40        let guard = self.0.read();
41        guard
42            .as_ref()
43            .is_some_and(|p| p.need_enable_mmap_cache() || p.need_enable_content_indexing())
44    }
45
46    /// Block until the background filesystem scan finishes.
47    /// Returns `true` if scan completed, `false` on timeout.
48    pub fn wait_for_scan(&self, timeout: Duration) -> bool {
49        let signal = {
50            let guard = self.0.read();
51            match &*guard {
52                Some(picker) => picker.scan_signal(),
53                None => return true,
54            }
55        };
56
57        let start = std::time::Instant::now();
58        while signal.load(std::sync::atomic::Ordering::Acquire) {
59            if start.elapsed() >= timeout {
60                return false;
61            }
62            std::thread::sleep(Duration::from_millis(10));
63        }
64        true
65    }
66
67    /// Block until the background file watcher is ready.
68    /// Returns `true` if watcher ready, `false` on timeout.
69    pub fn wait_for_watcher(&self, timeout: Duration) -> bool {
70        let signal = {
71            let guard = self.0.read();
72            match &*guard {
73                Some(picker) => picker.watcher_signal(),
74                None => return true,
75            }
76        };
77
78        let start = std::time::Instant::now();
79        while !signal.load(std::sync::atomic::Ordering::Acquire) {
80            if start.elapsed() >= timeout {
81                return false;
82            }
83            std::thread::sleep(Duration::from_millis(10));
84        }
85        true
86    }
87
88    /// Refresh git statuses for all indexed files.
89    pub fn refresh_git_status(&self, shared_frecency: &SharedFrecency) -> Result<usize, Error> {
90        use git2::StatusOptions;
91        use tracing::debug;
92
93        let git_status = {
94            let guard = self.read()?;
95            let Some(ref picker) = *guard else {
96                return Err(Error::FilePickerMissing);
97            };
98
99            debug!(
100                "Refreshing git statuses for picker: {:?}",
101                picker.git_root()
102            );
103
104            GitStatusCache::read_git_status(
105                picker.git_root(),
106                StatusOptions::new()
107                    .include_untracked(true)
108                    .recurse_untracked_dirs(true)
109                    .include_unmodified(true)
110                    .exclude_submodules(true),
111            )
112        };
113
114        let mut guard = self.write()?;
115        let picker = guard.as_mut().ok_or(Error::FilePickerMissing)?;
116
117        let statuses_count = if let Some(git_status) = git_status {
118            let count = git_status.statuses_len();
119            picker.update_git_statuses(git_status, shared_frecency)?;
120            count
121        } else {
122            0
123        };
124
125        Ok(statuses_count)
126    }
127}
128
129/// Thread-safe shared handle to the [`FrecencyTracker`] instance.
130#[derive(Clone)]
131pub struct SharedFrecency {
132    inner: Arc<RwLock<Option<FrecencyTracker>>>,
133    enabled: bool,
134}
135
136impl Default for SharedFrecency {
137    fn default() -> Self {
138        Self {
139            inner: Arc::new(RwLock::new(None)),
140            enabled: true,
141        }
142    }
143}
144
145impl std::fmt::Debug for SharedFrecency {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.debug_tuple("SharedFrecency").field(&"..").finish()
148    }
149}
150
151impl SharedFrecency {
152    /// Creates a disabled instance that silently ignores all writes.
153    pub fn noop() -> Self {
154        Self {
155            inner: Arc::new(RwLock::new(None)),
156            enabled: false,
157        }
158    }
159
160    pub fn read(&self) -> Result<RwLockReadGuard<'_, Option<FrecencyTracker>>, Error> {
161        self.inner.read().map_err(|_| Error::AcquireFrecencyLock)
162    }
163
164    pub fn write(&self) -> Result<RwLockWriteGuard<'_, Option<FrecencyTracker>>, Error> {
165        self.inner.write().map_err(|_| Error::AcquireFrecencyLock)
166    }
167
168    /// Initialize the frecency tracker. No-op if this is a disabled instance.
169    pub fn init(&self, tracker: FrecencyTracker) -> Result<(), Error> {
170        if !self.enabled {
171            return Ok(());
172        }
173        let mut guard = self.write()?;
174        *guard = Some(tracker);
175        Ok(())
176    }
177
178    /// Spawn a background GC thread for this frecency tracker.
179    pub fn spawn_gc(
180        &self,
181        db_path: String,
182        use_unsafe_no_lock: bool,
183    ) -> crate::Result<std::thread::JoinHandle<()>> {
184        FrecencyTracker::spawn_gc(self.clone(), db_path, use_unsafe_no_lock)
185    }
186
187    /// Drop the in-memory tracker and delete the on-disk database directory.
188    ///
189    /// Acquires the write lock, ensuring all readers (including any active mmap
190    /// access) are finished before the LMDB environment is closed and the files
191    /// are removed.
192    ///
193    /// Returns `Ok(Some(path))` with the deleted path, or `Ok(None)` if no
194    /// tracker was initialized.
195    pub fn destroy(&self) -> Result<Option<PathBuf>, Error> {
196        let mut guard = self.write()?;
197        let Some(tracker) = guard.take() else {
198            return Ok(None);
199        };
200        let db_path = tracker.db_path().to_path_buf();
201        // Drop closes the LMDB env and unmaps the files
202        drop(tracker);
203        drop(guard);
204        std::fs::remove_dir_all(&db_path).map_err(|source| Error::RemoveDbDir {
205            path: db_path.clone(),
206            source,
207        })?;
208        Ok(Some(db_path))
209    }
210}
211
212/// Thread-safe shared handle to the [`QueryTracker`] instance.
213#[derive(Clone)]
214pub struct SharedQueryTracker {
215    inner: Arc<RwLock<Option<QueryTracker>>>,
216    enabled: bool,
217}
218
219impl Default for SharedQueryTracker {
220    fn default() -> Self {
221        Self {
222            inner: Arc::new(RwLock::new(None)),
223            enabled: true,
224        }
225    }
226}
227
228impl std::fmt::Debug for SharedQueryTracker {
229    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230        f.debug_tuple("SharedQueryTracker").field(&"..").finish()
231    }
232}
233
234impl SharedQueryTracker {
235    /// Creates a disabled instance that silently ignores all writes.
236    pub fn noop() -> Self {
237        Self {
238            inner: Arc::new(RwLock::new(None)),
239            enabled: false,
240        }
241    }
242
243    pub fn read(&self) -> Result<RwLockReadGuard<'_, Option<QueryTracker>>, Error> {
244        self.inner.read().map_err(|_| Error::AcquireFrecencyLock)
245    }
246
247    pub fn write(&self) -> Result<RwLockWriteGuard<'_, Option<QueryTracker>>, Error> {
248        self.inner.write().map_err(|_| Error::AcquireFrecencyLock)
249    }
250
251    /// Initialize the query tracker. No-op if this is a disabled instance.
252    pub fn init(&self, tracker: QueryTracker) -> Result<(), Error> {
253        if !self.enabled {
254            return Ok(());
255        }
256        let mut guard = self.write()?;
257        *guard = Some(tracker);
258        Ok(())
259    }
260
261    /// Drop the in-memory tracker and delete the on-disk database directory.
262    ///
263    /// Acquires the write lock, ensuring all readers (including any active mmap
264    /// access) are finished before the LMDB environment is closed and the files
265    /// are removed.
266    ///
267    /// Returns `Ok(Some(path))` with the deleted path, or `Ok(None)` if no
268    /// tracker was initialized.
269    pub fn destroy(&self) -> Result<Option<PathBuf>, Error> {
270        let mut guard = self.write()?;
271        let Some(tracker) = guard.take() else {
272            return Ok(None);
273        };
274        let db_path = tracker.db_path().to_path_buf();
275        drop(tracker);
276        drop(guard);
277        std::fs::remove_dir_all(&db_path).map_err(|source| Error::RemoveDbDir {
278            path: db_path.clone(),
279            source,
280        })?;
281        Ok(Some(db_path))
282    }
283}