mcpls_core/bridge/notifications/
pulled_index.rs1use std::collections::{HashMap, HashSet};
5
6use lsp_types::Diagnostic as LspDiagnostic;
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10use super::message_as_str;
11
12pub(super) const DUPLICATE_RANGE_PROXIMITY_LINES: u32 = 3;
21
22#[derive(
28 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
29)]
30#[serde(rename_all = "lowercase")]
31#[schemars(rename = "DiagnosticSeverity")]
32#[schemars(description = "Diagnostic severity.")]
33pub enum ReportedSeverity {
34 Error,
36 Warning,
38 Information,
41 Hint,
43}
44
45impl ReportedSeverity {
46 #[must_use]
48 pub const fn of(diagnostic: &LspDiagnostic) -> Self {
49 match diagnostic.severity {
50 Some(lsp_types::DiagnosticSeverity::Error) => Self::Error,
51 Some(lsp_types::DiagnosticSeverity::Warning) => Self::Warning,
52 Some(lsp_types::DiagnosticSeverity::Hint) => Self::Hint,
53 _ => Self::Information,
54 }
55 }
56}
57
58pub fn reported_code(diagnostic: &LspDiagnostic) -> Option<std::borrow::Cow<'_, str>> {
61 diagnostic.code.as_ref().map(|code| match code {
62 lsp_types::Code::Int(n) => std::borrow::Cow::Owned(n.to_string()),
63 lsp_types::Code::String(s) => std::borrow::Cow::Borrowed(s.as_str()),
64 })
65}
66
67pub(super) const fn point(position: lsp_types::Position) -> (u32, u32) {
69 (position.line, position.character)
70}
71
72#[derive(Debug, Default)]
76struct RangeIndex {
77 ranges: Vec<((u32, u32), (u32, u32))>,
79 max_end_through: Vec<(u32, u32)>,
81}
82
83impl RangeIndex {
84 fn seal(&mut self) {
85 self.ranges.sort_unstable();
86 let mut max_end = (0, 0);
87 self.max_end_through = self
88 .ranges
89 .iter()
90 .map(|&(_, end)| {
91 max_end = max_end.max(end);
92 max_end
93 })
94 .collect();
95 }
96
97 fn is_near(&self, range: lsp_types::Range) -> bool {
98 let (start, end) = (point(range.start), point(range.end));
99 let started_before_end = self.ranges.partition_point(|&(s, _)| s <= end);
100 let overlaps = started_before_end
101 .checked_sub(1)
102 .and_then(|last| self.max_end_through.get(last))
103 .is_some_and(|&max_end| max_end >= start);
104 if overlaps {
105 return true;
106 }
107 let first_line = start.0.saturating_sub(DUPLICATE_RANGE_PROXIMITY_LINES);
108 let from = self.ranges.partition_point(|&(s, _)| s.0 < first_line);
109 self.ranges
110 .get(from)
111 .is_some_and(|&(s, _)| s.0 <= start.0.saturating_add(DUPLICATE_RANGE_PROXIMITY_LINES))
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118struct UncodedKey<'a> {
119 severity: ReportedSeverity,
120 start: (u32, u32),
121 end: (u32, u32),
122 message: &'a str,
123}
124
125impl<'a> UncodedKey<'a> {
126 fn of(diagnostic: &'a LspDiagnostic) -> Self {
127 Self {
128 severity: ReportedSeverity::of(diagnostic),
129 start: point(diagnostic.range.start),
130 end: point(diagnostic.range.end),
131 message: message_as_str(&diagnostic.message),
132 }
133 }
134}
135
136#[derive(Debug, Default)]
143pub(super) struct PulledIndex<'a> {
144 coded: HashMap<ReportedSeverity, HashMap<String, RangeIndex>>,
145 uncoded: HashSet<UncodedKey<'a>>,
146}
147
148impl<'a> PulledIndex<'a> {
149 pub(super) fn new(pulled: &'a [LspDiagnostic]) -> Self {
150 let mut index = Self::default();
151 for diagnostic in pulled {
152 let severity = ReportedSeverity::of(diagnostic);
153 let range = diagnostic.range;
154 match reported_code(diagnostic) {
155 Some(code) => {
156 index
157 .coded
158 .entry(severity)
159 .or_default()
160 .entry(code.into_owned())
161 .or_default()
162 .ranges
163 .push((point(range.start), point(range.end)));
164 }
165 None => {
166 index.uncoded.insert(UncodedKey::of(diagnostic));
167 }
168 }
169 }
170 for group in index.coded.values_mut().flat_map(HashMap::values_mut) {
171 group.seal();
172 }
173 index
174 }
175
176 pub(super) fn contains_same_problem(&self, pushed: &LspDiagnostic) -> bool {
186 let severity = ReportedSeverity::of(pushed);
187 reported_code(pushed).map_or_else(
188 || self.uncoded.contains(&UncodedKey::of(pushed)),
189 |code| {
190 self.coded
191 .get(&severity)
192 .and_then(|groups| groups.get(code.as_ref()))
193 .is_some_and(|group| group.is_near(pushed.range))
194 },
195 )
196 }
197}