1#![forbid(unsafe_code)]
28
29use clap::Parser;
30use futures::future::BoxFuture;
31use futures::future::FutureExt;
32
33#[derive(Parser)]
36pub struct Complete {
37 #[clap(long, requires = "position")]
46 pub raw: Option<String>,
47
48 #[clap(long, requires = "raw")]
54 pub position: Option<usize>,
55
56 #[clap(trailing_var_arg = true, allow_hyphen_values = true)]
62 pub cmd: Vec<String>,
63}
64
65impl Complete {
66 pub async fn println_to_stub_script<Cli: clap::CommandFactory>(
71 self,
72 maybe_subcommand_of: Option<&str>,
73 custom_completer_factory: impl CustomCompleterFactory,
74 ) {
75 let completions = self
76 .generate_completions::<Cli>(maybe_subcommand_of, custom_completer_factory)
77 .await;
78
79 for completion in completions {
80 log::debug!("suggesting: {}", completion);
81 println!("{}", completion);
82 }
83 }
84
85 pub async fn generate_completions<Cli: clap::CommandFactory>(
91 self,
92 maybe_subcommand_of: Option<&str>,
93 custom_completer_factory: impl CustomCompleterFactory,
94 ) -> Vec<String> {
95 let Self { position, raw, cmd } = self;
96
97 let cmd = {
100 let mut cmd = cmd;
101 if let Some(maybe_subcommand_of) = maybe_subcommand_of {
102 if cmd.first().map(|s| s.as_str()) == Some(maybe_subcommand_of) {
103 cmd.remove(0);
104 }
105 }
106
107 cmd
108 };
109
110 log::debug!("");
111 log::debug!("cmd: [{}]", cmd.clone().join(" , "));
112 log::debug!("raw: '{}'", raw.as_deref().unwrap_or_default());
113 log::debug!("position: {:?}", position);
114
115 let (prev_arg, to_complete) = match (position, &raw) {
116 (Some(position), Some(raw)) => {
117 if position <= raw.len() {
118 let (before, _) = raw.split_at(position);
119 log::debug!("completing from: '{}'", before);
120 let mut before_toks = before.split_whitespace().collect::<Vec<_>>();
121 if before.ends_with(|c: char| c.is_whitespace()) {
122 before_toks.push("")
123 }
124 match before_toks.as_slice() {
125 [] => ("", ""),
126 [a] => ("", *a),
127 [.., a, b] => (*a, *b),
128 }
129 } else {
130 (cmd.last().unwrap().as_str(), "")
131 }
132 }
133 _ => match cmd.as_slice() {
134 [] => ("", ""),
135 [a] => ("", a.as_ref()),
136 [.., a, b] => (a.as_ref(), b.as_ref()),
137 },
138 };
139
140 let base_command = Cli::command();
141 let command = {
143 command_visitor(
144 base_command.clone(),
145 &mut |arg| {
146 loosen_value_parser(arg)
148 },
149 &mut |mut command| {
150 if !command.is_disable_help_flag_set() {
153 command = command.disable_help_flag(true).arg(
154 clap::Arg::new("my_help")
155 .short('h')
156 .long("help")
157 .action(clap::ArgAction::SetTrue),
158 )
159 }
160
161 command
162 },
163 )
164 };
165
166 let matches = (command.clone())
167 .ignore_errors(true)
168 .try_get_matches_from(&cmd)
169 .unwrap();
170
171 let ctx = RootCtx {
172 command: base_command,
173 matches: matches.clone(),
174 to_complete,
175 prev_arg,
176 };
177
178 log::debug!("to_complete: {to_complete}");
179 log::debug!("prev_arg: {prev_arg}");
180
181 let mut completions = recurse_completions(
182 &ctx,
183 Vec::new(),
184 &command,
185 &matches,
186 Box::new(custom_completer_factory.build(&ctx).await),
187 )
188 .await;
189
190 completions.retain(|x| x.starts_with(to_complete));
192
193 completions.sort_by(|a, b| match (a.starts_with('-'), b.starts_with('-')) {
195 (true, true) => a.cmp(b),
196 (true, false) => std::cmp::Ordering::Greater,
197 (false, true) => std::cmp::Ordering::Less,
198 (false, false) => a.cmp(b),
199 });
200
201 completions
202 }
203}
204
205fn loosen_value_parser(arg: clap::Arg) -> clap::Arg {
214 use clap::builder::TypedValueParser;
215
216 #[derive(Clone)]
217 struct PossibleValueString(Vec<clap::builder::PossibleValue>);
218
219 impl TypedValueParser for PossibleValueString {
220 type Value = <clap::builder::StringValueParser as TypedValueParser>::Value;
221
222 fn parse_ref(
223 &self,
224 cmd: &clap::Command,
225 arg: Option<&clap::Arg>,
226 value: &std::ffi::OsStr,
227 ) -> Result<Self::Value, clap::Error> {
228 clap::builder::StringValueParser::new().parse_ref(cmd, arg, value)
229 }
230
231 fn possible_values(
232 &self,
233 ) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
234 Some(Box::new(self.0.clone().into_iter()))
235 }
236 }
237
238 let possible_vals = arg.get_possible_values();
239 arg.value_parser(PossibleValueString(possible_vals))
240}
241
242fn command_visitor(
247 mut command: clap::Command,
248 mut on_arg: &mut dyn FnMut(clap::Arg) -> clap::Arg,
249 mut on_subcommand: &mut dyn FnMut(clap::Command) -> clap::Command,
250) -> clap::Command {
251 for subcommand in command.clone().get_subcommands() {
252 command = command
253 .mut_subcommand(subcommand.get_name(), &mut *on_subcommand)
254 .mut_subcommand(subcommand.get_name(), |command| {
255 command_visitor(command, &mut on_arg, &mut on_subcommand)
256 })
257 }
258
259 for arg in command.clone().get_arguments() {
260 command = command.mut_arg(arg.get_id(), &mut *on_arg);
261 }
262
263 command
264}
265
266#[derive(Debug)]
268pub struct RootCtx<'a> {
269 pub command: clap::Command,
271 pub matches: clap::ArgMatches,
273 pub to_complete: &'a str,
275 pub prev_arg: &'a str,
277}
278
279pub trait CustomCompleterFactory: Send + Sync {
285 type CustomCompleter: CustomCompleter + 'static;
287
288 fn build(&self, ctx: &RootCtx<'_>) -> impl std::future::Future<Output = Self::CustomCompleter>;
290}
291
292pub trait CustomCompleter: Send + Sync {
294 fn complete(
296 &self,
297 ctx: &RootCtx<'_>,
298 subcommand_path: &[&str],
299 arg_id: &str,
300 ) -> impl Send + std::future::Future<Output = Vec<String>>;
301}
302
303#[async_trait::async_trait]
304trait DynCustomCompleter: Send + Sync {
305 async fn complete(
306 &self,
307 ctx: &RootCtx<'_>,
308 subcommand_path: &[&str],
309 arg_id: &str,
310 ) -> Vec<String>;
311}
312
313#[async_trait::async_trait]
314impl<T: CustomCompleter> DynCustomCompleter for T {
315 async fn complete(
316 &self,
317 ctx: &RootCtx<'_>,
318 subcommand_path: &[&str],
319 arg_id: &str,
320 ) -> Vec<String> {
321 self.complete(ctx, subcommand_path, arg_id).await
322 }
323}
324
325impl CustomCompleterFactory for () {
326 type CustomCompleter = ();
327 async fn build(&self, _ctx: &RootCtx<'_>) -> Self::CustomCompleter {}
328}
329
330impl CustomCompleter for () {
331 async fn complete(
332 &self,
333 _ctx: &RootCtx<'_>,
334 _subcommand_path: &[&str],
335 _arg_id: &str,
336 ) -> Vec<String> {
337 Vec::new()
338 }
339}
340
341fn recurse_completions<'a>(
343 ctx: &'a RootCtx<'_>,
344 subcommand_path: Vec<&'a str>,
345 command: &'a clap::Command,
346 matches: &'a clap::ArgMatches,
347 custom_complete_fn: Box<dyn DynCustomCompleter>,
348) -> BoxFuture<'a, Vec<String>> {
349 async move {
350 let mut subcommand_path = subcommand_path;
351 subcommand_path.push(command.get_name());
352
353 let mut completions = Vec::new();
354
355 if let Some((name, matches)) = matches.subcommand() {
358 let subcommand = command
359 .get_subcommands()
360 .find(|s| s.get_name() == name)
361 .unwrap();
362 let mut new_completions = recurse_completions(
363 ctx,
364 subcommand_path,
365 subcommand,
366 matches,
367 custom_complete_fn,
368 )
369 .await;
370 new_completions.extend_from_slice(&completions);
371 return new_completions;
372 }
373
374 for arg in command.get_arguments() {
380 let long = arg.get_long().map(|x| format!("--{x}")).unwrap_or_default();
381 let short = arg.get_short().map(|x| format!("-{x}")).unwrap_or_default();
382
383 if ctx.prev_arg != long && ctx.prev_arg != short {
384 continue;
385 }
386
387 if !matches!(
388 arg.get_action(),
389 clap::ArgAction::Append | clap::ArgAction::Set
390 ) {
391 continue;
392 }
393
394 for val in arg.get_possible_values() {
398 completions.push(val.get_name().into())
399 }
400
401 completions.extend(
402 custom_complete_fn
403 .complete(ctx, &subcommand_path, arg.get_id().as_str())
404 .await,
405 );
406
407 return completions;
409 }
410
411 let mut is_completing_positional = false;
415 for positional in command.get_positionals() {
416 if matches
419 .try_contains_id(positional.get_id().as_str())
420 .unwrap_or(true)
421 {
422 let val = matches
425 .get_raw(positional.get_id().as_str())
426 .unwrap_or_default()
427 .next_back()
428 .unwrap_or_default()
429 .to_str()
430 .unwrap_or_default();
431
432 if ctx.to_complete.is_empty() || ctx.to_complete.starts_with('-') {
433 continue;
434 }
435
436 if !val.starts_with(ctx.to_complete) {
437 continue;
438 }
439 }
440
441 is_completing_positional = true;
442
443 let possible_vals = positional.get_possible_values();
444 if !possible_vals.is_empty() {
445 for val in possible_vals {
446 completions.push(val.get_name().into())
447 }
448 }
449
450 completions.extend(
451 custom_complete_fn
452 .complete(ctx, &subcommand_path, positional.get_id().as_str())
453 .await,
454 );
455
456 break;
458 }
459
460 for arg in command.get_arguments() {
462 if matches!(
463 matches.value_source(arg.get_id().as_str()),
464 Some(clap::parser::ValueSource::CommandLine)
465 ) {
466 if let Some(x) = matches.get_raw_occurrences(arg.get_id().as_str()) {
470 if x.flatten().count() != 0 {
471 continue;
472 }
473 }
474 }
475
476 if let Some(long) = arg.get_long() {
477 completions.push(format!("--{long}"))
478 }
479
480 if let Some(short) = arg.get_short() {
481 completions.push(format!("-{short}"))
482 }
483 }
484
485 if command.has_subcommands() && !is_completing_positional {
490 if !command.is_disable_help_subcommand_set() {
491 completions.push("help".into());
492 }
493
494 for subcommand in command.get_subcommands() {
495 if let Some((name, _)) = matches.subcommand() {
496 if !ctx.to_complete.is_empty() && name.starts_with(ctx.to_complete) {
497 completions.push(subcommand.get_name().into())
498 }
499 } else {
500 completions.push(subcommand.get_name().into())
501 }
502 }
503 }
504
505 completions
506 }
507 .boxed()
508}
509
510#[derive(Clone, clap::ValueEnum)]
512pub enum Shell {
513 Fish,
515 Powershell,
517 Zsh,
519}
520
521pub fn emit_completion_stub(
534 shell: Shell,
535 bin_name: &str,
536 completion_subcommand: &str,
537 buf: &mut dyn std::io::Write,
538) -> std::io::Result<()> {
539 let stub = match shell {
540 Shell::Fish => include_str!("./templates/complete.fish"),
541 Shell::Powershell => include_str!("./templates/complete.ps1"),
542 Shell::Zsh => include_str!("./templates/complete.zsh"),
543 };
544
545 let stub = stub
546 .replace("__COMMAND_NAME__", bin_name)
547 .replace("__COMMAND_NAME_NODASH__", &bin_name.replace('-', "_"))
548 .replace("__COMPLETION_SUBCOMMAND__", completion_subcommand);
549
550 buf.write_all(stub.as_bytes())
551}