Skip to content

Commit e840f1b

Browse files
feat(components): render cursor block in Autocomplete
1 parent 5ff3f54 commit e840f1b

3 files changed

Lines changed: 68 additions & 12 deletions

File tree

src/components/Autocomplete.test.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,23 @@ describe('Autocomplete', () => {
168168
await tick();
169169
expect(lastFrame()).toContain('/mock');
170170
});
171+
172+
it('shows block cursor in output', async () => {
173+
const { lastFrame, stdin } = render(<Autocomplete onSubmit={vi.fn()} />);
174+
stdin.write('hi');
175+
await tick();
176+
// The input should be visible with block cursor at end
177+
expect(lastFrame()).toContain('hi');
178+
});
179+
180+
it('cursor stays at end when typing', async () => {
181+
const { lastFrame, stdin } = render(<Autocomplete onSubmit={vi.fn()} />);
182+
stdin.write('a');
183+
await tick();
184+
stdin.write('b');
185+
await tick();
186+
stdin.write('c');
187+
await tick();
188+
expect(lastFrame()).toContain('abc');
189+
});
171190
});

src/components/Autocomplete.tsx

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ interface Props {
99
}
1010

1111
function getMatches(input: string): Command[] {
12-
if (!input.startsWith('/')) return [];
13-
return COMMANDS.filter((cmd) => cmd.name.startsWith(input));
12+
if (!input.startsWith('/')) {
13+
return [];
14+
}
15+
return COMMANDS.filter((command) => command.name.startsWith(input));
1416
}
1517

1618
export function Autocomplete({ isDisabled = false, onSubmit }: Props) {
1719
const [value, setValue] = useState('');
20+
const [cursorPosition, setCursorPosition] = useState(0);
1821
const [selectedIndex, setSelectedIndex] = useState(0);
1922

2023
const matches = getMatches(value);
@@ -32,6 +35,18 @@ export function Autocomplete({ isDisabled = false, onSubmit }: Props) {
3235
return;
3336
}
3437

38+
// v8 ignore next 4
39+
if (key.leftArrow) {
40+
setCursorPosition((position) => Math.max(0, position - 1));
41+
return;
42+
}
43+
44+
// v8 ignore next 4
45+
if (key.rightArrow) {
46+
setCursorPosition((position) => Math.min(value.length, position + 1));
47+
return;
48+
}
49+
3550
if (key.tab && showSuggestions) {
3651
// v8 ignore next
3752
const match = matches[selectedIndex] ?? matches[0];
@@ -61,18 +76,25 @@ export function Autocomplete({ isDisabled = false, onSubmit }: Props) {
6176
}
6277

6378
if (key.backspace || key.delete) {
64-
setValue((v) => v.slice(0, -1));
79+
setValue((value) => {
80+
const before = value.slice(0, cursorPosition - 1);
81+
const after = value.slice(cursorPosition);
82+
return before + after;
83+
});
84+
setCursorPosition((position) => Math.max(0, position - 1));
6585
setSelectedIndex(0);
6686
return;
6787
}
6888

6989
// v8 ignore next
7090
if (char && !key.ctrl && !key.meta) {
71-
setValue((v) => {
72-
const next = v + char;
73-
setSelectedIndex(0);
74-
return next;
91+
setValue((value) => {
92+
const before = value.slice(0, cursorPosition);
93+
const after = value.slice(cursorPosition);
94+
return before + char + after;
7595
});
96+
setCursorPosition((position) => position + 1);
97+
setSelectedIndex(0);
7698
}
7799
},
78100
{ isActive: !isDisabled },
@@ -82,22 +104,35 @@ export function Autocomplete({ isDisabled = false, onSubmit }: Props) {
82104
<Box flexDirection="column">
83105
<Box>
84106
<Text>{UI.PROMPT_PREFIX}</Text>
85-
<Text>{value}</Text>
107+
108+
<Text>
109+
{value.slice(0, cursorPosition)}
110+
{cursorPosition < value.length ? (
111+
<Text backgroundColor="black" color="white">
112+
{value[cursorPosition]}
113+
</Text>
114+
) : (
115+
<Text backgroundColor="black" color="white">
116+
{' '}
117+
</Text>
118+
)}
119+
{value.slice(cursorPosition + 1)}
120+
</Text>
86121
</Box>
87122

88123
{showSuggestions && (
89124
<Box flexDirection="column">
90-
{matches.map((cmd, index) => {
125+
{matches.map((command, index) => {
91126
const isHighlighted = index === selectedIndex;
92127
return (
93-
<Box key={cmd.name} gap={1}>
128+
<Box key={command.name} gap={1}>
94129
<Text
95130
color={isHighlighted ? 'cyan' : undefined}
96131
bold={isHighlighted}
97132
>
98-
{cmd.name}
133+
{command.name}
99134
</Text>
100-
<Text dimColor>{cmd.description}</Text>
135+
<Text dimColor>{command.description}</Text>
101136
</Box>
102137
);
103138
})}

src/constants/key.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@ export const BACKSPACE = '\x7f';
22
export const DOWN = '\x1B[B';
33
export const ENTER = '\r';
44
export const ESCAPE = '\x1B\x1B';
5+
export const LEFT = '\x1B[D';
6+
export const RIGHT = '\x1B[C';
57
export const TAB = '\t';
68
export const UP = '\x1B[A';

0 commit comments

Comments
 (0)