gpui_component/highlighter/
diagnostics.rs

1use 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    /// The range [`Position`] at which the message applies.
24    ///
25    /// This is the column, character range within a single line.
26    pub range: Range<Position>,
27
28    /// The diagnostic's severity. Can be omitted. If omitted it is up to the
29    /// client to interpret diagnostics as error, warning, info or hint.
30    pub severity: DiagnosticSeverity,
31
32    /// The diagnostic's code. Can be omitted.
33    pub code: Option<SharedString>,
34
35    pub code_description: Option<CodeDescription>,
36
37    /// A human-readable string describing the source of this
38    /// diagnostic, e.g. 'typescript' or 'super lint'.
39    pub source: Option<SharedString>,
40
41    /// The diagnostic's message.
42    pub message: SharedString,
43
44    /// An array of related diagnostic information, e.g. when symbol-names within
45    /// a scope collide all definitions can be marked via this property.
46    pub related_information: Option<Vec<DiagnosticRelatedInformation>>,
47
48    /// Additional metadata about the diagnostic.
49    pub tags: Option<Vec<DiagnosticTag>>,
50
51    /// A data entry field that is preserved between a `textDocument/publishDiagnostics`
52    /// notification and `textDocument/codeAction` request.
53    ///
54    /// @since 3.16.0
55    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, // Default to Info if unknown
97        }
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    /// The byte range of the diagnostic in the rope.
183    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
230/// For seeking by byte range.
231impl 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}