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 let mut spec = CommandSpec::new(def.name.clone())
301 .args(def.args.iter().map(arg_node_from_def))
302 .flags(
303 def.flags
304 .iter()
305 .flat_map(flag_entries_from_def)
306 .collect::<Vec<_>>(),
307 )
308 .subcommands(def.subcommands.iter().map(command_spec_from_command_def));
309
310 if let Some(about) = def.about.as_deref() {
311 spec = spec.tooltip(about);
312 }
313 if let Some(sort_key) = def.sort_key.as_deref() {
314 spec = spec.sort(sort_key);
315 }
316 spec
317}
318
319fn arg_node_from_def(arg: &ArgDef) -> ArgNode {
320 let mut node = ArgNode::named(arg.value_name.as_deref().unwrap_or(&arg.id))
321 .suggestions(arg.choices.iter().map(suggestion_from_choice));
322 if let Some(help) = arg.help.as_deref() {
323 node = node.tooltip(help);
324 }
325 if arg.multi {
326 node = node.multi();
327 }
328 if let Some(value_type) = to_completion_value_type(arg.value_kind) {
329 node = node.value_type(value_type);
330 }
331 node
332}
333
334fn flag_entries_from_def(flag: &FlagDef) -> Vec<(String, FlagNode)> {
335 let mut node = FlagNode::new().suggestions(flag.choices.iter().map(suggestion_from_choice));
336 if let Some(help) = flag.help.as_deref() {
337 node = node.tooltip(help);
338 }
339 if !flag.takes_value {
340 node = node.flag_only();
341 }
342 if flag.multi {
343 node = node.multi();
344 }
345 if let Some(value_type) = to_completion_value_type(flag.value_kind) {
346 node = node.value_type(value_type);
347 }
348
349 flag_spellings(flag)
350 .into_iter()
351 .map(|name| (name, node.clone()))
352 .collect()
353}
354
355fn flag_spellings(flag: &FlagDef) -> Vec<String> {
356 let mut names = Vec::new();
357 if let Some(long) = flag.long.as_deref() {
358 names.push(format!("--{long}"));
359 }
360 if let Some(short) = flag.short {
361 names.push(format!("-{short}"));
362 }
363 names.extend(flag.aliases.iter().cloned());
364 names
365}
366
367fn suggestion_from_choice(choice: &ValueChoice) -> SuggestionEntry {
368 let mut entry = SuggestionEntry::value(choice.value.clone());
369 if let Some(meta) = choice.help.as_deref() {
370 entry = entry.meta(meta);
371 }
372 if let Some(display) = choice.display.as_deref() {
373 entry = entry.display(display);
374 }
375 if let Some(sort_key) = choice.sort_key.as_deref() {
376 entry = entry.sort(sort_key);
377 }
378 entry
379}
380
381fn to_completion_value_type(value_kind: Option<ValueKind>) -> Option<crate::completion::ValueType> {
382 match value_kind {
383 Some(ValueKind::Path) => Some(crate::completion::ValueType::Path),
384 Some(ValueKind::Enum | ValueKind::FreeText) | None => None,
385 }
386}
387
388#[derive(Debug, Clone, Default, PartialEq, Eq)]
390#[must_use]
391pub struct ConfigKeySpec {
392 pub key: String,
394 pub tooltip: Option<String>,
396 pub value_suggestions: Vec<SuggestionEntry>,
398}
399
400impl ConfigKeySpec {
401 pub fn new(key: impl Into<String>) -> Self {
403 Self {
404 key: key.into(),
405 ..Self::default()
406 }
407 }
408
409 pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
413 self.tooltip = Some(tooltip.into());
414 self
415 }
416
417 pub fn value_suggestions(
421 mut self,
422 suggestions: impl IntoIterator<Item = SuggestionEntry>,
423 ) -> Self {
424 self.value_suggestions = suggestions.into_iter().collect();
425 self
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use crate::completion::model::CompletionTree;
432 use crate::core::command_def::{ArgDef, CommandDef, FlagDef, ValueChoice, ValueKind};
433
434 use super::{
435 CommandSpec, CompletionTreeBuildError, CompletionTreeBuilder, ConfigKeySpec,
436 command_spec_from_command_def,
437 };
438
439 fn build_tree() -> CompletionTree {
440 CompletionTreeBuilder
441 .build_from_specs(
442 &[CommandSpec::new("config").subcommand(CommandSpec::new("set"))],
443 [("F".to_string(), "Filter".to_string())],
444 )
445 .expect("tree should build")
446 }
447
448 #[test]
449 fn duplicate_root_specs_return_an_error() {
450 let err = CompletionTreeBuilder
451 .build_from_specs(
452 &[CommandSpec::new("config"), CommandSpec::new("config")],
453 [],
454 )
455 .expect_err("duplicate root command should fail");
456
457 assert_eq!(
458 err,
459 CompletionTreeBuildError::DuplicateRootCommand {
460 name: "config".to_string()
461 }
462 );
463 }
464
465 #[test]
466 fn duplicate_config_keys_return_an_error() {
467 let mut tree = build_tree();
468 let err = CompletionTreeBuilder
469 .apply_config_set_keys(
470 &mut tree,
471 [
472 ConfigKeySpec::new("ui.format"),
473 ConfigKeySpec::new("ui.format"),
474 ],
475 )
476 .expect_err("duplicate config key should fail");
477
478 assert_eq!(
479 err,
480 CompletionTreeBuildError::DuplicateConfigSetKey {
481 key: "ui.format".to_string()
482 }
483 );
484 }
485
486 #[test]
487 fn duplicate_subcommands_return_an_error() {
488 let err = CompletionTreeBuilder
489 .build_from_specs(
490 &[CommandSpec::new("config")
491 .subcommands([CommandSpec::new("set"), CommandSpec::new("set")])],
492 [],
493 )
494 .expect_err("duplicate subcommand should fail");
495
496 assert_eq!(
497 err,
498 CompletionTreeBuildError::DuplicateSubcommand {
499 parent_path: "config".to_string(),
500 name: "set".to_string()
501 }
502 );
503 }
504
505 #[test]
506 fn command_spec_conversion_preserves_flag_spellings_and_choices_unit() {
507 let def = CommandDef::new("theme")
508 .about("Inspect themes")
509 .sort("10")
510 .arg(
511 ArgDef::new("name")
512 .help("Theme name")
513 .value_kind(ValueKind::Path)
514 .choices([ValueChoice::new("nord").help("Builtin theme")]),
515 )
516 .flag(
517 FlagDef::new("raw")
518 .long("raw")
519 .short('r')
520 .alias("--plain")
521 .help("Show raw values"),
522 );
523
524 let spec = command_spec_from_command_def(&def);
525
526 assert_eq!(spec.tooltip.as_deref(), Some("Inspect themes"));
527 assert_eq!(spec.sort.as_deref(), Some("10"));
528 assert!(spec.flags.contains_key("--raw"));
529 assert!(spec.flags.contains_key("-r"));
530 assert!(spec.flags.contains_key("--plain"));
531 assert_eq!(spec.args[0].tooltip.as_deref(), Some("Theme name"));
532 assert_eq!(spec.args[0].suggestions[0].value, "nord");
533 assert_eq!(
534 spec.args[0].value_type,
535 Some(crate::completion::ValueType::Path)
536 );
537 }
538}