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};
17
18#[derive(Debug, Clone, Default, PartialEq, Eq)]
19pub struct CommandSpec {
21 pub name: String,
23 pub tooltip: Option<String>,
25 pub sort: Option<String>,
27 pub args: Vec<ArgNode>,
29 pub flags: BTreeMap<String, FlagNode>,
31 pub subcommands: Vec<CommandSpec>,
33}
34
35impl CommandSpec {
36 pub fn new(name: impl Into<String>) -> Self {
38 Self {
39 name: name.into(),
40 ..Self::default()
41 }
42 }
43
44 pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
46 self.tooltip = Some(tooltip.into());
47 self
48 }
49
50 pub fn sort(mut self, sort: impl Into<String>) -> Self {
52 self.sort = Some(sort.into());
53 self
54 }
55
56 pub fn arg(mut self, arg: ArgNode) -> Self {
58 self.args.push(arg);
59 self
60 }
61
62 pub fn args(mut self, args: impl IntoIterator<Item = ArgNode>) -> Self {
64 self.args.extend(args);
65 self
66 }
67
68 pub fn flag(mut self, name: impl Into<String>, flag: FlagNode) -> Self {
70 self.flags.insert(name.into(), flag);
71 self
72 }
73
74 pub fn flags(mut self, flags: impl IntoIterator<Item = (String, FlagNode)>) -> Self {
76 self.flags.extend(flags);
77 self
78 }
79
80 pub fn subcommand(mut self, subcommand: CommandSpec) -> Self {
82 self.subcommands.push(subcommand);
83 self
84 }
85
86 pub fn subcommands(mut self, subcommands: impl IntoIterator<Item = CommandSpec>) -> Self {
88 self.subcommands.extend(subcommands);
89 self
90 }
91}
92
93#[derive(Debug, Clone, Default)]
94pub struct CompletionTreeBuilder;
98
99impl CompletionTreeBuilder {
100 pub fn build_from_specs(
120 &self,
121 specs: &[CommandSpec],
122 pipe_verbs: impl IntoIterator<Item = (String, String)>,
123 ) -> CompletionTree {
124 let mut root = CompletionNode::default();
125 for spec in specs {
126 let name = spec.name.clone();
127 assert!(
128 root.children
129 .insert(name.clone(), Self::node_from_spec(spec))
130 .is_none(),
131 "duplicate root command spec: {name}"
132 );
133 }
134
135 CompletionTree {
136 root,
137 pipe_verbs: pipe_verbs.into_iter().collect(),
138 }
139 }
140
141 pub fn apply_config_set_keys(
143 &self,
144 tree: &mut CompletionTree,
145 keys: impl IntoIterator<Item = ConfigKeySpec>,
146 ) {
147 let Some(config_node) = tree.root.children.get_mut("config") else {
148 return;
149 };
150 let Some(set_node) = config_node.children.get_mut("set") else {
151 return;
152 };
153
154 for key in keys {
155 let key_name = key.key.clone();
156 let mut node = CompletionNode {
157 tooltip: key.tooltip,
158 value_key: true,
159 ..CompletionNode::default()
160 };
161 for suggestion in key.value_suggestions {
162 node.children.insert(
163 suggestion.value.clone(),
164 CompletionNode {
165 value_leaf: true,
166 tooltip: suggestion.meta.clone(),
167 ..CompletionNode::default()
168 },
169 );
170 }
171 assert!(
172 set_node.children.insert(key_name.clone(), node).is_none(),
173 "duplicate config set key: {key_name}"
174 );
175 }
176 }
177
178 fn node_from_spec(spec: &CommandSpec) -> CompletionNode {
179 let mut node = CompletionNode {
180 tooltip: spec.tooltip.clone(),
181 sort: spec.sort.clone(),
182 args: spec.args.clone(),
183 flags: spec.flags.clone(),
184 ..CompletionNode::default()
185 };
186
187 for subcommand in &spec.subcommands {
188 let name = subcommand.name.clone();
189 assert!(
190 node.children
191 .insert(name.clone(), Self::node_from_spec(subcommand))
192 .is_none(),
193 "duplicate subcommand spec: {name}"
194 );
195 }
196
197 node
198 }
199}
200
201pub(crate) fn command_spec_from_command_def(def: &CommandDef) -> CommandSpec {
202 let mut spec = CommandSpec::new(def.name.clone())
203 .args(def.args.iter().map(arg_node_from_def))
204 .flags(
205 def.flags
206 .iter()
207 .flat_map(flag_entries_from_def)
208 .collect::<Vec<_>>(),
209 )
210 .subcommands(def.subcommands.iter().map(command_spec_from_command_def));
211
212 if let Some(about) = def.about.as_deref() {
213 spec = spec.tooltip(about);
214 }
215 if let Some(sort_key) = def.sort_key.as_deref() {
216 spec = spec.sort(sort_key);
217 }
218 spec
219}
220
221fn arg_node_from_def(arg: &ArgDef) -> ArgNode {
222 let mut node = ArgNode::named(arg.value_name.as_deref().unwrap_or(&arg.id))
223 .suggestions(arg.choices.iter().map(suggestion_from_choice));
224 if let Some(help) = arg.help.as_deref() {
225 node = node.tooltip(help);
226 }
227 if arg.multi {
228 node = node.multi();
229 }
230 if let Some(value_type) = to_completion_value_type(arg.value_kind) {
231 node = node.value_type(value_type);
232 }
233 node
234}
235
236fn flag_entries_from_def(flag: &FlagDef) -> Vec<(String, FlagNode)> {
237 let mut node = FlagNode::new().suggestions(flag.choices.iter().map(suggestion_from_choice));
238 if let Some(help) = flag.help.as_deref() {
239 node = node.tooltip(help);
240 }
241 if !flag.takes_value {
242 node = node.flag_only();
243 }
244 if flag.multi {
245 node = node.multi();
246 }
247 if let Some(value_type) = to_completion_value_type(flag.value_kind) {
248 node = node.value_type(value_type);
249 }
250
251 flag_spellings(flag)
252 .into_iter()
253 .map(|name| (name, node.clone()))
254 .collect()
255}
256
257fn flag_spellings(flag: &FlagDef) -> Vec<String> {
258 let mut names = Vec::new();
259 if let Some(long) = flag.long.as_deref() {
260 names.push(format!("--{long}"));
261 }
262 if let Some(short) = flag.short {
263 names.push(format!("-{short}"));
264 }
265 names.extend(flag.aliases.iter().cloned());
266 names
267}
268
269fn suggestion_from_choice(choice: &ValueChoice) -> SuggestionEntry {
270 let mut entry = SuggestionEntry::value(choice.value.clone());
271 if let Some(meta) = choice.help.as_deref() {
272 entry = entry.meta(meta);
273 }
274 if let Some(display) = choice.display.as_deref() {
275 entry = entry.display(display);
276 }
277 if let Some(sort_key) = choice.sort_key.as_deref() {
278 entry = entry.sort(sort_key);
279 }
280 entry
281}
282
283fn to_completion_value_type(value_kind: Option<ValueKind>) -> Option<crate::completion::ValueType> {
284 match value_kind {
285 Some(ValueKind::Path) => Some(crate::completion::ValueType::Path),
286 Some(ValueKind::Enum | ValueKind::FreeText) | None => None,
287 }
288}
289
290#[derive(Debug, Clone, Default, PartialEq, Eq)]
291pub struct ConfigKeySpec {
293 pub key: String,
295 pub tooltip: Option<String>,
297 pub value_suggestions: Vec<SuggestionEntry>,
299}
300
301impl ConfigKeySpec {
302 pub fn new(key: impl Into<String>) -> Self {
304 Self {
305 key: key.into(),
306 ..Self::default()
307 }
308 }
309
310 pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
312 self.tooltip = Some(tooltip.into());
313 self
314 }
315
316 pub fn value_suggestions(
318 mut self,
319 suggestions: impl IntoIterator<Item = SuggestionEntry>,
320 ) -> Self {
321 self.value_suggestions = suggestions.into_iter().collect();
322 self
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use crate::completion::model::CompletionTree;
329 use crate::core::command_def::{ArgDef, CommandDef, FlagDef, ValueChoice, ValueKind};
330
331 use super::{CommandSpec, CompletionTreeBuilder, ConfigKeySpec, command_spec_from_command_def};
332
333 fn build_tree() -> CompletionTree {
334 CompletionTreeBuilder.build_from_specs(
335 &[CommandSpec::new("config").subcommand(CommandSpec::new("set"))],
336 [("F".to_string(), "Filter".to_string())],
337 )
338 }
339
340 #[test]
341 fn builds_nested_tree_from_specs() {
342 let tree = build_tree();
343 assert!(tree.root.children.contains_key("config"));
344 assert!(
345 tree.root
346 .children
347 .get("config")
348 .and_then(|node| node.children.get("set"))
349 .is_some()
350 );
351 }
352
353 #[test]
354 fn injects_config_key_nodes() {
355 let mut tree = build_tree();
356 CompletionTreeBuilder.apply_config_set_keys(
357 &mut tree,
358 [
359 ConfigKeySpec::new("ui.format"),
360 ConfigKeySpec::new("log.level"),
361 ],
362 );
363
364 let set_node = &tree.root.children["config"].children["set"];
365 assert!(set_node.children.contains_key("ui.format"));
366 assert!(set_node.children.contains_key("log.level"));
367 assert!(set_node.children["ui.format"].value_key);
368 }
369
370 #[test]
371 #[should_panic(expected = "duplicate root command spec")]
372 fn duplicate_root_specs_fail_fast() {
373 let _ = CompletionTreeBuilder.build_from_specs(
374 &[CommandSpec::new("config"), CommandSpec::new("config")],
375 [],
376 );
377 }
378
379 #[test]
380 #[should_panic(expected = "duplicate config set key")]
381 fn duplicate_config_keys_fail_fast() {
382 let mut tree = build_tree();
383 CompletionTreeBuilder.apply_config_set_keys(
384 &mut tree,
385 [
386 ConfigKeySpec::new("ui.format"),
387 ConfigKeySpec::new("ui.format"),
388 ],
389 );
390 }
391
392 #[test]
393 fn command_spec_conversion_preserves_flag_spellings_and_choices_unit() {
394 let def = CommandDef::new("theme")
395 .about("Inspect themes")
396 .sort("10")
397 .arg(
398 ArgDef::new("name")
399 .help("Theme name")
400 .value_kind(ValueKind::Path)
401 .choices([ValueChoice::new("nord").help("Builtin theme")]),
402 )
403 .flag(
404 FlagDef::new("raw")
405 .long("raw")
406 .short('r')
407 .alias("--plain")
408 .help("Show raw values"),
409 );
410
411 let spec = command_spec_from_command_def(&def);
412
413 assert_eq!(spec.tooltip.as_deref(), Some("Inspect themes"));
414 assert_eq!(spec.sort.as_deref(), Some("10"));
415 assert!(spec.flags.contains_key("--raw"));
416 assert!(spec.flags.contains_key("-r"));
417 assert!(spec.flags.contains_key("--plain"));
418 assert_eq!(spec.args[0].tooltip.as_deref(), Some("Theme name"));
419 assert_eq!(spec.args[0].suggestions[0].value, "nord");
420 assert_eq!(
421 spec.args[0].value_type,
422 Some(crate::completion::ValueType::Path)
423 );
424 }
425}