1use std::collections::hash_map::DefaultHasher;
2use std::hash::{Hash, Hasher};
3use std::num::NonZeroUsize;
4use std::path::{Path, PathBuf};
5
6use lru::LruCache;
7
8use crate::prose::ProseRange;
9
10#[derive(Debug, Clone)]
12pub struct ParseCacheEntry {
13 pub content_hash: u64,
14 pub prose_ranges: Vec<ProseRange>,
15}
16
17pub struct ParseCache {
22 cache: LruCache<PathBuf, ParseCacheEntry>,
23}
24
25impl ParseCache {
26 #[must_use]
28 pub fn new(capacity: usize) -> Self {
29 Self {
30 cache: LruCache::new(
31 NonZeroUsize::new(capacity).unwrap_or(NonZeroUsize::new(128).unwrap()),
32 ),
33 }
34 }
35
36 #[must_use]
39 pub fn get(&mut self, path: &Path, content: &str) -> Option<Vec<ProseRange>> {
40 let hash = Self::hash_content(content);
41 self.cache
42 .get(path)
43 .filter(|entry| entry.content_hash == hash)
44 .map(|entry| entry.prose_ranges.clone())
45 }
46
47 pub fn put(&mut self, path: PathBuf, content: &str, prose_ranges: Vec<ProseRange>) {
49 let entry = ParseCacheEntry {
50 content_hash: Self::hash_content(content),
51 prose_ranges,
52 };
53 self.cache.put(path, entry);
54 }
55
56 #[must_use]
58 pub fn len(&self) -> usize {
59 self.cache.len()
60 }
61
62 #[must_use]
64 pub fn is_empty(&self) -> bool {
65 self.cache.is_empty()
66 }
67
68 pub fn invalidate(&mut self, path: &Path) {
70 self.cache.pop(path);
71 }
72
73 pub fn clear(&mut self) {
75 self.cache.clear();
76 }
77
78 fn hash_content(content: &str) -> u64 {
79 let mut hasher = DefaultHasher::new();
80 content.hash(&mut hasher);
81 hasher.finish()
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn cache_miss_on_empty() {
91 let mut cache = ParseCache::new(10);
92 assert!(cache.get(Path::new("foo.md"), "hello").is_none());
93 }
94
95 #[test]
96 fn cache_hit_after_put() {
97 let mut cache = ParseCache::new(10);
98 let ranges = vec![ProseRange {
99 start_byte: 0,
100 end_byte: 5,
101 exclusions: vec![],
102 }];
103 cache.put(PathBuf::from("foo.md"), "hello", ranges.clone());
104 let result = cache.get(Path::new("foo.md"), "hello");
105 assert_eq!(result, Some(ranges));
106 }
107
108 #[test]
109 fn cache_invalidated_on_content_change() {
110 let mut cache = ParseCache::new(10);
111 let ranges = vec![ProseRange {
112 start_byte: 0,
113 end_byte: 5,
114 exclusions: vec![],
115 }];
116 cache.put(PathBuf::from("foo.md"), "hello", ranges);
117 assert!(cache.get(Path::new("foo.md"), "hello world").is_none());
118 }
119
120 #[test]
121 fn cache_eviction_at_capacity() {
122 let mut cache = ParseCache::new(2);
123 let r = vec![ProseRange {
124 start_byte: 0,
125 end_byte: 1,
126 exclusions: vec![],
127 }];
128 cache.put(PathBuf::from("a.md"), "a", r.clone());
129 cache.put(PathBuf::from("b.md"), "b", r.clone());
130 cache.put(PathBuf::from("c.md"), "c", r.clone());
131
132 assert!(cache.get(Path::new("a.md"), "a").is_none());
134 assert!(cache.get(Path::new("b.md"), "b").is_some());
135 assert!(cache.get(Path::new("c.md"), "c").is_some());
136 }
137
138 #[test]
139 fn explicit_invalidation() {
140 let mut cache = ParseCache::new(10);
141 let r = vec![ProseRange {
142 start_byte: 0,
143 end_byte: 1,
144 exclusions: vec![],
145 }];
146 cache.put(PathBuf::from("foo.md"), "x", r);
147 cache.invalidate(Path::new("foo.md"));
148 assert!(cache.get(Path::new("foo.md"), "x").is_none());
149 }
150
151 #[test]
152 fn len_and_clear() {
153 let mut cache = ParseCache::new(10);
154 assert!(cache.is_empty());
155 cache.put(PathBuf::from("a.md"), "a", vec![]);
156 cache.put(PathBuf::from("b.md"), "b", vec![]);
157 assert_eq!(cache.len(), 2);
158 cache.clear();
159 assert!(cache.is_empty());
160 }
161}