1use std::collections::{HashMap, VecDeque};
11
12use super::types::LspDiagnostic;
13
14pub struct DiagnosticsCache {
31 entries: HashMap<String, Vec<LspDiagnostic>>,
32 order: VecDeque<String>,
33 max_files: usize,
34}
35
36impl DiagnosticsCache {
37 #[must_use]
48 pub fn new(max_files: usize) -> Self {
49 Self {
50 entries: HashMap::new(),
51 order: VecDeque::new(),
52 max_files: max_files.max(1),
53 }
54 }
55
56 pub fn update(&mut self, uri: String, diagnostics: Vec<LspDiagnostic>) {
58 if self.entries.contains_key(&uri) {
59 self.order.retain(|u| u != &uri);
61 } else if self.entries.len() >= self.max_files {
62 if let Some(evicted) = self.order.pop_front() {
64 self.entries.remove(&evicted);
65 }
66 }
67 self.order.push_back(uri.clone());
68 self.entries.insert(uri, diagnostics);
69 }
70
71 #[must_use]
76 pub fn peek(&self, uri: &str) -> Option<&[LspDiagnostic]> {
77 self.entries.get(uri).map(Vec::as_slice)
78 }
79
80 #[must_use]
82 pub fn all_non_empty(&self) -> Vec<(&str, &[LspDiagnostic])> {
83 self.entries
84 .iter()
85 .filter(|(_, diags)| !diags.is_empty())
86 .map(|(uri, diags)| (uri.as_str(), diags.as_slice()))
87 .collect()
88 }
89
90 pub fn clear(&mut self) {
92 self.entries.clear();
93 self.order.clear();
94 }
95
96 #[must_use]
98 pub fn len(&self) -> usize {
99 self.entries.len()
100 }
101
102 #[must_use]
104 pub fn is_empty(&self) -> bool {
105 self.entries.is_empty()
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::lsp::types::{LspDiagnosticSeverity, LspPosition, LspRange};
113
114 fn make_diag(msg: &str) -> LspDiagnostic {
115 LspDiagnostic {
116 range: LspRange {
117 start: LspPosition {
118 line: 1,
119 character: 0,
120 },
121 end: LspPosition {
122 line: 1,
123 character: 1,
124 },
125 },
126 severity: Some(LspDiagnosticSeverity::Error),
127 code: None,
128 source: None,
129 message: msg.to_owned(),
130 }
131 }
132
133 #[test]
134 fn insert_and_get() {
135 let mut cache = DiagnosticsCache::new(5);
136 cache.update("file:///a.rs".to_owned(), vec![make_diag("err1")]);
137 let diags = cache.peek("file:///a.rs").unwrap();
138 assert_eq!(diags.len(), 1);
139 assert_eq!(diags[0].message, "err1");
140 }
141
142 #[test]
143 fn missing_uri_returns_none() {
144 let cache = DiagnosticsCache::new(5);
145 assert!(cache.peek("file:///missing.rs").is_none());
146 }
147
148 #[test]
149 fn lru_eviction_removes_oldest() {
150 let mut cache = DiagnosticsCache::new(2);
151 cache.update("file:///a.rs".to_owned(), vec![make_diag("a")]);
152 cache.update("file:///b.rs".to_owned(), vec![make_diag("b")]);
153 cache.update("file:///c.rs".to_owned(), vec![make_diag("c")]);
155
156 assert!(cache.peek("file:///a.rs").is_none(), "a should be evicted");
157 assert!(cache.peek("file:///b.rs").is_some());
158 assert!(cache.peek("file:///c.rs").is_some());
159 assert_eq!(cache.len(), 2);
160 }
161
162 #[test]
163 fn update_existing_uri_does_not_grow() {
164 let mut cache = DiagnosticsCache::new(2);
165 cache.update("file:///a.rs".to_owned(), vec![make_diag("v1")]);
166 cache.update("file:///a.rs".to_owned(), vec![make_diag("v2")]);
167 assert_eq!(cache.len(), 1);
168 let diags = cache.peek("file:///a.rs").unwrap();
169 assert_eq!(diags[0].message, "v2");
170 }
171
172 #[test]
173 fn update_existing_moves_to_recent() {
174 let mut cache = DiagnosticsCache::new(2);
175 cache.update("file:///a.rs".to_owned(), vec![make_diag("a")]);
176 cache.update("file:///b.rs".to_owned(), vec![make_diag("b")]);
177 cache.update("file:///a.rs".to_owned(), vec![make_diag("a2")]);
179 cache.update("file:///c.rs".to_owned(), vec![make_diag("c")]);
181
182 assert!(cache.peek("file:///b.rs").is_none(), "b should be evicted");
183 assert!(cache.peek("file:///a.rs").is_some());
184 assert!(cache.peek("file:///c.rs").is_some());
185 }
186
187 #[test]
188 fn all_non_empty_skips_empty_diags() {
189 let mut cache = DiagnosticsCache::new(5);
190 cache.update("file:///a.rs".to_owned(), vec![make_diag("err")]);
191 cache.update("file:///b.rs".to_owned(), vec![]);
192 let non_empty = cache.all_non_empty();
193 assert_eq!(non_empty.len(), 1);
194 assert_eq!(non_empty[0].0, "file:///a.rs");
195 }
196
197 #[test]
198 fn clear_removes_all() {
199 let mut cache = DiagnosticsCache::new(5);
200 cache.update("file:///a.rs".to_owned(), vec![make_diag("err")]);
201 cache.clear();
202 assert!(cache.is_empty());
203 assert!(cache.peek("file:///a.rs").is_none());
204 }
205
206 #[test]
207 fn max_files_one_always_evicts() {
208 let mut cache = DiagnosticsCache::new(1);
209 cache.update("file:///a.rs".to_owned(), vec![make_diag("a")]);
210 cache.update("file:///b.rs".to_owned(), vec![make_diag("b")]);
211 assert!(cache.peek("file:///a.rs").is_none());
212 assert!(cache.peek("file:///b.rs").is_some());
213 assert_eq!(cache.len(), 1);
214 }
215}