use {
reovim_driver_command::{
ArgKind, ArgSpec, Command, CommandContext, CommandHandler, CommandResult,
},
reovim_driver_session::SessionRuntime,
reovim_kernel::api::v1::{
CommandId, ModuleId, OptionScopeId, OptionValue,
events::kernel::{ChangeSource, OptionChanged, OptionReset},
},
};
const COMMANDS_MODULE: ModuleId = ModuleId::new("commands");
#[derive(Debug, Clone, Copy)]
pub struct SetCommand;
impl Command for SetCommand {
fn id(&self) -> CommandId {
CommandId::new(COMMANDS_MODULE, "set")
}
fn description(&self) -> &'static str {
"Set editor options. Use :set option=value, :set option, :set nooption, etc."
}
fn args(&self) -> Vec<ArgSpec> {
vec![ArgSpec::optional(
"expr",
ArgKind::Rest,
"Option expression",
)]
}
fn names(&self) -> &[&'static str] {
&["set"]
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum SetAction {
ListChanged,
ListAll,
Show { name: String },
SetBool { name: String, value: bool },
Toggle { name: String },
Assign { name: String, raw_value: String },
Reset { name: String },
}
fn parse_set_expr(expr: &str) -> SetAction {
let expr = expr.trim();
if expr.is_empty() {
return SetAction::ListChanged;
}
if expr == "all" {
return SetAction::ListAll;
}
if let Some(name) = expr.strip_suffix('?') {
return SetAction::Show {
name: name.to_string(),
};
}
if let Some(name) = expr.strip_suffix('!') {
return SetAction::Toggle {
name: name.to_string(),
};
}
if let Some(name) = expr.strip_suffix('&') {
return SetAction::Reset {
name: name.to_string(),
};
}
if let Some((name, value)) = expr.split_once('=') {
return SetAction::Assign {
name: name.to_string(),
raw_value: value.to_string(),
};
}
if let Some(name) = expr.strip_prefix("no")
&& !name.is_empty()
{
return SetAction::SetBool {
name: name.to_string(),
value: false,
};
}
SetAction::SetBool {
name: expr.to_string(),
value: true,
}
}
fn parse_value_for_type(
raw: &str,
expected: &OptionValue,
name: &str,
) -> Result<OptionValue, String> {
match expected {
OptionValue::Bool(_) => match raw {
"true" | "1" | "on" => Ok(OptionValue::bool(true)),
"false" | "0" | "off" => Ok(OptionValue::bool(false)),
_ => Err(format!(
"invalid value for '{name}': expected bool (true/false/1/0/on/off), got '{raw}'"
)),
},
OptionValue::Integer(_) => {
let val: i64 = raw.parse().map_err(|_| {
format!("invalid value for '{name}': expected integer, got '{raw}'")
})?;
Ok(OptionValue::int(val))
}
OptionValue::String(_) => Ok(OptionValue::string(raw)),
OptionValue::Choice { choices, .. } => Ok(OptionValue::choice(raw, choices.clone())),
}
}
impl CommandHandler for SetCommand {
fn execute(&self, runtime: &mut SessionRuntime<'_>, ctx: &CommandContext) -> CommandResult {
let expr = ctx.string("expr").unwrap_or("");
let action = parse_set_expr(expr);
let scope = OptionScopeId::Global;
match action {
SetAction::ListChanged => execute_list_changed(runtime, scope),
SetAction::ListAll => execute_list_all(runtime, scope),
SetAction::Show { name } => execute_show(runtime, &name, scope),
SetAction::SetBool { name, value } => execute_set_bool(runtime, &name, value, scope),
SetAction::Toggle { name } => execute_toggle(runtime, &name, scope),
SetAction::Assign { name, raw_value } => {
execute_assign(runtime, &name, &raw_value, scope)
}
SetAction::Reset { name } => execute_reset(runtime, &name, scope),
}
}
}
fn execute_list_changed(runtime: &SessionRuntime<'_>, scope: OptionScopeId) -> CommandResult {
let kernel = runtime.kernel();
let options = &kernel.options;
let names = options.list_all();
let mut lines = Vec::new();
for name in &names {
let (spec, current) = guard_spec_and_value(options, name, scope);
if current != spec.default {
lines.push(format!(" {name}={current}"));
}
}
log_option_list("Changed options", "No changed options", &lines);
CommandResult::Success
}
fn execute_list_all(runtime: &SessionRuntime<'_>, scope: OptionScopeId) -> CommandResult {
let kernel = runtime.kernel();
let options = &kernel.options;
let names = options.list_all();
let mut lines = Vec::new();
for name in &names {
let value = guard_get_value(options, name, scope);
lines.push(format!(" {name}={value}"));
}
log_option_list("All options", "No options registered", &lines);
CommandResult::Success
}
fn execute_show(runtime: &SessionRuntime<'_>, name: &str, scope: OptionScopeId) -> CommandResult {
let kernel = runtime.kernel();
let options = &kernel.options;
let Some(full_name) = options.resolve_name(name) else {
return CommandResult::Error(format!("Unknown option: {name}"));
};
let value = guard_get_value(options, &full_name, scope);
log_option_value(&full_name, &value);
CommandResult::Success
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn log_option_list(header: &str, empty_msg: &str, lines: &[String]) {
if lines.is_empty() {
tracing::info!("{empty_msg}");
} else {
tracing::info!("{header}:\n{}", lines.join("\n"));
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn log_option_value(name: &str, value: &OptionValue) {
tracing::info!(" {name}={value}");
}
use reovim_kernel::api::v1::OptionRegistry;
#[cfg_attr(coverage_nightly, coverage(off))]
fn guard_get_spec(options: &OptionRegistry, full_name: &str) -> reovim_kernel::api::v1::OptionSpec {
options
.get_spec(full_name)
.expect("get_spec must succeed after resolve_name")
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn guard_get_value(options: &OptionRegistry, full_name: &str, scope: OptionScopeId) -> OptionValue {
options
.get(full_name, scope)
.expect("get must succeed after resolve_name")
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn guard_spec_and_value(
options: &OptionRegistry,
name: &str,
scope: OptionScopeId,
) -> (reovim_kernel::api::v1::OptionSpec, OptionValue) {
let spec = options
.get_spec(name)
.expect("get_spec must succeed for list_all name");
let value = options
.get(name, scope)
.expect("get must succeed for list_all name");
(spec, value)
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn guard_reset(
options: &OptionRegistry,
full_name: &str,
scope: OptionScopeId,
) -> Option<OptionValue> {
options
.reset(full_name, scope)
.expect("reset must succeed after resolve_name")
}
fn execute_set_bool(
runtime: &mut SessionRuntime<'_>,
name: &str,
value: bool,
scope: OptionScopeId,
) -> CommandResult {
let kernel = runtime.kernel();
let options = &kernel.options;
let Some(full_name) = options.resolve_name(name) else {
return CommandResult::Error(format!("Unknown option: {name}"));
};
let spec = guard_get_spec(options, &full_name);
if value && !matches!(spec.default, OptionValue::Bool(_)) {
return execute_show(runtime, &full_name, scope);
}
let new_value = OptionValue::bool(value);
match options.set(&full_name, new_value.clone(), scope) {
Ok(result) => {
let old_display = result
.old_value
.as_ref()
.map_or_else(|| spec.default.to_string(), ToString::to_string);
kernel.event_bus.emit(OptionChanged {
name: full_name.clone(),
old_value: old_display,
new_value: result.new_value.to_string(),
source: ChangeSource::UserCommand,
scope,
});
runtime.record_global_option_change(&full_name, new_value);
CommandResult::Success
}
Err(e) => CommandResult::Error(format!("{e}")),
}
}
fn execute_toggle(
runtime: &mut SessionRuntime<'_>,
name: &str,
scope: OptionScopeId,
) -> CommandResult {
let kernel = runtime.kernel();
let options = &kernel.options;
let Some(full_name) = options.resolve_name(name) else {
return CommandResult::Error(format!("Unknown option: {name}"));
};
let old_value = options.get(&full_name, scope);
let old_display = old_value
.as_ref()
.map_or_else(String::new, ToString::to_string);
match options.toggle(&full_name, scope) {
Ok(new_bool) => {
let new_value = OptionValue::bool(new_bool);
kernel.event_bus.emit(OptionChanged {
name: full_name.clone(),
old_value: old_display,
new_value: new_value.to_string(),
source: ChangeSource::UserCommand,
scope,
});
runtime.record_global_option_change(&full_name, new_value);
CommandResult::Success
}
Err(e) => CommandResult::Error(format!("{e}")),
}
}
fn execute_assign(
runtime: &mut SessionRuntime<'_>,
name: &str,
raw_value: &str,
scope: OptionScopeId,
) -> CommandResult {
let kernel = runtime.kernel();
let options = &kernel.options;
let Some(full_name) = options.resolve_name(name) else {
return CommandResult::Error(format!("Unknown option: {name}"));
};
let spec = guard_get_spec(options, &full_name);
let new_value = match parse_value_for_type(raw_value, &spec.default, &full_name) {
Ok(v) => v,
Err(msg) => return CommandResult::Error(msg),
};
match options.set(&full_name, new_value.clone(), scope) {
Ok(result) => {
let old_display = result
.old_value
.as_ref()
.map_or_else(|| spec.default.to_string(), ToString::to_string);
kernel.event_bus.emit(OptionChanged {
name: full_name.clone(),
old_value: old_display,
new_value: result.new_value.to_string(),
source: ChangeSource::UserCommand,
scope,
});
runtime.record_global_option_change(&full_name, new_value);
CommandResult::Success
}
Err(e) => CommandResult::Error(format!("{e}")),
}
}
fn execute_reset(
runtime: &mut SessionRuntime<'_>,
name: &str,
scope: OptionScopeId,
) -> CommandResult {
let kernel = runtime.kernel();
let options = &kernel.options;
let Some(full_name) = options.resolve_name(name) else {
return CommandResult::Error(format!("Unknown option: {name}"));
};
let spec = guard_get_spec(options, &full_name);
let old_value = guard_reset(options, &full_name, scope);
let old_display = old_value
.as_ref()
.map_or_else(|| spec.default.to_string(), ToString::to_string);
kernel.event_bus.emit(OptionReset {
name: full_name.clone(),
old_value: old_display,
default_value: spec.default.to_string(),
scope,
});
runtime.record_global_option_change(&full_name, spec.default);
CommandResult::Success
}
#[cfg(test)]
#[path = "set_tests.rs"]
mod tests;