gpui_component/highlighter/
diagnostics.rs1use std::{
2 cmp::Ordering,
3 ops::{Deref, Range},
4 usize,
5};
6
7use gpui::{px, App, HighlightStyle, Hsla, SharedString, UnderlineStyle};
8use ropey::Rope;
9use sum_tree::{Bias, SeekTarget, SumTree};
10
11use crate::{
12 input::{Position, RopeExt as _},
13 ActiveTheme,
14};
15
16pub type DiagnosticRelatedInformation = lsp_types::DiagnosticRelatedInformation;
17pub type CodeDescription = lsp_types::CodeDescription;
18pub type RelatedInformation = lsp_types::DiagnosticRelatedInformation;
19pub type DiagnosticTag = lsp_types::DiagnosticTag;
20
21#[derive(Debug, Eq, PartialEq, Clone, Default)]
22pub struct Diagnostic {
23 pub range: Range<Position>,
27
28 pub severity: DiagnosticSeverity,
31
32 pub code: Option<SharedString>,
34
35 pub code_description: Option<CodeDescription>,
36
37 pub source: Option<SharedString>,
40
41 pub message: SharedString,
43
44 pub related_information: Option<Vec<DiagnosticRelatedInformation>>,
47
48 pub tags: Option<Vec<DiagnosticTag>>,
50
51 pub data: Option<serde_json::Value>,
56}
57
58impl From<lsp_types::Diagnostic> for Diagnostic {
59 fn from(value: lsp_types::Diagnostic) -> Self {
60 Self {
61 range: value.range.start..value.range.end,
62 severity: value
63 .severity
64 .map(Into::into)
65 .unwrap_or(DiagnosticSeverity::Info),
66 code: value.code.map(|c| match c {
67 lsp_types::NumberOrString::Number(n) => SharedString::from(n.to_string()),
68 lsp_types::NumberOrString::String(s) => SharedString::from(s),
69 }),
70 code_description: value.code_description,
71 source: value.source.map(|s| s.into()),
72 message: value.message.into(),
73 related_information: value.related_information,
74 tags: value.tags,
75 data: value.data,
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
81pub enum DiagnosticSeverity {
82 #[default]
83 Hint,
84 Error,
85 Warning,
86 Info,
87}
88
89impl From<lsp_types::DiagnosticSeverity> for DiagnosticSeverity {
90 fn from(value: lsp_types::DiagnosticSeverity) -> Self {
91 match value {
92 lsp_types::DiagnosticSeverity::ERROR => Self::Error,
93 lsp_types::DiagnosticSeverity::WARNING => Self::Warning,
94 lsp_types::DiagnosticSeverity::INFORMATION => Self::Info,
95 lsp_types::DiagnosticSeverity::HINT => Self::Hint,
96 _ => Self::Info, }
98 }
99}
100
101impl DiagnosticSeverity {
102 pub(crate) fn bg(&self, cx: &App) -> Hsla {
103 let theme = &cx.theme().highlight_theme;
104
105 match self {
106 Self::Error => theme.style.status.error_background(cx),
107 Self::Warning => theme.style.status.warning_background(cx),
108 Self::Info => theme.style.status.info_background(cx),
109 Self::Hint => theme.style.status.hint_background(cx),
110 }
111 }
112
113 pub(crate) fn fg(&self, cx: &App) -> Hsla {
114 let theme = &cx.theme().highlight_theme;
115
116 match self {
117 Self::Error => theme.style.status.error(cx),
118 Self::Warning => theme.style.status.warning(cx),
119 Self::Info => theme.style.status.info(cx),
120 Self::Hint => theme.style.status.hint(cx),
121 }
122 }
123
124 pub(crate) fn border(&self, cx: &App) -> Hsla {
125 let theme = &cx.theme().highlight_theme;
126 match self {
127 Self::Error => theme.style.status.error_border(cx),
128 Self::Warning => theme.style.status.warning_border(cx),
129 Self::Info => theme.style.status.info_border(cx),
130 Self::Hint => theme.style.status.hint_border(cx),
131 }
132 }
133
134 pub(crate) fn highlight_style(&self, cx: &App) -> HighlightStyle {
135 let theme = &cx.theme().highlight_theme;
136
137 let color = match self {
138 Self::Error => Some(theme.style.status.error(cx)),
139 Self::Warning => Some(theme.style.status.warning(cx)),
140 Self::Info => Some(theme.style.status.info(cx)),
141 Self::Hint => Some(theme.style.status.hint(cx)),
142 };
143
144 let mut style = HighlightStyle::default();
145 style.underline = Some(UnderlineStyle {
146 color: color,
147 thickness: px(1.),
148 wavy: true,
149 });
150
151 style
152 }
153}
154
155impl Diagnostic {
156 pub fn new(range: Range<impl Into<Position>>, message: impl Into<SharedString>) -> Self {
157 Self {
158 range: range.start.into()..range.end.into(),
159 message: message.into(),
160 ..Default::default()
161 }
162 }
163
164 pub fn with_severity(mut self, severity: impl Into<DiagnosticSeverity>) -> Self {
165 self.severity = severity.into();
166 self
167 }
168
169 pub fn with_code(mut self, code: impl Into<SharedString>) -> Self {
170 self.code = Some(code.into());
171 self
172 }
173
174 pub fn with_source(mut self, source: impl Into<SharedString>) -> Self {
175 self.source = Some(source.into());
176 self
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Default)]
181pub(crate) struct DiagnosticEntry {
182 pub range: Range<usize>,
184 pub diagnostic: Diagnostic,
185}
186
187impl Deref for DiagnosticEntry {
188 type Target = Diagnostic;
189
190 fn deref(&self) -> &Self::Target {
191 &self.diagnostic
192 }
193}
194
195#[derive(Debug, Default, Clone)]
196pub struct DiagnosticSummary {
197 count: usize,
198 start: usize,
199 end: usize,
200}
201
202impl sum_tree::Item for DiagnosticEntry {
203 type Summary = DiagnosticSummary;
204 fn summary(&self, _cx: &()) -> Self::Summary {
205 DiagnosticSummary {
206 count: 1,
207 start: self.range.start,
208 end: self.range.end,
209 }
210 }
211}
212
213impl sum_tree::Summary for DiagnosticSummary {
214 type Context<'a> = &'a ();
215 fn zero(_: Self::Context<'_>) -> Self {
216 DiagnosticSummary {
217 count: 0,
218 start: usize::MIN,
219 end: usize::MIN,
220 }
221 }
222
223 fn add_summary(&mut self, other: &Self, _: Self::Context<'_>) {
224 self.start = other.start;
225 self.end = other.end;
226 self.count += other.count;
227 }
228}
229
230impl SeekTarget<'_, DiagnosticSummary, DiagnosticSummary> for usize {
232 fn cmp(&self, other: &DiagnosticSummary, _: &()) -> Ordering {
233 if *self < other.start {
234 Ordering::Less
235 } else if *self > other.end {
236 Ordering::Greater
237 } else {
238 Ordering::Equal
239 }
240 }
241}
242
243#[derive(Debug, Clone)]
244pub struct DiagnosticSet {
245 text: Rope,
246 diagnostics: SumTree<DiagnosticEntry>,
247}
248
249impl DiagnosticSet {
250 pub fn new(text: &Rope) -> Self {
251 Self {
252 text: text.clone(),
253 diagnostics: SumTree::new(&()),
254 }
255 }
256
257 pub fn reset(&mut self, text: &Rope) {
258 self.text = text.clone();
259 self.clear();
260 }
261
262 pub fn push(&mut self, diagnostic: impl Into<Diagnostic>) {
263 let diagnostic = diagnostic.into();
264 let start = self.text.position_to_offset(&diagnostic.range.start);
265 let end = self.text.position_to_offset(&diagnostic.range.end);
266
267 self.diagnostics.push(
268 DiagnosticEntry {
269 range: start..end,
270 diagnostic,
271 },
272 &(),
273 );
274 }
275
276 pub fn extend<D, I>(&mut self, diagnostics: D)
277 where
278 D: IntoIterator<Item = I>,
279 I: Into<Diagnostic>,
280 {
281 for diagnostic in diagnostics {
282 self.push(diagnostic.into());
283 }
284 }
285
286 pub fn len(&self) -> usize {
287 self.diagnostics.summary().count
288 }
289
290 pub fn clear(&mut self) {
291 self.diagnostics = SumTree::new(&());
292 }
293
294 pub fn is_empty(&self) -> bool {
295 self.diagnostics.is_empty()
296 }
297
298 pub(crate) fn range(&self, range: Range<usize>) -> impl Iterator<Item = &DiagnosticEntry> {
299 let mut cursor = self.diagnostics.cursor::<DiagnosticSummary>(&());
300 cursor.seek(&range.start, Bias::Left);
301 std::iter::from_fn(move || {
302 if let Some(entry) = cursor.item() {
303 if entry.range.start < range.end {
304 cursor.next();
305 return Some(entry);
306 }
307 }
308 None
309 })
310 }
311
312 pub(crate) fn for_offset(&self, offset: usize) -> Option<&DiagnosticEntry> {
313 self.range(offset..offset + 1).next()
314 }
315
316 pub(crate) fn styles_for_range(
317 &self,
318 range: &Range<usize>,
319 cx: &App,
320 ) -> Vec<(Range<usize>, HighlightStyle)> {
321 if self.diagnostics.is_empty() {
322 return vec![];
323 }
324
325 let mut styles = vec![];
326 for entry in self.range(range.clone()) {
327 let range = entry.range.clone();
328 styles.push((range, entry.diagnostic.severity.highlight_style(cx)));
329 }
330
331 styles
332 }
333
334 #[allow(unused)]
335 pub(crate) fn iter(&self) -> impl Iterator<Item = &DiagnosticEntry> {
336 self.diagnostics.iter()
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use crate::input::Position;
343
344 #[test]
345 fn test_diagnostic() {
346 use ropey::Rope;
347
348 use super::{Diagnostic, DiagnosticSet, DiagnosticSeverity};
349
350 let text = Rope::from("Hello, 你好warld!\nThis is a test.\nGoodbye, world!");
351 let mut diagnostics = DiagnosticSet::new(&text);
352
353 diagnostics.push(
354 Diagnostic::new(
355 Position::new(0, 7)..Position::new(0, 17),
356 "Spelling mistake",
357 )
358 .with_severity(DiagnosticSeverity::Warning),
359 );
360 diagnostics.push(
361 Diagnostic::new(Position::new(2, 9)..Position::new(2, 14), "Syntax error")
362 .with_severity(DiagnosticSeverity::Error),
363 );
364
365 assert_eq!(diagnostics.len(), 2);
366 let items = diagnostics.iter().collect::<Vec<_>>();
367
368 assert_eq!(items[0].message.as_str(), "Spelling mistake");
369 assert_eq!(items[0].range, 7..19);
370
371 assert_eq!(items[1].message.as_str(), "Syntax error");
372 assert_eq!(items[1].range, 45..50);
373
374 let items = diagnostics.range(6..48).collect::<Vec<_>>();
375 assert_eq!(items.len(), 2);
376
377 let item = diagnostics.for_offset(10).unwrap();
378 assert_eq!(item.message.as_str(), "Spelling mistake");
379
380 let item = diagnostics.for_offset(30);
381 assert!(item.is_none());
382
383 let item = diagnostics.for_offset(46).unwrap();
384 assert_eq!(item.message.as_str(), "Syntax error");
385
386 diagnostics.push(
387 Diagnostic::new(Position::new(1, 5)..Position::new(1, 7), "Info message")
388 .with_severity(DiagnosticSeverity::Info),
389 );
390 assert_eq!(diagnostics.len(), 3);
391
392 diagnostics.clear();
393 assert_eq!(diagnostics.len(), 0);
394 }
395}