use std::collections::{HashMap, HashSet};
use lsp_types::Diagnostic as LspDiagnostic;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::message_as_str;
pub(super) const DUPLICATE_RANGE_PROXIMITY_LINES: u32 = 3;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "lowercase")]
#[schemars(rename = "DiagnosticSeverity")]
#[schemars(description = "Diagnostic severity.")]
pub enum ReportedSeverity {
Error,
Warning,
Information,
Hint,
}
impl ReportedSeverity {
#[must_use]
pub const fn of(diagnostic: &LspDiagnostic) -> Self {
match diagnostic.severity {
Some(lsp_types::DiagnosticSeverity::Error) => Self::Error,
Some(lsp_types::DiagnosticSeverity::Warning) => Self::Warning,
Some(lsp_types::DiagnosticSeverity::Hint) => Self::Hint,
_ => Self::Information,
}
}
}
pub fn reported_code(diagnostic: &LspDiagnostic) -> Option<std::borrow::Cow<'_, str>> {
diagnostic.code.as_ref().map(|code| match code {
lsp_types::Code::Int(n) => std::borrow::Cow::Owned(n.to_string()),
lsp_types::Code::String(s) => std::borrow::Cow::Borrowed(s.as_str()),
})
}
pub(super) const fn point(position: lsp_types::Position) -> (u32, u32) {
(position.line, position.character)
}
#[derive(Debug, Default)]
struct RangeIndex {
ranges: Vec<((u32, u32), (u32, u32))>,
max_end_through: Vec<(u32, u32)>,
}
impl RangeIndex {
fn seal(&mut self) {
self.ranges.sort_unstable();
let mut max_end = (0, 0);
self.max_end_through = self
.ranges
.iter()
.map(|&(_, end)| {
max_end = max_end.max(end);
max_end
})
.collect();
}
fn is_near(&self, range: lsp_types::Range) -> bool {
let (start, end) = (point(range.start), point(range.end));
let started_before_end = self.ranges.partition_point(|&(s, _)| s <= end);
let overlaps = started_before_end
.checked_sub(1)
.and_then(|last| self.max_end_through.get(last))
.is_some_and(|&max_end| max_end >= start);
if overlaps {
return true;
}
let first_line = start.0.saturating_sub(DUPLICATE_RANGE_PROXIMITY_LINES);
let from = self.ranges.partition_point(|&(s, _)| s.0 < first_line);
self.ranges
.get(from)
.is_some_and(|&(s, _)| s.0 <= start.0.saturating_add(DUPLICATE_RANGE_PROXIMITY_LINES))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct UncodedKey<'a> {
severity: ReportedSeverity,
start: (u32, u32),
end: (u32, u32),
message: &'a str,
}
impl<'a> UncodedKey<'a> {
fn of(diagnostic: &'a LspDiagnostic) -> Self {
Self {
severity: ReportedSeverity::of(diagnostic),
start: point(diagnostic.range.start),
end: point(diagnostic.range.end),
message: message_as_str(&diagnostic.message),
}
}
}
#[derive(Debug, Default)]
pub(super) struct PulledIndex<'a> {
coded: HashMap<ReportedSeverity, HashMap<String, RangeIndex>>,
uncoded: HashSet<UncodedKey<'a>>,
}
impl<'a> PulledIndex<'a> {
pub(super) fn new(pulled: &'a [LspDiagnostic]) -> Self {
let mut index = Self::default();
for diagnostic in pulled {
let severity = ReportedSeverity::of(diagnostic);
let range = diagnostic.range;
match reported_code(diagnostic) {
Some(code) => {
index
.coded
.entry(severity)
.or_default()
.entry(code.into_owned())
.or_default()
.ranges
.push((point(range.start), point(range.end)));
}
None => {
index.uncoded.insert(UncodedKey::of(diagnostic));
}
}
}
for group in index.coded.values_mut().flat_map(HashMap::values_mut) {
group.seal();
}
index
}
pub(super) fn contains_same_problem(&self, pushed: &LspDiagnostic) -> bool {
let severity = ReportedSeverity::of(pushed);
reported_code(pushed).map_or_else(
|| self.uncoded.contains(&UncodedKey::of(pushed)),
|code| {
self.coded
.get(&severity)
.and_then(|groups| groups.get(code.as_ref()))
.is_some_and(|group| group.is_near(pushed.range))
},
)
}
}