Skip to main content

strop_engine/editor/
diagnostics.rs

1//! Per-document diagnostic queries (0009): gutter signs, the cursor
2//! line's end-of-line note, modeline chips. Data arrives via LSP events
3//! already resolved to byte-domain columns; these read it.
4
5use strop_lsp::{ResolvedDiag, Severity};
6
7use super::Editor;
8
9pub struct DocumentDiagnostics {
10    pub revision: strop_core::id::BufferRevision,
11    pub items: Vec<ResolvedDiag>,
12}
13
14impl Editor {
15    /// (errors, warnings) on the buffer — the modeline's diag chips.
16    pub fn diag_counts(&self, idx: strop_core::id::DocumentId) -> (usize, usize) {
17        let mut e = 0;
18        let mut w = 0;
19        for d in self.diags_for(idx).into_iter().flatten() {
20            match d.severity {
21                Severity::Error => e += 1,
22                Severity::Warning => w += 1,
23                _ => {}
24            }
25        }
26        (e, w)
27    }
28
29    /// Cached diagnostics belong to a document incarnation and text revision,
30    /// not a pathname shared by several full/range/tail views.
31    pub(super) fn diags_for(&self, idx: strop_core::id::DocumentId) -> Option<&[ResolvedDiag]> {
32        let cached = self.diags.get(&idx)?;
33        (self.docs.get(idx)?.buf.revision() == cached.revision).then_some(cached.items.as_slice())
34    }
35
36    /// The worst diagnostic's (severity, message) on a 1-based line —
37    /// the cursor line's end-of-line note (0009 UX).
38    pub fn diag_message_at(
39        &self,
40        idx: strop_core::id::DocumentId,
41        line_1based: usize,
42    ) -> Option<(Severity, &str)> {
43        self.diags_for(idx)?
44            .iter()
45            .filter(|d| d.line.get() + 1 == line_1based)
46            .min_by_key(|d| d.severity)
47            .map(|d| (d.severity, d.message.as_str()))
48    }
49
50    /// Diagnostic spans on a 1-based line as (col, end_col, severity)
51    /// — the undercurl layer (0009 UX). Same-line diags only; columns
52    /// are byte offsets into the line.
53    pub fn diag_ranges_at(
54        &self,
55        idx: strop_core::id::DocumentId,
56        line_1based: usize,
57    ) -> Vec<(usize, usize, Severity)> {
58        self.diags_for(idx)
59            .map(|ds| {
60                ds.iter()
61                    .filter(|d| d.line.get() + 1 == line_1based)
62                    .map(|d| {
63                        (
64                            d.col.get(),
65                            d.end_col.get().max(d.col.get() + 1),
66                            d.severity,
67                        )
68                    })
69                    .collect()
70            })
71            .unwrap_or_default()
72    }
73
74    /// Worst diagnostic severity for a 1-based line of buffer `idx`, if
75    /// any (0001 pillar 4: merges with the git gutter). Per-buffer, so
76    /// panes show their own diagnostics.
77    pub fn diag_severity_at(
78        &self,
79        idx: strop_core::id::DocumentId,
80        line_1based: usize,
81    ) -> Option<Severity> {
82        let diags = self.diags_for(idx)?;
83        let mut best: Option<Severity> = None;
84        for d in diags {
85            if d.line.get() + 1 == line_1based {
86                best = Some(best.map_or(d.severity, |b: Severity| b.min(d.severity)));
87            }
88        }
89        best
90    }
91}