1use std::collections::BTreeMap;
12
13use crate::completion::model::{
14 ArgNode, CompletionNode, CompletionTree, FlagNode, SuggestionEntry,
15};
16use crate::core::command_def::{ArgDef, CommandDef, FlagDef, ValueChoice, ValueKind};
17use thiserror::Error;
18
19#[derive(Debug, Clone, PartialEq, Eq, Error)]
22pub enum CompletionTreeBuildError {
23 #[error("duplicate root command spec: {name}")]
25 DuplicateRootCommand {
26 name: String,
28 },
29 #[error("duplicate subcommand spec under {parent_path}: {name}")]
31 DuplicateSubcommand {
32 parent_path: String,
34 name: String,
36 },
37 #[error("duplicate config set key: {key}")]
39 DuplicateConfigSetKey {
40 key: String,
42 },
43}
44
45impl CompletionTreeBuildError {
46 fn duplicate_subcommand(parent_path: &[String], name: &str) -> Self {
47 Self::DuplicateSubcommand {
48 parent_path: parent_path.join(" "),
49 name: name.to_string(),
50 }
51 }
52}
53
54#[derive(Debug, Clone, Default, PartialEq, Eq)]
56#[must_use]
57pub struct CommandSpec {
58 pub name: String,
60 pub tooltip: Option<String>,
62 pub sort: Option<String>,
64 pub args: Vec<ArgNode>,
66 pub flags: BTreeMap<String, FlagNode>,
68 pub subcommands: Vec<CommandSpec>,
70}
71
72impl CommandSpec {
73 pub fn new(name: impl Into<String>) -> Self {
75 Self {
76 name: name.into(),
77 ..Self::default()
78 }
79 }
80
81 pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
85 self.tooltip = Some(tooltip.into());
86 self
87 }
88
89 pub fn sort(mut self, sort: impl Into<String>) -> Self {
93 self.sort = Some(sort.into());
94 self
95 }
96
97 pub fn arg(mut self, arg: ArgNode) -> Self {
101 self.args.push(arg);
102 self
103 }
104
105 pub fn args(mut self, args: impl IntoIterator<Item = ArgNode>) -> Self {
109 self.args.extend(args);
110 self
111 }
112
113 pub fn flag(mut self, name: impl Into<String>, flag: FlagNode) -> Self {
117 self.flags.insert(name.into(), flag);
118 self
119 }
120
121 pub fn flags(mut self, flags: impl IntoIterator<Item = (String, FlagNode)>) -> Self {
125 self.flags.extend(flags);
126 self
127 }
128
129 pub fn subcommand(mut self, subcommand: CommandSpec) -> Self {
133 self.subcommands.push(subcommand);
134 self
135 }
136
137 pub fn subcommands(mut self, subcommands: impl IntoIterator<Item = CommandSpec>) -> Self {
141 self.subcommands.extend(subcommands);
142 self
143 }
144}
145
146#[derive(Debug, Clone, Default)]
147pub struct CompletionTreeBuilder;
151
152impl CompletionTreeBuilder {
153 pub fn build_from_specs(
184 &self,
185 specs: &[CommandSpec],
186 pipe_verbs: impl IntoIterator<Item = (String, String)>,
187 ) -> Result<CompletionTree, CompletionTreeBuildError> {
188 let mut root = CompletionNode::default();
189 for spec in specs {
190 let name = spec.name.clone();
191 let node = Self::node_from_spec(spec, &[])?;
192 if root.children.insert(name.clone(), node).is_some() {
193 return Err(CompletionTreeBuildError::DuplicateRootCommand { name });
194 }
195 }
196
197 Ok(CompletionTree {
198 root,
199 pipe_verbs: pipe_verbs.into_iter().collect(),
200 })
201 }
202
203 pub fn apply_config_set_keys(
237 &self,
238 tree: &mut CompletionTree,
239 keys: impl IntoIterator<Item = ConfigKeySpec>,
240 ) -> Result<(), CompletionTreeBuildError> {
241 let Some(config_node) = tree.root.children.get_mut("config") else {
242 return Ok(());
243 };
244 let Some(set_node) = config_node.children.get_mut("set") else {
245 return Ok(());
246 };
247
248 for key in keys {
249 let key_name = key.key.clone();
250 let mut node = CompletionNode {
251 tooltip: key.tooltip,
252 value_key: true,
253 ..CompletionNode::default()
254 };
255 for suggestion in key.value_suggestions {
256 node.children.insert(
257 suggestion.value.clone(),
258 CompletionNode {
259 value_leaf: true,
260 tooltip: suggestion.meta.clone(),
261 ..CompletionNode::default()
262 },
263 );
264 }
265 if set_node.children.insert(key_name.clone(), node).is_some() {
266 return Err(CompletionTreeBuildError::DuplicateConfigSetKey { key: key_name });
267 }
268 }
269
270 Ok(())
271 }
272
273 fn node_from_spec(
274 spec: &CommandSpec,
275 parent_path: &[String],
276 ) -> Result<CompletionNode, CompletionTreeBuildError> {
277 let mut node = CompletionNode {
278 tooltip: spec.tooltip.clone(),
279 sort: spec.sort.clone(),
280 args: spec.args.clone(),
281 flags: spec.flags.clone(),
282 ..CompletionNode::default()
283 };
284
285 let mut path = parent_path.to_vec();
286 path.push(spec.name.clone());
287 for subcommand in &spec.subcommands {
288 let name = subcommand.name.clone();
289 let child = Self::node_from_spec(subcommand, &path)?;
290 if node.children.insert(name.clone(), child).is_some() {
291 return Err(CompletionTreeBuildError::duplicate_subcommand(&path, &name));
292 }
293 }
294
295 Ok(node)
296 }
297}
298
299pub(crate) fn command_spec_from_command_def(def: &CommandDef) -> CommandSpec {
300 CommandSpec {
301 name: def.name.clone(),
302 tooltip: def.about.clone(),
303 sort: def.sort_key.clone(),
304 args: def.args.iter().map(arg_node_from_def).collect(),
305 flags: def.flags.iter().flat_map(flag_entries_from_def).collect(),
306 subcommands: def
307 .subcommands
308 .iter()
309 .map(command_spec_from_command_def)
310 .collect(),
311 }
312}
313
314fn arg_node_from_def(arg: &ArgDef) -> ArgNode {
315 ArgNode {
316 name: Some(arg.value_name.as_deref().unwrap_or(&arg.id).to_string()),
317 tooltip: arg.help.clone(),
318 multi: arg.multi,
319 value_type: to_completion_value_type(arg.value_kind),
320 suggestions: arg.choices.iter().map(suggestion_from_choice).collect(),
321 }
322}
323
324fn flag_entries_from_def(flag: &FlagDef) -> Vec<(String, FlagNode)> {
325 let node = FlagNode {
326 tooltip: flag.help.clone(),
327 flag_only: !flag.takes_value,
328 multi: flag.multi,
329 value_type: to_completion_value_type(flag.value_kind),
330 suggestions: flag.choices.iter().map(suggestion_from_choice).collect(),
331 ..FlagNode::default()
332 };
333
334 flag_spellings(flag)
335 .into_iter()
336 .map(|name| (name, node.clone()))
337 .collect()
338}
339
340fn flag_spellings(flag: &FlagDef) -> Vec<String> {
341 let mut names = Vec::new();
342 if let Some(long) = flag.long.as_deref() {
343 names.push(format!("--{long}"));
344 }
345 if let Some(short) = flag.short {
346 names.push(format!("-{short}"));
347 }
348 names.extend(flag.aliases.iter().cloned());
349 names
350}
351
352fn suggestion_from_choice(choice: &ValueChoice) -> SuggestionEntry {
353 SuggestionEntry {
354 value: choice.value.clone(),
355 meta: choice.help.clone(),
356 display: choice.display.clone(),
357 sort: choice.sort_key.clone(),
358 }
359}
360
361fn to_completion_value_type(value_kind: Option<ValueKind>) -> Option<crate::completion::ValueType> {
362 match value_kind {
363 Some(ValueKind::Path) => Some(crate::completion::ValueType::Path),
364 Some(ValueKind::Enum | ValueKind::FreeText) | None => None,
365 }
366}
367
368#[derive(Debug, Clone, Default, PartialEq, Eq)]
370#[must_use]
371pub struct ConfigKeySpec {
372 pub key: String,
374 pub tooltip: Option<String>,
376 pub value_suggestions: Vec<SuggestionEntry>,
378}
379
380impl ConfigKeySpec {
381 pub fn new(key: impl Into<String>) -> Self {
383 Self {
384 key: key.into(),
385 ..Self::default()
386 }
387 }
388
389 pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
393 self.tooltip = Some(tooltip.into());
394 self
395 }
396
397 pub fn value_suggestions(
401 mut self,
402 suggestions: impl IntoIterator<Item = SuggestionEntry>,
403 ) -> Self {
404 self.value_suggestions = suggestions.into_iter().collect();
405 self
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use crate::completion::model::CompletionTree;
412 use crate::core::command_def::{ArgDef, CommandDef, FlagDef, ValueChoice, ValueKind};
413
414 use super::{
415 CommandSpec, CompletionTreeBuildError, CompletionTreeBuilder, ConfigKeySpec,
416 command_spec_from_command_def,
417 };
418
419 fn build_tree() -> CompletionTree {
420 CompletionTreeBuilder
421 .build_from_specs(
422 &[CommandSpec::new("config").subcommand(CommandSpec::new("set"))],
423 [("F".to_string(), "Filter".to_string())],
424 )
425 .expect("tree should build")
426 }
427
428 #[test]
429 fn duplicate_root_specs_return_an_error() {
430 let err = CompletionTreeBuilder
431 .build_from_specs(
432 &[CommandSpec::new("config"), CommandSpec::new("config")],
433 [],
434 )
435 .expect_err("duplicate root command should fail");
436
437 assert_eq!(
438 err,
439 CompletionTreeBuildError::DuplicateRootCommand {
440 name: "config".to_string()
441 }
442 );
443 }
444
445 #[test]
446 fn duplicate_config_keys_return_an_error() {
447 let mut tree = build_tree();
448 let err = CompletionTreeBuilder
449 .apply_config_set_keys(
450 &mut tree,
451 [
452 ConfigKeySpec::new("ui.format"),
453 ConfigKeySpec::new("ui.format"),
454 ],
455 )
456 .expect_err("duplicate config key should fail");
457
458 assert_eq!(
459 err,
460 CompletionTreeBuildError::DuplicateConfigSetKey {
461 key: "ui.format".to_string()
462 }
463 );
464 }
465
466 #[test]
467 fn duplicate_subcommands_return_an_error() {
468 let err = CompletionTreeBuilder
469 .build_from_specs(
470 &[CommandSpec::new("config")
471 .subcommands([CommandSpec::new("set"), CommandSpec::new("set")])],
472 [],
473 )
474 .expect_err("duplicate subcommand should fail");
475
476 assert_eq!(
477 err,
478 CompletionTreeBuildError::DuplicateSubcommand {
479 parent_path: "config".to_string(),
480 name: "set".to_string()
481 }
482 );
483 }
484
485 #[test]
486 fn command_spec_conversion_preserves_flag_spellings_and_choices_unit() {
487 let def = CommandDef::new("theme")
488 .about("Inspect themes")
489 .sort("10")
490 .arg(
491 ArgDef::new("name")
492 .help("Theme name")
493 .value_kind(ValueKind::Path)
494 .choices([ValueChoice::new("nord").help("Builtin theme")]),
495 )
496 .flag(
497 FlagDef::new("raw")
498 .long("raw")
499 .short('r')
500 .alias("--plain")
501 .help("Show raw values"),
502 );
503
504 let spec = command_spec_from_command_def(&def);
505
506 assert_eq!(spec.tooltip.as_deref(), Some("Inspect themes"));
507 assert_eq!(spec.sort.as_deref(), Some("10"));
508 assert!(spec.flags.contains_key("--raw"));
509 assert!(spec.flags.contains_key("-r"));
510 assert!(spec.flags.contains_key("--plain"));
511 assert_eq!(spec.args[0].tooltip.as_deref(), Some("Theme name"));
512 assert_eq!(spec.args[0].suggestions[0].value, "nord");
513 assert_eq!(
514 spec.args[0].value_type,
515 Some(crate::completion::ValueType::Path)
516 );
517 }
518}