Skip to main content

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}