use std::collections::{HashMap, VecDeque};
use chrono::{DateTime, Utc};
use lsp_types::{Diagnostic as LspDiagnostic, Uri};
use serde::{Deserialize, Serialize};
const MAX_LOG_ENTRIES: usize = 100;
const MAX_SERVER_MESSAGES: usize = 50;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiagnosticInfo {
pub uri: Uri,
pub version: Option<i32>,
pub diagnostics: Vec<LspDiagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogEntry {
pub level: LogLevel,
pub message: String,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Error,
Warning,
Info,
Debug,
}
impl From<lsp_types::MessageType> for LogLevel {
fn from(msg_type: lsp_types::MessageType) -> Self {
match msg_type {
lsp_types::MessageType::ERROR => Self::Error,
lsp_types::MessageType::WARNING => Self::Warning,
lsp_types::MessageType::INFO => Self::Info,
_ => Self::Debug,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerMessage {
pub message_type: MessageType,
pub message: String,
pub timestamp: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum MessageType {
Error,
Warning,
Info,
Log,
}
impl From<lsp_types::MessageType> for MessageType {
fn from(msg_type: lsp_types::MessageType) -> Self {
match msg_type {
lsp_types::MessageType::ERROR => Self::Error,
lsp_types::MessageType::WARNING => Self::Warning,
lsp_types::MessageType::INFO => Self::Info,
_ => Self::Log,
}
}
}
#[derive(Debug)]
pub struct NotificationCache {
diagnostics: HashMap<String, DiagnosticInfo>,
logs: VecDeque<LogEntry>,
messages: VecDeque<ServerMessage>,
}
impl Default for NotificationCache {
fn default() -> Self {
Self::new()
}
}
impl NotificationCache {
#[must_use]
pub fn new() -> Self {
Self {
diagnostics: HashMap::with_capacity(32),
logs: VecDeque::with_capacity(MAX_LOG_ENTRIES),
messages: VecDeque::with_capacity(MAX_SERVER_MESSAGES),
}
}
pub fn store_diagnostics(
&mut self,
uri: &Uri,
version: Option<i32>,
diagnostics: Vec<LspDiagnostic>,
) {
let info = DiagnosticInfo {
uri: uri.clone(),
version,
diagnostics,
};
self.diagnostics.insert(uri.to_string(), info);
}
pub fn store_log(&mut self, level: LogLevel, message: String) {
let entry = LogEntry {
level,
message,
timestamp: Utc::now(),
};
if self.logs.len() >= MAX_LOG_ENTRIES {
self.logs.pop_front();
}
self.logs.push_back(entry);
}
pub fn store_message(&mut self, message_type: MessageType, message: String) {
let msg = ServerMessage {
message_type,
message,
timestamp: Utc::now(),
};
if self.messages.len() >= MAX_SERVER_MESSAGES {
self.messages.pop_front();
}
self.messages.push_back(msg);
}
#[inline]
#[must_use]
pub fn get_diagnostics(&self, uri: &str) -> Option<&DiagnosticInfo> {
self.diagnostics.get(uri)
}
#[inline]
#[must_use]
pub const fn get_logs(&self) -> &VecDeque<LogEntry> {
&self.logs
}
#[inline]
#[must_use]
pub const fn get_messages(&self) -> &VecDeque<ServerMessage> {
&self.messages
}
pub fn clear_diagnostics(&mut self, uri: &str) -> Option<DiagnosticInfo> {
self.diagnostics.remove(uri)
}
pub fn clear_all_diagnostics(&mut self) {
self.diagnostics.clear();
}
pub fn clear_logs(&mut self) {
self.logs.clear();
}
pub fn clear_messages(&mut self) {
self.messages.clear();
}
#[inline]
#[must_use]
pub fn diagnostics_count(&self) -> usize {
self.diagnostics.len()
}
#[inline]
#[must_use]
pub fn logs_count(&self) -> usize {
self.logs.len()
}
#[inline]
#[must_use]
pub fn messages_count(&self) -> usize {
self.messages.len()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use lsp_types::{Position, Range};
use super::*;
#[test]
fn test_notification_cache_new() {
let cache = NotificationCache::new();
assert_eq!(cache.diagnostics_count(), 0);
assert_eq!(cache.logs_count(), 0);
assert_eq!(cache.messages_count(), 0);
}
#[test]
fn test_store_and_get_diagnostics() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
let diagnostic = LspDiagnostic {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
},
severity: Some(lsp_types::DiagnosticSeverity::ERROR),
message: "test error".to_string(),
code: None,
source: None,
code_description: None,
related_information: None,
tags: None,
data: None,
};
cache.store_diagnostics(&uri, Some(1), vec![diagnostic]);
let stored = cache.get_diagnostics(uri.as_str()).unwrap();
assert_eq!(stored.uri, uri);
assert_eq!(stored.version, Some(1));
assert_eq!(stored.diagnostics.len(), 1);
assert_eq!(stored.diagnostics[0].message, "test error");
}
#[test]
fn test_store_diagnostics_replaces_existing() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
cache.store_diagnostics(&uri, Some(1), vec![]);
assert_eq!(cache.diagnostics_count(), 1);
cache.store_diagnostics(&uri, Some(2), vec![]);
assert_eq!(cache.diagnostics_count(), 1);
let stored = cache.get_diagnostics(uri.as_str()).unwrap();
assert_eq!(stored.version, Some(2));
}
#[test]
fn test_clear_diagnostics() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
cache.store_diagnostics(&uri, Some(1), vec![]);
assert_eq!(cache.diagnostics_count(), 1);
let cleared = cache.clear_diagnostics(uri.as_str());
assert!(cleared.is_some());
assert_eq!(cache.diagnostics_count(), 0);
}
#[test]
fn test_clear_all_diagnostics() {
let mut cache = NotificationCache::new();
let uri1: Uri = "file:///test1.rs".parse().unwrap();
let uri2: Uri = "file:///test2.rs".parse().unwrap();
cache.store_diagnostics(&uri1, Some(1), vec![]);
cache.store_diagnostics(&uri2, Some(1), vec![]);
assert_eq!(cache.diagnostics_count(), 2);
cache.clear_all_diagnostics();
assert_eq!(cache.diagnostics_count(), 0);
}
#[test]
fn test_store_and_get_logs() {
let mut cache = NotificationCache::new();
cache.store_log(LogLevel::Error, "error message".to_string());
cache.store_log(LogLevel::Info, "info message".to_string());
let logs = cache.get_logs();
assert_eq!(logs.len(), 2);
assert_eq!(logs[0].level, LogLevel::Error);
assert_eq!(logs[0].message, "error message");
assert_eq!(logs[1].level, LogLevel::Info);
assert_eq!(logs[1].message, "info message");
}
#[test]
fn test_logs_max_capacity() {
let mut cache = NotificationCache::new();
for i in 0..MAX_LOG_ENTRIES + 10 {
cache.store_log(LogLevel::Info, format!("message {i}"));
}
assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
let logs = cache.get_logs();
assert_eq!(logs.front().unwrap().message, "message 10");
assert_eq!(
logs.back().unwrap().message,
format!("message {}", MAX_LOG_ENTRIES + 9)
);
}
#[test]
fn test_clear_logs() {
let mut cache = NotificationCache::new();
cache.store_log(LogLevel::Info, "test".to_string());
assert_eq!(cache.logs_count(), 1);
cache.clear_logs();
assert_eq!(cache.logs_count(), 0);
}
#[test]
fn test_store_and_get_messages() {
let mut cache = NotificationCache::new();
cache.store_message(MessageType::Error, "error msg".to_string());
cache.store_message(MessageType::Warning, "warning msg".to_string());
let messages = cache.get_messages();
assert_eq!(messages.len(), 2);
assert_eq!(messages[0].message_type, MessageType::Error);
assert_eq!(messages[0].message, "error msg");
assert_eq!(messages[1].message_type, MessageType::Warning);
assert_eq!(messages[1].message, "warning msg");
}
#[test]
fn test_messages_max_capacity() {
let mut cache = NotificationCache::new();
for i in 0..MAX_SERVER_MESSAGES + 10 {
cache.store_message(MessageType::Info, format!("message {i}"));
}
assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
let messages = cache.get_messages();
assert_eq!(messages.front().unwrap().message, "message 10");
assert_eq!(
messages.back().unwrap().message,
format!("message {}", MAX_SERVER_MESSAGES + 9)
);
}
#[test]
fn test_clear_messages() {
let mut cache = NotificationCache::new();
cache.store_message(MessageType::Info, "test".to_string());
assert_eq!(cache.messages_count(), 1);
cache.clear_messages();
assert_eq!(cache.messages_count(), 0);
}
#[test]
fn test_log_levels() {
let mut cache = NotificationCache::new();
cache.store_log(LogLevel::Error, "error".to_string());
cache.store_log(LogLevel::Warning, "warning".to_string());
cache.store_log(LogLevel::Info, "info".to_string());
cache.store_log(LogLevel::Debug, "debug".to_string());
let logs = cache.get_logs();
assert_eq!(logs[0].level, LogLevel::Error);
assert_eq!(logs[1].level, LogLevel::Warning);
assert_eq!(logs[2].level, LogLevel::Info);
assert_eq!(logs[3].level, LogLevel::Debug);
}
#[test]
fn test_message_types() {
let mut cache = NotificationCache::new();
cache.store_message(MessageType::Error, "error".to_string());
cache.store_message(MessageType::Warning, "warning".to_string());
cache.store_message(MessageType::Info, "info".to_string());
cache.store_message(MessageType::Log, "log".to_string());
let messages = cache.get_messages();
assert_eq!(messages[0].message_type, MessageType::Error);
assert_eq!(messages[1].message_type, MessageType::Warning);
assert_eq!(messages[2].message_type, MessageType::Info);
assert_eq!(messages[3].message_type, MessageType::Log);
}
#[test]
fn test_timestamp_ordering() {
let mut cache = NotificationCache::new();
cache.store_log(LogLevel::Info, "first".to_string());
std::thread::sleep(std::time::Duration::from_millis(10));
cache.store_log(LogLevel::Info, "second".to_string());
let logs = cache.get_logs();
assert!(logs[0].timestamp < logs[1].timestamp);
}
#[test]
fn test_store_diagnostics_empty_list() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
let diagnostic = LspDiagnostic {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 5,
},
},
severity: Some(lsp_types::DiagnosticSeverity::ERROR),
message: "test error".to_string(),
code: None,
source: None,
code_description: None,
related_information: None,
tags: None,
data: None,
};
cache.store_diagnostics(&uri, Some(1), vec![diagnostic]);
assert_eq!(
cache
.get_diagnostics(uri.as_str())
.unwrap()
.diagnostics
.len(),
1
);
cache.store_diagnostics(&uri, Some(2), vec![]);
let stored = cache.get_diagnostics(uri.as_str()).unwrap();
assert_eq!(stored.diagnostics.len(), 0);
assert_eq!(stored.version, Some(2));
}
#[test]
fn test_store_many_diagnostics_single_file() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
let diagnostics: Vec<LspDiagnostic> = (0..100)
.map(|i| LspDiagnostic {
range: Range {
start: Position {
line: i,
character: 0,
},
end: Position {
line: i,
character: 10,
},
},
message: format!("Error {i}"),
severity: Some(lsp_types::DiagnosticSeverity::ERROR),
code: None,
source: None,
code_description: None,
related_information: None,
tags: None,
data: None,
})
.collect();
cache.store_diagnostics(&uri, Some(1), diagnostics);
let stored = cache.get_diagnostics(uri.as_str()).unwrap();
assert_eq!(stored.diagnostics.len(), 100);
}
#[test]
fn test_logs_exact_capacity_boundary() {
let mut cache = NotificationCache::new();
for i in 0..MAX_LOG_ENTRIES {
cache.store_log(LogLevel::Info, format!("message {i}"));
}
assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
cache.store_log(LogLevel::Info, "overflow".to_string());
assert_eq!(cache.logs_count(), MAX_LOG_ENTRIES);
assert_eq!(cache.get_logs().front().unwrap().message, "message 1");
}
#[test]
fn test_messages_exact_capacity_boundary() {
let mut cache = NotificationCache::new();
for i in 0..MAX_SERVER_MESSAGES {
cache.store_message(MessageType::Info, format!("message {i}"));
}
assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
cache.store_message(MessageType::Info, "overflow".to_string());
assert_eq!(cache.messages_count(), MAX_SERVER_MESSAGES);
assert_eq!(cache.get_messages().front().unwrap().message, "message 1");
}
#[test]
fn test_clear_diagnostics_nonexistent() {
let mut cache = NotificationCache::new();
let result = cache.clear_diagnostics("file:///nonexistent.rs");
assert!(result.is_none());
}
#[test]
fn test_store_diagnostics_no_version() {
let mut cache = NotificationCache::new();
let uri: Uri = "file:///test.rs".parse().unwrap();
cache.store_diagnostics(&uri, None, vec![]);
let stored = cache.get_diagnostics(uri.as_str()).unwrap();
assert_eq!(stored.version, None);
}
}