Skip to main content

lattice_config/
diagnostics_options.rs

1//! Value types for the `ui.diagnostics.*` typed options (slice L4a).
2//!
3//! The inline diagnostic presentation (`lsp-architecture.md` §15) is
4//! controlled by two typed options under the `diagnostics` group:
5//! [`DiagnosticsInline`] (where the end-of-line summary renders) and
6//! [`DiagnosticsSeverity`] (the least-severe level included). Both are
7//! pure display policy read host-side, so — like [`crate::ModelineZone`]
8//! — they live in `lattice-config` (no lattice-core-logic consumer) and
9//! impl [`OptionType`] locally.
10
11use crate::option_type::{EnumeratedValue, OptionType};
12
13/// `ui.diagnostics.inline` — where the inline (end-of-line virtual-text)
14/// diagnostic summary renders.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum DiagnosticsInline {
17    /// No inline summary — gutter severity column + inline underline
18    /// only.
19    Off,
20    /// The cursor line only (Helix's low-noise default), painted after
21    /// a short idle once the cursor settles on a new line and suppressed
22    /// in Insert mode.
23    #[default]
24    CursorLine,
25    /// Every viewport line (noisier; the L5 opt-in). O(viewport) only.
26    All,
27}
28
29impl DiagnosticsInline {
30    /// The on-disk / `:set` spelling: `off`, `cursor-line` or `all`.
31    /// The inverse of [`Self::parse_label`].
32    pub fn label(&self) -> &'static str {
33        match self {
34            DiagnosticsInline::Off => "off",
35            DiagnosticsInline::CursorLine => "cursor-line",
36            DiagnosticsInline::All => "all",
37        }
38    }
39
40    /// One-line description of this value for the `:set` value
41    /// completion marginalia.
42    pub fn doc(&self) -> &'static str {
43        match self {
44            DiagnosticsInline::Off => "No inline summary (gutter + underline only)",
45            DiagnosticsInline::CursorLine => "End-of-line summary on the cursor line, idle-gated",
46            DiagnosticsInline::All => "End-of-line summary on every viewport line",
47        }
48    }
49
50    /// Every value, in completion order (the closed value set).
51    pub fn all() -> [DiagnosticsInline; 3] {
52        [
53            DiagnosticsInline::Off,
54            DiagnosticsInline::CursorLine,
55            DiagnosticsInline::All,
56        ]
57    }
58
59    /// Parse a [`Self::label`] back into a value. Exact match; any
60    /// other input is an `Err` naming the accepted forms.
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// use lattice_config::DiagnosticsInline;
66    ///
67    /// assert_eq!(DiagnosticsInline::default(), DiagnosticsInline::CursorLine);
68    /// assert_eq!(
69    ///     DiagnosticsInline::parse_label("cursor-line"),
70    ///     Ok(DiagnosticsInline::CursorLine)
71    /// );
72    /// assert!(DiagnosticsInline::parse_label("cursorline").is_err());
73    /// ```
74    pub fn parse_label(s: &str) -> Result<Self, String> {
75        match s {
76            "off" => Ok(DiagnosticsInline::Off),
77            "cursor-line" => Ok(DiagnosticsInline::CursorLine),
78            "all" => Ok(DiagnosticsInline::All),
79            other => Err(format!(
80                "ui.diagnostics.inline: expected `off`, `cursor-line`, or `all`, got `{other}`"
81            )),
82        }
83    }
84}
85
86impl OptionType for DiagnosticsInline {
87    fn parse(s: &str) -> Result<Self, String> {
88        DiagnosticsInline::parse_label(s)
89    }
90    fn format(&self) -> String {
91        self.label().to_string()
92    }
93    fn type_label() -> &'static str {
94        "diagnostics-inline"
95    }
96    fn enumerate() -> Option<Vec<&'static str>> {
97        Some(DiagnosticsInline::all().iter().map(|v| v.label()).collect())
98    }
99
100    /// TC.1: closed — `parse` accepts these forms and nothing else, so
101    /// the schema is an `enum` and `:customize` can offer a picker.
102    fn enumerate_is_exhaustive() -> bool {
103        true
104    }
105    fn enumerate_with_docs() -> Option<Vec<EnumeratedValue>> {
106        Some(
107            DiagnosticsInline::all()
108                .iter()
109                .map(|v| EnumeratedValue {
110                    form: v.label(),
111                    doc: v.doc(),
112                })
113                .collect(),
114        )
115    }
116}
117
118/// `ui.diagnostics.inline-min-severity` — the least-severe level a
119/// diagnostic must be to appear in the inline summary. A diagnostic is
120/// included when its severity is **as severe or more severe** than this
121/// (i.e. its rank ≤ this rank).
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
123pub enum DiagnosticsSeverity {
124    /// Errors only (rank 0).
125    Error,
126    /// Warnings and errors (rank 1).
127    Warning,
128    /// Info, warnings and errors (rank 2).
129    Info,
130    /// The default: include everything down to hints.
131    #[default]
132    Hint,
133}
134
135impl DiagnosticsSeverity {
136    /// Severity rank matching `lattice_lsp::diagnostics_layer`'s
137    /// `severity_rank` (Error = 0 … Hint = 3) — lower is more severe.
138    /// The host includes a diagnostic when its rank ≤ this rank.
139    pub fn rank(&self) -> u8 {
140        match self {
141            DiagnosticsSeverity::Error => 0,
142            DiagnosticsSeverity::Warning => 1,
143            DiagnosticsSeverity::Info => 2,
144            DiagnosticsSeverity::Hint => 3,
145        }
146    }
147
148    /// The on-disk / `:set` spelling: `error`, `warning`, `info` or
149    /// `hint`. The inverse of [`Self::parse_label`].
150    pub fn label(&self) -> &'static str {
151        match self {
152            DiagnosticsSeverity::Error => "error",
153            DiagnosticsSeverity::Warning => "warning",
154            DiagnosticsSeverity::Info => "info",
155            DiagnosticsSeverity::Hint => "hint",
156        }
157    }
158
159    /// One-line description of this threshold for the `:set` value
160    /// completion marginalia.
161    pub fn doc(&self) -> &'static str {
162        match self {
163            DiagnosticsSeverity::Error => "Errors only",
164            DiagnosticsSeverity::Warning => "Warnings and errors",
165            DiagnosticsSeverity::Info => "Info, warnings, and errors",
166            DiagnosticsSeverity::Hint => "All diagnostics (hints and up)",
167        }
168    }
169
170    /// Every value, most severe first (the closed value set).
171    pub fn all() -> [DiagnosticsSeverity; 4] {
172        [
173            DiagnosticsSeverity::Error,
174            DiagnosticsSeverity::Warning,
175            DiagnosticsSeverity::Info,
176            DiagnosticsSeverity::Hint,
177        ]
178    }
179
180    /// Parse a [`Self::label`] back into a value. Exact match; any
181    /// other input is an `Err` naming the accepted forms.
182    ///
183    /// # Examples
184    ///
185    /// ```
186    /// use lattice_config::DiagnosticsSeverity;
187    ///
188    /// let min = DiagnosticsSeverity::parse_label("warning").unwrap();
189    /// // A diagnostic is shown when its rank is <= the threshold's rank.
190    /// assert!(DiagnosticsSeverity::Error.rank() <= min.rank());
191    /// assert!(DiagnosticsSeverity::Hint.rank() > min.rank());
192    /// ```
193    pub fn parse_label(s: &str) -> Result<Self, String> {
194        match s {
195            "error" => Ok(DiagnosticsSeverity::Error),
196            "warning" => Ok(DiagnosticsSeverity::Warning),
197            "info" => Ok(DiagnosticsSeverity::Info),
198            "hint" => Ok(DiagnosticsSeverity::Hint),
199            other => Err(format!(
200                "ui.diagnostics.inline-min-severity: expected `error`, `warning`, `info`, or `hint`, got `{other}`"
201            )),
202        }
203    }
204}
205
206impl OptionType for DiagnosticsSeverity {
207    fn parse(s: &str) -> Result<Self, String> {
208        DiagnosticsSeverity::parse_label(s)
209    }
210    fn format(&self) -> String {
211        self.label().to_string()
212    }
213    fn type_label() -> &'static str {
214        "diagnostics-severity"
215    }
216    fn enumerate() -> Option<Vec<&'static str>> {
217        Some(
218            DiagnosticsSeverity::all()
219                .iter()
220                .map(|v| v.label())
221                .collect(),
222        )
223    }
224
225    /// TC.1: closed — `parse` accepts these forms and nothing else, so
226    /// the schema is an `enum` and `:customize` can offer a picker.
227    fn enumerate_is_exhaustive() -> bool {
228        true
229    }
230    fn enumerate_with_docs() -> Option<Vec<EnumeratedValue>> {
231        Some(
232            DiagnosticsSeverity::all()
233                .iter()
234                .map(|v| EnumeratedValue {
235                    form: v.label(),
236                    doc: v.doc(),
237                })
238                .collect(),
239        )
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    #![allow(clippy::unwrap_used, clippy::panic)]
246    use super::*;
247
248    #[test]
249    fn inline_round_trips_and_default_is_cursor_line() {
250        assert_eq!(DiagnosticsInline::default(), DiagnosticsInline::CursorLine);
251        for v in DiagnosticsInline::all() {
252            assert_eq!(DiagnosticsInline::parse(&v.format()).unwrap(), v);
253        }
254        assert!(DiagnosticsInline::parse("nope").is_err());
255    }
256
257    #[test]
258    fn severity_round_trips_and_rank_orders_error_lowest() {
259        assert_eq!(DiagnosticsSeverity::default(), DiagnosticsSeverity::Hint);
260        for v in DiagnosticsSeverity::all() {
261            assert_eq!(DiagnosticsSeverity::parse(&v.format()).unwrap(), v);
262        }
263        assert!(DiagnosticsSeverity::Error.rank() < DiagnosticsSeverity::Warning.rank());
264        assert!(DiagnosticsSeverity::Warning.rank() < DiagnosticsSeverity::Info.rank());
265        assert!(DiagnosticsSeverity::Info.rank() < DiagnosticsSeverity::Hint.rank());
266        assert!(DiagnosticsSeverity::parse("fatal").is_err());
267    }
268
269    #[test]
270    fn enumerate_with_docs_covers_every_variant() {
271        assert_eq!(DiagnosticsInline::enumerate_with_docs().unwrap().len(), 3);
272        assert_eq!(DiagnosticsSeverity::enumerate_with_docs().unwrap().len(), 4);
273    }
274}