Skip to main content

bpaf/
complete_gen.rs

1// completion:
2// static: flag names, command names
3// dynamic: argument values, positional item values
4//
5// for static when running collect any parser that fails
6//
7// OR: combine completions
8// AND: usual logic without shortcircuits
9//
10// for static completion it's enough to collect items
11// for argument completion - only one argument(Comp::Meta) should be active at once
12//
13// for rendering prefer longer version of names
14//
15// complete short names to long names if possible
16
17use crate::{
18    args::{Arg, State},
19    complete_shell::{render_bash, render_fish, render_simple, render_test, render_zsh},
20    item::ShortLong,
21    parsers::NamedArg,
22    Doc, ShellComp,
23};
24use std::ffi::OsStr;
25
26#[derive(Clone, Debug)]
27pub(crate) struct Complete {
28    /// completions accumulated so far
29    comps: Vec<Comp>,
30    pub(crate) output_rev: usize,
31
32    /// don't try to suggest any more positional items after there's a positional item failure
33    /// or parsing in progress
34    pub(crate) no_pos_ahead: bool,
35}
36
37impl Complete {
38    pub(crate) fn new(output_rev: usize) -> Self {
39        Self {
40            comps: Vec::new(),
41            output_rev,
42            no_pos_ahead: false,
43        }
44    }
45}
46
47impl State {
48    /// Add a new completion hint for flag, if needed
49    pub(crate) fn push_flag(&mut self, named: &NamedArg) {
50        let depth = self.depth();
51        if let Some(comp) = self.comp_mut() {
52            if let Ok(name) = ShortLong::try_from(named) {
53                comp.comps.push(Comp::Flag {
54                    extra: CompExtra {
55                        depth,
56                        group: None,
57                        help: named.help.as_ref().and_then(Doc::to_completion),
58                    },
59                    name,
60                });
61            }
62        }
63    }
64
65    /// Add a new completion hint for an argument, if needed
66    pub(crate) fn push_argument(&mut self, named: &NamedArg, metavar: &'static str) {
67        let depth = self.depth();
68        if let Some(comp) = self.comp_mut() {
69            if let Ok(name) = ShortLong::try_from(named) {
70                comp.comps.push(Comp::Argument {
71                    extra: CompExtra {
72                        depth,
73                        group: None,
74                        help: named.help.as_ref().and_then(Doc::to_completion),
75                    },
76                    metavar,
77                    name,
78                });
79            }
80        }
81    }
82
83    /// Add a new completion hint for metadata, if needed
84    ///
85    /// `is_argument` is set to true when we are trying to parse the value and false if
86    /// when meta
87    pub(crate) fn push_metavar(
88        &mut self,
89        meta: &'static str,
90        help: &Option<Doc>,
91        is_argument: bool,
92    ) {
93        let depth = self.depth();
94        if let Some(comp) = self.comp_mut() {
95            let extra = CompExtra {
96                depth,
97                group: None,
98                help: help.as_ref().and_then(Doc::to_completion),
99            };
100
101            comp.comps.push(Comp::Metavariable {
102                extra,
103                meta,
104                is_argument,
105            });
106        }
107    }
108
109    /// Add a new completion hint for command, if needed
110    pub(crate) fn push_command(
111        &mut self,
112        name: &'static str,
113        short: Option<char>,
114        help: &Option<Doc>,
115    ) {
116        let depth = self.depth();
117        if let Some(comp) = self.comp_mut() {
118            comp.comps.push(Comp::Command {
119                extra: CompExtra {
120                    depth,
121                    group: None,
122                    help: help.as_ref().and_then(Doc::to_completion),
123                },
124                name,
125                short,
126            });
127        }
128    }
129
130    /// Clear collected completions if enabled
131    pub(crate) fn clear_comps(&mut self) {
132        if let Some(comp) = self.comp_mut() {
133            comp.comps.clear();
134        }
135    }
136
137    /// Insert a literal value with some description for completion
138    ///
139    /// In practice it's "--"
140    pub(crate) fn push_pos_sep(&mut self) {
141        let depth = self.depth();
142        if let Some(comp) = self.comp_mut() {
143            comp.comps.push(Comp::Value {
144                extra: CompExtra {
145                    depth,
146                    group: None,
147                    help: Some("Positional only items after this token".to_owned()),
148                },
149                body: "--".to_owned(),
150                is_argument: false,
151            });
152        }
153    }
154
155    /// Insert a bunch of items
156    pub(crate) fn push_with_group(&mut self, group: &Option<String>, comps: &mut Vec<Comp>) {
157        if let Some(comp) = self.comp_mut() {
158            for mut item in comps.drain(..) {
159                if let Some(group) = group.as_ref() {
160                    item.set_group(group.clone());
161                }
162                comp.comps.push(item);
163            }
164        }
165    }
166}
167
168impl Complete {
169    pub(crate) fn push_shell(&mut self, op: ShellComp, is_argument: bool, depth: usize) {
170        self.comps.push(Comp::Shell {
171            extra: CompExtra {
172                depth,
173                group: None,
174                help: None,
175            },
176            script: op,
177            is_argument,
178        });
179    }
180
181    pub(crate) fn push_value(
182        &mut self,
183        body: String,
184        help: Option<String>,
185        group: Option<String>,
186        depth: usize,
187        is_argument: bool,
188    ) {
189        self.comps.push(Comp::Value {
190            body,
191            is_argument,
192            extra: CompExtra { depth, group, help },
193        });
194    }
195
196    pub(crate) fn push_comp(&mut self, comp: Comp) {
197        self.comps.push(comp);
198    }
199
200    pub(crate) fn extend_comps(&mut self, comps: Vec<Comp>) {
201        self.comps.extend(comps);
202    }
203
204    pub(crate) fn drain_comps(&mut self) -> std::vec::Drain<'_, Comp> {
205        self.comps.drain(0..)
206    }
207
208    pub(crate) fn swap_comps(&mut self, other: &mut Vec<Comp>) {
209        std::mem::swap(other, &mut self.comps);
210    }
211}
212
213#[derive(Clone, Debug)]
214pub(crate) struct CompExtra {
215    /// Used by complete_gen to separate commands from each other
216    pub(crate) depth: usize,
217
218    /// Render this option in a group along with all other items with the same name
219    pub(crate) group: Option<String>,
220
221    /// help message attached to a completion item
222    pub(crate) help: Option<String>,
223}
224
225#[derive(Clone, Debug)]
226pub(crate) enum Comp {
227    /// short or long flag
228    Flag { extra: CompExtra, name: ShortLong },
229
230    /// argument + metadata
231    Argument {
232        extra: CompExtra,
233        name: ShortLong,
234        metavar: &'static str,
235    },
236
237    ///
238    Command {
239        extra: CompExtra,
240        name: &'static str,
241        short: Option<char>,
242    },
243
244    /// comes from completed values, part of "dynamic" completion
245    Value {
246        extra: CompExtra,
247        body: String,
248        /// values from arguments (say -p=SPEC and user already typed "-p b"
249        /// should suppress all other options except for metavaraiables?
250        ///
251        is_argument: bool,
252    },
253
254    Metavariable {
255        extra: CompExtra,
256        meta: &'static str,
257        /// AKA not positional
258        is_argument: bool,
259    },
260
261    Shell {
262        extra: CompExtra,
263        script: ShellComp,
264        /// AKA not positional
265        is_argument: bool,
266    },
267}
268
269impl Comp {
270    /// to avoid leaking items with higher depth into items with lower depth
271    fn depth(&self) -> usize {
272        match self {
273            Comp::Command { extra, .. }
274            | Comp::Value { extra, .. }
275            | Comp::Flag { extra, .. }
276            | Comp::Shell { extra, .. }
277            | Comp::Metavariable { extra, .. }
278            | Comp::Argument { extra, .. } => extra.depth,
279        }
280    }
281
282    /// completer needs to replace meta placeholder with actual values - uses this
283    ///
284    /// value indicates if it's an argument or a positional meta
285    pub(crate) fn is_metavar(&self) -> Option<bool> {
286        if let Comp::Metavariable { is_argument, .. } = self {
287            Some(*is_argument)
288        } else {
289            None
290        }
291    }
292
293    pub(crate) fn set_group(&mut self, group: String) {
294        let extra = match self {
295            Comp::Flag { extra, .. }
296            | Comp::Argument { extra, .. }
297            | Comp::Command { extra, .. }
298            | Comp::Value { extra, .. }
299            | Comp::Shell { extra, .. }
300            | Comp::Metavariable { extra, .. } => extra,
301        };
302        if extra.group.is_none() {
303            extra.group = Some(group);
304        }
305    }
306}
307
308#[derive(Debug)]
309pub(crate) struct ShowComp<'a> {
310    /// value to be actually inserted by the autocomplete system
311    pub(crate) subst: String,
312
313    /// pretty rendering which might include metavars, etc
314    pub(crate) pretty: String,
315
316    pub(crate) extra: &'a CompExtra,
317}
318
319impl std::fmt::Display for ShowComp<'_> {
320    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321        if let (Some(help), true) = (&self.extra.help, self.subst.is_empty()) {
322            write!(f, "{}: {}", self.pretty, help)
323        } else if let Some(help) = &self.extra.help {
324            write!(f, "{:24} -- {}", self.pretty, help)
325        } else {
326            write!(f, "{}", self.pretty)
327        }
328    }
329}
330
331impl Arg {
332    fn and_os_string(&self) -> Option<(&Self, &OsStr)> {
333        match self {
334            Arg::Short(_, _, s) => {
335                if s.is_empty() {
336                    None
337                } else {
338                    Some((self, s))
339                }
340            }
341            Arg::Long(_, _, s) | Arg::ArgWord(s) | Arg::Word(s) | Arg::PosWord(s) => {
342                Some((self, s))
343            }
344        }
345    }
346}
347
348fn pair_to_os_string<'a>(pair: (&'a Arg, &'a OsStr)) -> Option<(&'a Arg, &'a str)> {
349    Some((pair.0, pair.1.to_str()?))
350}
351
352/// What is the preceeding item, if any
353///
354/// Mostly is there to tell if we are trying to complete and argument or not...
355#[derive(Debug, Copy, Clone)]
356enum Prefix<'a> {
357    NA,
358    Short(char),
359    Long(&'a str),
360}
361
362impl State {
363    /// Generate completion from collected heads
364    ///
365    /// before calling this method we run parser in "complete" mode and collect live heads inside
366    /// `self.comp`, this part goes over collected heads and generates possible completions from
367    /// that
368    pub(crate) fn check_complete(&self) -> Option<String> {
369        let comp = self.comp_ref()?;
370
371        let mut items = self
372            .items
373            .iter()
374            .rev()
375            .filter_map(Arg::and_os_string)
376            .filter_map(pair_to_os_string);
377
378        // try get a current item to complete - must be non-virtual right most one
379        // value must be present here, and can fail only for non-utf8 values
380        // can't do much completing with non-utf8 values since bpaf needs to print them to stdout
381        let (cur, lit) = items.next()?;
382
383        // For cases like "-k=val", "-kval", "--key=val", "--key val"
384        // last value is going  to be either Arg::Word or Arg::ArgWord
385        // so to perform full completion we look at the preceeding item
386        // and use it's value if it was a composite short/long argument
387        let preceeding = items.next();
388        let (pos_only, full_lit) = match preceeding {
389            Some((Arg::Short(_, true, _os) | Arg::Long(_, true, _os), full_lit)) => {
390                (false, full_lit)
391            }
392            Some((Arg::PosWord(_), _)) => (true, lit),
393            _ => (false, lit),
394        };
395
396        let is_named = match cur {
397            Arg::Short(_, _, _) | Arg::Long(_, _, _) => true,
398            Arg::ArgWord(_) | Arg::Word(_) | Arg::PosWord(_) => false,
399        };
400
401        let prefix = match preceeding {
402            Some((Arg::Short(s, true, _os), _lit)) => Prefix::Short(*s),
403            Some((Arg::Long(l, true, _os), _lit)) => Prefix::Long(l.as_str()),
404            _ => Prefix::NA,
405        };
406
407        let (items, shell) = comp.complete(lit, pos_only, is_named, prefix);
408
409        Some(match comp.output_rev {
410            0 => render_test(&items, &shell, full_lit),
411            1 => render_simple(&items), // <- AKA elvish
412            7 => render_zsh(&items, &shell, full_lit),
413            8 => render_bash(&items, &shell, full_lit),
414            9 => render_fish(&items, &shell, full_lit, self.path[0].as_str()),
415            unk => {
416                #[cfg(debug_assertions)]
417                {
418                    eprintln!("Unsupported output revision {}, you need to genenerate your shell completion files for the app", unk);
419                    std::process::exit(1);
420                }
421                #[cfg(not(debug_assertions))]
422                {
423                    std::process::exit(0);
424                }
425            }
426        }.unwrap())
427    }
428}
429
430/// Try to expand short string names into long names if possible
431fn preferred_name(name: ShortLong) -> String {
432    match name {
433        ShortLong::Short(s) => format!("-{}", s),
434        ShortLong::Long(l) | ShortLong::Both(_, l) => format!("--{}", l),
435    }
436}
437
438// check if argument can possibly match the argument passed in and returns a preferrable replacement
439fn arg_matches(arg: &str, name: ShortLong) -> Option<String> {
440    // "" and "-" match any flag
441    if arg.is_empty() || arg == "-" {
442        return Some(preferred_name(name));
443    }
444
445    let mut can_match = false;
446
447    // separately check for short and long names, fancy strip prefix things is here to avoid
448    // allocations and cloning
449    match name {
450        ShortLong::Long(_) => {}
451        ShortLong::Short(s) | ShortLong::Both(s, _) => {
452            can_match |= arg
453                .strip_prefix('-')
454                .and_then(|a| a.strip_prefix(s))
455                .map_or(false, str::is_empty);
456        }
457    }
458
459    // and long string too
460    match name {
461        ShortLong::Short(_) => {}
462        ShortLong::Long(l) | ShortLong::Both(_, l) => {
463            can_match |= arg.strip_prefix("--").map_or(false, |s| l.starts_with(s));
464        }
465    }
466
467    if can_match {
468        Some(preferred_name(name))
469    } else {
470        None
471    }
472}
473fn cmd_matches(arg: &str, name: &'static str, short: Option<char>) -> Option<&'static str> {
474    // partial long name and exact short name match anything
475    if name.starts_with(arg)
476        || short.map_or(false, |s| {
477            // avoid allocations
478            arg.strip_prefix(s).map_or(false, str::is_empty)
479        })
480    {
481        Some(name)
482    } else {
483        None
484    }
485}
486
487impl Comp {
488    /// this completion should suppress anything else that is not a value
489    fn only_value(&self) -> bool {
490        match self {
491            Comp::Flag { .. } | Comp::Argument { .. } | Comp::Command { .. } => false,
492            Comp::Metavariable { is_argument, .. }
493            | Comp::Value { is_argument, .. }
494            | Comp::Shell { is_argument, .. } => *is_argument,
495        }
496    }
497    fn is_pos(&self) -> bool {
498        match self {
499            Comp::Flag { .. } | Comp::Argument { .. } | Comp::Command { .. } => false,
500            Comp::Value { is_argument, .. } => !is_argument,
501            Comp::Metavariable { .. } | Comp::Shell { .. } => true,
502        }
503    }
504}
505
506impl Complete {
507    fn complete(
508        &self,
509        arg: &str,
510        pos_only: bool,
511        is_named: bool,
512        prefix: Prefix,
513    ) -> (Vec<ShowComp<'_>>, Vec<ShellComp>) {
514        let mut items: Vec<ShowComp> = Vec::new();
515        let mut shell = Vec::new();
516        let max_depth = self.comps.iter().map(Comp::depth).max().unwrap_or(0);
517        let mut only_values = false;
518
519        for item in self
520            .comps
521            .iter()
522            .filter(|c| c.depth() == max_depth && (!pos_only || c.is_pos()))
523        {
524            match (only_values, item.only_value()) {
525                (true, true) | (false, false) => {}
526                (true, false) => continue,
527                (false, true) => {
528                    only_values = true;
529                    items.clear();
530                }
531            }
532
533            match item {
534                Comp::Command { name, short, extra } => {
535                    if let Some(long) = cmd_matches(arg, name, *short) {
536                        items.push(ShowComp {
537                            subst: long.to_string(),
538                            pretty: long.to_string(),
539                            extra,
540                        });
541                    }
542                }
543
544                Comp::Flag { name, extra } => {
545                    if let Some(long) = arg_matches(arg, *name) {
546                        items.push(ShowComp {
547                            pretty: long.clone(),
548                            subst: long,
549                            extra,
550                        });
551                    }
552                }
553
554                Comp::Argument {
555                    name,
556                    metavar,
557                    extra,
558                } => {
559                    if let Some(long) = arg_matches(arg, *name) {
560                        items.push(ShowComp {
561                            pretty: format!("{}={}", long, metavar),
562                            subst: long,
563                            extra,
564                        });
565                    }
566                }
567
568                Comp::Value {
569                    body,
570                    extra,
571                    is_argument: _,
572                } => {
573                    items.push(ShowComp {
574                        pretty: body.clone(),
575                        extra,
576                        subst: match prefix {
577                            Prefix::NA => body.clone(),
578                            Prefix::Short(s) => format!("-{}={}", s, body),
579                            Prefix::Long(l) => format!("--{}={}", l, body),
580                        },
581                    });
582                }
583
584                Comp::Metavariable {
585                    extra,
586                    meta,
587                    is_argument,
588                } => {
589                    if !is_argument && !pos_only && arg.starts_with('-') {
590                        continue;
591                    }
592                    items.push(ShowComp {
593                        subst: String::new(),
594                        pretty: (*meta).to_string(),
595                        extra,
596                    });
597                }
598
599                Comp::Shell { script, .. } => {
600                    if !is_named {
601                        shell.push(*script);
602                    }
603                }
604            }
605        }
606
607        (items, shell)
608    }
609}