1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use crate::lsp::registry::ServerKind;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct StoredDiagnostic {
9 pub file: PathBuf,
10 pub line: u32,
11 pub column: u32,
12 pub end_line: u32,
13 pub end_column: u32,
14 pub severity: DiagnosticSeverity,
15 pub message: String,
16 pub code: Option<String>,
17 pub source: Option<String>,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum DiagnosticSeverity {
22 Error,
23 Warning,
24 Information,
25 Hint,
26}
27
28impl DiagnosticSeverity {
29 pub fn as_str(self) -> &'static str {
30 match self {
31 Self::Error => "error",
32 Self::Warning => "warning",
33 Self::Information => "information",
34 Self::Hint => "hint",
35 }
36 }
37}
38
39pub struct DiagnosticsStore {
45 store: HashMap<(ServerKind, PathBuf), Vec<StoredDiagnostic>>,
48}
49
50impl DiagnosticsStore {
51 pub fn new() -> Self {
52 Self {
53 store: HashMap::new(),
54 }
55 }
56
57 pub fn publish(
59 &mut self,
60 server: ServerKind,
61 file: PathBuf,
62 diagnostics: Vec<StoredDiagnostic>,
63 ) {
64 if diagnostics.is_empty() {
65 self.store.remove(&(server, file));
66 } else {
67 self.store.insert((server, file), diagnostics);
68 }
69 }
70
71 pub fn for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
73 self.store
74 .iter()
75 .filter(|((_, stored_file), _)| stored_file == file)
76 .flat_map(|(_, diagnostics)| diagnostics.iter())
77 .collect()
78 }
79
80 pub fn for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
82 self.store
83 .iter()
84 .filter(|((_, stored_file), _)| stored_file.starts_with(dir))
85 .flat_map(|(_, diagnostics)| diagnostics.iter())
86 .collect()
87 }
88
89 pub fn all(&self) -> Vec<&StoredDiagnostic> {
91 self.store.values().flat_map(|value| value.iter()).collect()
92 }
93
94 pub fn clear_server(&mut self, server: ServerKind) {
96 self.store
97 .retain(|(stored_server, _), _| *stored_server != server);
98 }
99}
100
101impl Default for DiagnosticsStore {
102 fn default() -> Self {
103 Self::new()
104 }
105}
106
107pub fn from_lsp_diagnostics(
110 file: PathBuf,
111 lsp_diagnostics: Vec<lsp_types::Diagnostic>,
112) -> Vec<StoredDiagnostic> {
113 lsp_diagnostics
114 .into_iter()
115 .map(|diagnostic| StoredDiagnostic {
116 file: file.clone(),
117 line: diagnostic.range.start.line + 1,
118 column: diagnostic.range.start.character + 1,
119 end_line: diagnostic.range.end.line + 1,
120 end_column: diagnostic.range.end.character + 1,
121 severity: match diagnostic.severity {
122 Some(lsp_types::DiagnosticSeverity::ERROR) => DiagnosticSeverity::Error,
123 Some(lsp_types::DiagnosticSeverity::WARNING) => DiagnosticSeverity::Warning,
124 Some(lsp_types::DiagnosticSeverity::INFORMATION) => DiagnosticSeverity::Information,
125 Some(lsp_types::DiagnosticSeverity::HINT) => DiagnosticSeverity::Hint,
126 _ => DiagnosticSeverity::Warning,
127 },
128 message: diagnostic.message,
129 code: diagnostic.code.map(|code| match code {
130 lsp_types::NumberOrString::Number(value) => value.to_string(),
131 lsp_types::NumberOrString::String(value) => value,
132 }),
133 source: diagnostic.source,
134 })
135 .collect()
136}
137
138#[cfg(test)]
139mod tests {
140 use std::path::PathBuf;
141
142 use lsp_types::{
143 Diagnostic, DiagnosticSeverity as LspDiagnosticSeverity, NumberOrString, Position, Range,
144 };
145
146 use super::{from_lsp_diagnostics, DiagnosticSeverity, DiagnosticsStore, StoredDiagnostic};
147 use crate::lsp::registry::ServerKind;
148
149 #[test]
150 fn converts_lsp_positions_to_one_based() {
151 let file = PathBuf::from("/tmp/demo.rs");
152 let diagnostics = from_lsp_diagnostics(
153 file.clone(),
154 vec![Diagnostic {
155 range: Range::new(Position::new(0, 0), Position::new(1, 4)),
156 severity: Some(LspDiagnosticSeverity::ERROR),
157 code: Some(NumberOrString::String("E1".into())),
158 code_description: None,
159 source: Some("fake".into()),
160 message: "boom".into(),
161 related_information: None,
162 tags: None,
163 data: None,
164 }],
165 );
166
167 assert_eq!(diagnostics.len(), 1);
168 assert_eq!(diagnostics[0].file, file);
169 assert_eq!(diagnostics[0].line, 1);
170 assert_eq!(diagnostics[0].column, 1);
171 assert_eq!(diagnostics[0].end_line, 2);
172 assert_eq!(diagnostics[0].end_column, 5);
173 assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Error);
174 assert_eq!(diagnostics[0].code.as_deref(), Some("E1"));
175 }
176
177 #[test]
178 fn publish_replaces_existing_file_diagnostics() {
179 let file = PathBuf::from("/tmp/demo.rs");
180 let mut store = DiagnosticsStore::new();
181
182 store.publish(
183 ServerKind::Rust,
184 file.clone(),
185 vec![StoredDiagnostic {
186 file: file.clone(),
187 line: 1,
188 column: 1,
189 end_line: 1,
190 end_column: 2,
191 severity: DiagnosticSeverity::Warning,
192 message: "first".into(),
193 code: None,
194 source: None,
195 }],
196 );
197 store.publish(
198 ServerKind::Rust,
199 file.clone(),
200 vec![StoredDiagnostic {
201 file: file.clone(),
202 line: 2,
203 column: 1,
204 end_line: 2,
205 end_column: 2,
206 severity: DiagnosticSeverity::Error,
207 message: "second".into(),
208 code: None,
209 source: None,
210 }],
211 );
212
213 let stored = store.for_file(&file);
214 assert_eq!(stored.len(), 1);
215 assert_eq!(stored[0].message, "second");
216 }
217}