├── .gitignore
├── example.gif
├── Cargo.toml
├── README.md
├── src
└── main.rs
└── Cargo.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 |
--------------------------------------------------------------------------------
/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexwlchan/inquire_tag_autocomplete/main/example.gif
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "inquire_tag_autocomplete"
3 | version = "1.0.0"
4 | edition = "2021"
5 |
6 | [dependencies]
7 | inquire = "0.3.0-alpha.2"
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # inquire_tag_autocomplete
2 |
3 | This is an experiment with the autocomplete functionality in the Rust crate [inquire](https://crates.io/crates/inquire).
4 |
5 | In particular, suppose I want the user to pick some tags.
6 | I already have some tags they might want to use, or they can add new ones.
7 |
8 | I want a `Text` input where I can:
9 |
10 | * enter multiple tags, separated by spaces
11 | * get suggestions and autocomplete for already-known tags
12 | * create brand new tags
13 |
14 | This is what it looks like:
15 |
16 |
17 |
18 | Here's what I'm doing:
19 |
20 | 1. Initially I'm shown all five known tags (`adventure`, `action`, `mystery`, `romance` and `scifi`).
21 |
22 | 2. I type `adv` and then press `tab`.
23 | This autocompletes to the known tag `adventure`.
24 |
25 | 3. I type `s`, which shows me the two tags that contain `s` (`mystery` and `scifi`).
26 | I use the arrow keys to select `scifi`.
27 |
28 | 4. I type `a` and then press `tab`.
29 | This autocompletes to the known tag `action`.
30 |
31 | Notice that `adventure` isn't offered as a suggestion: it knows I already have this tag.
32 |
33 | 5. I start typing `ro`.
34 | It suggests the known tag `romance`, but I keep typing the new tag `robots`.
35 |
36 | 6. I press `enter` to complete my typing, and I see the list of tags I've given (`adventure`, `scifi`, `action` and `robots`).
37 |
38 | I already know one project where I want to use it, but I can imagine this is the sort of component I might want to use in a bunch of places.
39 | I've pulled it into a standalone repo so it's easier to find and maintain as a reusable snippet.
40 |
41 | ## Usage
42 |
43 | Copy the code from `main.rs` into your project.
44 | Play around with it.
45 | Tweak it.
46 | Fiddle with the details.
47 |
48 | The behaviour probably isn't quite right for what you need, but hopefully it's a starting point.
49 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashSet;
2 |
3 | use inquire::{error::CustomUserError, Text};
4 |
5 | // The list of tags is fetched as a function so it can be created
6 | // at runtime, e.g. by querying a database -- it doesn't have to be
7 | // compiled into the binary.
8 | fn get_tags<'a>() -> Vec<&'a str> {
9 | vec!["adventure", "action", "mystery", "romance", "scifi"]
10 | }
11 |
12 | fn suggester(val: &str) -> Result, CustomUserError> {
13 | let tags = HashSet::from_iter(get_tags());
14 |
15 | // What tags have already been used? Tags can only be selected
16 | // once, so we don't want to suggest a tag already in the input.
17 | let used_tags: HashSet<&str> = HashSet::from_iter(val.split_whitespace());
18 | let mut available_tags = tags.difference(&used_tags).cloned().collect::>();
19 | available_tags.sort();
20 |
21 | // What's the latest tag the user is typing? i.e. what are we trying
22 | // to autocomplete on this tag.
23 | let this_tag = val.split_whitespace().last();
24 |
25 | let prefix = match this_tag {
26 | None => val,
27 | Some(t) => &val[..(val.len() - t.len())],
28 | };
29 |
30 | Ok(available_tags
31 | .iter()
32 | // Note: this will filter to all the matching tags if the user
33 | // is midway through matching a tag (e.g. "adventure ac" -> "action"),
34 | // but will also display *all* the tags on the initial prompt.
35 | //
36 | // If there are lots of tags, that might be unwieldy.
37 | .filter(|s| match this_tag {
38 | None => true,
39 | Some(t) => s.contains(&t),
40 | })
41 | // Note: the prefix may be empty if the user hasn't typed
42 | // anything yet.
43 | .map(|s| {
44 | if prefix.is_empty() {
45 | format!("{} ", s)
46 | } else {
47 | format!("{} {} ", prefix.trim_end(), s)
48 | }
49 | })
50 | .collect())
51 | }
52 |
53 | fn completer(val: &str) -> Result