1use std::path::{Path, PathBuf};
2use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, Weak};
3use std::time::{Duration, Instant};
4
5use crate::dbs::lmdb::spawn_lmdb_gc;
6use crate::error::Error;
7use crate::file_picker::FilePicker;
8use crate::frecency::FrecencyTracker;
9use crate::git::GitStatusCache;
10use crate::query_tracker::QueryTracker;
11use crate::scan::ScanJob;
12
13fn wait_for_git_index_lock_release(git_root: &Path) {
20 const GIT_LOCK_POLL: Duration = Duration::from_millis(10);
21 const GIT_LOCK_MAX_WAIT: Duration = Duration::from_millis(500);
22
23 let lock = git_root.join(".git").join("index.lock");
24 if !lock.exists() {
26 return;
27 }
28 let deadline = Instant::now() + GIT_LOCK_MAX_WAIT;
29 while lock.exists() && Instant::now() < deadline {
30 std::thread::sleep(GIT_LOCK_POLL);
31 }
32 if lock.exists() {
33 tracing::warn!(
34 "Proceeding with git status refresh despite lingering \
35 .git/index.lock at {} — will retry once it clears",
36 lock.display()
37 );
38 }
39}
40
41#[derive(Clone, Default)]
58pub struct SharedFilePicker(pub(crate) Arc<SharedPickerInner>);
59
60pub struct SharedPickerInner {
61 picker: parking_lot::RwLock<Option<FilePicker>>,
62}
63
64impl Default for SharedPickerInner {
65 fn default() -> Self {
66 Self {
67 picker: parking_lot::RwLock::new(None),
68 }
69 }
70}
71
72#[derive(Clone)]
74pub(crate) struct WeakFilePicker(Weak<SharedPickerInner>);
75
76impl WeakFilePicker {
77 pub(crate) fn upgrade(&self) -> Option<SharedFilePicker> {
83 self.0.upgrade().map(SharedFilePicker)
84 }
85}
86
87impl std::fmt::Debug for SharedFilePicker {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 f.debug_tuple("SharedPicker").field(&"..").finish()
90 }
91}
92
93impl SharedFilePicker {
94 pub fn read(&self) -> Result<parking_lot::RwLockReadGuard<'_, Option<FilePicker>>, Error> {
95 Ok(self.0.picker.read())
96 }
97
98 pub fn write(&self) -> Result<parking_lot::RwLockWriteGuard<'_, Option<FilePicker>>, Error> {
99 Ok(self.0.picker.write())
100 }
101
102 pub(crate) fn weaken(&self) -> WeakFilePicker {
105 WeakFilePicker(Arc::downgrade(&self.0))
106 }
107
108 pub fn need_complex_rebuild(&self) -> bool {
111 let guard = self.0.picker.read();
112 guard
113 .as_ref()
114 .is_some_and(|p| p.has_mmap_cache() || p.has_content_indexing())
115 }
116
117 pub fn wait_for_scan(&self, timeout: Duration) -> bool {
120 let signal = {
121 let guard = self.0.picker.read();
122 match &*guard {
123 Some(picker) => Arc::clone(&picker.signals.scanning),
124 None => return true,
125 }
126 };
127
128 let start = std::time::Instant::now();
129 while signal.load(std::sync::atomic::Ordering::Acquire) {
130 if start.elapsed() >= timeout {
131 return false;
132 }
133 std::thread::sleep(Duration::from_millis(10));
134 }
135 true
136 }
137
138 pub fn wait_for_watcher(&self, timeout: Duration) -> bool {
141 let watch_ready_signal = {
142 let guard = self.0.picker.read();
143 match &*guard {
144 Some(picker) => Arc::clone(&picker.signals.watcher_ready),
145 None => return true,
146 }
147 };
148
149 let start = std::time::Instant::now();
150 while !watch_ready_signal.load(std::sync::atomic::Ordering::Acquire) {
151 if start.elapsed() >= timeout {
152 return false;
153 }
154 std::thread::sleep(Duration::from_millis(10));
155 }
156 true
157 }
158
159 pub fn wait_for_indexing_complete(&self, timeout: Duration) -> bool {
162 let (scanning, post_scan_active) = {
163 let guard = self.0.picker.read();
164 match &*guard {
165 Some(picker) => (
166 Arc::clone(&picker.signals.scanning),
167 Arc::clone(&picker.signals.post_scan_indexing_active),
168 ),
169 None => return true,
170 }
171 };
172
173 let start = std::time::Instant::now();
174 loop {
175 if start.elapsed() >= timeout {
176 return false;
177 }
178 let s = scanning.load(std::sync::atomic::Ordering::Acquire);
179 let p = post_scan_active.load(std::sync::atomic::Ordering::Acquire);
180 if !s && !p {
181 return true;
182 }
183 std::thread::sleep(Duration::from_millis(10));
184 }
185 }
186
187 pub fn trigger_full_rescan_async(&self, shared_frecency: &SharedFrecency) -> Result<(), Error> {
191 match ScanJob::new_rescan(self, shared_frecency)? {
192 Some(job) => {
193 job.spawn();
194 }
195 None => {
196 if let Ok(guard) = self.read()
198 && let Some(picker) = guard.as_ref()
199 {
200 picker
201 .scan_signals()
202 .rescan_pending
203 .store(true, std::sync::atomic::Ordering::Release);
204 tracing::info!(
205 "Full rescan requested while another scan is active — \
206 deferred via rescan_pending flag"
207 );
208 }
209 }
210 }
211 Ok(())
212 }
213
214 pub fn refresh_git_status(&self, shared_frecency: &SharedFrecency) -> Result<usize, Error> {
216 use tracing::debug;
217
218 let git_status = {
219 let guard = self.read()?;
220 let Some(ref picker) = *guard else {
221 return Err(Error::FilePickerMissing);
222 };
223
224 let git_root = picker.git_root().map(|p| p.to_path_buf());
225 drop(guard); debug!(?git_root, "Refreshing git status for picker");
229
230 if let Some(ref root) = git_root {
231 wait_for_git_index_lock_release(root);
232 }
233
234 GitStatusCache::read_git_status(
235 git_root.as_deref(),
236 &mut crate::git::default_status_options(),
237 )
238 };
239
240 let mut guard = self.write()?;
241 let picker = guard.as_mut().ok_or(Error::FilePickerMissing)?;
242
243 let statuses_count = if let Some(git_status) = git_status {
244 let count = git_status.statuses_len();
245 picker.update_git_statuses(git_status, shared_frecency)?;
246 count
247 } else {
248 0
249 };
250
251 Ok(statuses_count)
252 }
253}
254
255#[derive(Clone)]
257pub struct SharedFrecency {
258 inner: Arc<RwLock<Option<FrecencyTracker>>>,
259 enabled: bool,
260}
261
262impl Default for SharedFrecency {
263 fn default() -> Self {
264 Self {
265 inner: Arc::new(RwLock::new(None)),
266 enabled: true,
267 }
268 }
269}
270
271impl std::fmt::Debug for SharedFrecency {
272 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273 f.debug_tuple("SharedFrecency").field(&"..").finish()
274 }
275}
276
277impl SharedFrecency {
278 pub fn noop() -> Self {
280 Self {
281 inner: Arc::new(RwLock::new(None)),
282 enabled: false,
283 }
284 }
285
286 pub fn read(&self) -> Result<RwLockReadGuard<'_, Option<FrecencyTracker>>, Error> {
287 self.inner.read().map_err(|_| Error::AcquireFrecencyLock)
288 }
289
290 pub fn write(&self) -> Result<RwLockWriteGuard<'_, Option<FrecencyTracker>>, Error> {
291 self.inner.write().map_err(|_| Error::AcquireFrecencyLock)
292 }
293
294 pub fn init(&self, tracker: FrecencyTracker) -> Result<(), Error> {
295 if !self.enabled {
296 return Ok(());
297 }
298
299 {
300 let mut guard = self.write()?;
301 *guard = Some(tracker);
302 }
303
304 spawn_lmdb_gc(self.inner.clone());
307 Ok(())
308 }
309
310 pub fn destroy(&self) -> Result<Option<PathBuf>, Error> {
319 let mut guard = self.write()?;
320 let Some(tracker) = guard.take() else {
321 return Ok(None);
322 };
323 let db_path = tracker.db_path().to_path_buf();
324 drop(tracker);
326 drop(guard);
327 std::fs::remove_dir_all(&db_path).map_err(|source| Error::RemoveDbDir {
328 path: db_path.clone(),
329 source,
330 })?;
331 Ok(Some(db_path))
332 }
333}
334
335#[derive(Clone)]
337pub struct SharedQueryTracker {
338 inner: Arc<RwLock<Option<QueryTracker>>>,
339 enabled: bool,
340}
341
342impl Default for SharedQueryTracker {
343 fn default() -> Self {
344 Self {
345 inner: Arc::new(RwLock::new(None)),
346 enabled: true,
347 }
348 }
349}
350
351impl std::fmt::Debug for SharedQueryTracker {
352 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353 f.debug_tuple("SharedQueryTracker").field(&"..").finish()
354 }
355}
356
357impl SharedQueryTracker {
358 pub fn noop() -> Self {
360 Self {
361 inner: Arc::new(RwLock::new(None)),
362 enabled: false,
363 }
364 }
365
366 pub fn read(&self) -> Result<RwLockReadGuard<'_, Option<QueryTracker>>, Error> {
367 self.inner.read().map_err(|_| Error::AcquireFrecencyLock)
368 }
369
370 pub fn write(&self) -> Result<RwLockWriteGuard<'_, Option<QueryTracker>>, Error> {
371 self.inner.write().map_err(|_| Error::AcquireFrecencyLock)
372 }
373
374 pub fn init(&self, tracker: QueryTracker) -> Result<(), Error> {
377 if !self.enabled {
378 return Ok(());
379 }
380 {
381 let mut guard = self.write()?;
382 *guard = Some(tracker);
383 }
384
385 spawn_lmdb_gc(self.inner.clone());
386 Ok(())
387 }
388
389 pub fn destroy(&self) -> Result<Option<PathBuf>, Error> {
398 let mut guard = self.write()?;
399 let Some(tracker) = guard.take() else {
400 return Ok(None);
401 };
402 let db_path = tracker.db_path().to_path_buf();
403 drop(tracker);
404 drop(guard);
405 std::fs::remove_dir_all(&db_path).map_err(|source| Error::RemoveDbDir {
406 path: db_path.clone(),
407 source,
408 })?;
409 Ok(Some(db_path))
410 }
411}