ryo_symbol/
content_cache.rs1use std::collections::HashMap;
7use std::fs;
8use std::io;
9use std::path::PathBuf;
10use std::time::SystemTime;
11
12use crate::file_path::WorkspaceFilePath;
13
14#[derive(Debug, Clone)]
16pub struct CacheEntry {
17 pub hash: u64,
19 pub mtime: SystemTime,
21}
22
23impl CacheEntry {
24 pub fn new(hash: u64, mtime: SystemTime) -> Self {
26 Self { hash, mtime }
27 }
28
29 pub fn is_fresh(&self, current_mtime: SystemTime) -> bool {
31 self.mtime == current_mtime
32 }
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum Freshness {
38 Fresh,
40 Stale,
42 NotCached,
44 Missing,
46}
47
48#[derive(Debug, Clone, Default)]
73pub struct ContentCache {
74 entries: HashMap<PathBuf, CacheEntry>,
75}
76
77impl ContentCache {
78 pub fn new() -> Self {
80 Self {
81 entries: HashMap::new(),
82 }
83 }
84
85 pub fn with_capacity(capacity: usize) -> Self {
87 Self {
88 entries: HashMap::with_capacity(capacity),
89 }
90 }
91
92 pub fn register(&mut self, path: &WorkspaceFilePath, hash: u64, mtime: SystemTime) {
94 let key = path.as_relative().to_path_buf();
95 self.entries.insert(key, CacheEntry::new(hash, mtime));
96 }
97
98 pub fn register_with_current_mtime(
100 &mut self,
101 path: &WorkspaceFilePath,
102 hash: u64,
103 ) -> io::Result<()> {
104 let absolute = path.to_absolute();
105 let metadata = fs::metadata(&absolute)?;
106 let mtime = metadata.modified()?;
107 self.register(path, hash, mtime);
108 Ok(())
109 }
110
111 pub fn get(&self, path: &WorkspaceFilePath) -> Option<&CacheEntry> {
113 self.entries.get(path.as_relative())
114 }
115
116 pub fn contains(&self, path: &WorkspaceFilePath) -> bool {
118 self.entries.contains_key(path.as_relative())
119 }
120
121 pub fn remove(&mut self, path: &WorkspaceFilePath) -> Option<CacheEntry> {
123 self.entries.remove(path.as_relative())
124 }
125
126 pub fn check_freshness(&self, path: &WorkspaceFilePath) -> Freshness {
130 let entry = match self.entries.get(path.as_relative()) {
131 Some(e) => e,
132 None => return Freshness::NotCached,
133 };
134
135 let absolute = path.to_absolute();
136 let metadata = match fs::metadata(&absolute) {
137 Ok(m) => m,
138 Err(_) => return Freshness::Missing,
139 };
140
141 let current_mtime = match metadata.modified() {
142 Ok(t) => t,
143 Err(_) => return Freshness::Stale, };
145
146 if entry.is_fresh(current_mtime) {
147 Freshness::Fresh
148 } else {
149 Freshness::Stale
150 }
151 }
152
153 pub fn get_stale_files<'a>(
155 &self,
156 paths: &'a [WorkspaceFilePath],
157 ) -> Vec<&'a WorkspaceFilePath> {
158 paths
159 .iter()
160 .filter(|p| self.check_freshness(p) == Freshness::Stale)
161 .collect()
162 }
163
164 pub fn clear(&mut self) {
166 self.entries.clear();
167 }
168
169 pub fn len(&self) -> usize {
171 self.entries.len()
172 }
173
174 pub fn is_empty(&self) -> bool {
176 self.entries.is_empty()
177 }
178
179 pub fn iter(&self) -> impl Iterator<Item = (&PathBuf, &CacheEntry)> {
181 self.entries.iter()
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 fn make_path(relative: &str) -> WorkspaceFilePath {
190 WorkspaceFilePath::new_for_test(relative, "/workspace", "test_crate")
191 }
192
193 #[test]
194 fn test_register_and_get() {
195 let mut cache = ContentCache::new();
196 let path = make_path("src/lib.rs");
197 let mtime = SystemTime::now();
198
199 cache.register(&path, 12345, mtime);
200
201 let entry = cache.get(&path).unwrap();
202 assert_eq!(entry.hash, 12345);
203 assert_eq!(entry.mtime, mtime);
204 }
205
206 #[test]
207 fn test_contains() {
208 let mut cache = ContentCache::new();
209 let path = make_path("src/lib.rs");
210
211 assert!(!cache.contains(&path));
212
213 cache.register(&path, 12345, SystemTime::now());
214
215 assert!(cache.contains(&path));
216 }
217
218 #[test]
219 fn test_remove() {
220 let mut cache = ContentCache::new();
221 let path = make_path("src/lib.rs");
222
223 cache.register(&path, 12345, SystemTime::now());
224 assert!(cache.contains(&path));
225
226 let removed = cache.remove(&path);
227 assert!(removed.is_some());
228 assert!(!cache.contains(&path));
229 }
230
231 #[test]
232 fn test_freshness_not_cached() {
233 let cache = ContentCache::new();
234 let path = make_path("src/lib.rs");
235
236 assert_eq!(cache.check_freshness(&path), Freshness::NotCached);
237 }
238
239 #[test]
240 fn test_entry_is_fresh() {
241 let mtime = SystemTime::now();
242 let entry = CacheEntry::new(12345, mtime);
243
244 assert!(entry.is_fresh(mtime));
245
246 let different_mtime = mtime + std::time::Duration::from_secs(1);
248 assert!(!entry.is_fresh(different_mtime));
249 }
250}