Skip to main content

lattice_config/
signcolumn.rs

1//! Value type for the `signcolumn` typed option.
2//!
3//! `signcolumn` controls whether the renderer reserves the gutter
4//! **sign columns** — the diagnostics-severity column and the
5//! diff-sign column. It is pure display policy read by the renderers
6//! (no lattice-core-logic consumer), so — like
7//! [`crate::DiagnosticsInline`] / [`crate::ModelineZone`] — the value
8//! type lives in `lattice-config` and impls [`OptionType`] locally.
9//!
10//! Default is [`SignColumn::Yes`]: the columns are reserved
11//! unconditionally so the buffer layout never shifts when a sign
12//! appears or clears (the no-flicker contract). Modes that render
13//! gutterless content — help-mode and other synthetic buffers — set
14//! `signcolumn=no`. The renderer derives the column layout from the
15//! resolved option alone and never branches on buffer kind / popup /
16//! pane / tab, so a regular buffer with `:set signcolumn=no` renders
17//! identically to a help popup.
18
19use crate::option_type::{EnumeratedValue, OptionType};
20
21/// `signcolumn` — whether to reserve the gutter sign columns.
22///
23/// Only `yes` / `no` are wired today. Vim's `auto` (reserve only when
24/// a sign is present) is intentionally deferred: it re-introduces the
25/// layout shift the unconditional reserve exists to avoid.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum SignColumn {
28    /// Always reserve the severity + diff-sign columns, so the buffer
29    /// layout never shifts when a sign appears or clears. The default.
30    #[default]
31    Yes,
32    /// Never reserve them — content abuts the line-number gutter.
33    /// Help / synthetic buffers set this for clean, gutterless render.
34    No,
35}
36
37impl SignColumn {
38    /// The on-disk / `:set` spelling: `yes` or `no`. The inverse of
39    /// [`Self::parse_label`].
40    pub fn label(&self) -> &'static str {
41        match self {
42            SignColumn::Yes => "yes",
43            SignColumn::No => "no",
44        }
45    }
46
47    /// One-line description of this value for the `:set` value
48    /// completion marginalia.
49    pub fn doc(&self) -> &'static str {
50        match self {
51            SignColumn::Yes => {
52                "Always reserve the diagnostics + diff sign columns (no layout shift)"
53            }
54            SignColumn::No => "Never reserve the sign columns (gutterless)",
55        }
56    }
57
58    /// Whether the sign columns should be reserved for this value.
59    /// The single predicate every renderer reads to gate the two
60    /// sign-column cells.
61    pub fn reserved(&self) -> bool {
62        matches!(self, SignColumn::Yes)
63    }
64
65    /// Every value, in completion order (the closed value set).
66    pub fn all() -> [SignColumn; 2] {
67        [SignColumn::Yes, SignColumn::No]
68    }
69
70    /// Parse a [`Self::label`] back into a value. Exact match — vim's
71    /// `auto` / `number` forms are rejected, not approximated.
72    ///
73    /// # Examples
74    ///
75    /// ```
76    /// use lattice_config::SignColumn;
77    ///
78    /// assert!(SignColumn::default().reserved());
79    /// assert_eq!(SignColumn::parse_label("no"), Ok(SignColumn::No));
80    /// assert!(!SignColumn::No.reserved());
81    /// assert!(SignColumn::parse_label("auto").is_err());
82    /// ```
83    pub fn parse_label(s: &str) -> Result<Self, String> {
84        match s {
85            "yes" => Ok(SignColumn::Yes),
86            "no" => Ok(SignColumn::No),
87            other => Err(format!("signcolumn: expected `yes` or `no`, got `{other}`")),
88        }
89    }
90}
91
92impl OptionType for SignColumn {
93    fn parse(s: &str) -> Result<Self, String> {
94        SignColumn::parse_label(s)
95    }
96    fn format(&self) -> String {
97        self.label().to_string()
98    }
99    fn type_label() -> &'static str {
100        "signcolumn"
101    }
102    fn enumerate() -> Option<Vec<&'static str>> {
103        Some(SignColumn::all().iter().map(|v| v.label()).collect())
104    }
105
106    /// TC.1: closed — `parse` accepts these forms and nothing else, so
107    /// the schema is an `enum` and `:customize` can offer a picker.
108    fn enumerate_is_exhaustive() -> bool {
109        true
110    }
111    fn enumerate_with_docs() -> Option<Vec<EnumeratedValue>> {
112        Some(
113            SignColumn::all()
114                .iter()
115                .map(|v| EnumeratedValue {
116                    form: v.label(),
117                    doc: v.doc(),
118                })
119                .collect(),
120        )
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn default_is_yes_and_reserved() {
130        assert_eq!(SignColumn::default(), SignColumn::Yes);
131        assert!(SignColumn::default().reserved());
132    }
133
134    #[test]
135    fn parse_round_trips_every_value() {
136        for v in SignColumn::all() {
137            assert_eq!(SignColumn::parse_label(v.label()).unwrap(), v);
138            assert_eq!(SignColumn::parse(v.label()).unwrap(), v);
139            assert_eq!(v.format(), v.label());
140        }
141    }
142
143    #[test]
144    fn parse_rejects_unknown() {
145        // `auto` is deliberately not wired yet.
146        assert!(SignColumn::parse_label("auto").is_err());
147        assert!(SignColumn::parse_label("true").is_err());
148    }
149
150    #[test]
151    fn no_is_not_reserved() {
152        assert!(!SignColumn::No.reserved());
153    }
154
155    #[test]
156    fn enumerate_lists_both_forms() {
157        assert_eq!(SignColumn::enumerate().unwrap(), vec!["yes", "no"]);
158    }
159}