Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fix-autocomplete-double-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clack/core': minor
---

feat(core): skip default filter when options is a function

When `options` is provided as a function, the default filter is no longer applied on top of the function's results. If both `options` (function) and a custom `filter` are provided, the filter is still applied.
11 changes: 10 additions & 1 deletion packages/core/src/prompts/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
#cursor = 0;
#lastUserInput = '';
#filterFn: FilterFunction<T>;
#hasCustomFilter: boolean;
#options: T[] | (() => T[]);

get cursor(): number {
Expand Down Expand Up @@ -97,6 +98,7 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
const options = this.options;
this.filteredOptions = [...options];
this.multiple = opts.multiple === true;
this.#hasCustomFilter = opts.filter !== undefined;
this.#filterFn = opts.filter ?? defaultFilter;
let initialValues: unknown[] | undefined;
if (opts.initialValue && Array.isArray(opts.initialValue)) {
Expand Down Expand Up @@ -197,9 +199,16 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
this.#lastUserInput = value;

const options = this.options;
const isOptionsFunction = typeof this.#options === 'function';

if (value) {
this.filteredOptions = options.filter((opt) => this.#filterFn(value, opt));
// When options is a function and no custom filter was provided,
// skip filtering since the function already returns filtered results.
if (isOptionsFunction && !this.#hasCustomFilter) {
this.filteredOptions = [...options];
} else {
this.filteredOptions = options.filter((opt) => this.#filterFn(value, opt));
}
} else {
this.filteredOptions = [...options];
}
Expand Down
55 changes: 55 additions & 0 deletions packages/core/test/prompts/autocomplete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,59 @@ describe('AutocompletePrompt', () => {
expect(instance.selectedValues).to.deep.equal([]);
expect(result).to.deep.equal([]);
});

test('options function skips default filter when no custom filter provided', () => {
const optionsFn = function (this: any) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you don't need any in this repo as we always have a type. this one is an AutocompletePrompt

const input = this.userInput || '';
if (!input) return testOptions;
return testOptions.filter((opt) => opt.value.startsWith(input));
};

const instance = new AutocompletePrompt({
input,
output,
render: () => 'foo',
options: optionsFn,
});

instance.prompt();

// Type 'b' - options function returns only 'banana'
input.emit('keypress', 'b', { name: 'b' });

// Should use the options function result as-is, no additional filtering
expect(instance.filteredOptions).toEqual([
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't this pass if the default filter was being applied too?

you should probably make your options function do some unusual filtering. e.g. filter to a constant result

{ value: 'banana', label: 'Banana' },
]);
});

test('options function applies custom filter when both are provided', () => {
const optionsFn = function (this: any) {
return testOptions;
};

const customFilter = (search: string, opt: { value: string; label?: string }) => {
return opt.value.includes(search);
};

const instance = new AutocompletePrompt({
input,
output,
render: () => 'foo',
options: optionsFn,
filter: customFilter,
});

instance.prompt();

// Type 'an' - custom filter should run on top of options function result
input.emit('keypress', 'a', { name: 'a' });
input.emit('keypress', 'n', { name: 'n' });

// Custom filter: 'an' matches banana and orange
expect(instance.filteredOptions).toEqual([
{ value: 'banana', label: 'Banana' },
{ value: 'orange', label: 'Orange' },
]);
});
});
Loading