From d54e568ff7c16b7dfd6340a10aa724750e194137 Mon Sep 17 00:00:00 2001 From: James Garbutt <43081j@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:27:57 +0000 Subject: [PATCH] feat(autocomplete): do not set default filter for options getters Basically: - If `options` is a function, do not set the default filter - If a filter is explicitly set, keep it - If `options` is not a function, set the default filter --- .changeset/silent-sides-call.md | 5 +++ packages/core/src/prompts/autocomplete.ts | 13 +++--- .../core/test/prompts/autocomplete.test.ts | 41 +++++++++++++++++++ 3 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 .changeset/silent-sides-call.md diff --git a/.changeset/silent-sides-call.md b/.changeset/silent-sides-call.md new file mode 100644 index 00000000..8e6e6b2e --- /dev/null +++ b/.changeset/silent-sides-call.md @@ -0,0 +1,5 @@ +--- +"@clack/core": patch +--- + +Only apply autocomplete default filter if it has been explicitly set or if options is not a getter. diff --git a/packages/core/src/prompts/autocomplete.ts b/packages/core/src/prompts/autocomplete.ts index 9b406c14..e023462b 100644 --- a/packages/core/src/prompts/autocomplete.ts +++ b/packages/core/src/prompts/autocomplete.ts @@ -71,7 +71,7 @@ export default class AutocompletePrompt extends Prompt< focusedValue: T['value'] | undefined; #cursor = 0; #lastUserInput = ''; - #filterFn: FilterFunction; + #filterFn: FilterFunction | undefined; #options: T[] | (() => T[]); #placeholder: string | undefined; @@ -106,7 +106,8 @@ export default class AutocompletePrompt extends Prompt< const options = this.options; this.filteredOptions = [...options]; this.multiple = opts.multiple === true; - this.#filterFn = opts.filter ?? defaultFilter; + this.#filterFn = + typeof opts.options === 'function' ? opts.filter : (opts.filter ?? defaultFilter); let initialValues: unknown[] | undefined; if (opts.initialValue && Array.isArray(opts.initialValue)) { if (this.multiple) { @@ -160,7 +161,9 @@ export default class AutocompletePrompt extends Prompt< const placeholderMatchesOption = placeholder !== undefined && placeholder !== '' && - options.some((opt) => !opt.disabled && this.#filterFn(placeholder, opt)); + options.some( + (opt) => !opt.disabled && (this.#filterFn ? this.#filterFn(placeholder, opt) : true) + ); if (key.name === 'tab' && isEmptyOrOnlyTab && placeholderMatchesOption) { if (this.userInput === '\t') { this._clearUserInput(); @@ -225,8 +228,8 @@ export default class AutocompletePrompt extends Prompt< const options = this.options; - if (value) { - this.filteredOptions = options.filter((opt) => this.#filterFn(value, opt)); + if (value && this.#filterFn) { + this.filteredOptions = options.filter((opt) => this.#filterFn?.(value, opt)); } else { this.filteredOptions = [...options]; } diff --git a/packages/core/test/prompts/autocomplete.test.ts b/packages/core/test/prompts/autocomplete.test.ts index fca95f30..f1d98e24 100644 --- a/packages/core/test/prompts/autocomplete.test.ts +++ b/packages/core/test/prompts/autocomplete.test.ts @@ -216,6 +216,47 @@ describe('AutocompletePrompt', () => { expect(result).to.equal('apple'); }); + test('options as function skips default filter', () => { + const dynamicOptions = [ + { value: 'apple', label: 'Apple' }, + { value: 'banana', label: 'Banana' }, + ]; + const instance = new AutocompletePrompt({ + input, + output, + render: () => 'foo', + options: () => dynamicOptions, + }); + + instance.prompt(); + + input.emit('keypress', 'z', { name: 'z' }); + + expect(instance.filteredOptions).toEqual(dynamicOptions); + }); + + test('options as function applies user-provided filter', () => { + const dynamicOptions = [ + { value: 'apple', label: 'Apple' }, + { value: 'banana', label: 'Banana' }, + { value: 'cherry', label: 'Cherry' }, + ]; + const instance = new AutocompletePrompt({ + input, + output, + render: () => 'foo', + options: () => dynamicOptions, + filter: (search, opt) => (opt.label ?? '').toLowerCase().endsWith(search.toLowerCase()), + }); + + instance.prompt(); + + input.emit('keypress', 'a', { name: 'a' }); + + // 'endsWith' matches Banana but not Apple or Cherry + expect(instance.filteredOptions).toEqual([{ value: 'banana', label: 'Banana' }]); + }); + test('Tab with non-matching placeholder does not fill input', async () => { const instance = new AutocompletePrompt({ input,