cmdtree/
completion.rs

1//! Completion of tree paths and action arguments.
2//!
3//! Completion is done functionally, see examples on github for how to implement.
4
5use super::*;
6#[cfg(feature = "runnable")]
7use colored::*;
8#[cfg(feature = "runnable")]
9pub use linefeed::{Completer, Completion, Interface, Prompter, ReadResult, Terminal};
10
11impl<'r, R> Commander<R> {
12    /// Run the `Commander` interactively, with a completer constructed on every loop.
13    /// Consumes the instance, and blocks the thread until the loop is exited.
14    ///
15    /// See examples for how to construct a completer.
16    #[cfg(feature = "runnable")]
17    pub fn run_with_completion<
18        C: 'static + Completer<linefeed::DefaultTerminal>,
19        F: Fn(&Self) -> C,
20    >(
21        mut self,
22        completer_fn: F,
23    ) {
24        let interface = Interface::new("commander").expect("failed to start interface");
25        let mut exit = false;
26
27        while !exit {
28            interface
29                .set_prompt(&format!("{}=> ", self.path().bright_cyan()))
30                .expect("failed to set prompt");
31
32            let completer = completer_fn(&self);
33            interface.set_completer(Arc::new(completer));
34
35            if let Ok(ReadResult::Input(s)) = interface.read_line() {
36                if let LineResult::Exit = self.parse_line(&s, true, &mut std::io::stdout()) {
37                    exit = true
38                }
39                interface.add_history_unique(s);
40            }
41        }
42    }
43}
44
45/// Match string and qualified name of action.
46#[derive(Debug, PartialEq)]
47pub struct ActionMatch {
48    /// The match str, space delimited from current path.
49    ///
50    /// > Notice the extra space at the end. This is intentional.
51    ///
52    /// eg `"a nested action "`.
53    pub info: CompletionInfo,
54    /// Qualified action name from root, as produced from [`structure`].
55    /// eg `a.nested..action`
56    ///
57    /// [`structure`]: Commander::structure
58    pub qualified_path: String,
59}
60
61/// Completion item.
62#[derive(Debug, PartialEq)]
63pub struct CompletionInfo {
64    /// The string to match. Similar to path but space delimited.
65    pub completestr: String,
66    /// Class or action.
67    pub itemtype: ItemType,
68    /// The help message.
69    pub help_msg: CmdStr,
70}
71
72/// Constructs a set of space delimited items that could be completed at the
73/// current path.
74///
75/// # Examples
76/// ```rust
77/// # use cmdtree::*;
78/// # use cmdtree::completion::create_tree_completion_items;
79///
80/// let mut cmder = Builder::default_config("eg")
81///     .begin_class("one", "") // a class
82///         .begin_class("two", "")
83///         .add_action("three", "", |_, _| ())
84///         .end_class()
85///     .end_class()
86///     .begin_class("hello", "").end_class()
87///     .into_commander().unwrap();
88///
89/// let v: Vec<_> = create_tree_completion_items(&cmder).into_iter().map(|x| x.completestr).collect();
90/// assert_eq!(v, vec!["hello", "one", "one two", "one two three"]
91///     .into_iter().map(|x| x.to_string()).collect::<Vec<_>>());
92///
93/// cmder.parse_line("one", true, &mut std::io::sink());
94///
95/// let v: Vec<_> = create_tree_completion_items(&cmder).into_iter().map(|x| x.completestr).collect();
96/// assert_eq!(v, vec!["two", "two three"]
97///     .into_iter().map(|x| x.to_string()).collect::<Vec<_>>());
98/// ```
99pub fn create_tree_completion_items<R>(cmdr: &Commander<R>) -> Vec<CompletionInfo> {
100    cmdr.structure(false)
101        .into_iter()
102        .filter_map(|info| {
103            let StructureInfo {
104                path,
105                itemtype,
106                help_msg,
107            } = info;
108
109            let completestr =
110                path.split('.')
111                    .filter(|x| !x.is_empty())
112                    .fold(String::new(), |mut s, x| {
113                        if !s.is_empty() {
114                            s.push(' ');
115                        }
116                        s.push_str(x);
117                        s
118                    });
119
120            if completestr.is_empty() {
121                None
122            } else {
123                Some(CompletionInfo {
124                    completestr,
125                    itemtype,
126                    help_msg,
127                })
128            }
129        })
130        .collect()
131}
132
133/// Constructs a set of space delimited actions that could be completed at the
134/// current path.
135pub fn create_action_completion_items<R>(cmdr: &Commander<R>) -> Vec<ActionMatch> {
136    let cpath = cmdr.path();
137    let rname = cmdr.root_name();
138
139    let starter = if cpath == rname {
140        "" // no starting prefix
141    } else {
142        &cpath[rname.len() + 1..] // remove the 'root_name.' portion
143    };
144
145    cmdr.structure(true)
146        .into_iter()
147        .filter(|x| x.path.contains("..") && x.path.starts_with(starter))
148        .filter_map(|x| {
149            let StructureInfo {
150                path,
151                itemtype,
152                help_msg,
153            } = x;
154
155            let qualified_path = path.clone();
156
157            let completestr = path[starter.len()..]
158                .split('.')
159                .filter(|x| !x.is_empty())
160                .fold(String::new(), |mut s, x| {
161                    s.push_str(x);
162                    s.push(' ');
163                    s
164                });
165
166            if completestr.is_empty() {
167                None
168            } else {
169                let info = CompletionInfo {
170                    completestr,
171                    itemtype,
172                    help_msg,
173                };
174
175                Some(ActionMatch {
176                    info,
177                    qualified_path,
178                })
179            }
180        })
181        .collect()
182}
183
184/// Determines from a set of items the ones that could be
185/// completed from the given line.
186///
187/// Effectively loops through each item and checks if it
188/// starts with `line`.
189/// `items` should be constructed by [`create_tree_completion_items`].
190///
191/// The returned items are only the slice from the final _word_ in `line`,
192/// such that `hello wo` would return `world`, and `he` would return `hello world`.
193///
194/// [`create_tree_completion_items`]: completion::create_tree_completion_items
195pub fn tree_completions<'l: 'i, 'i, 'a: 'i, I>(
196    line: &'l str,
197    items: I,
198) -> impl Iterator<Item = (&'i str, &'i CompletionInfo)>
199where
200    I: Iterator<Item = &'i CompletionInfo>,
201{
202    items
203        .filter(move |x| x.completestr.starts_with(line))
204        .map(move |x| {
205            // src code makes word_idx = line.len(), then counts backwards.
206            // will not panic on out of bounds.
207            let word_idx = word_break_start(line, &[' ']);
208            let word = &x.completestr[word_idx..];
209            (word, x)
210        })
211}
212
213/// Returns the start position of the _last_ word, delimited by any character.
214pub fn word_break_start(s: &str, word_break_ch: &[char]) -> usize {
215    let mut start = s.len();
216
217    for (idx, ch) in s.char_indices().rev() {
218        if word_break_ch.contains(&ch) {
219            break;
220        }
221        start = idx;
222    }
223
224    start
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn create_tree_completion_items_test() {
233        let mut cmder = Builder::default_config("cmdtree-example")
234            .begin_class("class1", "") // a class
235            .begin_class("inner-class1", "") // can nest a class
236            .add_action("name", "print class name", |_, _| ())
237            .end_class()
238            .end_class()
239            .begin_class("print", "")
240            .add_action("echo", "", |_, _| ())
241            .add_action("countdown", "", |_, _| ())
242            .into_commander()
243            .unwrap();
244
245        let v: Vec<_> = {
246            let v: Vec<_> = create_tree_completion_items(&cmder)
247                .into_iter()
248                .map(|x| x.completestr)
249                .collect();
250            v
251        };
252        assert_eq!(
253            v,
254            vec_str(vec![
255                "class1",
256                "class1 inner-class1",
257                "class1 inner-class1 name",
258                "print",
259                "print countdown",
260                "print echo"
261            ])
262        );
263
264        cmder.parse_line("class1", true, &mut std::io::sink());
265
266        let v: Vec<_> = create_tree_completion_items(&cmder)
267            .into_iter()
268            .map(|x| x.completestr)
269            .collect();
270        assert_eq!(v, vec_str(vec!["inner-class1", "inner-class1 name",]));
271    }
272
273    #[test]
274    fn create_action_completion_items_test() {
275        let mut cmder = Builder::default_config("eg")
276            .begin_class("one", "") // a class
277            .begin_class("two", "")
278            .add_action("three", "", |_, _| ())
279            .end_class()
280            .end_class()
281            .begin_class("hello", "")
282            .end_class()
283            .into_commander()
284            .unwrap();
285
286        let v: Vec<_> = create_action_completion_items(&cmder)
287            .into_iter()
288            .map(|x| (x.info.completestr, x.qualified_path))
289            .collect();
290        assert_eq!(
291            v,
292            vec![("one two three ".to_string(), "one.two..three".to_string(),)]
293        );
294
295        cmder.parse_line("one", true, &mut std::io::sink());
296
297        let v: Vec<_> = create_action_completion_items(&cmder)
298            .into_iter()
299            .map(|x| (x.info.completestr, x.qualified_path))
300            .collect();
301        assert_eq!(
302            v,
303            vec![("two three ".to_string(), "one.two..three".to_string(),)]
304        );
305    }
306
307    #[test]
308    fn tree_completions_test() {
309        let mut cmder = Builder::default_config("cmdtree-example")
310            .begin_class("class1", "")
311            .begin_class("inner-class1", "")
312            .add_action("name", "", |_, _| ())
313            .end_class()
314            .end_class()
315            .begin_class("print", "")
316            .add_action("echo", "", |_, _| ())
317            .add_action("countdown", "", |_, _| ())
318            .end_class()
319            .add_action("clone", "", |_, _| ())
320            .into_commander()
321            .unwrap();
322
323        let v = create_tree_completion_items(&cmder);
324        let completions = tree_completions("", v.iter())
325            .map(|x| x.0)
326            .collect::<Vec<_>>();
327        assert_eq!(
328            completions,
329            vec![
330                "clone",
331                "class1",
332                "class1 inner-class1",
333                "class1 inner-class1 name",
334                "print",
335                "print countdown",
336                "print echo",
337            ]
338        );
339
340        let completions = tree_completions("cl", v.iter())
341            .map(|x| x.0)
342            .collect::<Vec<_>>();
343        assert_eq!(
344            completions,
345            vec![
346                "clone",
347                "class1",
348                "class1 inner-class1",
349                "class1 inner-class1 name",
350            ]
351        );
352
353        let completions = tree_completions("class1 ", v.iter())
354            .map(|x| x.0)
355            .collect::<Vec<_>>();
356        assert_eq!(completions, vec!["inner-class1", "inner-class1 name",]);
357
358        cmder.parse_line("class1", true, &mut std::io::sink());
359
360        let v = create_tree_completion_items(&cmder);
361        let completions = tree_completions("inn", v.iter())
362            .map(|x| x.0)
363            .collect::<Vec<_>>();
364        assert_eq!(completions, vec!["inner-class1", "inner-class1 name",]);
365    }
366
367    fn vec_str(v: Vec<&str>) -> Vec<String> {
368        v.into_iter().map(|x| x.to_string()).collect()
369    }
370}