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}