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}