Skip to main content

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/// Normalize a URI string to a stable cache key.
15///
16/// On Windows, URI comparisons must be case-insensitive: the filesystem is
17/// case-insensitive and different tools (e.g. rust-analyzer vs std) may
18/// produce drive letters in different cases (`C:` vs `c:`).
19/// Lowercasing the entire URI is safe for `file://` URIs because they have
20/// no case-sensitive query or fragment components.
21fn 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
29/// Maximum number of server messages to store.
30const MAX_SERVER_MESSAGES: usize = 50;
31
32/// Information about diagnostics for a document.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct DiagnosticInfo {
35    /// URI of the document.
36    pub uri: Uri,
37    /// Document version when diagnostics were received.
38    pub version: Option<i32>,
39    /// List of diagnostics.
40    pub diagnostics: Vec<LspDiagnostic>,
41}
42
43/// A log entry from the LSP server.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct LogEntry {
46    /// Log level.
47    pub level: LogLevel,
48    /// Log message.
49    pub message: String,
50    /// Timestamp when the log was received.
51    pub timestamp: DateTime<Utc>,
52}
53
54/// Log severity level.
55#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
56#[serde(rename_all = "lowercase")]
57pub enum LogLevel {
58    /// Error log level.
59    Error,
60    /// Warning log level.
61    Warning,
62    /// Info log level.
63    Info,
64    /// Debug log level.
65    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            // LOG and unknown message types default to Debug
75            _ => Self::Debug,
76        }
77    }
78}
79
80/// A message from the LSP server.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct ServerMessage {
83    /// Message type.
84    pub message_type: MessageType,
85    /// Message content.
86    pub message: String,
87    /// Timestamp when the message was received.
88    pub timestamp: DateTime<Utc>,
89}
90
91/// Server message type.
92#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
93#[serde(rename_all = "lowercase")]
94pub enum MessageType {
95    /// Error message.
96    Error,
97    /// Warning message.
98    Warning,
99    /// Info message.
100    Info,
101    /// Log message.
102    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            // LOG and unknown message types default to Log
112            _ => Self::Log,
113        }
114    }
115}
116
117/// Cache for LSP server notifications.
118#[derive(Debug)]
119pub struct NotificationCache {
120    /// Diagnostics indexed by document URI.
121    diagnostics: HashMap<String, DiagnosticInfo>,
122    /// Recent log entries (FIFO queue with max size).
123    logs: VecDeque<LogEntry>,
124    /// Recent server messages (FIFO queue with max size).
125    messages: VecDeque<ServerMessage>,
126}
127
128impl Default for NotificationCache {
129    fn default() -> Self {
130        Self::new()
131    }
132}
133
134impl NotificationCache {
135    /// Create a new notification cache.
136    #[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    /// Store diagnostics for a document.
146    ///
147    /// If diagnostics already exist for the URI, they are replaced.
148    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    /// Store a log entry.
164    ///
165    /// Maintains a maximum of `MAX_LOG_ENTRIES` entries, removing oldest when full.
166    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    /// Store a server message.
180    ///
181    /// Maintains a maximum of `MAX_SERVER_MESSAGES` entries, removing oldest when full.
182    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    /// Get diagnostics for a document URI.
196    #[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    /// Get all stored log entries.
203    #[inline]
204    #[must_use]
205    pub const fn get_logs(&self) -> &VecDeque<LogEntry> {
206        &self.logs
207    }
208
209    /// Get all stored server messages.
210    #[inline]
211    #[must_use]
212    pub const fn get_messages(&self) -> &VecDeque<ServerMessage> {
213        &self.messages
214    }
215
216    /// Clear diagnostics for a specific document URI.
217    ///
218    /// Returns the cleared diagnostics if they existed.
219    pub fn clear_diagnostics(&mut self, uri: &str) -> Option<DiagnosticInfo> {
220        self.diagnostics.remove(uri_cache_key(uri).as_ref())
221    }
222
223    /// Clear all diagnostics.
224    pub fn clear_all_diagnostics(&mut self) {
225        self.diagnostics.clear();
226    }
227
228    /// Clear all logs.
229    pub fn clear_logs(&mut self) {
230        self.logs.clear();
231    }
232
233    /// Clear all messages.
234    pub fn clear_messages(&mut self) {
235        self.messages.clear();
236    }
237
238    /// Get the number of documents with stored diagnostics.
239    #[inline]
240    #[must_use]
241    pub fn diagnostics_count(&self) -> usize {
242        self.diagnostics.len()
243    }
244
245    /// Get the number of stored log entries.
246    #[inline]
247    #[must_use]
248    pub fn logs_count(&self) -> usize {
249        self.logs.len()
250    }
251
252    /// Get the number of stored server messages.
253    #[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        // Add more than MAX_LOG_ENTRIES
372        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        // Oldest entries should be removed (FIFO)
379        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        // Add more than MAX_SERVER_MESSAGES
417        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        // Oldest entries should be removed (FIFO)
424        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}