Skip to main content

guise/editor/
diagnostic.rs

1//! Diagnostics for the editor: LSP-shaped severity + line/column ranges.
2//!
3//! Pure data — the host produces them (from a compiler, linter, or language
4//! server) and hands them to [`Editor::set_diagnostics`](super::Editor::set_diagnostics).
5//! The editor draws a gutter dot per affected line, underlines the range,
6//! and shows the active line's first message in a strip under the buffer.
7
8use std::ops::Range;
9
10use gpui::{Hsla, SharedString};
11
12use crate::theme::Theme;
13
14/// Diagnostic severity, ordered so `max` picks the worst.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub enum Severity {
17    Hint,
18    Info,
19    Warning,
20    Error,
21}
22
23impl Severity {
24    /// The theme accent for this severity.
25    pub fn color(self, t: &Theme) -> Hsla {
26        match self {
27            Severity::Error => t.danger().hsla(),
28            Severity::Warning => t.warning().hsla(),
29            Severity::Info => t.info().hsla(),
30            Severity::Hint => t.dimmed().hsla(),
31        }
32    }
33}
34
35/// One diagnostic, anchored to a line and a char-column range on it.
36#[derive(Debug, Clone)]
37pub struct Diagnostic {
38    /// 0-based line index.
39    pub line: usize,
40    /// Char columns on the line; an empty range means the whole line.
41    pub cols: Range<usize>,
42    pub severity: Severity,
43    pub message: SharedString,
44}
45
46impl Diagnostic {
47    pub fn new(
48        line: usize,
49        cols: Range<usize>,
50        severity: Severity,
51        message: impl Into<SharedString>,
52    ) -> Self {
53        Diagnostic {
54            line,
55            cols,
56            severity,
57            message: message.into(),
58        }
59    }
60
61    /// An error/warning/… covering the whole line.
62    pub fn line_wide(line: usize, severity: Severity, message: impl Into<SharedString>) -> Self {
63        Diagnostic::new(line, 0..0, severity, message)
64    }
65}
66
67/// The worst severity among `line`'s diagnostics, if any — drives the gutter
68/// dot color.
69pub(crate) fn line_severity(diagnostics: &[Diagnostic], line: usize) -> Option<Severity> {
70    diagnostics
71        .iter()
72        .filter(|d| d.line == line)
73        .map(|d| d.severity)
74        .max()
75}
76
77/// The first (worst-first, then declaration order) message on `line`.
78pub(crate) fn line_message(diagnostics: &[Diagnostic], line: usize) -> Option<&Diagnostic> {
79    diagnostics
80        .iter()
81        .filter(|d| d.line == line)
82        .max_by_key(|d| d.severity)
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn severity_orders_worst_last() {
91        assert!(Severity::Error > Severity::Warning);
92        assert!(Severity::Warning > Severity::Info);
93        assert!(Severity::Info > Severity::Hint);
94    }
95
96    #[test]
97    fn line_lookups_pick_the_worst() {
98        let diags = vec![
99            Diagnostic::new(2, 0..4, Severity::Warning, "unused"),
100            Diagnostic::new(2, 6..9, Severity::Error, "type mismatch"),
101            Diagnostic::line_wide(5, Severity::Hint, "style"),
102        ];
103        assert_eq!(line_severity(&diags, 2), Some(Severity::Error));
104        assert_eq!(line_severity(&diags, 5), Some(Severity::Hint));
105        assert_eq!(line_severity(&diags, 0), None);
106        assert_eq!(
107            line_message(&diags, 2).unwrap().message.as_ref(),
108            "type mismatch"
109        );
110    }
111
112    #[test]
113    fn line_wide_uses_an_empty_range() {
114        let d = Diagnostic::line_wide(3, Severity::Error, "boom");
115        assert!(d.cols.is_empty());
116        assert_eq!(d.line, 3);
117    }
118}