use super::common::uri_to_path;
use lsp_types::{Diagnostic, DiagnosticSeverity, PublishDiagnosticsParams, Uri};
use schemars::JsonSchema;
use serde::Serialize;
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct FormattedDiagnostic {
pub file: String,
pub line: u32,
pub column: u32,
pub severity: Severity,
pub message: String,
pub source: Option<String>,
pub code: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Error,
Warning,
Info,
Hint,
}
impl From<Option<DiagnosticSeverity>> for Severity {
fn from(severity: Option<DiagnosticSeverity>) -> Self {
match severity {
Some(DiagnosticSeverity::ERROR) | None => Severity::Error,
Some(DiagnosticSeverity::WARNING) => Severity::Warning,
Some(DiagnosticSeverity::HINT) => Severity::Hint,
_ => Severity::Info, }
}
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Severity::Error => write!(f, "error"),
Severity::Warning => write!(f, "warning"),
Severity::Info => write!(f, "info"),
Severity::Hint => write!(f, "hint"),
}
}
}
impl FormattedDiagnostic {
pub fn from_diagnostic(uri: &Uri, diagnostic: &Diagnostic) -> Self {
let file = uri_to_path(uri);
let code = diagnostic.code.as_ref().map(|c| match c {
lsp_types::NumberOrString::Number(n) => n.to_string(),
lsp_types::NumberOrString::String(s) => s.clone(),
});
Self {
file,
line: diagnostic.range.start.line + 1, column: diagnostic.range.start.character + 1,
severity: diagnostic.severity.into(),
message: diagnostic.message.clone(),
source: diagnostic.source.clone(),
code,
}
}
pub fn format(&self) -> String {
let source = self.source.as_ref().map(|s| format!("[{s}] ")).unwrap_or_default();
let code = self.code.as_ref().map(|c| format!("[{c}] ")).unwrap_or_default();
format!("{}: {}{}{}:{}:{}: {}", self.severity, source, code, self.file, self.line, self.column, self.message)
}
}
pub fn format_diagnostics(params: &PublishDiagnosticsParams) -> Vec<FormattedDiagnostic> {
params.diagnostics.iter().map(|d| FormattedDiagnostic::from_diagnostic(¶ms.uri, d)).collect()
}
pub fn filter_by_severity(diagnostics: &[FormattedDiagnostic], min_severity: Severity) -> Vec<&FormattedDiagnostic> {
diagnostics
.iter()
.filter(|d| {
matches!(
(d.severity, min_severity),
(Severity::Error, _)
| (Severity::Warning, Severity::Warning | Severity::Info | Severity::Hint)
| (Severity::Info, Severity::Info | Severity::Hint)
| (Severity::Hint, Severity::Hint)
)
})
.collect()
}
pub fn count_by_severity(diagnostics: &[FormattedDiagnostic]) -> DiagnosticCounts {
let mut counts = DiagnosticCounts::default();
for d in diagnostics {
match d.severity {
Severity::Error => counts.errors += 1,
Severity::Warning => counts.warnings += 1,
Severity::Info => counts.infos += 1,
Severity::Hint => counts.hints += 1,
}
}
counts.total = counts.errors + counts.warnings + counts.infos + counts.hints;
counts
}
#[derive(Debug, Default, Clone, Copy, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct DiagnosticCounts {
pub errors: usize,
pub warnings: usize,
pub infos: usize,
pub hints: usize,
pub total: usize,
}
impl DiagnosticCounts {
pub fn has_errors(&self) -> bool {
self.errors > 0
}
}
impl std::fmt::Display for DiagnosticCounts {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} errors, {} warnings, {} info, {} hints", self.errors, self.warnings, self.infos, self.hints)
}
}
#[cfg(test)]
mod tests {
use super::*;
use lsp_types::{Position, Range};
fn make_diagnostic(severity: DiagnosticSeverity, message: &str, line: u32) -> Diagnostic {
Diagnostic {
range: Range { start: Position { line, character: 0 }, end: Position { line, character: 10 } },
severity: Some(severity),
code: None,
code_description: None,
source: Some("test".to_string()),
message: message.to_string(),
related_information: None,
tags: None,
data: None,
}
}
fn make_uri(path: &str) -> Uri {
format!("file://{path}").parse().unwrap()
}
#[test]
fn test_severity_conversion() {
assert_eq!(Severity::from(Some(DiagnosticSeverity::ERROR)), Severity::Error);
assert_eq!(Severity::from(Some(DiagnosticSeverity::WARNING)), Severity::Warning);
assert_eq!(Severity::from(Some(DiagnosticSeverity::INFORMATION)), Severity::Info);
assert_eq!(Severity::from(Some(DiagnosticSeverity::HINT)), Severity::Hint);
assert_eq!(Severity::from(None), Severity::Error);
}
#[test]
fn test_formatted_diagnostic() {
let uri = make_uri("/path/to/file.rs");
let diagnostic = make_diagnostic(DiagnosticSeverity::ERROR, "test error", 5);
let formatted = FormattedDiagnostic::from_diagnostic(&uri, &diagnostic);
assert_eq!(formatted.file, "/path/to/file.rs");
assert_eq!(formatted.line, 6); assert_eq!(formatted.column, 1);
assert_eq!(formatted.severity, Severity::Error);
assert_eq!(formatted.message, "test error");
}
#[test]
fn test_count_by_severity() {
let uri = make_uri("/path/to/file.rs");
let diagnostics = [
make_diagnostic(DiagnosticSeverity::ERROR, "error 1", 0),
make_diagnostic(DiagnosticSeverity::ERROR, "error 2", 1),
make_diagnostic(DiagnosticSeverity::WARNING, "warning 1", 2),
];
let formatted: Vec<_> = diagnostics.iter().map(|d| FormattedDiagnostic::from_diagnostic(&uri, d)).collect();
let counts = count_by_severity(&formatted);
assert_eq!(counts.errors, 2);
assert_eq!(counts.warnings, 1);
assert_eq!(counts.infos, 0);
assert_eq!(counts.hints, 0);
assert_eq!(counts.total, 3);
assert!(counts.has_errors());
}
#[test]
fn test_uri_to_path() {
assert_eq!(uri_to_path(&make_uri("/path/to/file.rs")), "/path/to/file.rs");
let non_file_uri: Uri = "https://example.com/file.rs".parse().unwrap();
assert_eq!(uri_to_path(&non_file_uri), "https://example.com/file.rs");
}
}