1use std::io::Read;
2use std::path::{Path, PathBuf};
3#[cfg(not(target_os = "windows"))]
4use std::sync::atomic::AtomicPtr;
5use std::sync::atomic::{AtomicI32, AtomicU8, AtomicU64, AtomicUsize, Ordering};
6
7#[cfg(not(target_os = "windows"))]
8use crate::constants::{FRESH_MMAP_THRESHOLD, MMAP_THRESHOLD};
9use crate::constants::{MAX_CACHED_CONTENT_BYTES, MAX_FFFILE_SIZE, PATH_BUF_SIZE};
10use crate::index::constraints::Constrainable;
11use crate::query_tracker::QueryMatchEntry;
12use crate::simd_path::ArenaPtr;
13use fff_query_parser::{FFFQuery, FuzzyQuery, Location};
14
15pub trait FFFStringStorage {
19 fn arena_for(&self, file: &FileItem) -> ArenaPtr;
21
22 fn base_arena(&self) -> ArenaPtr;
24 fn overflow_arena(&self) -> ArenaPtr;
26}
27
28impl FFFStringStorage for ArenaPtr {
29 #[inline]
30 fn arena_for(&self, _file: &FileItem) -> ArenaPtr {
31 *self
32 }
33
34 #[inline]
35 fn base_arena(&self) -> ArenaPtr {
36 *self
37 }
38
39 #[inline]
40 fn overflow_arena(&self) -> ArenaPtr {
41 *self
42 }
43}
44
45pub trait FileSliceExt {
46 fn live_count(&self) -> usize;
47}
48
49impl FileSliceExt for [FileItem] {
50 #[inline]
51 fn live_count(&self) -> usize {
52 self.iter().filter(|f| !f.is_deleted()).count()
53 }
54}
55
56pub struct FileItemFlags;
57
58impl FileItemFlags {
59 pub const BINARY: u8 = 1 << 0;
60 pub const DELETED: u8 = 1 << 1;
63 pub const OVERFLOW: u8 = 1 << 2;
66}
67
68pub struct DirFlags;
69
70impl DirFlags {
71 pub const OVERFLOW: u8 = 1 << 0;
72 pub const DELETED: u8 = 1 << 1;
73}
74
75#[derive(Debug)]
77pub struct DirItem {
78 flags: u8,
79 pub(crate) path: crate::simd_path::ChunkedString,
80 last_segment_offset: u16,
83 max_access_frecency: AtomicI32,
86}
87
88impl Clone for DirItem {
89 fn clone(&self) -> Self {
90 Self {
91 flags: self.flags,
92 path: self.path.clone(),
93 last_segment_offset: self.last_segment_offset,
94 max_access_frecency: AtomicI32::new(self.max_access_frecency()),
95 }
96 }
97}
98
99impl DirItem {
100 #[inline(always)]
101 pub fn is_overflow(&self) -> bool {
102 self.flags & DirFlags::OVERFLOW != 0
103 }
104
105 #[inline(always)]
106 pub fn is_deleted(&self) -> bool {
107 self.flags & DirFlags::DELETED != 0
108 }
109
110 pub(crate) fn set_deleted(&mut self, deleted: bool) -> bool {
112 if self.is_deleted() == deleted {
113 return false;
114 }
115 if deleted {
116 self.flags |= DirFlags::DELETED;
117 } else {
118 self.flags &= !DirFlags::DELETED;
119 }
120 true
121 }
122
123 pub(crate) fn new(path: crate::simd_path::ChunkedString, last_segment_offset: u16) -> Self {
124 Self {
125 path,
126 flags: 0,
127 last_segment_offset,
128 max_access_frecency: AtomicI32::new(0),
129 }
130 }
131
132 pub(crate) fn new_overflow(
134 path: crate::simd_path::ChunkedString,
135 last_segment_offset: u16,
136 ) -> Self {
137 Self {
138 path,
139 flags: DirFlags::OVERFLOW,
140 last_segment_offset,
141 max_access_frecency: AtomicI32::new(0),
142 }
143 }
144
145 #[inline]
147 pub fn last_segment_offset(&self) -> u16 {
148 self.last_segment_offset
149 }
150
151 #[inline]
153 pub fn max_access_frecency(&self) -> i32 {
154 self.max_access_frecency.load(Ordering::Relaxed)
155 }
156
157 #[inline]
160 pub fn update_frecency_if_larger(&self, score: i32) {
161 self.max_access_frecency.fetch_max(score, Ordering::Relaxed);
162 }
163
164 #[inline]
166 pub fn reset_frecency(&self) {
167 self.max_access_frecency.store(0, Ordering::Relaxed);
168 }
169
170 pub(crate) fn read_relative_path<'a>(&self, arena: ArenaPtr, buf: &'a mut [u8]) -> &'a str {
171 self.path.read_to_buf(arena, buf)
172 }
173
174 pub fn relative_path(&self, arena: impl FFFStringStorage) -> String {
176 let mut out = String::new();
177 let ptr = if self.is_overflow() {
178 arena.overflow_arena()
179 } else {
180 arena.base_arena()
181 };
182
183 self.path.write_to_string(ptr, &mut out);
184 out
185 }
186
187 pub fn write_dir_name(&self, arena: ArenaPtr, out: &mut String) {
189 out.clear();
190 let total = self.path.byte_len as usize;
191 let offset = self.last_segment_offset as usize;
192 if offset >= total {
193 return;
194 }
195 let mut buf = [0u8; PATH_BUF_SIZE];
197 let full = self.path.read_to_buf(arena, &mut buf);
198 out.push_str(&full[offset..]);
199 }
200
201 pub fn dir_name(&self, arena: impl FFFStringStorage) -> String {
203 let mut out = String::new();
204 let ptr = if self.is_overflow() {
205 arena.overflow_arena()
206 } else {
207 arena.base_arena()
208 };
209 self.write_dir_name(ptr, &mut out);
210 out
211 }
212
213 pub fn absolute_path(&self, arena: impl FFFStringStorage, base_path: &Path) -> PathBuf {
215 let rel = self.relative_path(arena);
216 if rel.is_empty() {
217 base_path.to_path_buf()
218 } else {
219 base_path.join(&rel)
220 }
221 }
222}
223
224impl Constrainable for DirItem {
225 #[inline]
226 fn write_file_name(&self, arena: ArenaPtr, out: &mut String) {
227 self.write_dir_name(arena, out);
229 }
230
231 #[inline]
232 fn write_relative_path(&self, arena: ArenaPtr, out: &mut String) {
233 self.path.write_to_string(arena, out);
234 }
235
236 #[inline]
237 fn git_status(&self) -> Option<git2::Status> {
238 None
239 }
240
241 #[inline]
242 fn is_overflow(&self) -> bool {
243 DirItem::is_overflow(self)
244 }
245}
246
247#[derive(Debug)]
248pub struct FileItem {
249 pub size: u64,
250 pub modified: u64,
251 pub access_frecency_score: i16,
252 pub modification_frecency_score: i16,
253 pub git_recency_score: i16,
254 pub git_status: Option<git2::Status>,
255 pub(crate) path: crate::simd_path::ChunkedString,
256 pub(crate) parent_dir_index: u32,
257 flags: AtomicU8,
258 #[cfg(not(target_os = "windows"))]
261 content: AtomicPtr<memmap2::Mmap>,
262}
263
264#[cfg(not(target_os = "windows"))]
265impl Drop for FileItem {
266 fn drop(&mut self) {
267 self.take_content();
268 }
269}
270
271impl Clone for FileItem {
272 fn clone(&self) -> Self {
273 Self {
274 path: self.path.clone(),
275 parent_dir_index: self.parent_dir_index,
276 size: self.size,
277 modified: self.modified,
278 access_frecency_score: self.access_frecency_score,
279 modification_frecency_score: self.modification_frecency_score,
280 git_recency_score: self.git_recency_score,
281 git_status: self.git_status,
282 flags: AtomicU8::new(self.flags.load(Ordering::Relaxed)),
283 #[cfg(not(target_os = "windows"))]
285 content: AtomicPtr::new(std::ptr::null_mut()),
286 }
287 }
288}
289
290pub const BINARY_CLASSIFICATION_CHUNK_SIZE: usize = 16 * 1024;
295
296#[inline]
298pub(crate) fn detect_binary_content(content: &[u8]) -> bool {
299 memchr::memchr(0, content).is_some()
300}
301
302impl FileItem {
303 pub fn new_raw(
304 filename_start: u16,
305 size: u64,
306 modified: u64,
307 git_status: Option<git2::Status>,
308 is_binary: bool,
309 ) -> Self {
310 let mut flags = 0u8;
311 if is_binary {
312 flags |= FileItemFlags::BINARY;
313 }
314
315 let mut path = crate::simd_path::ChunkedString::empty();
316 path.filename_offset = filename_start;
317
318 Self {
319 path,
320 parent_dir_index: u32::MAX,
321 size,
322 modified,
323 access_frecency_score: 0,
324 modification_frecency_score: 0,
325 git_recency_score: 0,
326 git_status,
327 flags: AtomicU8::new(flags),
328 #[cfg(not(target_os = "windows"))]
329 content: AtomicPtr::new(std::ptr::null_mut()),
330 }
331 }
332
333 pub fn absolute_path(&self, arena: impl FFFStringStorage, base_path: &Path) -> PathBuf {
335 let mut buf = [0u8; PATH_BUF_SIZE];
336 let rel = self.path.read_to_buf(arena.arena_for(self), &mut buf);
337 base_path.join(rel)
338 }
339
340 pub(crate) fn set_path(&mut self, path: crate::simd_path::ChunkedString) {
341 self.path = path;
342 }
343
344 pub fn dir_str(&self, arena: impl FFFStringStorage) -> String {
345 let mut s = String::with_capacity(64);
346 self.path.write_dir_to(arena.arena_for(self), &mut s);
347 s
348 }
349
350 pub(crate) fn write_dir_str(&self, arena: ArenaPtr, out: &mut String) {
351 self.path.write_dir_to(arena, out);
352 }
353
354 pub fn file_name(&self, arena: impl FFFStringStorage) -> String {
355 let mut s = String::with_capacity(32);
356 self.path.write_filename_to(arena.arena_for(self), &mut s);
357 s
358 }
359
360 pub(crate) fn write_file_name_from_arena(&self, arena: ArenaPtr, out: &mut String) {
361 self.path.write_filename_to(arena, out);
362 }
363
364 pub fn relative_path(&self, arena: impl FFFStringStorage) -> String {
365 let mut s = String::with_capacity(64);
366 self.path.write_to_string(arena.arena_for(self), &mut s);
367 s
368 }
369
370 pub(crate) fn write_relative_path_from_arena(&self, arena: ArenaPtr, out: &mut String) {
371 self.path.write_to_string(arena, out);
372 }
373
374 pub fn relative_path_len(&self) -> usize {
375 self.path.byte_len as usize
376 }
377
378 pub fn filename_offset_in_relative_path(&self) -> usize {
379 self.path.filename_offset as usize
380 }
381
382 pub(crate) fn relative_path_eq(&self, arena: ArenaPtr, other: &str) -> bool {
383 if other.len() != self.path.byte_len as usize {
384 return false;
385 }
386 let mut buf = [0u8; PATH_BUF_SIZE];
387 let mine = self.path.read_to_buf(arena, &mut buf);
388 mine == other
389 }
390
391 pub(crate) fn relative_path_starts_with(&self, arena: ArenaPtr, prefix: &str) -> bool {
392 let mut buf = [0u8; PATH_BUF_SIZE];
393 let path = self.path.read_to_buf(arena, &mut buf);
394 path.starts_with(prefix)
395 }
396
397 pub(crate) fn write_absolute_path<'a>(
401 &self,
402 arena: ArenaPtr,
403 base_path: &Path,
404 buf: &'a mut [u8; PATH_BUF_SIZE],
405 ) -> &'a Path {
406 let base = base_path.as_os_str().as_encoded_bytes();
407 let base_len = base.len();
408 buf[..base_len].copy_from_slice(base);
409 let sep_len = if base_len > 0 && base[base_len - 1] != std::path::MAIN_SEPARATOR as u8 {
410 buf[base_len] = std::path::MAIN_SEPARATOR as u8;
411 1
412 } else {
413 0
414 };
415
416 let base_end_idx = base_len + sep_len;
417 let relative_portion_str = self.path.read_to_buf(arena, &mut buf[base_end_idx..]);
418 let rel_len = relative_portion_str.len();
419 let total = base_end_idx + rel_len;
420 crate::path_utils::nativize_slashes_in_place(&mut buf[base_end_idx..total]);
424 Path::new(unsafe { std::str::from_utf8_unchecked(&buf[..total]) })
425 }
426
427 #[cfg(unix)]
434 pub(crate) fn write_relative_cstr<'a>(
435 &self,
436 arena: ArenaPtr,
437 buf: &'a mut [u8; PATH_BUF_SIZE],
438 ) -> &'a std::ffi::CStr {
439 let rel = self.path.read_to_buf(arena, &mut buf[..PATH_BUF_SIZE - 1]);
441 let n = rel.len();
442 buf[n] = 0;
443 unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(&buf[..=n]) }
446 }
447
448 #[inline]
449 pub fn total_frecency_score(&self) -> i32 {
450 self.access_frecency_score as i32 + self.modification_frecency_score as i32
451 }
452
453 #[allow(dead_code)]
454 #[inline]
455 pub(crate) fn is_likely_hot(&self) -> bool {
456 self.access_frecency_score > 0 || self.git_status.is_some()
457 }
458
459 #[inline]
461 pub(crate) fn read_trimmed_into_buf(
462 &self,
463 base_fd: i32,
464 base_path: &Path,
465 arena: ArenaPtr,
466 path_buf: &mut [u8; PATH_BUF_SIZE],
467 buf: &mut [u8],
468 ) -> usize {
469 #[cfg(unix)]
470 {
471 self.read_into_buf_unix(base_fd, base_path, arena, path_buf, buf)
472 }
473 #[cfg(not(unix))]
474 {
475 let _ = base_fd;
476 self.read_into_buf_std(base_path, arena, path_buf, buf)
477 }
478 }
479
480 #[cfg(unix)]
481 fn read_into_buf_unix(
482 &self,
483 base_fd: libc::c_int,
484 base_path: &Path,
485 arena: ArenaPtr,
486 path_buf: &mut [u8; PATH_BUF_SIZE],
487 buf: &mut [u8],
488 ) -> usize {
489 let fd = if base_fd >= 0 {
490 let relative_path = self.write_relative_cstr(arena, path_buf);
491 unsafe { libc::openat(base_fd, relative_path.as_ptr(), libc::O_RDONLY) }
494 } else {
495 use std::os::unix::io::IntoRawFd;
496 let abs = self.write_absolute_path(arena, base_path, path_buf);
497 match std::fs::File::open(abs) {
498 Ok(f) => f.into_raw_fd(),
499 Err(e) => {
500 tracing::error!(?e, "Failed to fopen file");
501 return 0;
502 }
503 }
504 };
505 if fd < 0 {
506 return 0;
507 }
508
509 let mut filled = 0usize;
510 while filled < buf.len() {
511 let n = unsafe {
514 libc::read(
515 fd,
516 buf[filled..].as_mut_ptr() as *mut libc::c_void,
517 (buf.len() - filled) as libc::size_t,
518 )
519 };
520 if n <= 0 {
521 break;
522 }
523 filled += n as usize;
524 }
525
526 unsafe { libc::close(fd) };
528 filled
529 }
530
531 #[cfg(not(unix))]
532 fn read_into_buf_std(
533 &self,
534 base_path: &Path,
535 arena: ArenaPtr,
536 path_buf: &mut [u8; PATH_BUF_SIZE],
537 buf: &mut [u8],
538 ) -> usize {
539 let abs = self.write_absolute_path(arena, base_path, path_buf);
540 let Ok(mut f) = std::fs::File::open(abs) else {
541 return 0;
542 };
543 let mut filled = 0usize;
544 while filled < buf.len() {
545 match f.read(&mut buf[filled..]) {
546 Ok(0) => break,
547 Ok(n) => filled += n,
548 Err(_) => return 0,
549 }
550 }
551 filled
552 }
553
554 #[inline]
555 pub fn is_binary(&self) -> bool {
556 self.flags.load(Ordering::Relaxed) & FileItemFlags::BINARY != 0
557 }
558
559 #[inline]
560 pub fn set_binary(&self, val: bool) {
561 if val {
562 self.flags
563 .fetch_or(FileItemFlags::BINARY, Ordering::Relaxed);
564 } else {
565 self.flags
566 .fetch_and(!FileItemFlags::BINARY, Ordering::Relaxed);
567 }
568 }
569
570 pub(crate) fn detect_binary_per_byte(&self, path: &Path, chunk: &mut [u8]) {
573 if self.size == 0 {
574 return;
575 }
576
577 let Ok(mut file) = std::fs::OpenOptions::new()
578 .write(false)
579 .read(true)
580 .open(path)
581 else {
582 tracing::error!(path = ?path.display(), "Failed to open indexed file");
583 return;
584 };
585
586 loop {
587 match file.read(chunk) {
588 Ok(0) => break,
589 Err(e) => {
590 tracing::error!(?e, "Failed to read file chunk");
591 break;
592 }
593 Ok(n) => {
594 if detect_binary_content(&chunk[..n]) {
595 self.set_binary(true);
596 }
597 }
598 }
599 }
600 }
601
602 #[inline]
603 pub fn is_deleted(&self) -> bool {
604 self.flags.load(Ordering::Relaxed) & FileItemFlags::DELETED != 0
605 }
606
607 #[inline]
608 #[doc(hidden)]
609 pub fn set_deleted(&self, val: bool) {
611 if val {
612 self.flags
613 .fetch_or(FileItemFlags::DELETED, Ordering::Relaxed);
614 } else {
615 self.flags
616 .fetch_and(!FileItemFlags::DELETED, Ordering::Relaxed);
617 }
618 }
619
620 #[inline]
621 pub fn is_overflow(&self) -> bool {
622 self.flags.load(Ordering::Relaxed) & FileItemFlags::OVERFLOW != 0
623 }
624
625 #[inline]
626 pub fn set_overflow(&self, val: bool) {
627 if val {
628 self.flags
629 .fetch_or(FileItemFlags::OVERFLOW, Ordering::Relaxed);
630 } else {
631 self.flags
632 .fetch_and(!FileItemFlags::OVERFLOW, Ordering::Relaxed);
633 }
634 }
635}
636
637impl FileItem {
638 #[cfg(not(target_os = "windows"))]
645 pub fn invalidate_mmap(&mut self, budget: &ContentCacheBudget) {
646 if self.take_content().is_some() {
647 budget.cached_count.fetch_sub(1, Ordering::Relaxed);
648 budget.cached_bytes.fetch_sub(self.size, Ordering::Relaxed);
649 }
650 }
651
652 #[cfg(not(target_os = "windows"))]
653 #[inline]
654 fn cached_content(&self) -> Option<&[u8]> {
655 let ptr = self.content.load(Ordering::Acquire);
656 (!ptr.is_null()).then(|| unsafe { (&*ptr).as_ref() })
658 }
659
660 #[cfg(not(target_os = "windows"))]
661 fn take_content(&mut self) -> Option<Box<memmap2::Mmap>> {
662 let ptr = self.content.swap(std::ptr::null_mut(), Ordering::AcqRel);
663 (!ptr.is_null()).then(|| unsafe { Box::from_raw(ptr) })
665 }
666
667 #[cfg(target_os = "windows")]
668 pub fn invalidate_mmap(&mut self, _: &ContentCacheBudget) {}
669
670 pub fn update_metadata(
671 &mut self,
672 budget: &ContentCacheBudget,
673 modified_secs: Option<u64>,
674 new_size: Option<u64>,
675 ) {
676 if let Some(modified) = modified_secs
677 && self.modified < modified
678 {
679 self.modified = modified;
680 }
681
682 self.invalidate_mmap(budget);
683
684 if let Some(size) = new_size {
685 self.size = size;
686 }
687 }
688
689 #[cfg(target_os = "windows")]
702 pub(crate) fn get_cached_content(
703 &self,
704 _arena: ArenaPtr,
705 _base_path: &Path,
706 _budget: &ContentCacheBudget,
707 ) -> Option<&[u8]> {
708 None
709 }
710
711 #[cfg(not(target_os = "windows"))]
715 pub(crate) fn get_cached_content(
716 &self,
717 arena: ArenaPtr,
718 base_path: &Path,
719 budget: &ContentCacheBudget,
720 ) -> Option<&[u8]> {
721 if let Some(content) = self.cached_content() {
722 return Some(content);
723 }
724
725 if self.size < MMAP_THRESHOLD || self.size > budget.max_file_size {
726 return None;
727 }
728
729 let count = budget.cached_count.load(Ordering::Relaxed);
731 let bytes = budget.cached_bytes.load(Ordering::Relaxed);
732 let max_files = budget.max_files;
733 let max_bytes = budget.max_bytes;
734 if count >= max_files || bytes + self.size > max_bytes {
735 return None;
736 }
737
738 let path = self.absolute_path(arena, base_path);
739 let file = std::fs::File::open(&path).ok()?;
740 let mmap = unsafe { memmap2::Mmap::map(&file) }.ok()?;
744 let fresh = Box::into_raw(Box::new(mmap));
745 match self.content.compare_exchange(
746 std::ptr::null_mut(),
747 fresh,
748 Ordering::AcqRel,
749 Ordering::Acquire,
750 ) {
751 Ok(_) => {
752 budget.cached_count.fetch_add(1, Ordering::Relaxed);
753 budget.cached_bytes.fetch_add(self.size, Ordering::Relaxed);
754 }
755 Err(_) => drop(unsafe { Box::from_raw(fresh) }),
757 }
758
759 self.cached_content()
760 }
761
762 #[inline]
768 pub(crate) fn get_content_for_search<'a>(
769 &'a self,
770 buf: &'a mut Vec<u8>,
771 #[cfg_attr(target_os = "windows", allow(unused_variables))] mmap_slot: &'a mut MmapSlot,
772 arena: ArenaPtr,
773 base_path: &Path,
774 budget: &ContentCacheBudget,
775 ) -> Option<&'a [u8]> {
776 #[cfg(not(target_os = "windows"))]
777 {
778 if let Some(cached) = self.get_cached_content(arena, base_path, budget) {
782 return Some(cached);
783 }
784 }
785
786 let max_file_size = budget.max_file_size;
787 if self.is_binary() || self.size == 0 || self.size > max_file_size {
788 return None;
789 }
790
791 let abs = self.absolute_path(arena, base_path);
792
793 #[cfg(not(target_os = "windows"))]
794 if self.size >= FRESH_MMAP_THRESHOLD {
795 let file = std::fs::File::open(&abs).ok()?;
796 let mmap = unsafe { memmap2::Mmap::map(&file) }.ok()?;
797 let stored = mmap_slot.insert(mmap);
798 return Some(&stored[..]);
799 } else {
800 let _ = (mmap_slot, arena);
801 }
802
803 let len = self.size as usize;
804 buf.resize(len, 0);
805
806 let mut file = std::fs::File::open(&abs).ok()?;
807 file.read_exact(buf).ok()?;
808 Some(buf.as_slice())
809 }
810}
811
812#[cfg(not(target_os = "windows"))]
816pub type MmapSlot = Option<memmap2::Mmap>;
817#[cfg(target_os = "windows")]
818pub type MmapSlot = ();
819
820impl Constrainable for FileItem {
821 #[inline]
822 fn write_file_name(&self, arena: ArenaPtr, out: &mut String) {
823 self.path.write_filename_to(arena, out);
824 }
825
826 #[inline]
827 fn write_relative_path(&self, arena: ArenaPtr, out: &mut String) {
828 self.path.write_to_string(arena, out);
829 }
830
831 #[inline]
832 fn git_status(&self) -> Option<git2::Status> {
833 self.git_status
834 }
835
836 #[inline]
837 fn is_overflow(&self) -> bool {
838 FileItem::is_overflow(self)
839 }
840}
841
842#[derive(Debug, Clone, Default)]
843pub struct Score {
844 pub total: i32,
845 pub base_score: i32,
846 pub filename_bonus: i32,
847 pub special_filename_bonus: i32,
848 pub frecency_boost: i32,
849 pub git_status_boost: i32,
850 pub git_recency_boost: i32,
851 pub distance_penalty: i32,
852 pub current_file_penalty: i32,
853 pub combo_match_boost: i32,
854 pub path_alignment_bonus: i32,
855 pub exact_match: bool,
856 pub match_type: &'static str,
857}
858
859#[derive(Debug, Clone, Copy)]
860pub struct PaginationArgs {
861 pub offset: usize,
862 pub limit: usize,
863}
864
865impl Default for PaginationArgs {
866 fn default() -> Self {
867 Self {
868 offset: 0,
869 limit: 100,
870 }
871 }
872}
873
874#[derive(Debug, Clone)]
875pub struct ScoringContext<'a> {
876 pub query: &'a FFFQuery<'a>,
877 pub project_path: Option<&'a Path>,
878 pub current_file: Option<&'a str>,
879 pub max_typos: u16,
880 pub max_threads: usize,
881 pub last_same_query_match: Option<QueryMatchEntry>,
882 pub combo_boost_score_multiplier: i32,
883 pub min_combo_count: u32,
884 pub pagination: PaginationArgs,
885}
886
887impl ScoringContext<'_> {
888 pub fn effective_query(&self) -> &str {
889 match &self.query.fuzzy_query {
890 FuzzyQuery::Text(t) => t,
891 FuzzyQuery::Parts(parts) if !parts.is_empty() => parts[0],
892 _ => self.query.raw_query.trim(),
893 }
894 }
895}
896
897#[derive(Debug, Clone, Default)]
898pub struct SearchResult<'a> {
899 pub items: Vec<&'a FileItem>,
900 pub scores: Vec<Score>,
901 pub match_byte_offsets: Vec<smallvec::SmallVec<[(u32, u32); 4]>>,
902 pub total_matched: usize,
903 pub total_files: usize,
904 pub location: Option<Location>,
905}
906
907#[derive(Debug, Clone, Default)]
909pub struct DirSearchResult<'a> {
910 pub items: Vec<&'a DirItem>,
911 pub scores: Vec<Score>,
912 pub total_matched: usize,
913 pub total_dirs: usize,
914}
915
916#[derive(Debug, Clone)]
918pub enum MixedItemRef<'a> {
919 File(&'a FileItem),
920 Dir(&'a DirItem),
921}
922
923#[derive(Debug, Clone, Default)]
926pub struct MixedSearchResult<'a> {
927 pub items: Vec<MixedItemRef<'a>>,
928 pub scores: Vec<Score>,
929 pub total_matched: usize,
930 pub total_files: usize,
931 pub total_dirs: usize,
932 pub location: Option<Location>,
933}
934
935impl Default for MixedItemRef<'_> {
936 fn default() -> Self {
937 unreachable!("MixedItemRef::default should not be called")
939 }
940}
941
942#[derive(Debug)]
943pub struct ContentCacheBudget {
944 pub max_files: usize,
945 pub max_bytes: u64,
946 pub max_file_size: u64,
947 pub cached_count: AtomicUsize,
948 pub cached_bytes: AtomicU64,
949}
950
951impl ContentCacheBudget {
952 pub fn unlimited() -> Self {
953 Self {
954 max_files: usize::MAX,
955 max_bytes: u64::MAX,
956 max_file_size: MAX_FFFILE_SIZE,
957 cached_count: AtomicUsize::new(0),
958 cached_bytes: AtomicU64::new(0),
959 }
960 }
961
962 pub fn zero() -> Self {
963 Self {
964 max_files: 0,
965 max_bytes: 0,
966 max_file_size: 0,
967 cached_count: AtomicUsize::new(0),
968 cached_bytes: AtomicU64::new(0),
969 }
970 }
971
972 pub fn is_exhausted(&self) -> bool {
974 self.cached_count.load(Ordering::Relaxed) >= self.max_files
975 || self.cached_bytes.load(Ordering::Relaxed) >= self.max_bytes
976 }
977
978 pub fn new_for_repo(file_count: usize) -> Self {
979 let max_files = if file_count > 50_000 {
980 5_000
981 } else if file_count > 10_000 {
982 10_000
983 } else {
984 30_000 };
986
987 let max_bytes = if file_count > 50_000 {
988 128 * 1024 * 1024 } else if file_count > 10_000 {
990 256 * 1024 * 1024 } else {
992 MAX_CACHED_CONTENT_BYTES };
994
995 Self {
996 max_files,
997 max_bytes,
998 max_file_size: MAX_FFFILE_SIZE,
999 cached_count: AtomicUsize::new(0),
1000 cached_bytes: AtomicU64::new(0),
1001 }
1002 }
1003
1004 pub fn with_max_files(max_files: usize) -> Self {
1008 Self {
1009 max_files,
1010 ..Self::default()
1011 }
1012 }
1013
1014 pub fn from_overrides(max_files: usize, max_bytes: u64, max_file_size: u64) -> Option<Self> {
1022 if max_files == 0 && max_bytes == 0 && max_file_size == 0 {
1023 return None;
1024 }
1025
1026 let mut budget = Self::default();
1027 if max_files > 0 {
1028 budget.max_files = max_files;
1029 }
1030 if max_bytes > 0 {
1031 budget.max_bytes = max_bytes;
1032 }
1033 if max_file_size > 0 {
1034 budget.max_file_size = max_file_size;
1035 }
1036 Some(budget)
1037 }
1038
1039 pub fn reset(&self) {
1040 self.cached_count.store(0, Ordering::Relaxed);
1041 self.cached_bytes.store(0, Ordering::Relaxed);
1042 }
1043}
1044
1045impl Default for ContentCacheBudget {
1046 fn default() -> Self {
1047 Self::new_for_repo(30_000)
1048 }
1049}
1050
1051#[cfg(test)]
1052mod content_cache_budget_tests {
1053 use super::*;
1054
1055 #[test]
1056 fn with_max_files_applies_the_cap_verbatim() {
1057 assert_eq!(ContentCacheBudget::with_max_files(2000).max_files, 2000);
1060 assert_eq!(ContentCacheBudget::with_max_files(7).max_files, 7);
1061 assert_eq!(
1062 ContentCacheBudget::with_max_files(1_000_000).max_files,
1063 1_000_000
1064 );
1065 }
1066
1067 #[test]
1068 fn with_max_files_zero_disables_persistent_caching_but_keeps_grep() {
1069 let budget = ContentCacheBudget::with_max_files(0);
1070 assert_eq!(budget.max_files, 0);
1071 assert!(budget.is_exhausted());
1072 assert_eq!(budget.max_file_size, MAX_FFFILE_SIZE);
1074 assert!(budget.max_bytes > 0);
1075 }
1076
1077 #[test]
1078 fn with_max_files_keeps_default_byte_caps() {
1079 let budget = ContentCacheBudget::with_max_files(2000);
1080 let default = ContentCacheBudget::default();
1081 assert_eq!(budget.max_bytes, default.max_bytes);
1082 assert_eq!(budget.max_file_size, default.max_file_size);
1083 }
1084}