1use super::grep_file::{GrepSearchInput, GrepSearchResult};
9use crate::cache::{CacheKey, DEFAULT_CACHE_TTL, EvictionPolicy, UnifiedCache, estimate_json_size};
10use std::sync::Arc;
11
12#[derive(Debug, Clone, Hash, PartialEq, Eq)]
14struct SearchCacheKey {
15 pattern: String,
16 path: String,
17 case_sensitive: bool,
18 max_results: usize,
19 glob_pattern: Option<String>,
20 type_pattern: Option<String>,
21 max_result_bytes: Option<usize>,
22 respect_ignore_files: bool,
23 search_hidden: bool,
24 search_binary: bool,
25 literal: bool,
26}
27
28impl CacheKey for SearchCacheKey {
29 fn to_cache_key(&self) -> String {
30 format!(
35 "grep\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{:?}\u{1f}{:?}\u{1f}{:?}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}",
36 self.pattern,
37 self.path,
38 self.case_sensitive,
39 self.max_results,
40 self.glob_pattern,
41 self.type_pattern,
42 self.max_result_bytes,
43 self.respect_ignore_files,
44 self.search_hidden,
45 self.search_binary,
46 self.literal,
47 )
48 }
49}
50
51impl From<&GrepSearchInput> for SearchCacheKey {
52 fn from(input: &GrepSearchInput) -> Self {
53 Self {
54 pattern: input.pattern.clone(),
55 path: input.path.clone(),
56 case_sensitive: input.case_sensitive.unwrap_or(false),
57 max_results: input.max_results.unwrap_or(5), glob_pattern: input.glob_pattern.clone(),
59 type_pattern: input.type_pattern.clone(),
60 max_result_bytes: input.max_result_bytes,
61 respect_ignore_files: input.respect_ignore_files.unwrap_or(true),
62 search_hidden: input.search_hidden.unwrap_or(false),
63 search_binary: input.search_binary.unwrap_or(false),
64 literal: input.literal.unwrap_or(false),
65 }
66 }
67}
68
69pub struct GrepSearchCache {
71 cache: UnifiedCache<SearchCacheKey, GrepSearchResult>,
72}
73
74impl GrepSearchCache {
75 pub fn new(capacity: usize) -> Self {
77 Self {
78 cache: UnifiedCache::new(capacity, DEFAULT_CACHE_TTL, EvictionPolicy::Lru),
79 }
80 }
81
82 pub fn get(&self, input: &GrepSearchInput) -> Option<Arc<GrepSearchResult>> {
84 let key = SearchCacheKey::from(input);
85 self.cache.get(&key)
86 }
87
88 pub fn put(&self, input: &GrepSearchInput, result: GrepSearchResult) {
90 let key = SearchCacheKey::from(input);
91 let size_bytes = size_of::<GrepSearchResult>() as u64
92 + result.query.len() as u64
93 + result.matches.iter().map(estimate_json_size).sum::<u64>();
94 self.cache.insert(key, result, size_bytes);
95 }
96
97 pub fn should_cache(result: &GrepSearchResult) -> bool {
99 !result.matches.is_empty()
100 }
101
102 pub fn clear(&self) {
104 self.cache.clear();
105 }
106
107 pub fn stats(&self) -> (usize, usize) {
109 let stats = self.cache.stats();
110 (stats.current_size, stats.max_size)
111 }
112}
113
114impl Default for GrepSearchCache {
115 fn default() -> Self {
116 Self::new(100) }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 fn make_test_input(pattern: &str, path: &str) -> GrepSearchInput {
125 GrepSearchInput {
126 pattern: pattern.to_string(),
127 path: path.to_string(),
128 case_sensitive: Some(true),
129 literal: None,
130 glob_pattern: Some("*.rs".to_string()),
131 context_lines: None,
132 include_hidden: None,
133 max_results: Some(5), respect_ignore_files: None,
135 max_file_size: None,
136 search_hidden: None,
137 search_binary: None,
138 files_with_matches: None,
139 type_pattern: None,
140 invert_match: None,
141 word_boundaries: None,
142 line_number: None,
143 column: None,
144 only_matching: None,
145 trim: None,
146 max_result_bytes: None,
147 timeout: None,
148 extra_ignore_globs: None,
149 }
150 }
151
152 #[test]
153 fn test_cache_key_equality() {
154 let input1 = make_test_input("test", "/path");
155 let input2 = make_test_input("test", "/path");
156
157 let key1 = SearchCacheKey::from(&input1);
158 let key2 = SearchCacheKey::from(&input2);
159 assert_eq!(key1, key2);
160 }
161
162 #[test]
163 fn test_cache_operations() {
164 let cache = GrepSearchCache::new(10);
165
166 let input = make_test_input("test", "/path");
167
168 let result = GrepSearchResult {
169 query: "test".to_string(),
170 matches: vec![serde_json::json!({"file": "test.rs", "line": 1})],
171 truncated: false,
172 total_matches: None,
173 };
174
175 assert!(cache.get(&input).is_none());
177
178 cache.put(&input, result.clone());
180
181 let cached = cache.get(&input).unwrap();
183 assert_eq!(cached.query, result.query);
184 assert_eq!(cached.matches.len(), result.matches.len());
185 assert_eq!(cached.truncated, result.truncated);
186 }
187}