Support command/ctrl/alt backspace correctly (#17175)

This commit is contained in:
Tommaso Sciortino
2026-01-21 10:13:26 -08:00
committed by GitHub
parent e894871afc
commit f190b87223
27 changed files with 487 additions and 298 deletions
@@ -851,8 +851,9 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
completion.promptCompletion.text &&
key.sequence &&
key.sequence.length === 1 &&
!key.alt &&
!key.ctrl &&
!key.meta
!key.cmd
) {
completion.promptCompletion.clear();
setExpandedSuggestionIndex(-1);
@@ -91,9 +91,10 @@ describe('MultiFolderTrustDialog', () => {
await act(async () => {
keypressCallback({
name: 'escape',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
sequence: '',
insertable: false,
});
@@ -93,10 +93,10 @@ const createMockConfig = (overrides: Partial<Config> = {}): Config =>
const triggerKey = (
partialKey: Partial<{
name: string;
ctrl: boolean;
meta: boolean;
shift: boolean;
paste: boolean;
alt: boolean;
ctrl: boolean;
cmd: boolean;
insertable: boolean;
sequence: string;
}>,
@@ -108,9 +108,10 @@ const triggerKey = (
const key = {
name: '',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: false,
sequence: '',
...partialKey,
@@ -263,7 +264,13 @@ describe('SessionBrowser component', () => {
// Type the query "query".
for (const ch of ['q', 'u', 'e', 'r', 'y']) {
triggerKey({ sequence: ch, name: ch, ctrl: false, meta: false });
triggerKey({
sequence: ch,
name: ch,
alt: false,
ctrl: false,
cmd: false,
});
}
await waitFor(() => {
@@ -781,9 +781,10 @@ export const useSessionBrowserInput = (
state.setScrollOffset(0);
} else if (
key.sequence &&
key.sequence.length === 1 &&
!key.alt &&
!key.ctrl &&
!key.meta &&
key.sequence.length === 1
!key.cmd
) {
state.setSearchQuery((prev) => prev + key.sequence);
state.setActiveIndex(0);
@@ -53,7 +53,14 @@ describe('ShellInputPrompt', () => {
const handler = mockUseKeypress.mock.calls[0][0];
// Simulate keypress
handler({ name, sequence, ctrl: false, shift: false, meta: false });
handler({
name,
shift: false,
alt: false,
ctrl: false,
cmd: false,
sequence,
});
expect(mockWriteToPty).toHaveBeenCalledWith(1, sequence);
});
@@ -66,7 +73,7 @@ describe('ShellInputPrompt', () => {
const handler = mockUseKeypress.mock.calls[0][0];
handler({ name: key, ctrl: true, shift: true, meta: false });
handler({ name: key, shift: true, alt: false, ctrl: true, cmd: false });
expect(mockScrollPty).toHaveBeenCalledWith(1, direction);
});
@@ -78,10 +85,11 @@ describe('ShellInputPrompt', () => {
handler({
name: 'a',
sequence: 'a',
ctrl: false,
shift: false,
meta: false,
alt: false,
ctrl: false,
cmd: false,
sequence: 'a',
});
expect(mockWriteToPty).not.toHaveBeenCalled();
@@ -94,10 +102,11 @@ describe('ShellInputPrompt', () => {
handler({
name: 'a',
sequence: 'a',
ctrl: false,
shift: false,
meta: false,
alt: false,
ctrl: false,
cmd: false,
sequence: 'a',
});
expect(mockWriteToPty).not.toHaveBeenCalled();
@@ -151,18 +151,20 @@ describe('TextInput', () => {
keypressHandler({
name: 'a',
sequence: 'a',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
sequence: 'a',
});
expect(mockBuffer.handleInput).toHaveBeenCalledWith({
name: 'a',
sequence: 'a',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
sequence: 'a',
});
expect(mockBuffer.text).toBe('a');
});
@@ -176,18 +178,20 @@ describe('TextInput', () => {
keypressHandler({
name: 'backspace',
sequence: '',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
sequence: '',
});
expect(mockBuffer.handleInput).toHaveBeenCalledWith({
name: 'backspace',
sequence: '',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
sequence: '',
});
expect(mockBuffer.text).toBe('tes');
});
@@ -201,10 +205,11 @@ describe('TextInput', () => {
keypressHandler({
name: 'left',
sequence: '',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
sequence: '',
});
// Cursor moves from end to before 't'
@@ -221,10 +226,11 @@ describe('TextInput', () => {
keypressHandler({
name: 'right',
sequence: '',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
sequence: '',
});
expect(mockBuffer.visualCursor[1]).toBe(3);
@@ -239,10 +245,11 @@ describe('TextInput', () => {
keypressHandler({
name: 'return',
sequence: '',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
sequence: '',
});
expect(onSubmit).toHaveBeenCalledWith('test');
@@ -257,10 +264,11 @@ describe('TextInput', () => {
keypressHandler({
name: 'escape',
sequence: '',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
sequence: '',
});
await vi.runAllTimersAsync();
@@ -1059,9 +1059,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'h',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: true,
sequence: 'h',
}),
@@ -1069,9 +1070,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'i',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: true,
sequence: 'i',
}),
@@ -1086,9 +1088,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'return',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: true,
sequence: '\r',
}),
@@ -1103,9 +1106,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'j',
ctrl: true,
meta: false,
shift: false,
alt: false,
ctrl: true,
cmd: false,
insertable: false,
sequence: '\n',
}),
@@ -1120,9 +1124,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'tab',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: false,
sequence: '\t',
}),
@@ -1137,9 +1142,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'tab',
ctrl: false,
meta: false,
shift: true,
alt: false,
ctrl: false,
cmd: false,
insertable: false,
sequence: '\u001b[9;2u',
}),
@@ -1159,9 +1165,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'backspace',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: false,
sequence: '\x7f',
}),
@@ -1183,25 +1190,28 @@ describe('useTextBuffer', () => {
act(() => {
result.current.handleInput({
name: 'backspace',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: false,
sequence: '\x7f',
});
result.current.handleInput({
name: 'backspace',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: false,
sequence: '\x7f',
});
result.current.handleInput({
name: 'backspace',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: false,
sequence: '\x7f',
});
@@ -1258,24 +1268,26 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'left',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: false,
sequence: '\x1b[D',
}),
); // cursor [0,1]
);
expect(getBufferState(result).cursor).toEqual([0, 1]);
act(() =>
result.current.handleInput({
name: 'right',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: false,
sequence: '\x1b[C',
}),
); // cursor [0,2]
);
expect(getBufferState(result).cursor).toEqual([0, 2]);
});
@@ -1288,9 +1300,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: '',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: true,
sequence: textWithAnsi,
}),
@@ -1305,9 +1318,10 @@ describe('useTextBuffer', () => {
act(() =>
result.current.handleInput({
name: 'return',
ctrl: false,
meta: false,
shift: true,
alt: false,
ctrl: false,
cmd: false,
insertable: true,
sequence: '\r',
}),
@@ -1509,13 +1523,13 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
describe('Input Sanitization', () => {
const createInput = (sequence: string) => ({
name: '',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: true,
sequence,
});
it.each([
{
input: '\x1B[31mHello\x1B[0m \x1B[32mWorld\x1B[0m',
@@ -1567,9 +1581,10 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
act(() =>
result.current.handleInput({
name: '',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: true,
sequence: largeTextWithUnsafe,
}),
@@ -1601,9 +1616,10 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
act(() =>
result.current.handleInput({
name: '',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: true,
sequence: largeTextWithAnsi,
}),
@@ -1625,9 +1641,10 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
act(() =>
result.current.handleInput({
name: '',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: true,
sequence: emojis,
}),
@@ -1816,9 +1833,10 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
act(() =>
result.current.handleInput({
name: 'return',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: true,
sequence: '\r',
}),
@@ -1837,9 +1855,10 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots
act(() =>
result.current.handleInput({
name: 'f1',
ctrl: false,
meta: false,
shift: false,
alt: false,
ctrl: false,
cmd: false,
insertable: false,
sequence: '\u001bOP',
}),