terkelg / prompts

❯ Lightweight, beautiful and user-friendly interactive prompts
MIT License
8.86k stars 304 forks source link

autocomplete: no way for suggest callback to sort suggestions #285

Open elliot-nelson opened 3 years ago

elliot-nelson commented 3 years ago

Describe the bug

When you have an autocomplete element with many (thousands) of options, you may implement a custom suggest callback (for example, you might use a fuzzy search to assist user). This works, but it's not useable because the current logic defaults to using the "current index" without moving it if possible.


  async complete(cb) {
    const p = (this.completing = this.suggest(this.input, this.choices));
    const suggestions = await p;

    if (this.completing !== p) return;
    this.suggestions = suggestions
      .map((s, i, arr) => ({ title: getTitle(arr, i), value: getVal(arr, i), description: s.description }));
    this.completing = false;
    const l = Math.max(suggestions.length - 1, 0);
    this.moveSelect(Math.min(l, this.select));     // <------

    cb && cb();
  }

If you have overriden suggest() to return a list of possible suggestions, sorted with the best suggestion at the top, then what you really want after every keypress is for moveSelect to move to index 0. Not sure if it's possible to do this always, with a new boolean flag, or with some kind of additional override property.

(I'd be happy to work on a potential patch for this issue, but more interested in suggestions on how I could get the behavior I'd like even without changes to the library.)

elliot-nelson commented 3 years ago

I'll append one way I've discovered to do what I want, which is to abuse onRender (this is the easiest way to get access to the underlying prompt object).

This solution is working for me locally, although not exactly elegant:

let reset = false;

return await prompts({
    type: 'autocomplete',
    name: 'branchName',
    message: 'Select a branch in the origin repository:',
    choices: [ /* many branches */ ],
    suggest: (input, choices) => {
        if (!input) return choices;
        reset = true;
        return superFancyFuzzySearchAndSort(input, choices);
    },
    onRender: function () {
        if (reset) {
            this.moveSelect(0);
            reset = false;
        }
    }
});

The result is that every time superFancyFuzzySearchAndSort gets called, the cursor is reset to index 0 of the newly modified suggestion list, which is what I needed.

terkelg commented 3 years ago

Thank you for sharing your solution. I agree it's not elegant. It's worth rethinking how to do this for the next major version