Skip to main content

gpui_base/input/editor/
diagnostics.rs

1use std::{
2    cmp::Ordering,
3    ops::{Deref, Range},
4};
5
6use gpui::SharedString;
7use ropey::Rope;
8use sum_tree::{Bias, SeekTarget, SumTree};
9
10use crate::input::{Position, RopeExt as _};
11
12pub type DiagnosticRelatedInformation = lsp_types::DiagnosticRelatedInformation;
13pub(super) type CodeDescription = lsp_types::CodeDescription;
14pub type RelatedInformation = lsp_types::DiagnosticRelatedInformation;
15pub type DiagnosticTag = lsp_types::DiagnosticTag;
16
17#[derive(Debug, Eq, PartialEq, Clone, Default)]
18pub struct Diagnostic {
19    /// The range [`Position`] at which the message applies.
20    ///
21    /// This is the column, character range within a single line.
22    pub range: Range<Position>,
23
24    /// The diagnostic's severity. Can be omitted. If omitted it is up to the
25    /// client to interpret diagnostics as error, warning, info or hint.
26    pub severity: DiagnosticSeverity,
27
28    /// The diagnostic's code. Can be omitted.
29    pub code: Option<SharedString>,
30
31    pub code_description: Option<CodeDescription>,
32
33    /// A human-readable string describing the source of this
34    /// diagnostic, e.g. 'typescript' or 'super lint'.
35    pub source: Option<SharedString>,
36
37    /// The diagnostic's message.
38    pub message: SharedString,
39
40    /// An array of related diagnostic information, e.g. when symbol-names within
41    /// a scope collide all definitions can be marked via this property.
42    pub related_information: Option<Vec<DiagnosticRelatedInformation>>,
43
44    /// Additional metadata about the diagnostic.
45    pub tags: Option<Vec<DiagnosticTag>>,
46
47    /// A data entry field that is preserved between a `textDocument/publishDiagnostics`
48    /// notification and `textDocument/codeAction` request.
49    ///
50    /// @since 3.16.0
51    pub data: Option<serde_json::Value>,
52}
53
54impl From<lsp_types::Diagnostic> for Diagnostic {
55    fn from(value: lsp_types::Diagnostic) -> Self {
56        Self {
57            range: value.range.start..value.range.end,
58            severity: value
59                .severity
60                .map(Into::into)
61                .unwrap_or(DiagnosticSeverity::Info),
62            code: value.code.map(|c| match c {
63                lsp_types::NumberOrString::Number(n) => SharedString::from(n.to_string()),
64                lsp_types::NumberOrString::String(s) => SharedString::from(s),
65            }),
66            code_description: value.code_description,
67            source: value.source.map(|s| s.into()),
68            message: value.message.into(),
69            related_information: value.related_information,
70            tags: value.tags,
71            data: value.data,
72        }
73    }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
77pub enum DiagnosticSeverity {
78    #[default]
79    Hint,
80    Error,
81    Warning,
82    Info,
83}
84
85impl From<lsp_types::DiagnosticSeverity> for DiagnosticSeverity {
86    fn from(value: lsp_types::DiagnosticSeverity) -> Self {
87        match value {
88            lsp_types::DiagnosticSeverity::ERROR => Self::Error,
89            lsp_types::DiagnosticSeverity::WARNING => Self::Warning,
90            lsp_types::DiagnosticSeverity::INFORMATION => Self::Info,
91            lsp_types::DiagnosticSeverity::HINT => Self::Hint,
92            _ => Self::Info, // Default to Info if unknown
93        }
94    }
95}
96
97impl Diagnostic {
98    pub fn new(range: Range<impl Into<Position>>, message: impl Into<SharedString>) -> Self {
99        Self {
100            range: range.start.into()..range.end.into(),
101            message: message.into(),
102            ..Default::default()
103        }
104    }
105
106    pub fn with_severity(mut self, severity: impl Into<DiagnosticSeverity>) -> Self {
107        self.severity = severity.into();
108        self
109    }
110
111    pub fn with_code(mut self, code: impl Into<SharedString>) -> Self {
112        self.code = Some(code.into());
113        self
114    }
115
116    pub fn with_source(mut self, source: impl Into<SharedString>) -> Self {
117        self.source = Some(source.into());
118        self
119    }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq, Default)]
123pub struct DiagnosticEntry {
124    /// The byte range of the diagnostic in the rope.
125    pub range: Range<usize>,
126    pub diagnostic: Diagnostic,
127}
128
129impl Deref for DiagnosticEntry {
130    type Target = Diagnostic;
131
132    fn deref(&self) -> &Self::Target {
133        &self.diagnostic
134    }
135}
136
137#[derive(Debug, Default, Clone)]
138pub struct DiagnosticSummary {
139    count: usize,
140    start: usize,
141    end: usize,
142}
143
144impl sum_tree::Item for DiagnosticEntry {
145    type Summary = DiagnosticSummary;
146    fn summary(&self, _cx: &()) -> Self::Summary {
147        DiagnosticSummary {
148            count: 1,
149            start: self.range.start,
150            end: self.range.end,
151        }
152    }
153}
154
155impl sum_tree::Summary for DiagnosticSummary {
156    type Context<'a> = &'a ();
157    fn zero(_: Self::Context<'_>) -> Self {
158        DiagnosticSummary {
159            count: 0,
160            start: usize::MIN,
161            end: usize::MIN,
162        }
163    }
164
165    fn add_summary(&mut self, other: &Self, _: Self::Context<'_>) {
166        self.start = other.start;
167        self.end = other.end;
168        self.count += other.count;
169    }
170}
171
172/// For seeking by byte range.
173impl SeekTarget<'_, DiagnosticSummary, DiagnosticSummary> for usize {
174    fn cmp(&self, other: &DiagnosticSummary, _: &()) -> Ordering {
175        if *self < other.start {
176            Ordering::Less
177        } else if *self > other.end {
178            Ordering::Greater
179        } else {
180            Ordering::Equal
181        }
182    }
183}
184
185#[derive(Debug, Clone)]
186pub struct DiagnosticSet {
187    text: Rope,
188    diagnostics: SumTree<DiagnosticEntry>,
189}
190
191impl DiagnosticSet {
192    pub fn new(text: &Rope) -> Self {
193        Self {
194            text: text.clone(),
195            diagnostics: SumTree::new(&()),
196        }
197    }
198
199    pub fn reset(&mut self, text: &Rope) {
200        self.text = text.clone();
201        self.clear();
202    }
203
204    pub fn push(&mut self, diagnostic: impl Into<Diagnostic>) {
205        let diagnostic = diagnostic.into();
206        let start = self.text.position_to_offset(&diagnostic.range.start);
207        let end = self.text.position_to_offset(&diagnostic.range.end);
208
209        self.diagnostics.push(
210            DiagnosticEntry {
211                range: start..end,
212                diagnostic,
213            },
214            &(),
215        );
216    }
217
218    pub fn extend<D, I>(&mut self, diagnostics: D)
219    where
220        D: IntoIterator<Item = I>,
221        I: Into<Diagnostic>,
222    {
223        for diagnostic in diagnostics {
224            self.push(diagnostic.into());
225        }
226    }
227
228    pub fn len(&self) -> usize {
229        self.diagnostics.summary().count
230    }
231
232    pub fn clear(&mut self) {
233        self.diagnostics = SumTree::new(&());
234    }
235
236    pub fn is_empty(&self) -> bool {
237        self.diagnostics.is_empty()
238    }
239
240    pub fn range(&self, range: Range<usize>) -> impl Iterator<Item = &DiagnosticEntry> {
241        let mut cursor = self.diagnostics.cursor::<DiagnosticSummary>(&());
242        cursor.seek(&range.start, Bias::Left);
243        std::iter::from_fn(move || {
244            if let Some(entry) = cursor.item() {
245                if entry.range.start < range.end {
246                    cursor.next();
247                    return Some(entry);
248                }
249            }
250            None
251        })
252    }
253
254    pub fn for_offset(&self, offset: usize) -> Option<&DiagnosticEntry> {
255        self.range(offset..offset + 1).next()
256    }
257
258    #[allow(unused)]
259    pub fn iter(&self) -> impl Iterator<Item = &DiagnosticEntry> {
260        self.diagnostics.iter()
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use crate::input::Position;
267
268    #[test]
269    fn test_diagnostic() {
270        use ropey::Rope;
271
272        use super::{Diagnostic, DiagnosticSet, DiagnosticSeverity};
273
274        let text = Rope::from("Hello, 你好warld!\nThis is a test.\nGoodbye, world!");
275        let mut diagnostics = DiagnosticSet::new(&text);
276
277        diagnostics.push(
278            Diagnostic::new(
279                Position::new(0, 7)..Position::new(0, 17),
280                "Spelling mistake",
281            )
282            .with_severity(DiagnosticSeverity::Warning),
283        );
284        diagnostics.push(
285            Diagnostic::new(Position::new(2, 9)..Position::new(2, 14), "Syntax error")
286                .with_severity(DiagnosticSeverity::Error),
287        );
288
289        assert_eq!(diagnostics.len(), 2);
290        let items = diagnostics.iter().collect::<Vec<_>>();
291
292        assert_eq!(items[0].message.as_str(), "Spelling mistake");
293        assert_eq!(items[0].range, 7..19);
294
295        assert_eq!(items[1].message.as_str(), "Syntax error");
296        assert_eq!(items[1].range, 45..50);
297
298        let items = diagnostics.range(6..48).collect::<Vec<_>>();
299        assert_eq!(items.len(), 2);
300
301        let item = diagnostics.for_offset(10).unwrap();
302        assert_eq!(item.message.as_str(), "Spelling mistake");
303
304        let item = diagnostics.for_offset(30);
305        assert!(item.is_none());
306
307        let item = diagnostics.for_offset(46).unwrap();
308        assert_eq!(item.message.as_str(), "Syntax error");
309
310        diagnostics.push(
311            Diagnostic::new(Position::new(1, 5)..Position::new(1, 7), "Info message")
312                .with_severity(DiagnosticSeverity::Info),
313        );
314        assert_eq!(diagnostics.len(), 3);
315
316        diagnostics.clear();
317        assert_eq!(diagnostics.len(), 0);
318    }
319}