1use {
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#[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#[derive(Debug, Clone, PartialEq, Eq)]
61enum SetAction {
62 ListChanged,
64 ListAll,
66 Show { name: String },
68 SetBool { name: String, value: bool },
70 Toggle { name: String },
72 Assign { name: String, raw_value: String },
74 Reset { name: String },
76}
77
78fn 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 if let Some(name) = expr.strip_suffix('?') {
97 return SetAction::Show {
98 name: name.to_string(),
99 };
100 }
101
102 if let Some(name) = expr.strip_suffix('!') {
104 return SetAction::Toggle {
105 name: name.to_string(),
106 };
107 }
108
109 if let Some(name) = expr.strip_suffix('&') {
111 return SetAction::Reset {
112 name: name.to_string(),
113 };
114 }
115
116 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 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 SetAction::SetBool {
136 name: expr.to_string(),
137 value: true,
138 }
139}
140
141fn 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
166impl 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
190fn 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 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
209fn 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 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
226fn 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 let value = guard_get_value(options, &full_name, scope);
237 log_option_value(&full_name, &value);
238 CommandResult::Success
239}
240
241#[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#[cfg_attr(coverage_nightly, coverage(off))]
253fn log_option_value(name: &str, value: &OptionValue) {
254 tracing::info!(" {name}={value}");
255}
256
257use reovim_kernel::api::v1::OptionRegistry;
266
267#[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#[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#[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#[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
311fn 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 let spec = guard_get_spec(options, &full_name);
330
331 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
359fn 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 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
397fn 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 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
441fn 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 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;