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