diff --git a/.changeset/modern-views-stay.md b/.changeset/modern-views-stay.md new file mode 100644 index 00000000..7b04ed1d --- /dev/null +++ b/.changeset/modern-views-stay.md @@ -0,0 +1,6 @@ +--- +"@clack/prompts": minor +"@clack/core": minor +--- + +Add tab-completion to the `path` prompt: pressing Tab fills the input with the focused suggestion, so you can quickly descend into deep directories (type `/` and Tab again). Powered by a new opt-in `completeOnTab` option on `autocomplete`, which also shows a `Tab: complete` hint in the instructions footer. Default `autocomplete` behavior is unchanged. diff --git a/packages/core/src/prompts/autocomplete.ts b/packages/core/src/prompts/autocomplete.ts index fa9a43dd..68091dff 100644 --- a/packages/core/src/prompts/autocomplete.ts +++ b/packages/core/src/prompts/autocomplete.ts @@ -58,6 +58,11 @@ export interface AutocompleteOptions * the prompt's filter (so the value remains selectable). */ placeholder?: string; + /** + * When `true` (single-select only), pressing Tab fills the input with the + * currently focused option's value, as if the user had typed it. + */ + completeOnTab?: boolean; } export default class AutocompletePrompt extends Prompt< @@ -74,6 +79,7 @@ export default class AutocompletePrompt extends Prompt< #filterFn: FilterFunction | undefined; #options: T[] | (() => T[]); #placeholder: string | undefined; + #completeOnTab: boolean; get cursor(): number { return this.#cursor; @@ -104,6 +110,7 @@ export default class AutocompletePrompt extends Prompt< this.#options = opts.options; this.#placeholder = opts.placeholder; + this.#completeOnTab = opts.completeOnTab === true; const options = this.options; this.filteredOptions = [...options]; this.multiple = opts.multiple === true; @@ -174,6 +181,20 @@ export default class AutocompletePrompt extends Prompt< return; } + // Tab completion: fill input with the focused option's value + if ( + key.name === 'tab' && + this.#completeOnTab && + !this.multiple && + this.focusedValue !== undefined + ) { + const completion = String(this.focusedValue); // capture before clearing resets focusedValue + this._clearUserInput(); + this._setUserInput(completion, true); + this.isNavigating = false; + return; + } + // Start navigation mode with up/down arrows if (isUpKey || isDownKey) { this.#cursor = findCursor(this.#cursor, isUpKey ? -1 : 1, this.filteredOptions); diff --git a/packages/core/test/prompts/autocomplete.test.ts b/packages/core/test/prompts/autocomplete.test.ts index f1d98e24..e8aabd98 100644 --- a/packages/core/test/prompts/autocomplete.test.ts +++ b/packages/core/test/prompts/autocomplete.test.ts @@ -272,4 +272,60 @@ describe('AutocompletePrompt', () => { // Placeholder does not match any option, so input must not be filled with placeholder expect(instance.userInput).not.to.equal('Type to search...'); }); + + test('completeOnTab fills input with focused option and submit returns it', async () => { + const instance = new AutocompletePrompt({ + input, + output, + render: () => 'foo', + options: testOptions, + completeOnTab: true, + }); + + const promise = instance.prompt(); + input.emit('keypress', 'b', { name: 'b' }); + input.emit('keypress', 'a', { name: 'a' }); + input.emit('keypress', 'n', { name: 'n' }); + input.emit('keypress', '\t', { name: 'tab' }); + input.emit('keypress', '', { name: 'return' }); + const result = await promise; + + expect(instance.userInput).to.equal('banana'); + expect(result).to.equal('banana'); + }); + + test('completeOnTab with no matches leaves input unchanged', () => { + const instance = new AutocompletePrompt({ + input, + output, + render: () => 'foo', + options: testOptions, + completeOnTab: true, + }); + + instance.prompt(); + input.emit('keypress', 'z', { name: 'z' }); + input.emit('keypress', 'z', { name: 'z' }); + input.emit('keypress', 'z', { name: 'z' }); + input.emit('keypress', '\t', { name: 'tab' }); + + expect(instance.userInput).to.equal('zzz'); + }); + + test('Tab does not complete when completeOnTab is not set', () => { + const instance = new AutocompletePrompt({ + input, + output, + render: () => 'foo', + options: testOptions, + }); + + instance.prompt(); + input.emit('keypress', 'b', { name: 'b' }); + input.emit('keypress', 'a', { name: 'a' }); + input.emit('keypress', 'n', { name: 'n' }); + input.emit('keypress', '\t', { name: 'tab' }); + + expect(instance.userInput).to.equal('ban'); + }); }); diff --git a/packages/prompts/src/autocomplete.ts b/packages/prompts/src/autocomplete.ts index 0f52c095..af6d76a2 100644 --- a/packages/prompts/src/autocomplete.ts +++ b/packages/prompts/src/autocomplete.ts @@ -93,6 +93,12 @@ export interface AutocompleteOptions extends AutocompleteSharedOptions(opts: AutocompleteOptions) => { initialValue: opts.initialValue ? [opts.initialValue] : undefined, initialUserInput: opts.initialUserInput, placeholder: opts.placeholder, + completeOnTab: opts.completeOnTab, filter: opts.filter ?? ((search: string, opt: Option) => { @@ -223,6 +230,7 @@ export const autocomplete = (opts: AutocompleteOptions) => { // Show instructions const instructions = [ `${styleText('dim', '↑/↓')} to select`, + ...(opts.completeOnTab ? [`${styleText('dim', 'Tab:')} complete`] : []), `${styleText('dim', 'Enter:')} confirm`, `${styleText('dim', 'Type:')} to search`, ]; diff --git a/packages/prompts/src/path.ts b/packages/prompts/src/path.ts index 1a719ef6..f9de592c 100644 --- a/packages/prompts/src/path.ts +++ b/packages/prompts/src/path.ts @@ -65,6 +65,7 @@ export const path = (opts: PathOptions) => { ...opts, initialUserInput: opts.initialValue ?? opts.root ?? process.cwd(), maxItems: 5, + completeOnTab: true, validate(value) { if (Array.isArray(value)) { // Shouldn't ever happen since we don't enable `multiple: true` diff --git a/packages/prompts/test/__snapshots__/path.test.ts.snap b/packages/prompts/test/__snapshots__/path.test.ts.snap index 0153e166..f1cab738 100644 --- a/packages/prompts/test/__snapshots__/path.test.ts.snap +++ b/packages/prompts/test/__snapshots__/path.test.ts.snap @@ -11,7 +11,7 @@ exports[`text (isCI = false) > can cancel 1`] = ` β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ β—‹ /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -35,14 +35,14 @@ exports[`text (isCI = false) > cannot submit unknown value 1`] = ` β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ β—‹ /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", "", "β”‚ Search: /tmp/_β–ˆ β”‚ No matches found -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -52,7 +52,7 @@ exports[`text (isCI = false) > cannot submit unknown value 1`] = ` β”‚ Search: /tmp/_β–ˆ β”‚ No matches found β”‚ Please select a path -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -64,14 +64,14 @@ exports[`text (isCI = false) > cannot submit unknown value 1`] = ` β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ β—‹ /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", "", "β”‚ Search: /tmp/bβ–ˆ β”‚ ● /tmp/bar -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -92,7 +92,7 @@ exports[`text (isCI = false) > initialValue sets the value 1`] = ` β”‚ β”‚ Search: /tmp/barβ–ˆ β”‚ ● /tmp/bar -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -116,14 +116,14 @@ exports[`text (isCI = false) > renders cancelled value if one set 1`] = ` β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ β—‹ /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", "", "β”‚ Search: /tmp/xβ–ˆ β”‚ No matches found -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -152,7 +152,7 @@ exports[`text (isCI = false) > renders message 1`] = ` β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ β—‹ /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -176,14 +176,14 @@ exports[`text (isCI = false) > renders submitted value 1`] = ` β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ β—‹ /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", "", "β”‚ Search: /tmp/bβ–ˆ β”‚ ● /tmp/bar -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -201,6 +201,96 @@ exports[`text (isCI = false) > renders submitted value 1`] = ` ] `; +exports[`text (isCI = false) > tab completes the focused suggestion 1`] = ` +[ + "", + "β”‚ +β—† foo +β”‚ +β”‚ Search: /tmp/foo/β–ˆ +β”‚ ● /tmp/foo/bar.txt +β”‚ β—‹ /tmp/foo/baz.text +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search +β””", + "", + "", + "", + "β”‚ Search: /tmp/foo/bβ–ˆ", + "", + "", + "", + "", + "β”‚ Search: /tmp/foo/bar.txtβ–ˆ +β”‚ ● /tmp/foo/bar.txt +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search +β””", + "", + "", + "", + "β—‡ foo +β”‚ /tmp/foo/bar.txt", + " +", + "", +] +`; + +exports[`text (isCI = false) > tab completion can descend into directories 1`] = ` +[ + "", + "β”‚ +β—† foo +β”‚ +β”‚ Search: /tmp/β–ˆ +β”‚ ● /tmp/bar +β”‚ β—‹ /tmp/foo +β”‚ β—‹ /tmp/hello +β”‚ β—‹ /tmp/root.zip +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search +β””", + "", + "", + "", + "β”‚ Search: /tmp/fβ–ˆ +β”‚ ● /tmp/foo +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search +β””", + "", + "", + "", + "β”‚ Search: /tmp/fooβ–ˆ +β”‚ ● /tmp/foo/bar.txt +β”‚ β—‹ /tmp/foo/baz.text +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search +β””", + "", + "", + "", + "β”‚ Search: /tmp/foo/β–ˆ", + "", + "", + "", + "", + "β”‚ Search: /tmp/foo/bβ–ˆ", + "", + "", + "", + "", + "β”‚ Search: /tmp/foo/bar.txtβ–ˆ +β”‚ ● /tmp/foo/bar.txt +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search +β””", + "", + "", + "", + "β—‡ foo +β”‚ /tmp/foo/bar.txt", + " +", + "", +] +`; + exports[`text (isCI = false) > validation errors render and clear (using Error) 1`] = ` [ "", @@ -212,14 +302,14 @@ exports[`text (isCI = false) > validation errors render and clear (using Error) β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ β—‹ /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", "", "β”‚ Search: /tmp/rβ–ˆ β”‚ ● /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -229,7 +319,7 @@ exports[`text (isCI = false) > validation errors render and clear (using Error) β”‚ Search: /tmp/rβ–ˆ β”‚ should be /tmp/bar β”‚ ● /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -241,14 +331,14 @@ exports[`text (isCI = false) > validation errors render and clear (using Error) β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ ● /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", "", "β”‚ Search: /tmp/bβ–ˆ β”‚ ● /tmp/bar -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -272,14 +362,14 @@ exports[`text (isCI = false) > validation errors render and clear 1`] = ` β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ β—‹ /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", "", "β”‚ Search: /tmp/rβ–ˆ β”‚ ● /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -289,7 +379,7 @@ exports[`text (isCI = false) > validation errors render and clear 1`] = ` β”‚ Search: /tmp/rβ–ˆ β”‚ should be /tmp/bar β”‚ ● /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -301,14 +391,14 @@ exports[`text (isCI = false) > validation errors render and clear 1`] = ` β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ ● /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", "", "β”‚ Search: /tmp/bβ–ˆ β”‚ ● /tmp/bar -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -332,7 +422,7 @@ exports[`text (isCI = true) > can cancel 1`] = ` β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ β—‹ /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -356,14 +446,14 @@ exports[`text (isCI = true) > cannot submit unknown value 1`] = ` β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ β—‹ /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", "", "β”‚ Search: /tmp/_β–ˆ β”‚ No matches found -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -373,7 +463,7 @@ exports[`text (isCI = true) > cannot submit unknown value 1`] = ` β”‚ Search: /tmp/_β–ˆ β”‚ No matches found β”‚ Please select a path -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -385,14 +475,14 @@ exports[`text (isCI = true) > cannot submit unknown value 1`] = ` β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ β—‹ /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", "", "β”‚ Search: /tmp/bβ–ˆ β”‚ ● /tmp/bar -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -413,7 +503,7 @@ exports[`text (isCI = true) > initialValue sets the value 1`] = ` β”‚ β”‚ Search: /tmp/barβ–ˆ β”‚ ● /tmp/bar -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -437,14 +527,14 @@ exports[`text (isCI = true) > renders cancelled value if one set 1`] = ` β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ β—‹ /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", "", "β”‚ Search: /tmp/xβ–ˆ β”‚ No matches found -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -473,7 +563,7 @@ exports[`text (isCI = true) > renders message 1`] = ` β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ β—‹ /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -497,14 +587,14 @@ exports[`text (isCI = true) > renders submitted value 1`] = ` β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ β—‹ /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", "", "β”‚ Search: /tmp/bβ–ˆ β”‚ ● /tmp/bar -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -522,6 +612,96 @@ exports[`text (isCI = true) > renders submitted value 1`] = ` ] `; +exports[`text (isCI = true) > tab completes the focused suggestion 1`] = ` +[ + "", + "β”‚ +β—† foo +β”‚ +β”‚ Search: /tmp/foo/β–ˆ +β”‚ ● /tmp/foo/bar.txt +β”‚ β—‹ /tmp/foo/baz.text +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search +β””", + "", + "", + "", + "β”‚ Search: /tmp/foo/bβ–ˆ", + "", + "", + "", + "", + "β”‚ Search: /tmp/foo/bar.txtβ–ˆ +β”‚ ● /tmp/foo/bar.txt +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search +β””", + "", + "", + "", + "β—‡ foo +β”‚ /tmp/foo/bar.txt", + " +", + "", +] +`; + +exports[`text (isCI = true) > tab completion can descend into directories 1`] = ` +[ + "", + "β”‚ +β—† foo +β”‚ +β”‚ Search: /tmp/β–ˆ +β”‚ ● /tmp/bar +β”‚ β—‹ /tmp/foo +β”‚ β—‹ /tmp/hello +β”‚ β—‹ /tmp/root.zip +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search +β””", + "", + "", + "", + "β”‚ Search: /tmp/fβ–ˆ +β”‚ ● /tmp/foo +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search +β””", + "", + "", + "", + "β”‚ Search: /tmp/fooβ–ˆ +β”‚ ● /tmp/foo/bar.txt +β”‚ β—‹ /tmp/foo/baz.text +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search +β””", + "", + "", + "", + "β”‚ Search: /tmp/foo/β–ˆ", + "", + "", + "", + "", + "β”‚ Search: /tmp/foo/bβ–ˆ", + "", + "", + "", + "", + "β”‚ Search: /tmp/foo/bar.txtβ–ˆ +β”‚ ● /tmp/foo/bar.txt +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search +β””", + "", + "", + "", + "β—‡ foo +β”‚ /tmp/foo/bar.txt", + " +", + "", +] +`; + exports[`text (isCI = true) > validation errors render and clear (using Error) 1`] = ` [ "", @@ -533,14 +713,14 @@ exports[`text (isCI = true) > validation errors render and clear (using Error) 1 β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ β—‹ /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", "", "β”‚ Search: /tmp/rβ–ˆ β”‚ ● /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -550,7 +730,7 @@ exports[`text (isCI = true) > validation errors render and clear (using Error) 1 β”‚ Search: /tmp/rβ–ˆ β”‚ should be /tmp/bar β”‚ ● /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -562,14 +742,14 @@ exports[`text (isCI = true) > validation errors render and clear (using Error) 1 β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ ● /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", "", "β”‚ Search: /tmp/bβ–ˆ β”‚ ● /tmp/bar -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -593,14 +773,14 @@ exports[`text (isCI = true) > validation errors render and clear 1`] = ` β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ β—‹ /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", "", "β”‚ Search: /tmp/rβ–ˆ β”‚ ● /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -610,7 +790,7 @@ exports[`text (isCI = true) > validation errors render and clear 1`] = ` β”‚ Search: /tmp/rβ–ˆ β”‚ should be /tmp/bar β”‚ ● /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", @@ -622,14 +802,14 @@ exports[`text (isCI = true) > validation errors render and clear 1`] = ` β”‚ β—‹ /tmp/foo β”‚ β—‹ /tmp/hello β”‚ ● /tmp/root.zip -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", "", "β”‚ Search: /tmp/bβ–ˆ β”‚ ● /tmp/bar -β”‚ ↑/↓ to select β€’ Enter: confirm β€’ Type: to search +β”‚ ↑/↓ to select β€’ Tab: complete β€’ Enter: confirm β€’ Type: to search β””", "", "", diff --git a/packages/prompts/test/path.test.ts b/packages/prompts/test/path.test.ts index 5c1fe1f9..7561b7cb 100644 --- a/packages/prompts/test/path.test.ts +++ b/packages/prompts/test/path.test.ts @@ -129,6 +129,45 @@ describe.each(['true', 'false'])('text (isCI = %s)', (isCI) => { expect(output.buffer).toMatchSnapshot(); }); + test('tab completes the focused suggestion', async () => { + const result = prompts.path({ + message: 'foo', + root: '/tmp/foo/', + input, + output, + }); + + input.emit('keypress', 'b', { name: 'b' }); + input.emit('keypress', '\t', { name: 'tab' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('/tmp/foo/bar.txt'); + expect(output.buffer).toMatchSnapshot(); + }); + + test('tab completion can descend into directories', async () => { + const result = prompts.path({ + message: 'foo', + root: '/tmp/', + input, + output, + }); + + input.emit('keypress', 'f', { name: 'f' }); + input.emit('keypress', '\t', { name: 'tab' }); + input.emit('keypress', '/', { name: '/' }); + input.emit('keypress', 'b', { name: 'b' }); + input.emit('keypress', '\t', { name: 'tab' }); + input.emit('keypress', '', { name: 'return' }); + + const value = await result; + + expect(value).toBe('/tmp/foo/bar.txt'); + expect(output.buffer).toMatchSnapshot(); + }); + test('initialValue sets the value', async () => { const result = prompts.path({ message: 'foo',