mcpls_core/bridge/
notifications.rs

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