Skip to main content

reovim_module_commands/
set.rs

1//! Set command - modify editor options at runtime.
2//!
3//! Implements the `:set` ex-command with vim-style syntax for querying,
4//! setting, toggling, and resetting editor options.
5
6use {
7    reovim_driver_command::{
8        ArgKind, ArgSpec, Command, CommandContext, CommandHandler, CommandResult,
9    },
10    reovim_driver_session::SessionRuntime,
11    reovim_kernel::api::v1::{
12        CommandId, ModuleId, OptionScopeId, OptionValue,
13        events::kernel::{ChangeSource, OptionChanged, OptionReset},
14    },
15};
16
17const COMMANDS_MODULE: ModuleId = ModuleId::new("commands");
18
19/// Set command - modify editor options.
20///
21/// Supports vim-style syntax:
22/// - `:set option` - Set boolean true (or show value for non-bool)
23/// - `:set nooption` - Set boolean false
24/// - `:set option!` - Toggle boolean
25/// - `:set option?` - Query current value
26/// - `:set option=value` - Set value
27/// - `:set option&` - Reset to default
28/// - `:set all` - List all options
29/// - `:set` - List changed options
30#[derive(Debug, Clone, Copy)]
31pub struct SetCommand;
32
33impl Command for SetCommand {
34    fn id(&self) -> CommandId {
35        CommandId::new(COMMANDS_MODULE, "set")
36    }
37
38    fn description(&self) -> &'static str {
39        "Set editor options. Use :set option=value, :set option, :set nooption, etc."
40    }
41
42    fn args(&self) -> Vec<ArgSpec> {
43        vec![ArgSpec::optional(
44            "expr",
45            ArgKind::Rest,
46            "Option expression",
47        )]
48    }
49
50    fn names(&self) -> &[&'static str] {
51        &["set"]
52    }
53}
54
55// ============================================================================
56// Parsing
57// ============================================================================
58
59/// Parsed action from a `:set` expression.
60#[derive(Debug, Clone, PartialEq, Eq)]
61enum SetAction {
62    /// `:set` (no args) - list all non-default options.
63    ListChanged,
64    /// `:set all` - list all options.
65    ListAll,
66    /// `:set option?` - query current value.
67    Show { name: String },
68    /// `:set option` (bool) or `:set nooption` - set boolean.
69    SetBool { name: String, value: bool },
70    /// `:set option!` - toggle boolean.
71    Toggle { name: String },
72    /// `:set option=value` - assign a value.
73    Assign { name: String, raw_value: String },
74    /// `:set option&` - reset to default.
75    Reset { name: String },
76}
77
78/// Parse a `:set` expression into a `SetAction`.
79///
80/// This performs structural parsing only. The ambiguous "bare name" case
81/// (`:set option` — is it set-bool-true or show-for-non-bool?) is resolved
82/// as `SetBool` here; the caller checks the option type and converts to
83/// `Show` if the option is non-boolean.
84fn parse_set_expr(expr: &str) -> SetAction {
85    let expr = expr.trim();
86
87    if expr.is_empty() {
88        return SetAction::ListChanged;
89    }
90
91    if expr == "all" {
92        return SetAction::ListAll;
93    }
94
95    // :set option?
96    if let Some(name) = expr.strip_suffix('?') {
97        return SetAction::Show {
98            name: name.to_string(),
99        };
100    }
101
102    // :set option!
103    if let Some(name) = expr.strip_suffix('!') {
104        return SetAction::Toggle {
105            name: name.to_string(),
106        };
107    }
108
109    // :set option&
110    if let Some(name) = expr.strip_suffix('&') {
111        return SetAction::Reset {
112            name: name.to_string(),
113        };
114    }
115
116    // :set option=value
117    if let Some((name, value)) = expr.split_once('=') {
118        return SetAction::Assign {
119            name: name.to_string(),
120            raw_value: value.to_string(),
121        };
122    }
123
124    // :set nooption
125    if let Some(name) = expr.strip_prefix("no")
126        && !name.is_empty()
127    {
128        return SetAction::SetBool {
129            name: name.to_string(),
130            value: false,
131        };
132    }
133
134    // :set option (bare name — assume bool true; caller refines if non-bool)
135    SetAction::SetBool {
136        name: expr.to_string(),
137        value: true,
138    }
139}
140
141/// Parse a raw string value into an `OptionValue` matching the expected type.
142fn parse_value_for_type(
143    raw: &str,
144    expected: &OptionValue,
145    name: &str,
146) -> Result<OptionValue, String> {
147    match expected {
148        OptionValue::Bool(_) => match raw {
149            "true" | "1" | "on" => Ok(OptionValue::bool(true)),
150            "false" | "0" | "off" => Ok(OptionValue::bool(false)),
151            _ => Err(format!(
152                "invalid value for '{name}': expected bool (true/false/1/0/on/off), got '{raw}'"
153            )),
154        },
155        OptionValue::Integer(_) => {
156            let val: i64 = raw.parse().map_err(|_| {
157                format!("invalid value for '{name}': expected integer, got '{raw}'")
158            })?;
159            Ok(OptionValue::int(val))
160        }
161        OptionValue::String(_) => Ok(OptionValue::string(raw)),
162        OptionValue::Choice { choices, .. } => Ok(OptionValue::choice(raw, choices.clone())),
163    }
164}
165
166// ============================================================================
167// Execution
168// ============================================================================
169
170impl CommandHandler for SetCommand {
171    fn execute(&self, runtime: &mut SessionRuntime<'_>, ctx: &CommandContext) -> CommandResult {
172        let expr = ctx.string("expr").unwrap_or("");
173        let action = parse_set_expr(expr);
174        let scope = OptionScopeId::Global;
175
176        match action {
177            SetAction::ListChanged => execute_list_changed(runtime, scope),
178            SetAction::ListAll => execute_list_all(runtime, scope),
179            SetAction::Show { name } => execute_show(runtime, &name, scope),
180            SetAction::SetBool { name, value } => execute_set_bool(runtime, &name, value, scope),
181            SetAction::Toggle { name } => execute_toggle(runtime, &name, scope),
182            SetAction::Assign { name, raw_value } => {
183                execute_assign(runtime, &name, &raw_value, scope)
184            }
185            SetAction::Reset { name } => execute_reset(runtime, &name, scope),
186        }
187    }
188}
189
190/// Execute `:set` (no args) — list options with non-default values.
191fn execute_list_changed(runtime: &SessionRuntime<'_>, scope: OptionScopeId) -> CommandResult {
192    let kernel = runtime.kernel();
193    let options = &kernel.options;
194    let names = options.list_all();
195
196    let mut lines = Vec::new();
197    for name in &names {
198        // list_all() returns registered names, so get_spec/get are guaranteed Some.
199        let (spec, current) = guard_spec_and_value(options, name, scope);
200        if current != spec.default {
201            lines.push(format!("  {name}={current}"));
202        }
203    }
204
205    log_option_list("Changed options", "No changed options", &lines);
206    CommandResult::Success
207}
208
209/// Execute `:set all` — list all options.
210fn execute_list_all(runtime: &SessionRuntime<'_>, scope: OptionScopeId) -> CommandResult {
211    let kernel = runtime.kernel();
212    let options = &kernel.options;
213    let names = options.list_all();
214
215    let mut lines = Vec::new();
216    for name in &names {
217        // list_all() returns registered names, so get is guaranteed Some.
218        let value = guard_get_value(options, name, scope);
219        lines.push(format!("  {name}={value}"));
220    }
221
222    log_option_list("All options", "No options registered", &lines);
223    CommandResult::Success
224}
225
226/// Execute `:set option?` — show the current value.
227fn execute_show(runtime: &SessionRuntime<'_>, name: &str, scope: OptionScopeId) -> CommandResult {
228    let kernel = runtime.kernel();
229    let options = &kernel.options;
230
231    let Some(full_name) = options.resolve_name(name) else {
232        return CommandResult::Error(format!("Unknown option: {name}"));
233    };
234
235    // resolve_name succeeded, so get is guaranteed Some.
236    let value = guard_get_value(options, &full_name, scope);
237    log_option_value(&full_name, &value);
238    CommandResult::Success
239}
240
241/// Log an option list (tracing output only).
242#[cfg_attr(coverage_nightly, coverage(off))]
243fn log_option_list(header: &str, empty_msg: &str, lines: &[String]) {
244    if lines.is_empty() {
245        tracing::info!("{empty_msg}");
246    } else {
247        tracing::info!("{header}:\n{}", lines.join("\n"));
248    }
249}
250
251/// Log a single option value (tracing output only).
252#[cfg_attr(coverage_nightly, coverage(off))]
253fn log_option_value(name: &str, value: &OptionValue) {
254    tracing::info!("  {name}={value}");
255}
256
257// =============================================================================
258// Defensive guards for unreachable branches
259//
260// After `resolve_name()` / `list_all()` confirms an option exists, `get_spec()`,
261// `get()`, and `reset()` are guaranteed to succeed. These helpers isolate the
262// unreachable `None`/`Err` branches so MC/DC coverage is not penalized.
263// =============================================================================
264
265use reovim_kernel::api::v1::OptionRegistry;
266
267/// Get spec for a resolved option name (guaranteed `Some` after `resolve_name`).
268#[cfg_attr(coverage_nightly, coverage(off))]
269fn guard_get_spec(options: &OptionRegistry, full_name: &str) -> reovim_kernel::api::v1::OptionSpec {
270    options
271        .get_spec(full_name)
272        .expect("get_spec must succeed after resolve_name")
273}
274
275/// Get value for a resolved option name (guaranteed `Some` after `resolve_name`).
276#[cfg_attr(coverage_nightly, coverage(off))]
277fn guard_get_value(options: &OptionRegistry, full_name: &str, scope: OptionScopeId) -> OptionValue {
278    options
279        .get(full_name, scope)
280        .expect("get must succeed after resolve_name")
281}
282
283/// Get spec and value together for a known-registered option name.
284#[cfg_attr(coverage_nightly, coverage(off))]
285fn guard_spec_and_value(
286    options: &OptionRegistry,
287    name: &str,
288    scope: OptionScopeId,
289) -> (reovim_kernel::api::v1::OptionSpec, OptionValue) {
290    let spec = options
291        .get_spec(name)
292        .expect("get_spec must succeed for list_all name");
293    let value = options
294        .get(name, scope)
295        .expect("get must succeed for list_all name");
296    (spec, value)
297}
298
299/// Reset a resolved option (guaranteed `Ok` after `resolve_name`).
300#[cfg_attr(coverage_nightly, coverage(off))]
301fn guard_reset(
302    options: &OptionRegistry,
303    full_name: &str,
304    scope: OptionScopeId,
305) -> Option<OptionValue> {
306    options
307        .reset(full_name, scope)
308        .expect("reset must succeed after resolve_name")
309}
310
311/// Execute `:set option` or `:set nooption` — set a boolean option.
312///
313/// For bare names (`:set option`), if the option is non-boolean, falls
314/// back to showing the current value instead of setting.
315fn execute_set_bool(
316    runtime: &mut SessionRuntime<'_>,
317    name: &str,
318    value: bool,
319    scope: OptionScopeId,
320) -> CommandResult {
321    let kernel = runtime.kernel();
322    let options = &kernel.options;
323
324    let Some(full_name) = options.resolve_name(name) else {
325        return CommandResult::Error(format!("Unknown option: {name}"));
326    };
327
328    // resolve_name succeeded, so get_spec is guaranteed Some.
329    let spec = guard_get_spec(options, &full_name);
330
331    // If setting to true via bare name and option is non-bool, show value instead
332    if value && !matches!(spec.default, OptionValue::Bool(_)) {
333        return execute_show(runtime, &full_name, scope);
334    }
335
336    let new_value = OptionValue::bool(value);
337    match options.set(&full_name, new_value.clone(), scope) {
338        Ok(result) => {
339            let old_display = result
340                .old_value
341                .as_ref()
342                .map_or_else(|| spec.default.to_string(), ToString::to_string);
343
344            kernel.event_bus.emit(OptionChanged {
345                name: full_name.clone(),
346                old_value: old_display,
347                new_value: result.new_value.to_string(),
348                source: ChangeSource::UserCommand,
349                scope,
350            });
351
352            runtime.record_global_option_change(&full_name, new_value);
353            CommandResult::Success
354        }
355        Err(e) => CommandResult::Error(format!("{e}")),
356    }
357}
358
359/// Execute `:set option!` — toggle a boolean option.
360fn execute_toggle(
361    runtime: &mut SessionRuntime<'_>,
362    name: &str,
363    scope: OptionScopeId,
364) -> CommandResult {
365    let kernel = runtime.kernel();
366    let options = &kernel.options;
367
368    let Some(full_name) = options.resolve_name(name) else {
369        return CommandResult::Error(format!("Unknown option: {name}"));
370    };
371
372    // Get old value before toggle for the event
373    let old_value = options.get(&full_name, scope);
374    let old_display = old_value
375        .as_ref()
376        .map_or_else(String::new, ToString::to_string);
377
378    match options.toggle(&full_name, scope) {
379        Ok(new_bool) => {
380            let new_value = OptionValue::bool(new_bool);
381
382            kernel.event_bus.emit(OptionChanged {
383                name: full_name.clone(),
384                old_value: old_display,
385                new_value: new_value.to_string(),
386                source: ChangeSource::UserCommand,
387                scope,
388            });
389
390            runtime.record_global_option_change(&full_name, new_value);
391            CommandResult::Success
392        }
393        Err(e) => CommandResult::Error(format!("{e}")),
394    }
395}
396
397/// Execute `:set option=value` — assign a typed value.
398fn execute_assign(
399    runtime: &mut SessionRuntime<'_>,
400    name: &str,
401    raw_value: &str,
402    scope: OptionScopeId,
403) -> CommandResult {
404    let kernel = runtime.kernel();
405    let options = &kernel.options;
406
407    let Some(full_name) = options.resolve_name(name) else {
408        return CommandResult::Error(format!("Unknown option: {name}"));
409    };
410
411    // resolve_name succeeded, so get_spec is guaranteed Some.
412    let spec = guard_get_spec(options, &full_name);
413
414    let new_value = match parse_value_for_type(raw_value, &spec.default, &full_name) {
415        Ok(v) => v,
416        Err(msg) => return CommandResult::Error(msg),
417    };
418
419    match options.set(&full_name, new_value.clone(), scope) {
420        Ok(result) => {
421            let old_display = result
422                .old_value
423                .as_ref()
424                .map_or_else(|| spec.default.to_string(), ToString::to_string);
425
426            kernel.event_bus.emit(OptionChanged {
427                name: full_name.clone(),
428                old_value: old_display,
429                new_value: result.new_value.to_string(),
430                source: ChangeSource::UserCommand,
431                scope,
432            });
433
434            runtime.record_global_option_change(&full_name, new_value);
435            CommandResult::Success
436        }
437        Err(e) => CommandResult::Error(format!("{e}")),
438    }
439}
440
441/// Execute `:set option&` — reset to default.
442fn execute_reset(
443    runtime: &mut SessionRuntime<'_>,
444    name: &str,
445    scope: OptionScopeId,
446) -> CommandResult {
447    let kernel = runtime.kernel();
448    let options = &kernel.options;
449
450    let Some(full_name) = options.resolve_name(name) else {
451        return CommandResult::Error(format!("Unknown option: {name}"));
452    };
453
454    // resolve_name succeeded, so get_spec and reset are guaranteed to succeed.
455    let spec = guard_get_spec(options, &full_name);
456    let old_value = guard_reset(options, &full_name, scope);
457
458    let old_display = old_value
459        .as_ref()
460        .map_or_else(|| spec.default.to_string(), ToString::to_string);
461
462    kernel.event_bus.emit(OptionReset {
463        name: full_name.clone(),
464        old_value: old_display,
465        default_value: spec.default.to_string(),
466        scope,
467    });
468
469    runtime.record_global_option_change(&full_name, spec.default);
470    CommandResult::Success
471}
472
473#[cfg(test)]
474#[path = "set_tests.rs"]
475mod tests;