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}