Skip to content

Commit 2837845

Browse files
authored
feat(core+prompts): Add suggestion + path prompt (bombshell-dev#314)
1 parent 57d0958 commit 2837845

File tree

18 files changed

+1792
-2
lines changed

18 files changed

+1792
-2
lines changed

.changeset/curvy-seals-sit.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@clack/prompts": minor
3+
"@clack/core": minor
4+
---
5+
6+
Adds suggestion and path prompts

examples/basic/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"stream": "jiti ./stream.ts",
1414
"progress": "jiti ./progress.ts",
1515
"spinner": "jiti ./spinner.ts",
16+
"path": "jiti ./path.ts",
1617
"spinner-ci": "npx cross-env CI=\"true\" jiti ./spinner-ci.ts",
1718
"spinner-timer": "jiti ./spinner-timer.ts",
1819
"task-log": "jiti ./task-log.ts"

examples/basic/path.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import * as p from '@clack/prompts';
2+
3+
async function demo() {
4+
p.intro('path start...');
5+
6+
const path = await p.path({
7+
message: 'Read file',
8+
});
9+
10+
p.outro('path stop...');
11+
}
12+
13+
void demo();

packages/core/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type { ClackState as State } from './types.js';
1+
export type { ClackState as State, ValueWithCursorPart } from './types.js';
22
export type { ClackSettings } from './utils/settings.js';
33

44
export { default as ConfirmPrompt } from './prompts/confirm.js';
@@ -10,5 +10,6 @@ export { default as SelectPrompt } from './prompts/select.js';
1010
export { default as SelectKeyPrompt } from './prompts/select-key.js';
1111
export { default as TextPrompt } from './prompts/text.js';
1212
export { default as AutocompletePrompt } from './prompts/autocomplete.js';
13+
export { default as SuggestionPrompt } from './prompts/suggestion.js';
1314
export { block, isCancel, getColumns } from './utils/index.js';
1415
export { updateSettings, settings } from './utils/settings.js';

packages/core/src/prompts/prompt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default class Prompt {
2626
protected output: Writable;
2727
private _abortSignal?: AbortSignal;
2828

29-
private rl: ReadLine | undefined;
29+
protected rl: ReadLine | undefined;
3030
private opts: Omit<PromptOptions<Prompt>, 'render' | 'input' | 'output'>;
3131
private _render: (context: Omit<Prompt, 'prompt'>) => string | undefined;
3232
private _track = false;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import color from 'picocolors';
2+
import type { ValueWithCursorPart } from '../types.js';
3+
import Prompt, { type PromptOptions } from './prompt.js';
4+
5+
interface SuggestionOptions extends PromptOptions<SuggestionPrompt> {
6+
suggest: (value: string) => Array<string>;
7+
initialValue: string;
8+
}
9+
10+
export default class SuggestionPrompt extends Prompt {
11+
value: string;
12+
protected suggest: (value: string) => Array<string>;
13+
private selectionIndex = 0;
14+
private nextItems: Array<string> = [];
15+
16+
constructor(opts: SuggestionOptions) {
17+
super(opts);
18+
19+
this.value = opts.initialValue;
20+
this.suggest = opts.suggest;
21+
this.getNextItems();
22+
this.selectionIndex = 0;
23+
this._cursor = this.value.length;
24+
25+
this.on('cursor', (key) => {
26+
switch (key) {
27+
case 'up':
28+
this.selectionIndex = Math.max(
29+
0,
30+
this.selectionIndex === 0 ? this.nextItems.length - 1 : this.selectionIndex - 1
31+
);
32+
break;
33+
case 'down':
34+
this.selectionIndex =
35+
this.nextItems.length === 0 ? 0 : (this.selectionIndex + 1) % this.nextItems.length;
36+
break;
37+
}
38+
});
39+
this.on('key', (key, info) => {
40+
if (info.name === 'tab' && this.nextItems.length > 0) {
41+
const delta = this.nextItems[this.selectionIndex].substring(this.value.length);
42+
this.value = this.nextItems[this.selectionIndex];
43+
this.rl?.write(delta);
44+
this._cursor = this.value.length;
45+
this.selectionIndex = 0;
46+
this.getNextItems();
47+
}
48+
});
49+
this.on('value', () => {
50+
this.getNextItems();
51+
});
52+
}
53+
54+
get displayValue(): Array<ValueWithCursorPart> {
55+
const result: Array<ValueWithCursorPart> = [];
56+
if (this._cursor > 0) {
57+
result.push({
58+
text: this.value.substring(0, this._cursor),
59+
type: 'value',
60+
});
61+
}
62+
if (this._cursor < this.value.length) {
63+
result.push({
64+
text: this.value.substring(this._cursor, this._cursor + 1),
65+
type: 'cursor_on_value',
66+
});
67+
const left = this.value.substring(this._cursor + 1);
68+
if (left.length > 0) {
69+
result.push({
70+
text: left,
71+
type: 'value',
72+
});
73+
}
74+
if (this.suggestion.length > 0) {
75+
result.push({
76+
text: this.suggestion,
77+
type: 'suggestion',
78+
});
79+
}
80+
return result;
81+
}
82+
if (this.suggestion.length === 0) {
83+
result.push({
84+
text: '\u00A0',
85+
type: 'cursor_on_value',
86+
});
87+
return result;
88+
}
89+
result.push(
90+
{
91+
text: this.suggestion[0],
92+
type: 'cursor_on_suggestion',
93+
},
94+
{
95+
text: this.suggestion.substring(1),
96+
type: 'suggestion',
97+
}
98+
);
99+
return result;
100+
}
101+
102+
get suggestion(): string {
103+
return this.nextItems[this.selectionIndex]?.substring(this.value.length) ?? '';
104+
}
105+
106+
private getNextItems(): void {
107+
this.nextItems = this.suggest(this.value).filter((item) => {
108+
return item.startsWith(this.value) && item !== this.value;
109+
});
110+
if (this.selectionIndex > this.nextItems.length) {
111+
this.selectionIndex = 0;
112+
}
113+
}
114+
}

packages/core/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,11 @@ export interface ClackEvents {
2121
confirm: (value?: boolean) => void;
2222
finalize: () => void;
2323
}
24+
25+
/**
26+
* Display a value
27+
*/
28+
export interface ValueWithCursorPart {
29+
text: string;
30+
type: 'value' | 'cursor_on_value' | 'suggestion' | 'cursor_on_suggestion';
31+
}

0 commit comments

Comments
 (0)