mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-28 22:14:52 -07:00
Support command/ctrl/alt backspace correctly (#17175)
This commit is contained in:
committed by
GitHub
parent
e894871afc
commit
f190b87223
@@ -1660,9 +1660,10 @@ describe('AppContainer State Management', () => {
|
||||
act(() => {
|
||||
handleGlobalKeypress({
|
||||
name: 'c',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
...key,
|
||||
} as Key);
|
||||
});
|
||||
@@ -1870,9 +1871,10 @@ describe('AppContainer State Management', () => {
|
||||
act(() => {
|
||||
handleGlobalKeypress({
|
||||
name: 's',
|
||||
ctrl: true,
|
||||
meta: false,
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: true,
|
||||
cmd: false,
|
||||
insertable: false,
|
||||
sequence: '\x13',
|
||||
});
|
||||
@@ -1896,9 +1898,10 @@ describe('AppContainer State Management', () => {
|
||||
act(() => {
|
||||
handleGlobalKeypress({
|
||||
name: 's',
|
||||
ctrl: true,
|
||||
meta: false,
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: true,
|
||||
cmd: false,
|
||||
insertable: false,
|
||||
sequence: '\x13',
|
||||
});
|
||||
@@ -1910,9 +1913,10 @@ describe('AppContainer State Management', () => {
|
||||
act(() => {
|
||||
handleGlobalKeypress({
|
||||
name: 'any', // Any key should exit copy mode
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
insertable: true,
|
||||
sequence: 'a',
|
||||
});
|
||||
@@ -1930,9 +1934,10 @@ describe('AppContainer State Management', () => {
|
||||
act(() => {
|
||||
handleGlobalKeypress({
|
||||
name: 's',
|
||||
ctrl: true,
|
||||
meta: false,
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: true,
|
||||
cmd: false,
|
||||
insertable: false,
|
||||
sequence: '\x13',
|
||||
});
|
||||
@@ -1945,9 +1950,10 @@ describe('AppContainer State Management', () => {
|
||||
act(() => {
|
||||
handleGlobalKeypress({
|
||||
name: 'a',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
insertable: true,
|
||||
sequence: 'a',
|
||||
});
|
||||
|
||||
@@ -108,10 +108,10 @@ describe('ApiAuthDialog', () => {
|
||||
|
||||
keypressHandler({
|
||||
name: keyName,
|
||||
sequence,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
sequence,
|
||||
});
|
||||
|
||||
expect(expectedCall).toHaveBeenCalledWith(...args);
|
||||
@@ -137,9 +137,9 @@ describe('ApiAuthDialog', () => {
|
||||
|
||||
await keypressHandler({
|
||||
name: 'c',
|
||||
ctrl: true,
|
||||
meta: false,
|
||||
shift: false,
|
||||
ctrl: true,
|
||||
cmd: false,
|
||||
});
|
||||
|
||||
expect(clearApiKey).toHaveBeenCalled();
|
||||
|
||||
@@ -48,10 +48,10 @@ describe('LoginWithGoogleRestartDialog', () => {
|
||||
|
||||
keypressHandler({
|
||||
name: 'escape',
|
||||
sequence: '\u001b',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
sequence: '\u001b',
|
||||
});
|
||||
|
||||
expect(onDismiss).toHaveBeenCalledTimes(1);
|
||||
@@ -67,10 +67,10 @@ describe('LoginWithGoogleRestartDialog', () => {
|
||||
|
||||
keypressHandler({
|
||||
name: keyName,
|
||||
sequence: keyName,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
sequence: keyName,
|
||||
});
|
||||
|
||||
// Advance timers to trigger the setTimeout callback
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
|
||||
@@ -91,7 +91,7 @@ export const INFORMATIVE_TIPS = [
|
||||
'See full, untruncated responses with Ctrl+S…',
|
||||
'Toggle auto-approval (YOLO mode) for all tools with Ctrl+Y…',
|
||||
'Cycle through approval modes (Default, Plan, Auto-Edit) with Shift+Tab…',
|
||||
'Toggle Markdown rendering (raw markdown mode) with Option+M…',
|
||||
'Toggle Markdown rendering (raw markdown mode) with Alt+M…',
|
||||
'Toggle shell mode by typing ! in an empty prompt…',
|
||||
'Insert a newline with a backslash (\\) followed by Enter…',
|
||||
'Navigate your prompt history with the Up and Down arrows…',
|
||||
|
||||
@@ -101,9 +101,9 @@ describe('KeypressContext', () => {
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'return',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -116,9 +116,9 @@ describe('KeypressContext', () => {
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'return',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: true,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -127,17 +127,17 @@ describe('KeypressContext', () => {
|
||||
{
|
||||
modifier: 'Shift',
|
||||
sequence: '\x1b[57414;2u',
|
||||
expected: { ctrl: false, meta: false, shift: true },
|
||||
expected: { shift: true, ctrl: false, cmd: false },
|
||||
},
|
||||
{
|
||||
modifier: 'Ctrl',
|
||||
sequence: '\x1b[57414;5u',
|
||||
expected: { ctrl: true, meta: false, shift: false },
|
||||
expected: { shift: false, ctrl: true, cmd: false },
|
||||
},
|
||||
{
|
||||
modifier: 'Alt',
|
||||
sequence: '\x1b[57414;3u',
|
||||
expected: { ctrl: false, meta: true, shift: false },
|
||||
expected: { shift: false, alt: true, ctrl: false, cmd: false },
|
||||
},
|
||||
])(
|
||||
'should handle numpad enter with $modifier modifier',
|
||||
@@ -163,9 +163,9 @@ describe('KeypressContext', () => {
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'j',
|
||||
ctrl: true,
|
||||
meta: false,
|
||||
shift: false,
|
||||
ctrl: true,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -178,9 +178,10 @@ describe('KeypressContext', () => {
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'return',
|
||||
ctrl: false,
|
||||
meta: true,
|
||||
shift: false,
|
||||
alt: true,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -202,7 +203,13 @@ describe('KeypressContext', () => {
|
||||
|
||||
act(() => stdin.write('a'));
|
||||
expect(keyHandler).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ name: 'a' }),
|
||||
expect.objectContaining({
|
||||
name: 'a',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
|
||||
act(() => stdin.write('\r'));
|
||||
@@ -212,6 +219,10 @@ describe('KeypressContext', () => {
|
||||
name: 'return',
|
||||
sequence: '\r',
|
||||
insertable: true,
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -228,6 +239,10 @@ describe('KeypressContext', () => {
|
||||
expect(keyHandler).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'return',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -245,6 +260,10 @@ describe('KeypressContext', () => {
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'escape',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -266,11 +285,21 @@ describe('KeypressContext', () => {
|
||||
|
||||
expect(keyHandler).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ name: 'escape', meta: true }),
|
||||
expect.objectContaining({
|
||||
name: 'escape',
|
||||
shift: false,
|
||||
alt: true,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
expect(keyHandler).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({ name: 'escape', meta: true }),
|
||||
expect.objectContaining({
|
||||
name: 'escape',
|
||||
shift: false,
|
||||
alt: true,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -296,7 +325,9 @@ describe('KeypressContext', () => {
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'escape',
|
||||
meta: true,
|
||||
shift: false,
|
||||
alt: true,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -318,17 +349,17 @@ describe('KeypressContext', () => {
|
||||
{
|
||||
name: 'Backspace',
|
||||
inputSequence: '\x1b[127u',
|
||||
expected: { name: 'backspace', meta: false },
|
||||
expected: { name: 'backspace', alt: false, cmd: false },
|
||||
},
|
||||
{
|
||||
name: 'Option+Backspace',
|
||||
name: 'Alt+Backspace',
|
||||
inputSequence: '\x1b[127;3u',
|
||||
expected: { name: 'backspace', meta: true },
|
||||
expected: { name: 'backspace', alt: true, cmd: false },
|
||||
},
|
||||
{
|
||||
name: 'Ctrl+Backspace',
|
||||
inputSequence: '\x1b[127;5u',
|
||||
expected: { name: 'backspace', ctrl: true },
|
||||
expected: { name: 'backspace', alt: false, ctrl: true, cmd: false },
|
||||
},
|
||||
{
|
||||
name: 'Shift+Space',
|
||||
@@ -612,14 +643,17 @@ describe('KeypressContext', () => {
|
||||
{ sequence: `\x1b[27;5;9~`, expected: { name: 'tab', ctrl: true } },
|
||||
{
|
||||
sequence: `\x1b[27;6;9~`,
|
||||
expected: { name: 'tab', ctrl: true, shift: true },
|
||||
expected: { name: 'tab', shift: true, ctrl: true },
|
||||
},
|
||||
// XTerm Function Key
|
||||
{ sequence: `\x1b[1;129A`, expected: { name: 'up' } },
|
||||
{ sequence: `\x1b[1;2H`, expected: { name: 'home', shift: true } },
|
||||
{ sequence: `\x1b[1;5F`, expected: { name: 'end', ctrl: true } },
|
||||
{ sequence: `\x1b[1;1P`, expected: { name: 'f1' } },
|
||||
{ sequence: `\x1b[1;3Q`, expected: { name: 'f2', meta: true } },
|
||||
{
|
||||
sequence: `\x1b[1;3Q`,
|
||||
expected: { name: 'f2', alt: true, cmd: false },
|
||||
},
|
||||
// Tilde Function Keys
|
||||
{ sequence: `\x1b[3~`, expected: { name: 'delete' } },
|
||||
{ sequence: `\x1b[5~`, expected: { name: 'pageup' } },
|
||||
@@ -637,33 +671,75 @@ describe('KeypressContext', () => {
|
||||
// Legacy Arrows
|
||||
{
|
||||
sequence: `\x1b[A`,
|
||||
expected: { name: 'up', ctrl: false, meta: false, shift: false },
|
||||
expected: {
|
||||
name: 'up',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequence: `\x1b[B`,
|
||||
expected: { name: 'down', ctrl: false, meta: false, shift: false },
|
||||
expected: {
|
||||
name: 'down',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequence: `\x1b[C`,
|
||||
expected: { name: 'right', ctrl: false, meta: false, shift: false },
|
||||
expected: {
|
||||
name: 'right',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequence: `\x1b[D`,
|
||||
expected: { name: 'left', ctrl: false, meta: false, shift: false },
|
||||
expected: {
|
||||
name: 'left',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
},
|
||||
},
|
||||
|
||||
// Legacy Home/End
|
||||
{
|
||||
sequence: `\x1b[H`,
|
||||
expected: { name: 'home', ctrl: false, meta: false, shift: false },
|
||||
expected: {
|
||||
name: 'home',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequence: `\x1b[F`,
|
||||
expected: { name: 'end', ctrl: false, meta: false, shift: false },
|
||||
expected: {
|
||||
name: 'end',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequence: `\x1b[5H`,
|
||||
expected: { name: 'home', ctrl: true, meta: false, shift: false },
|
||||
expected: {
|
||||
name: 'home',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: true,
|
||||
cmd: false,
|
||||
},
|
||||
},
|
||||
])(
|
||||
'should recognize sequence "$sequence" as $expected.name',
|
||||
@@ -690,11 +766,23 @@ describe('KeypressContext', () => {
|
||||
|
||||
expect(keyHandler).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ name: 'delete' }),
|
||||
expect.objectContaining({
|
||||
name: 'delete',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
expect(keyHandler).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({ name: 'delete' }),
|
||||
expect.objectContaining({
|
||||
name: 'delete',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -751,9 +839,10 @@ describe('KeypressContext', () => {
|
||||
chunk: `\x1b[${keycode};3u`,
|
||||
expected: {
|
||||
name: key,
|
||||
ctrl: false,
|
||||
meta: true,
|
||||
shift: false,
|
||||
alt: true,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
},
|
||||
};
|
||||
} else if (terminal === 'MacTerminal') {
|
||||
@@ -766,24 +855,26 @@ describe('KeypressContext', () => {
|
||||
expected: {
|
||||
sequence: `\x1b${key}`,
|
||||
name: key,
|
||||
ctrl: false,
|
||||
meta: true,
|
||||
shift: false,
|
||||
alt: true,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// iTerm2 and VSCode send accented characters (å, ø, µ)
|
||||
// Note: µ (mu) is sent with meta:false on iTerm2/VSCode but
|
||||
// gets converted to m with meta:true
|
||||
// Note: µ (mu) is sent with alt:false on iTerm2/VSCode but
|
||||
// gets converted to m with alt:true
|
||||
return {
|
||||
terminal,
|
||||
key,
|
||||
chunk: accentedChar,
|
||||
expected: {
|
||||
name: key,
|
||||
ctrl: false,
|
||||
meta: true, // Always expect meta:true after conversion
|
||||
shift: false,
|
||||
alt: true, // Always expect alt:true after conversion
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
sequence: accentedChar,
|
||||
},
|
||||
};
|
||||
@@ -825,7 +916,10 @@ describe('KeypressContext', () => {
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sequence: '\\',
|
||||
meta: false,
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -858,6 +952,10 @@ describe('KeypressContext', () => {
|
||||
expect.objectContaining({
|
||||
name: 'undefined',
|
||||
sequence: INCOMPLETE_KITTY_SEQUENCE,
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -876,6 +974,10 @@ describe('KeypressContext', () => {
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sequence: '\x1b[m',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1048,6 +1150,10 @@ describe('KeypressContext', () => {
|
||||
expect.objectContaining({
|
||||
name: 'a',
|
||||
sequence: 'a',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1162,7 +1268,14 @@ describe('KeypressContext', () => {
|
||||
});
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'f12', sequence: '\u001b[24~' }),
|
||||
expect.objectContaining({
|
||||
name: 'f12',
|
||||
sequence: '\u001b[24~',
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -251,9 +251,10 @@ function bufferPaste(keypressHandler: KeypressHandler): KeypressHandler {
|
||||
if (buffer.length > 0) {
|
||||
keypressHandler({
|
||||
name: 'paste',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
insertable: true,
|
||||
sequence: buffer,
|
||||
});
|
||||
@@ -300,9 +301,10 @@ function* emitKeys(
|
||||
let escaped = false;
|
||||
|
||||
let name = undefined;
|
||||
let ctrl = false;
|
||||
let meta = false;
|
||||
let shift = false;
|
||||
let alt = false;
|
||||
let ctrl = false;
|
||||
let cmd = false;
|
||||
let code = undefined;
|
||||
let insertable = false;
|
||||
|
||||
@@ -353,9 +355,10 @@ function* emitKeys(
|
||||
const decoded = Buffer.from(base64Data, 'base64').toString('utf-8');
|
||||
keypressHandler({
|
||||
name: 'paste',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
insertable: true,
|
||||
sequence: decoded,
|
||||
});
|
||||
@@ -490,9 +493,10 @@ function* emitKeys(
|
||||
}
|
||||
|
||||
// Parse the key modifier
|
||||
ctrl = !!(modifier & 4);
|
||||
meta = !!(modifier & 10); // use 10 to catch both alt (2) and meta (8).
|
||||
shift = !!(modifier & 1);
|
||||
alt = !!(modifier & 2);
|
||||
ctrl = !!(modifier & 4);
|
||||
cmd = !!(modifier & 8);
|
||||
|
||||
const keyInfo = KEY_INFO_MAP[code];
|
||||
if (keyInfo) {
|
||||
@@ -503,13 +507,16 @@ function* emitKeys(
|
||||
if (keyInfo.ctrl) {
|
||||
ctrl = true;
|
||||
}
|
||||
if (name === 'space' && !ctrl && !meta) {
|
||||
if (name === 'space' && !ctrl && !cmd && !alt) {
|
||||
sequence = ' ';
|
||||
insertable = true;
|
||||
}
|
||||
} else {
|
||||
name = 'undefined';
|
||||
if ((ctrl || meta) && (code.endsWith('u') || code.endsWith('~'))) {
|
||||
if (
|
||||
(ctrl || cmd || alt) &&
|
||||
(code.endsWith('u') || code.endsWith('~'))
|
||||
) {
|
||||
// CSI-u or tilde-coded functional keys: ESC [ <code> ; <mods> (u|~)
|
||||
const codeNumber = parseInt(code.slice(1, -1), 10);
|
||||
if (
|
||||
@@ -523,26 +530,26 @@ function* emitKeys(
|
||||
} else if (ch === '\r') {
|
||||
// carriage return
|
||||
name = 'return';
|
||||
meta = escaped;
|
||||
alt = escaped;
|
||||
} else if (escaped && ch === '\n') {
|
||||
// Alt+Enter (linefeed), should be consistent with carriage return
|
||||
name = 'return';
|
||||
meta = escaped;
|
||||
alt = escaped;
|
||||
} else if (ch === '\t') {
|
||||
// tab
|
||||
name = 'tab';
|
||||
meta = escaped;
|
||||
alt = escaped;
|
||||
} else if (ch === '\b' || ch === '\x7f') {
|
||||
// backspace or ctrl+h
|
||||
name = 'backspace';
|
||||
meta = escaped;
|
||||
alt = escaped;
|
||||
} else if (ch === ESC) {
|
||||
// escape key
|
||||
name = 'escape';
|
||||
meta = escaped;
|
||||
alt = escaped;
|
||||
} else if (ch === ' ') {
|
||||
name = 'space';
|
||||
meta = escaped;
|
||||
alt = escaped;
|
||||
insertable = true;
|
||||
} else if (!escaped && ch <= '\x1a') {
|
||||
// ctrl+letter
|
||||
@@ -552,29 +559,30 @@ function* emitKeys(
|
||||
// Letter, number, shift+letter
|
||||
name = ch.toLowerCase();
|
||||
shift = /^[A-Z]$/.exec(ch) !== null;
|
||||
meta = escaped;
|
||||
alt = escaped;
|
||||
insertable = true;
|
||||
} else if (MAC_ALT_KEY_CHARACTER_MAP[ch] && process.platform === 'darwin') {
|
||||
name = MAC_ALT_KEY_CHARACTER_MAP[ch];
|
||||
meta = true;
|
||||
alt = true;
|
||||
} else if (sequence === `${ESC}${ESC}`) {
|
||||
// Double escape
|
||||
name = 'escape';
|
||||
meta = true;
|
||||
alt = true;
|
||||
|
||||
// Emit first escape key here, then continue processing
|
||||
keypressHandler({
|
||||
name: 'escape',
|
||||
ctrl,
|
||||
meta,
|
||||
shift,
|
||||
alt,
|
||||
ctrl,
|
||||
cmd,
|
||||
insertable: false,
|
||||
sequence: ESC,
|
||||
});
|
||||
} else if (escaped) {
|
||||
// Escape sequence timeout
|
||||
name = ch.length ? undefined : 'escape';
|
||||
meta = true;
|
||||
alt = true;
|
||||
} else {
|
||||
// Any other character is considered printable.
|
||||
insertable = true;
|
||||
@@ -586,9 +594,10 @@ function* emitKeys(
|
||||
) {
|
||||
keypressHandler({
|
||||
name: name || '',
|
||||
ctrl,
|
||||
meta,
|
||||
shift,
|
||||
alt,
|
||||
ctrl,
|
||||
cmd,
|
||||
insertable,
|
||||
sequence,
|
||||
});
|
||||
@@ -599,9 +608,10 @@ function* emitKeys(
|
||||
|
||||
export interface Key {
|
||||
name: string;
|
||||
ctrl: boolean;
|
||||
meta: boolean;
|
||||
shift: boolean;
|
||||
alt: boolean;
|
||||
ctrl: boolean;
|
||||
cmd: boolean; // Command/Windows/Super key
|
||||
insertable: boolean;
|
||||
sequence: string;
|
||||
}
|
||||
|
||||
@@ -139,63 +139,63 @@ describe('MouseContext', () => {
|
||||
sequence: '\x1b[<0;10;20M',
|
||||
expected: {
|
||||
name: 'left-press',
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequence: '\x1b[<0;10;20m',
|
||||
expected: {
|
||||
name: 'left-release',
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequence: '\x1b[<2;10;20M',
|
||||
expected: {
|
||||
name: 'right-press',
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequence: '\x1b[<1;10;20M',
|
||||
expected: {
|
||||
name: 'middle-press',
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequence: '\x1b[<64;10;20M',
|
||||
expected: {
|
||||
name: 'scroll-up',
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequence: '\x1b[<65;10;20M',
|
||||
expected: {
|
||||
name: 'scroll-down',
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequence: '\x1b[<32;10;20M',
|
||||
expected: {
|
||||
name: 'move',
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -208,7 +208,7 @@ describe('MouseContext', () => {
|
||||
}, // Alt + left press
|
||||
{
|
||||
sequence: '\x1b[<20;10;20M',
|
||||
expected: { name: 'left-press', ctrl: true, shift: true },
|
||||
expected: { name: 'left-press', shift: true, ctrl: true },
|
||||
}, // Ctrl + Shift + left press
|
||||
{
|
||||
sequence: '\x1b[<68;10;20M',
|
||||
|
||||
@@ -69,7 +69,7 @@ export function keyToAnsi(key: Key): string | null {
|
||||
}
|
||||
|
||||
// If it's a simple character, return it.
|
||||
if (!key.ctrl && !key.meta && key.sequence) {
|
||||
if (!key.ctrl && !key.cmd && key.sequence) {
|
||||
return key.sequence;
|
||||
}
|
||||
|
||||
|
||||
@@ -314,8 +314,8 @@ describe('useApprovalModeIndicator', () => {
|
||||
act(() => {
|
||||
capturedUseKeypressHandler({
|
||||
name: 'a',
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
ctrl: true,
|
||||
} as Key);
|
||||
});
|
||||
expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled();
|
||||
|
||||
@@ -114,7 +114,13 @@ describe(`useKeypress`, () => {
|
||||
const key = { name: 'return', sequence: '\x1B\r' };
|
||||
act(() => stdin.write(key.sequence));
|
||||
expect(onKeypress).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ ...key, meta: true }),
|
||||
expect.objectContaining({
|
||||
...key,
|
||||
shift: false,
|
||||
alt: true,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -140,9 +146,10 @@ describe(`useKeypress`, () => {
|
||||
expect(onKeypress).toHaveBeenCalledTimes(1);
|
||||
expect(onKeypress).toHaveBeenCalledWith({
|
||||
name: 'paste',
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
insertable: true,
|
||||
sequence: pasteText,
|
||||
});
|
||||
|
||||
@@ -59,7 +59,8 @@ describe('useSelectionList', () => {
|
||||
name,
|
||||
sequence,
|
||||
ctrl: options.ctrl ?? false,
|
||||
meta: false,
|
||||
cmd: false,
|
||||
alt: false,
|
||||
shift: options.shift ?? false,
|
||||
insertable: false,
|
||||
};
|
||||
@@ -328,7 +329,8 @@ describe('useSelectionList', () => {
|
||||
name,
|
||||
sequence: name,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
cmd: false,
|
||||
alt: false,
|
||||
shift: false,
|
||||
insertable: true,
|
||||
};
|
||||
@@ -377,7 +379,8 @@ describe('useSelectionList', () => {
|
||||
name,
|
||||
sequence: name,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
cmd: false,
|
||||
alt: false,
|
||||
shift: false,
|
||||
insertable: false,
|
||||
};
|
||||
|
||||
@@ -36,9 +36,10 @@ vi.mock('../contexts/VimModeContext.js', () => ({
|
||||
const createKey = (partial: Partial<Key>): Key => ({
|
||||
name: partial.name || '',
|
||||
sequence: partial.sequence || '',
|
||||
ctrl: partial.ctrl || false,
|
||||
meta: partial.meta || false,
|
||||
shift: partial.shift || false,
|
||||
alt: partial.alt || false,
|
||||
ctrl: partial.ctrl || false,
|
||||
cmd: partial.cmd || false,
|
||||
insertable: partial.insertable || false,
|
||||
...partial,
|
||||
});
|
||||
|
||||
@@ -280,8 +280,9 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
|
||||
// Special handling for Enter key to allow command submission (lower priority than completion)
|
||||
if (
|
||||
normalizedKey.name === 'return' &&
|
||||
!normalizedKey.alt &&
|
||||
!normalizedKey.ctrl &&
|
||||
!normalizedKey.meta
|
||||
!normalizedKey.cmd
|
||||
) {
|
||||
if (buffer.text.trim() && onSubmit) {
|
||||
// Handle command submission directly
|
||||
@@ -309,9 +310,10 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
|
||||
(key: Key): Key => ({
|
||||
name: key.name || '',
|
||||
sequence: key.sequence || '',
|
||||
ctrl: key.ctrl || false,
|
||||
meta: key.meta || false,
|
||||
shift: key.shift || false,
|
||||
alt: key.alt || false,
|
||||
ctrl: key.ctrl || false,
|
||||
cmd: key.cmd || false,
|
||||
insertable: key.insertable || false,
|
||||
}),
|
||||
[],
|
||||
|
||||
@@ -13,9 +13,10 @@ import type { Key } from './hooks/useKeypress.js';
|
||||
describe('keyMatchers', () => {
|
||||
const createKey = (name: string, mods: Partial<Key> = {}): Key => ({
|
||||
name,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
insertable: false,
|
||||
sequence: name,
|
||||
...mods,
|
||||
@@ -70,8 +71,8 @@ describe('keyMatchers', () => {
|
||||
command: Command.MOVE_WORD_LEFT,
|
||||
positive: [
|
||||
createKey('left', { ctrl: true }),
|
||||
createKey('left', { meta: true }),
|
||||
createKey('b', { meta: true }),
|
||||
createKey('left', { alt: true }),
|
||||
createKey('b', { alt: true }),
|
||||
],
|
||||
negative: [createKey('left'), createKey('b', { ctrl: true })],
|
||||
},
|
||||
@@ -79,8 +80,8 @@ describe('keyMatchers', () => {
|
||||
command: Command.MOVE_WORD_RIGHT,
|
||||
positive: [
|
||||
createKey('right', { ctrl: true }),
|
||||
createKey('right', { meta: true }),
|
||||
createKey('f', { meta: true }),
|
||||
createKey('right', { alt: true }),
|
||||
createKey('f', { alt: true }),
|
||||
],
|
||||
negative: [createKey('right'), createKey('f', { ctrl: true })],
|
||||
},
|
||||
@@ -115,7 +116,7 @@ describe('keyMatchers', () => {
|
||||
command: Command.DELETE_WORD_BACKWARD,
|
||||
positive: [
|
||||
createKey('backspace', { ctrl: true }),
|
||||
createKey('backspace', { meta: true }),
|
||||
createKey('backspace', { alt: true }),
|
||||
createKey('w', { ctrl: true }),
|
||||
],
|
||||
negative: [createKey('backspace'), createKey('delete', { ctrl: true })],
|
||||
@@ -124,19 +125,19 @@ describe('keyMatchers', () => {
|
||||
command: Command.DELETE_WORD_FORWARD,
|
||||
positive: [
|
||||
createKey('delete', { ctrl: true }),
|
||||
createKey('delete', { meta: true }),
|
||||
createKey('delete', { alt: true }),
|
||||
],
|
||||
negative: [createKey('delete'), createKey('backspace', { ctrl: true })],
|
||||
},
|
||||
{
|
||||
command: Command.UNDO,
|
||||
positive: [createKey('z', { ctrl: true, shift: false })],
|
||||
negative: [createKey('z'), createKey('z', { ctrl: true, shift: true })],
|
||||
positive: [createKey('z', { shift: false, ctrl: true })],
|
||||
negative: [createKey('z'), createKey('z', { shift: true, ctrl: true })],
|
||||
},
|
||||
{
|
||||
command: Command.REDO,
|
||||
positive: [createKey('z', { ctrl: true, shift: true })],
|
||||
negative: [createKey('z'), createKey('z', { ctrl: true, shift: false })],
|
||||
positive: [createKey('z', { shift: true, ctrl: true })],
|
||||
negative: [createKey('z'), createKey('z', { shift: false, ctrl: true })],
|
||||
},
|
||||
|
||||
// Screen control
|
||||
@@ -243,14 +244,16 @@ describe('keyMatchers', () => {
|
||||
positive: [createKey('return')],
|
||||
negative: [
|
||||
createKey('return', { ctrl: true }),
|
||||
createKey('return', { meta: true }),
|
||||
createKey('return', { cmd: true }),
|
||||
createKey('return', { alt: true }),
|
||||
],
|
||||
},
|
||||
{
|
||||
command: Command.NEWLINE,
|
||||
positive: [
|
||||
createKey('return', { ctrl: true }),
|
||||
createKey('return', { meta: true }),
|
||||
createKey('return', { cmd: true }),
|
||||
createKey('return', { alt: true }),
|
||||
],
|
||||
negative: [createKey('return'), createKey('n')],
|
||||
},
|
||||
@@ -285,13 +288,13 @@ describe('keyMatchers', () => {
|
||||
},
|
||||
{
|
||||
command: Command.TOGGLE_MARKDOWN,
|
||||
positive: [createKey('m', { meta: true })],
|
||||
positive: [createKey('m', { alt: true })],
|
||||
negative: [createKey('m'), createKey('m', { shift: true })],
|
||||
},
|
||||
{
|
||||
command: Command.TOGGLE_COPY_MODE,
|
||||
positive: [createKey('s', { ctrl: true })],
|
||||
negative: [createKey('s'), createKey('s', { meta: true })],
|
||||
negative: [createKey('s'), createKey('s', { alt: true })],
|
||||
},
|
||||
{
|
||||
command: Command.QUIT,
|
||||
@@ -333,7 +336,7 @@ describe('keyMatchers', () => {
|
||||
{
|
||||
command: Command.TOGGLE_YOLO,
|
||||
positive: [createKey('y', { ctrl: true })],
|
||||
negative: [createKey('y'), createKey('y', { meta: true })],
|
||||
negative: [createKey('y'), createKey('y', { alt: true })],
|
||||
},
|
||||
{
|
||||
command: Command.CYCLE_APPROVAL_MODE,
|
||||
@@ -401,13 +404,13 @@ describe('keyMatchers', () => {
|
||||
...defaultKeyBindings,
|
||||
[Command.QUIT]: [
|
||||
{ key: 'q', ctrl: true },
|
||||
{ key: 'q', command: true },
|
||||
{ key: 'q', alt: true },
|
||||
],
|
||||
};
|
||||
|
||||
const matchers = createKeyMatchers(config);
|
||||
expect(matchers[Command.QUIT](createKey('q', { ctrl: true }))).toBe(true);
|
||||
expect(matchers[Command.QUIT](createKey('q', { meta: true }))).toBe(true);
|
||||
expect(matchers[Command.QUIT](createKey('q', { alt: true }))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,28 +13,17 @@ import { Command, defaultKeyBindings } from '../config/keyBindings.js';
|
||||
* Pure data-driven matching logic
|
||||
*/
|
||||
function matchKeyBinding(keyBinding: KeyBinding, key: Key): boolean {
|
||||
if (keyBinding.key !== key.name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check modifiers - follow original logic:
|
||||
// undefined = ignore this modifier (original behavior)
|
||||
// true = modifier must be pressed
|
||||
// false = modifier must NOT be pressed
|
||||
|
||||
if (keyBinding.ctrl !== undefined && key.ctrl !== keyBinding.ctrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (keyBinding.shift !== undefined && key.shift !== keyBinding.shift) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (keyBinding.command !== undefined && key.meta !== keyBinding.command) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return (
|
||||
keyBinding.key === key.name &&
|
||||
(keyBinding.shift === undefined || key.shift === keyBinding.shift) &&
|
||||
(keyBinding.alt === undefined || key.alt === keyBinding.alt) &&
|
||||
(keyBinding.ctrl === undefined || key.ctrl === keyBinding.ctrl) &&
|
||||
(keyBinding.cmd === undefined || key.cmd === keyBinding.cmd)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user