lattice_core/labeled_enum.rs
1//! `labeled_enum!` — declarative macro for enum-typed options that
2//! participate in `:set foo=<Tab>` cmdline completion.
3//!
4//! Generates the enum together with four colocated accessors:
5//!
6//! - `label()` — canonical string form, used by
7//! `:set foo=...` parsing + the `:set foo?` echo.
8//! - `parse_label` — string → variant (accepts the canonical
9//! form + any registered aliases per variant).
10//! - `doc()` — short marginalia doc shown in the
11//! completion popup's right-aligned column.
12//! - `all()` — variants in declaration order (drives
13//! `:set foo=<Tab>` enumeration).
14//!
15//! Single source of truth: each variant's label and doc are
16//! declared together. Adding a new variant requires one new line;
17//! the macro extends every accessor in lockstep.
18//!
19//! ## Syntax
20//!
21//! ```
22//! use lattice_core::labeled_enum;
23//!
24//! labeled_enum! {
25//! /// `:set foldmethod=...` — decides which provider feeds
26//! /// the per-buffer fold list.
27//! pub enum FoldMethod {
28//! /// `manual` — only user `zf` ranges.
29//! #[default]
30//! Manual = "manual" => "User-defined folds only (zf, zd)",
31//! /// `indent` — universal indent walker.
32//! Indent = "indent" => "Fold by indent level",
33//! /// `markdown` — ATX heading nesting.
34//! Markdown = "markdown" => "Fold by markdown headings",
35//! }
36//! }
37//!
38//! assert_eq!(FoldMethod::default(), FoldMethod::Manual);
39//! assert_eq!(FoldMethod::Indent.label(), "indent");
40//! assert_eq!(FoldMethod::parse_label("markdown"), Ok(FoldMethod::Markdown));
41//! assert_eq!(FoldMethod::all().len(), 3);
42//! ```
43//!
44//! ### Aliases
45//!
46//! A variant can accept multiple parse forms; the first is the
47//! canonical label, the rest are aliases:
48//!
49//! ```
50//! # use lattice_core::labeled_enum;
51//! labeled_enum! {
52//! /// Where a produced buffer is displayed.
53//! pub enum Placement {
54//! /// Built-in default.
55//! #[default]
56//! Default = "default" => "Use the category's built-in default",
57//! /// Centred popup.
58//! PopupCentered = "popup-centered" | "popup" => "Centred focused popup",
59//! /// Hover-style popup.
60//! FloatingCursor = "floating-cursor" | "floating" => "Floating popup",
61//! }
62//! }
63//!
64//! // The alias parses to the same variant …
65//! assert_eq!(Placement::parse_label("popup"), Ok(Placement::PopupCentered));
66//! // … but only canonical labels are enumerated and echoed.
67//! assert_eq!(Placement::PopupCentered.label(), "popup-centered");
68//! assert_eq!(
69//! Placement::all().iter().map(|p| p.label()).collect::<Vec<_>>(),
70//! ["default", "popup-centered", "floating-cursor"],
71//! );
72//! // Unknown input lists the canonical forms.
73//! assert_eq!(
74//! Placement::parse_label("pop"),
75//! Err("expected `default`, `popup-centered`, or `floating-cursor`, got `pop`".to_string()),
76//! );
77//! ```
78//!
79//! Aliases parse to the same variant but DON'T appear in `all()` /
80//! completion (only the canonical does).
81//!
82//! ### Derives
83//!
84//! The macro derives `Debug, Clone, Copy, PartialEq, Eq, Default`
85//! on the enum. Exactly one variant must carry `#[default]`. Add
86//! extra derives by stacking `#[derive(...)]` attributes BEFORE
87//! `pub enum`:
88//!
89//! ```
90//! # use lattice_core::labeled_enum;
91//! labeled_enum! {
92//! /// Log verbosity.
93//! #[derive(Hash)]
94//! pub enum LogLevel {
95//! /// Errors only.
96//! #[default]
97//! Error = "error" => "Errors only",
98//! /// Everything.
99//! Debug = "debug" => "Everything",
100//! }
101//! }
102//!
103//! let set: std::collections::HashSet<LogLevel> = LogLevel::all().iter().copied().collect();
104//! assert!(set.contains(&LogLevel::Debug));
105//! ```
106//!
107//! Every variant, and the enum itself, should carry a `///` doc: the
108//! macro forwards attributes, so an undocumented variant trips
109//! `missing_docs` in a crate that has opted into it.
110
111/// Declare an option enum whose variants carry a canonical label, optional
112/// parse aliases and a one-line completion doc.
113///
114/// Expands to the enum (deriving `Debug, Clone, Copy, PartialEq, Eq,
115/// Default`) plus inherent `label()`, `doc()`, `all()` and
116/// `parse_label()`. Exactly one variant must be `#[default]`. See the
117/// [module docs](mod@crate::labeled_enum) for syntax, aliases and extra derives.
118///
119/// # Examples
120///
121/// ```
122/// use lattice_core::labeled_enum;
123///
124/// labeled_enum! {
125/// /// `:set bell=...`.
126/// pub enum Bell {
127/// /// Silent.
128/// #[default]
129/// Off = "off" | "false" => "No bell",
130/// /// Flash the screen.
131/// Visual = "visual" => "Flash instead of beeping",
132/// }
133/// }
134///
135/// assert_eq!(Bell::parse_label("false"), Ok(Bell::Off));
136/// assert_eq!(Bell::Visual.doc(), "Flash instead of beeping");
137/// assert!(Bell::parse_label("loud").is_err());
138/// ```
139#[macro_export]
140macro_rules! labeled_enum {
141 (
142 $(#[$enum_attr:meta])*
143 $vis:vis enum $name:ident {
144 $(
145 $(#[$variant_attr:meta])*
146 $variant:ident = $canonical:literal $(| $alias:literal)* => $doc:literal
147 ),* $(,)?
148 }
149 ) => {
150 $(#[$enum_attr])*
151 #[derive(
152 ::std::fmt::Debug,
153 ::std::clone::Clone,
154 ::std::marker::Copy,
155 ::std::cmp::PartialEq,
156 ::std::cmp::Eq,
157 ::std::default::Default,
158 )]
159 $vis enum $name {
160 $(
161 $(#[$variant_attr])*
162 $variant,
163 )*
164 }
165
166 impl $name {
167 /// Canonical string label.
168 pub fn label(self) -> &'static str {
169 match self {
170 $( Self::$variant => $canonical, )*
171 }
172 }
173
174 /// Short marginalia doc shown in cmdline-completion's
175 /// right-aligned column.
176 pub fn doc(self) -> &'static str {
177 match self {
178 $( Self::$variant => $doc, )*
179 }
180 }
181
182 /// Variants in declaration order.
183 pub fn all() -> &'static [Self] {
184 &[ $( Self::$variant ),* ]
185 }
186
187 /// Parse from canonical label or any registered alias.
188 pub fn parse_label(s: &str) -> ::std::result::Result<Self, ::std::string::String> {
189 match s {
190 $(
191 $canonical $( | $alias )* => Ok(Self::$variant),
192 )*
193 other => {
194 // Build "expected `a`, `b`, or `c`, got `x`"
195 // — Oxford comma, single-or before the last
196 // canonical. Matches the prior hand-written
197 // wording so user-visible error text doesn't
198 // drift on the macro migration.
199 let canonicals = [ $( $canonical ),* ];
200 let expected = match canonicals.as_slice() {
201 [] => ::std::string::String::new(),
202 [only] => format!("`{only}`"),
203 [a, b] => format!("`{a}` or `{b}`"),
204 many => {
205 let (last, rest) = many.split_last().unwrap();
206 let rest_quoted = rest
207 .iter()
208 .map(|s| format!("`{s}`"))
209 .collect::<::std::vec::Vec<_>>()
210 .join(", ");
211 format!("{rest_quoted}, or `{last}`")
212 }
213 };
214 Err(format!("expected {expected}, got `{other}`"))
215 }
216 }
217 }
218 }
219 };
220}