use std::path::PathBuf;
use crate::lsp::{Diagnostic, Severity};
#[derive(Debug, Clone)]
pub struct DiagItem {
pub path: PathBuf,
pub rel: String,
pub line: u32,
pub col: u32,
pub severity: Severity,
pub message: String,
pub source: Option<String>,
}
pub struct DiagnosticsPane {
pub items: Vec<DiagItem>,
pub selected: usize,
pub scroll: usize,
pub min_severity: Severity,
}
fn sev_rank(s: Severity) -> u8 {
match s {
Severity::Error => 0,
Severity::Warning => 1,
Severity::Info => 2,
Severity::Hint => 3,
}
}
impl DiagnosticsPane {
pub fn build<'a>(
sources: impl IntoIterator<Item = (PathBuf, String, &'a [Diagnostic])>,
) -> Self {
let mut items: Vec<DiagItem> = Vec::new();
for (path, rel, diags) in sources {
for d in diags {
let message = d
.message
.lines()
.map(str::trim)
.find(|l| !l.is_empty())
.unwrap_or("")
.to_string();
items.push(DiagItem {
path: path.clone(),
rel: rel.clone(),
line: d.range.start.line,
col: d.range.start.character,
severity: d.severity,
message,
source: d.source.clone(),
});
}
}
items.sort_by(|a, b| {
sev_rank(a.severity)
.cmp(&sev_rank(b.severity))
.then_with(|| a.rel.cmp(&b.rel))
.then_with(|| a.line.cmp(&b.line))
.then_with(|| a.col.cmp(&b.col))
});
DiagnosticsPane {
items,
selected: 0,
scroll: 0,
min_severity: Severity::Hint,
}
}
pub fn visible(&self) -> Vec<&DiagItem> {
let cap = sev_rank(self.min_severity);
self.items
.iter()
.filter(|it| sev_rank(it.severity) <= cap)
.collect()
}
pub fn visible_indices(&self) -> Vec<usize> {
let cap = sev_rank(self.min_severity);
self.items
.iter()
.enumerate()
.filter(|(_, it)| sev_rank(it.severity) <= cap)
.map(|(i, _)| i)
.collect()
}
pub fn severity_label(&self) -> &'static str {
match self.min_severity {
Severity::Error => "errors only",
Severity::Warning => "errors + warnings",
Severity::Info => "errors + warnings + info",
Severity::Hint => "all",
}
}
pub fn cycle_severity_filter(&mut self) {
self.min_severity = match self.min_severity {
Severity::Hint => Severity::Error,
Severity::Error => Severity::Warning,
Severity::Warning => Severity::Info,
Severity::Info => Severity::Hint,
};
self.selected = 0;
self.scroll = 0;
}
pub fn counts(&self) -> (usize, usize) {
let errors = self
.items
.iter()
.filter(|i| i.severity == Severity::Error)
.count();
let warnings = self
.items
.iter()
.filter(|i| i.severity == Severity::Warning)
.count();
(errors, warnings)
}
pub fn tab_title(&self) -> String {
let (e, w) = self.counts();
match (e, w) {
(0, 0) => "problems ✓".to_string(),
(e, 0) => format!("problems ✗{e}"),
(0, w) => format!("problems ⚠{w}"),
(e, w) => format!("problems ✗{e} ⚠{w}"),
}
}
pub fn clamp(&mut self) {
if self.items.is_empty() {
self.selected = 0;
} else if self.selected >= self.items.len() {
self.selected = self.items.len() - 1;
}
}
pub fn move_selection(&mut self, delta: isize) {
let n = self.visible_indices().len();
if n == 0 {
return;
}
self.selected = (self.selected as isize + delta).clamp(0, n as isize - 1) as usize;
}
pub fn selected_item(&self) -> Option<&DiagItem> {
let vis = self.visible_indices();
vis.get(self.selected).and_then(|&i| self.items.get(i))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lsp::{Pos, Range};
fn diag(line: u32, sev: Severity, msg: &str) -> Diagnostic {
Diagnostic {
range: Range {
start: Pos { line, character: 0 },
end: Pos { line, character: 1 },
},
severity: sev,
message: msg.to_string(),
source: None,
}
}
#[test]
fn build_sorts_errors_first_then_file_then_line() {
let a = [
diag(10, Severity::Warning, "w"),
diag(2, Severity::Error, "e"),
];
let b = [diag(5, Severity::Error, "e2")];
let p = DiagnosticsPane::build(vec![
(PathBuf::from("/x/b.rs"), "b.rs".into(), a.as_slice()),
(PathBuf::from("/x/a.rs"), "a.rs".into(), b.as_slice()),
]);
assert_eq!(p.items.len(), 3);
assert_eq!(p.items[0].rel, "a.rs");
assert_eq!(p.items[0].severity, Severity::Error);
assert_eq!(p.items[1].rel, "b.rs");
assert_eq!(p.items[1].severity, Severity::Error);
assert_eq!(p.items[2].severity, Severity::Warning);
assert_eq!(p.counts(), (2, 1));
}
#[test]
fn move_selection_clamps() {
let a = [diag(1, Severity::Error, "e")];
let mut p = DiagnosticsPane::build(vec![(PathBuf::from("/x"), "x".into(), a.as_slice())]);
p.move_selection(-5);
assert_eq!(p.selected, 0);
p.move_selection(99);
assert_eq!(p.selected, 0);
}
}