Skip to content

feat: rework path, remove suggestion prompt #335

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9c4aa08
wip: use autocomplete for file selection
43081j May 16, 2025
b385a5e
wip: values and not values
43081j May 25, 2025
3b5044b
Merge branch 'main' into maybe-autocomplete-path
43081j May 26, 2025
5fbb422
Merge branch 'main' into values-up-on-bricks
43081j May 26, 2025
2e4e3a2
wip: make text prompt work
43081j May 26, 2025
b0e4d5f
fix(text): use value on submit render
43081j May 27, 2025
1b0e9a9
fix(password): shadow input to value
43081j May 27, 2025
22ded8b
fix(suggestion): use new userInput logic
43081j May 27, 2025
df35f26
fix: only accept strings in selectKey
43081j May 27, 2025
9e29f3e
fix(autocomplete): move to userInput logic
43081j May 27, 2025
7414136
fix(prompt): rework validation tests
43081j May 27, 2025
f81798c
chore: update path snapshots
43081j May 28, 2025
d13a439
chore: change test name
43081j May 28, 2025
892fc96
Merge branch 'values-up-on-bricks' into maybe-autocomplete-path
43081j May 28, 2025
33218e4
fix: make path work!
43081j May 28, 2025
d903423
chore: changeset
43081j May 28, 2025
231800e
fix: require a value
43081j May 28, 2025
1784b78
chore: add changeset
43081j May 28, 2025
ae329d7
fix: use node dirname
43081j Jun 3, 2025
87136d2
fix: use required default
43081j Jun 3, 2025
831029d
Merge branch 'main' into values-up-on-bricks
43081j Jun 3, 2025
c0448ef
Merge branch 'values-up-on-bricks' into maybe-autocomplete-path
43081j Jun 3, 2025
e89cd5b
Merge branch 'main' into maybe-autocomplete-path
43081j Jun 3, 2025
da7311c
chore: drop unused interface
43081j Jun 3, 2025
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
6 changes: 6 additions & 0 deletions .changeset/short-taxis-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@clack/prompts": minor
"@clack/core": minor
---

Remove `suggestion` prompt and change `path` prompt to be an autocomplete prompt.
3 changes: 1 addition & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type { ClackState as State, ValueWithCursorPart } from './types.js';
export type { ClackState as State } from './types.js';
export type { ClackSettings } from './utils/settings.js';

export { default as ConfirmPrompt } from './prompts/confirm.js';
Expand All @@ -10,6 +10,5 @@ export { default as SelectPrompt } from './prompts/select.js';
export { default as SelectKeyPrompt } from './prompts/select-key.js';
export { default as TextPrompt } from './prompts/text.js';
export { default as AutocompletePrompt } from './prompts/autocomplete.js';
export { default as SuggestionPrompt } from './prompts/suggestion.js';
export { block, isCancel, getColumns } from './utils/index.js';
export { updateSettings, settings } from './utils/settings.js';
48 changes: 31 additions & 17 deletions packages/core/src/prompts/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,14 @@ function normalisedValue<T>(multiple: boolean, values: T[] | undefined): T | T[]

interface AutocompleteOptions<T extends OptionLike>
extends PromptOptions<T['value'] | T['value'][], AutocompletePrompt<T>> {
options: T[];
options: T[] | ((this: AutocompletePrompt<T>) => T[]);
filter?: FilterFunction<T>;
multiple?: boolean;
}

