1use std::collections::{HashMap, VecDeque};
6
7use chrono::{DateTime, Utc};
8use lsp_types::{Diagnostic as LspDiagnostic, Uri};
9use serde::{Deserialize, Serialize};
10
11const MAX_LOG_ENTRIES: usize = 100;
13
14fn uri_cache_key(uri: &str) -> std::borrow::Cow<'_, str> {
22 if cfg!(windows) {
23 std::borrow::Cow::Owned(uri.to_ascii_lowercase())
24 } else {
25 std::borrow::Cow::Borrowed(uri)
26 }
27}
28
29const MAX_SERVER_MESSAGES: usize = 50;
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct DiagnosticInfo {
35 pub uri: Uri,
37 pub version: Option<i32>,
39 pub diagnostics: Vec<LspDiagnostic>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct LogEntry {
46 pub level: LogLevel,
48 pub message: String,
50 pub timestamp: DateTime<Utc>,
52}
53
54#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
56#[serde(rename_all = "lowercase")]
57pub enum LogLevel {
58 Error,
60 Warning,
62 Info,
64 Debug,
66}
67
68impl From<lsp_types::MessageType> for LogLevel {
69 fn from(msg_type: lsp_types::MessageType) -> Self {
70 match msg_type {
71 lsp_types::MessageType::ERROR => Self::Error,
72 lsp_types::MessageType::WARNING => Self::Warning,
73 lsp_types::MessageType::INFO => Self::Info,
74 _ => Self::Debug,
76 }
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct ServerMessage {
83 pub message_type: MessageType,
85 pub message: String,
87 pub timestamp: DateTime<Utc>,
89}
90
91#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
93#[serde(rename_all = "lowercase")]
94pub enum MessageType {
95 Error,
97 Warning,
99 Info,
101 Log,
103}
104
105impl From<lsp_types::MessageType> for MessageType {
106 fn from(msg_type: lsp_types::MessageType) -> Self {
107 match msg_type {
108 lsp_types::MessageType::ERROR => Self::Error,
109 lsp_types::MessageType::WARNING => Self::Warning,
110 lsp_types::MessageType::INFO => Self::Info,
111 _ => Self::Log,
113 }
114 }
115}
116
117#[derive(Debug)]
119pub struct NotificationCache {
120 diagnostics: HashMap<String, DiagnosticInfo>,
122 logs: VecDeque<LogEntry>,
124 messages: VecDeque<ServerMessage>,
126}
127
128impl Default for NotificationCache {
129 fn default() -> Self {
130 Self::new()
131 }
132}
133
134impl NotificationCache {
135 #[must_use]
137 pub fn new() -> Self {
138 Self {
139 diagnostics: HashMap::with_capacity(32),
140 logs: VecDeque::with_capacity(MAX_LOG_ENTRIES),
141 messages: VecDeque::with_capacity(MAX_SERVER_MESSAGES),
142 }
143 }
144
145 pub fn store_diagnostics(
149 &mut self,
150 uri: &Uri,
151 version: Option<i32>,
152 diagnostics: Vec<LspDiagnostic>,
153 ) {
154 let info = DiagnosticInfo {
155 uri: uri.clone(),
156 version,
157 diagnostics,
158 };
159 self.diagnostics
160 .insert(uri_cache_key(uri.as_str()).into_owned(), info);
161 }
162
163 pub fn store_log(&mut self, level: LogLevel, message: String) {
167 let entry = LogEntry {
168 level,
169 message,
170 timestamp: Utc::now(),
171 };
172
173 if self.logs.len() >= MAX_LOG_ENTRIES {
174 self.logs.pop_front();
175 }
176 self.logs.push_back(entry);
177 }
178
179 pub fn store_message(&mut self, message_type: MessageType, message: String) {
183 let msg = ServerMessage {
184 message_type,
185 message,
186 timestamp: Utc::now(),
187 };
188
189 if self.messages.len() >= MAX_SERVER_MESSAGES {
190 self.messages.pop_front();
191 }
192 self.messages.push_back(msg);
193 }
194
195 #[inline]
197 #[must_use]
198 pub fn get_diagnostics(&self, uri: &str) -> Option<&DiagnosticInfo> {
199 self.diagnostics.get(uri_cache_key(uri).as_ref())
200 }
201
202 #[inline]
204 #[must_use]
205 pub const fn get_logs(&self) -> &VecDeque<LogEntry> {
206 &self.logs
207 }
208
209 #[inline]
211 #[must_use]
212 pub const fn get_messages(&self) -> &VecDeque<ServerMessage> {
213 &self.messages
214 }
215
216 pub fn clear_diagnostics(&mut self, uri: &str) -> Option<DiagnosticInfo> {
220 self.diagnostics.remove(uri_cache_key(uri).as_ref())
221 }
222
223 pub fn clear_all_diagnostics(&mut self) {
225 self.diagnostics.clear();
226 }
227
228 pub fn clear_logs(&mut self) {
230 self.logs.clear();
231 }
232
233 pub fn clear_messages(&mut self) {
235 self.messages.clear();
236 }
237
238 #[inline]
240 #[must_use]
241 pub fn diagnostics_count(&self) -> usize {
242 self.diagnostics.len()
243 }
244
245 #[inline]
247 #[must_use]
248 pub fn logs_count(&self) -> usize {
249 self.logs.len()
250 }
251
252 #[inline]
254 #[must_use]
255 pub fn messages_count(&self) -> usize {
256 self.messages.len()
257 }
258}
259
260#[cfg(test)]
261#[allow(clippy::unwrap_used)]
262mod tests {
263 use lsp_types::{Position, Range};
264
265 use super::*;
266
267 #[test]
268 fn test_notification_cache_new() {
269 let cache = NotificationCache::new();
270 assert_eq!(cache.diagnostics_count(), 0);
271 assert_eq!(cache.logs_count(), 0);
272 assert_eq!(cache.messages_count(), 0);
273 }
274
275 #[test]
276 fn test_store_and_get_diagnostics() {
277 let mut cache = NotificationCache::new();
278 let uri: Uri = "file:///test.rs".parse().unwrap();
279
280 let diagnostic = LspDiagnostic {
281 range: Range {
282 start: Position {
283 line: 0,
284 character: 0,
285 },
286 end: Position {
287 line: 0,
288 character: 5,
289 },
290 },
291 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
292 message: "test error".to_string(),
293 code: None,
294 source: None,
295 code_description: None,
296 related_information: None,
297 tags: None,
298 data: None,
299 };
300
301 cache.store_diagnostics(&uri, Some(1), vec![diagnostic]);
302
303 let stored = cache.get_diagnostics(uri.as_str()).unwrap();
304 assert_eq!(stored.uri, uri);
305 assert_eq!(stored.version, Some(1));
306 assert_eq!(stored.diagnostics.len(), 1);
307 assert_eq!(stored.diagnostics[0].message, "test error");
308 }
309
310 #[test]
311 fn test_store_diagnostics_replaces_existing() {
312 let mut cache = NotificationCache::new();
313 let uri: Uri = "file:///test.rs".parse().unwrap();
314
315 cache.store_diagnostics(&uri, Some(1), vec![]);
316 assert_eq!(cache.diagnostics_count(), 1);
317
318 cache.store_diagnostics(&uri, Some(2), vec![]);
319 assert_eq!(cache.diagnostics_count(), 1);
320
321 let stored = cache.get_diagnostics(uri.as_str()).unwrap();
322 assert_eq!(stored.version, Some(2));
323 }
324
325 #[test]
326 fn test_clear_diagnostics() {
327 let mut cache = NotificationCache::new();
328 let uri: Uri = "file:///test.rs".parse().unwrap();
329
330 cache.store_diagnostics(&uri, Some(1), vec![]);
331 assert_eq!(cache.diagnostics_count(), 1);
332
333 let cleared = cache.clear_diagnostics(uri.as_str());
334 assert!(cleared.is_some());
335 assert_eq!(cache.diagnostics_count(), 0);
336 }
337
338 #[test]
339 fn test_clear_all_diagnostics() {
340 let mut cache = NotificationCache::new();
341 let uri1: Uri = "file:///test1.rs".parse().unwrap();
342 let uri2: Uri = "file:///test2.rs".parse().unwrap();
343
344 cache.store_diagnostics(&uri1, Some(1), vec![]);
345 cache.store_diagnostics(&uri2, Some(1), vec![]);
346 assert_eq!(cache.diagnostics_count(), 2);
347
348 cache.clear_all_diagnostics();
349 assert_eq!(cache.diagnostics_count(), 0);
350 }
351
352 #[test]
353 fn test_store_and_get_logs() {
354 let mut cache = NotificationCache::new();
355
356 cache.store_log(LogLevel::Error, "error message".to_string());
357 cache.store_log(LogLevel::Info, "info message".to_string());
358
359 let logs = cache.get_logs();
360 assert_eq!(logs.len(), 2);
361 assert_eq!(logs[0].level, LogLevel::Error);
362 assert_eq!(logs[0].message, "error message");
363 assert_eq!(logs[1].level, LogLevel::Info);
364 assert_eq!(logs[1].message, "info message");
365 }
366
367 #[test]
368 fn test_logs_max_capacity() {
369 let mut cache = NotificationCache::new();
370
371 for i in 0..MAX_LOG_ENTRIES + 10 {
373 cache.store_log(LogLevel::Info, format!("message {i}"));
374 }
375
376 assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
377
378 let logs = cache.get_logs();
380 assert_eq!(logs.front().unwrap().message, "message 10");
381 assert_eq!(
382 logs.back().unwrap().message,
383 format!("message {}", MAX_LOG_ENTRIES + 9)
384 );
385 }
386
387 #[test]
388 fn test_clear_logs() {
389 let mut cache = NotificationCache::new();
390 cache.store_log(LogLevel::Info, "test".to_string());
391 assert_eq!(cache.logs_count(), 1);
392
393 cache.clear_logs();
394 assert_eq!(cache.logs_count(), 0);
395 }
396
397 #[test]
398 fn test_store_and_get_messages() {
399 let mut cache = NotificationCache::new();
400
401 cache.store_message(MessageType::Error, "error msg".to_string());
402 cache.store_message(MessageType::Warning, "warning msg".to_string());
403
404 let messages = cache.get_messages();
405 assert_eq!(messages.len(), 2);
406 assert_eq!(messages[0].message_type, MessageType::Error);
407 assert_eq!(messages[0].message, "error msg");
408 assert_eq!(messages[1].message_type, MessageType::Warning);
409 assert_eq!(messages[1].message, "warning msg");
410 }
411
412 #[test]
413 fn test_messages_max_capacity() {
414 let mut cache = NotificationCache::new();
415
416 for i in 0..MAX_SERVER_MESSAGES + 10 {
418 cache.store_message(MessageType::Info, format!("message {i}"));
419 }
420
421 assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
422
423 let messages = cache.get_messages();
425 assert_eq!(messages.front().unwrap().message, "message 10");
426 assert_eq!(
427 messages.back().unwrap().message,
428 format!("message {}", MAX_SERVER_MESSAGES + 9)
429 );
430 }
431
432 #[test]
433 fn test_clear_messages() {
434 let mut cache = NotificationCache::new();
435 cache.store_message(MessageType::Info, "test".to_string());
436 assert_eq!(cache.messages_count(), 1);
437
438 cache.clear_messages();
439 assert_eq!(cache.messages_count(), 0);
440 }
441
442 #[test]
443 fn test_log_levels() {
444 let mut cache = NotificationCache::new();
445
446 cache.store_log(LogLevel::Error, "error".to_string());
447 cache.store_log(LogLevel::Warning, "warning".to_string());
448 cache.store_log(LogLevel::Info, "info".to_string());
449 cache.store_log(LogLevel::Debug, "debug".to_string());
450
451 let logs = cache.get_logs();
452 assert_eq!(logs[0].level, LogLevel::Error);
453 assert_eq!(logs[1].level, LogLevel::Warning);
454 assert_eq!(logs[2].level, LogLevel::Info);
455 assert_eq!(logs[3].level, LogLevel::Debug);
456 }
457
458 #[test]
459 fn test_message_types() {
460 let mut cache = NotificationCache::new();
461
462 cache.store_message(MessageType::Error, "error".to_string());
463 cache.store_message(MessageType::Warning, "warning".to_string());
464 cache.store_message(MessageType::Info, "info".to_string());
465 cache.store_message(MessageType::Log, "log".to_string());
466
467 let messages = cache.get_messages();
468 assert_eq!(messages[0].message_type, MessageType::Error);
469 assert_eq!(messages[1].message_type, MessageType::Warning);
470 assert_eq!(messages[2].message_type, MessageType::Info);
471 assert_eq!(messages[3].message_type, MessageType::Log);
472 }
473
474 #[test]
475 fn test_timestamp_ordering() {
476 let mut cache = NotificationCache::new();
477
478 cache.store_log(LogLevel::Info, "first".to_string());
479 std::thread::sleep(std::time::Duration::from_millis(10));
480 cache.store_log(LogLevel::Info, "second".to_string());
481
482 let logs = cache.get_logs();
483 assert!(logs[0].timestamp < logs[1].timestamp);
484 }
485
486 #[test]
487 fn test_store_diagnostics_empty_list() {
488 let mut cache = NotificationCache::new();
489 let uri: Uri = "file:///test.rs".parse().unwrap();
490
491 let diagnostic = LspDiagnostic {
492 range: Range {
493 start: Position {
494 line: 0,
495 character: 0,
496 },
497 end: Position {
498 line: 0,
499 character: 5,
500 },
501 },
502 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
503 message: "test error".to_string(),
504 code: None,
505 source: None,
506 code_description: None,
507 related_information: None,
508 tags: None,
509 data: None,
510 };
511
512 cache.store_diagnostics(&uri, Some(1), vec![diagnostic]);
513 assert_eq!(
514 cache
515 .get_diagnostics(uri.as_str())
516 .unwrap()
517 .diagnostics
518 .len(),
519 1
520 );
521
522 cache.store_diagnostics(&uri, Some(2), vec![]);
523 let stored = cache.get_diagnostics(uri.as_str()).unwrap();
524 assert_eq!(stored.diagnostics.len(), 0);
525 assert_eq!(stored.version, Some(2));
526 }
527
528 #[test]
529 fn test_store_many_diagnostics_single_file() {
530 let mut cache = NotificationCache::new();
531 let uri: Uri = "file:///test.rs".parse().unwrap();
532
533 let diagnostics: Vec<LspDiagnostic> = (0..100)
534 .map(|i| LspDiagnostic {
535 range: Range {
536 start: Position {
537 line: i,
538 character: 0,
539 },
540 end: Position {
541 line: i,
542 character: 10,
543 },
544 },
545 message: format!("Error {i}"),
546 severity: Some(lsp_types::DiagnosticSeverity::ERROR),
547 code: None,
548 source: None,
549 code_description: None,
550 related_information: None,
551 tags: None,
552 data: None,
553 })
554 .collect();
555
556 cache.store_diagnostics(&uri, Some(1), diagnostics);
557
558 let stored = cache.get_diagnostics(uri.as_str()).unwrap();
559 assert_eq!(stored.diagnostics.len(), 100);
560 }
561
562 #[test]
563 fn test_logs_exact_capacity_boundary() {
564 let mut cache = NotificationCache::new();
565
566 for i in 0..MAX_LOG_ENTRIES {
567 cache.store_log(LogLevel::Info, format!("message {i}"));
568 }
569 assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
570
571 cache.store_log(LogLevel::Info, "overflow".to_string());
572 assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
573 assert_eq!(cache.get_logs().front().unwrap().message, "message 1");
574 }
575
576 #[test]
577 fn test_messages_exact_capacity_boundary() {
578 let mut cache = NotificationCache::new();
579
580 for i in 0..MAX_SERVER_MESSAGES {
581 cache.store_message(MessageType::Info, format!("message {i}"));
582 }
583 assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
584
585 cache.store_message(MessageType::Info, "overflow".to_string());
586 assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
587 assert_eq!(cache.get_messages().front().unwrap().message, "message 1");
588 }
589
590 #[test]
591 fn test_clear_diagnostics_nonexistent() {
592 let mut cache = NotificationCache::new();
593 let result = cache.clear_diagnostics("file:///nonexistent.rs");
594 assert!(result.is_none());
595 }
596
597 #[test]
598 fn test_store_diagnostics_no_version() {
599 let mut cache = NotificationCache::new();
600 let uri: Uri = "file:///test.rs".parse().unwrap();
601
602 cache.store_diagnostics(&uri, None, vec![]);
603 let stored = cache.get_diagnostics(uri.as_str()).unwrap();
604 assert_eq!(stored.version, None);
605 }
606}