Skip to main content

lattice_config/
parse.rs

1//! `:set` command-line syntax parser. Lifted from
2//! `lattice-ui-tui::options` and decoupled from any specific
3//! option-value type — the syntax is the same regardless of the
4//! underlying typed system.
5//!
6//! Forms accepted:
7//! - `name` -- query / boolean-on.
8//! - `name?` -- always print current value.
9//! - `name=value` -- set typed value (registry parses against the
10//!   option's [`crate::OptionType`]).
11//! - `noname` -- clear boolean.
12//!
13//! Multiple-option forms (`:set ic hls scs`) are deferred; one
14//! `parse_set` call handles one option per dispatch today.
15
16/// One `:set` argument, classified by syntax alone.
17///
18/// Produced by [`parse_set`]; no option lookup has happened yet, so the
19/// name may not exist and (for [`ParsedSet::Negate`]) may not be
20/// boolean. [`crate::ConfigRegistry::parse_and_set_command`] and
21/// [`crate::ConfigRegistry::parse_for_buffer_local`] give each form
22/// its meaning.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum ParsedSet {
25    /// `:set name` -- query (non-bool) / boolean-on (bool).
26    NameOnly(String),
27    /// `:set name?` -- always print current value.
28    Query(String),
29    /// `:set name=value` -- set typed value.
30    Assign {
31        /// Text left of the first `=`, trimmed.
32        name: String,
33        /// Text right of the first `=`, trimmed; may itself contain
34        /// `=` and may be empty.
35        value: String,
36    },
37    /// `:set noname` -- clear boolean.
38    Negate(String),
39    /// `:set name&` / `:setlocal name&` -- reset to registered default
40    /// / clear local override. Empty name means "clear all" (`:setlocal &`).
41    Reset(String),
42}
43
44/// Classify one `:set` argument (without the `:set ` prefix).
45///
46/// Checked in this order, first match wins: contains `=` →
47/// [`ParsedSet::Assign`]; ends in `?` → [`ParsedSet::Query`]; ends in
48/// `&` → [`ParsedSet::Reset`]; starts with `no` →
49/// [`ParsedSet::Negate`]; otherwise [`ParsedSet::NameOnly`]. `Err` for
50/// empty input or an empty name before `=` / `?`.
51///
52/// The `no` test is purely lexical: any name beginning with `no`
53/// parses as a negation of the remainder, so an option whose own name
54/// starts with `no` cannot be reached by the bare `name` form.
55///
56/// # Examples
57///
58/// ```
59/// use lattice_config::{ParsedSet, parse_set};
60///
61/// assert_eq!(
62///     parse_set("tabstop = 4"),
63///     Ok(ParsedSet::Assign { name: "tabstop".into(), value: "4".into() })
64/// );
65/// assert_eq!(parse_set("number?"), Ok(ParsedSet::Query("number".into())));
66/// assert_eq!(parse_set("nowrap"), Ok(ParsedSet::Negate("wrap".into())));
67/// assert_eq!(parse_set("&"), Ok(ParsedSet::Reset(String::new())));
68/// assert!(parse_set("=4").is_err());
69/// ```
70pub fn parse_set(input: &str) -> Result<ParsedSet, String> {
71    let trimmed = input.trim();
72    if trimmed.is_empty() {
73        return Err("empty :set".into());
74    }
75    if let Some(eq) = trimmed.find('=') {
76        let name = trimmed[..eq].trim().to_string();
77        let value = trimmed[eq + 1..].trim().to_string();
78        if name.is_empty() {
79            return Err("empty option name".into());
80        }
81        return Ok(ParsedSet::Assign { name, value });
82    }
83    if let Some(name) = trimmed.strip_suffix('?') {
84        let name = name.trim().to_string();
85        if name.is_empty() {
86            return Err("empty option name".into());
87        }
88        return Ok(ParsedSet::Query(name));
89    }
90    if let Some(name) = trimmed.strip_suffix('&') {
91        // `:set name&` resets to default; `:setlocal &` (bare `&`) clears all.
92        return Ok(ParsedSet::Reset(name.trim().to_string()));
93    }
94    if let Some(rest) = trimmed.strip_prefix("no") {
95        // Every `no...` form is a candidate negation; the registry
96        // bounces it through `ErasedOption::negate()` which rejects
97        // non-bool options.
98        return Ok(ParsedSet::Negate(rest.to_string()));
99    }
100    Ok(ParsedSet::NameOnly(trimmed.to_string()))
101}
102
103#[cfg(test)]
104mod tests {
105    #![allow(clippy::unwrap_used, clippy::panic)]
106    use super::*;
107
108    #[test]
109    fn parse_set_name_only() {
110        assert_eq!(
111            parse_set("number").unwrap(),
112            ParsedSet::NameOnly("number".into())
113        );
114    }
115
116    #[test]
117    fn parse_set_assign() {
118        assert_eq!(
119            parse_set("tabstop=4").unwrap(),
120            ParsedSet::Assign {
121                name: "tabstop".into(),
122                value: "4".into()
123            }
124        );
125    }
126
127    #[test]
128    fn parse_set_negate() {
129        assert_eq!(
130            parse_set("nonumber").unwrap(),
131            ParsedSet::Negate("number".into())
132        );
133    }
134
135    #[test]
136    fn parse_set_query() {
137        assert_eq!(
138            parse_set("number?").unwrap(),
139            ParsedSet::Query("number".into())
140        );
141    }
142
143    #[test]
144    fn parse_set_assign_trims_whitespace_around_eq() {
145        assert_eq!(
146            parse_set("tabstop = 4").unwrap(),
147            ParsedSet::Assign {
148                name: "tabstop".into(),
149                value: "4".into()
150            }
151        );
152    }
153
154    #[test]
155    fn parse_set_assign_value_can_contain_chars_after_eq() {
156        // `:set foldmethod=indent`
157        assert_eq!(
158            parse_set("foldmethod=indent").unwrap(),
159            ParsedSet::Assign {
160                name: "foldmethod".into(),
161                value: "indent".into()
162            }
163        );
164    }
165
166    #[test]
167    fn parse_set_empty_errors() {
168        assert!(parse_set("   ").is_err());
169        assert!(parse_set("").is_err());
170    }
171
172    #[test]
173    fn parse_set_empty_name_errors() {
174        assert!(parse_set("=4").is_err());
175        assert!(parse_set("?").is_err());
176    }
177}