export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
T['value'] | T['value'][]
> {
options: T[];
filteredOptions: T[];
multiple: boolean;
isNavigating = false;
Expand All @@ -64,6 +63,7 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
#cursor = 0;
#lastUserInput = '';
#filterFn: FilterFunction<T>;
#options: T[] | (() => T[]);

get cursor(): number {
return this.#cursor;
Expand All @@ -81,11 +81,19 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
return `${s1}${color.inverse(s2)}${s3.join('')}`;
}

get options(): T[] {
if (typeof this.#options === 'function') {
return this.#options();
}
return this.#options;
}

constructor(opts: AutocompleteOptions<T>) {
super(opts);

this.options = opts.options;
this.filteredOptions = [...this.options];
this.#options = opts.options;
const options = this.options;
this.filteredOptions = [...options];
this.multiple = opts.multiple === true;
this.#filterFn = opts.filter ?? defaultFilter;
let initialValues: unknown[] | undefined;
Expand All @@ -103,7 +111,7 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<

if (initialValues) {
for (const selectedValue of initialValues) {
const selectedIndex = this.options.findIndex((opt) => opt.value === selectedValue);
const selectedIndex = options.findIndex((opt) => opt.value === selectedValue);
if (selectedIndex !== -1) {
this.toggleSelected(selectedValue);
this.#cursor = selectedIndex;
Expand All @@ -113,16 +121,6 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<

this.focusedValue = this.options[this.#cursor]?.value;

this.on('finalize', () => {
if (!this.value) {
this.value = normalisedValue(this.multiple, initialValues);
}

if (this.state === 'submit') {
this.value = normalisedValue(this.multiple, this.selectedValues);
}
});

this.on('key', (char, key) => this.#onKey(char, key));
this.on('userInput', (value) => this.#onUserInputChanged(value));
}
Expand All @@ -141,6 +139,7 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
#onKey(_char: string | undefined, key: Key): void {
const isUpKey = key.name === 'up';
const isDownKey = key.name === 'down';
const isReturnKey = key.name === 'return';

// Start navigation mode with up/down arrows
if (isUpKey || isDownKey) {
Expand All @@ -153,6 +152,8 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
this.selectedValues = [this.focusedValue];
}
this.isNavigating = true;
} else if (isReturnKey) {
this.value = normalisedValue(this.multiple, this.selectedValues);
} else {
if (this.multiple) {
if (
Expand All @@ -171,6 +172,10 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
}
}

deselectAll() {
this.selectedValues = [];
}

toggleSelected(value: T['value']) {
if (this.filteredOptions.length === 0) {
return;
Expand All @@ -191,13 +196,22 @@ export default class AutocompletePrompt<T extends OptionLike> extends Prompt<
if (value !== this.#lastUserInput) {
this.#lastUserInput = value;

const options = this.options;

if (value) {
this.filteredOptions = this.options.filter((opt) => this.#filterFn(value, opt));
this.filteredOptions = options.filter((opt) => this.#filterFn(value, opt));
} else {
this.filteredOptions = [...this.options];
this.filteredOptions = [...options];
}
this.#cursor = getCursorForValue(this.focusedValue, this.filteredOptions);
this.focusedValue = this.filteredOptions[this.#cursor]?.value;
if (!this.multiple) {
if (this.focusedValue !== undefined) {
this.toggleSelected(this.focusedValue);
} else {
this.deselectAll();
}
}
}
}
}
7 changes: 5 additions & 2 deletions packages/core/src/prompts/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { Action } from '../utils/index.js';
export interface PromptOptions<TValue, Self extends Prompt<TValue>> {
render(this: Omit<Self, 'prompt'>): string | undefined;
initialValue?: any;
initialUserInput?: string;
validate?: ((value: TValue | undefined) => string | Error | undefined) | undefined;
input?: Readable;
output?: Writable;
Expand All @@ -25,7 +26,7 @@ export default class Prompt<TValue> {
protected output: Writable;
private _abortSignal?: AbortSignal;

protected rl: ReadLine | undefined;
private rl: ReadLine | undefined;
private opts: Omit<PromptOptions<TValue, Prompt<TValue>>, 'render' | 'input' | 'output'>;
private _render: (context: Omit<Prompt<TValue>, 'prompt'>) => string | undefined;
private _track = false;
Expand Down Expand Up @@ -145,7 +146,9 @@ export default class Prompt<TValue> {
});
this.rl.prompt();

this.emit('beforePrompt');
if (this.opts.initialUserInput !== undefined) {
this._setUserInput(this.opts.initialUserInput, true);
}

this.input.on('keypress', this.onKeypress);
setRawMode(this.input, true);
Expand Down
126 changes: 0 additions & 126 deletions packages/core/src/prompts/suggestion.ts

This file was deleted.

10 changes: 4 additions & 6 deletions packages/core/src/prompts/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,11 @@ export default class TextPrompt extends Prompt<string> {
return this._cursor;
}
constructor(opts: TextOptions) {
super(opts);

this.on('beforePrompt', () => {
if (opts.initialValue !== undefined) {
this._setUserInput(opts.initialValue, true);
}
super({
...opts,
initialUserInput: opts.initialUserInput ?? opts.initialValue,
});

this.on('userInput', (input) => {
this._setValue(input);
});
Expand Down
8 changes: 0 additions & 8 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,3 @@ export interface ClackEvents<TValue> {
finalize: () => void;
beforePrompt: () => void;
}

/**
* Display a value
*/
export interface ValueWithCursorPart {
text: string;
type: 'value' | 'cursor_on_value' | 'suggestion' | 'cursor_on_suggestion';
}
Loading