mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-02-01 22:48:03 +00:00
Support command/ctrl/alt backspace correctly (#17175)
This commit is contained in:
committed by
GitHub
parent
e894871afc
commit
f190b87223
@@ -19,14 +19,14 @@ available combinations.
|
||||
|
||||
| Action | Keys |
|
||||
| ------------------------------------------- | ------------------------------------------------------------ |
|
||||
| Move the cursor to the start of the line. | `Ctrl + A`<br />`Home (no Ctrl, no Shift)` |
|
||||
| Move the cursor to the end of the line. | `Ctrl + E`<br />`End (no Ctrl, no Shift)` |
|
||||
| Move the cursor up one line. | `Up Arrow (no Ctrl, no Cmd)` |
|
||||
| Move the cursor down one line. | `Down Arrow (no Ctrl, no Cmd)` |
|
||||
| Move the cursor one character to the left. | `Left Arrow (no Ctrl, no Cmd)`<br />`Ctrl + B` |
|
||||
| Move the cursor one character to the right. | `Right Arrow (no Ctrl, no Cmd)`<br />`Ctrl + F` |
|
||||
| Move the cursor one word to the left. | `Ctrl + Left Arrow`<br />`Cmd + Left Arrow`<br />`Cmd + B` |
|
||||
| Move the cursor one word to the right. | `Ctrl + Right Arrow`<br />`Cmd + Right Arrow`<br />`Cmd + F` |
|
||||
| Move the cursor to the start of the line. | `Ctrl + A`<br />`Home (no Shift, Ctrl)` |
|
||||
| Move the cursor to the end of the line. | `Ctrl + E`<br />`End (no Shift, Ctrl)` |
|
||||
| Move the cursor up one line. | `Up Arrow (no Shift, Alt, Ctrl, Cmd)` |
|
||||
| Move the cursor down one line. | `Down Arrow (no Shift, Alt, Ctrl, Cmd)` |
|
||||
| Move the cursor one character to the left. | `Left Arrow (no Shift, Alt, Ctrl, Cmd)`<br />`Ctrl + B` |
|
||||
| Move the cursor one character to the right. | `Right Arrow (no Shift, Alt, Ctrl, Cmd)`<br />`Ctrl + F` |
|
||||
| Move the cursor one word to the left. | `Ctrl + Left Arrow`<br />`Alt + Left Arrow`<br />`Alt + B` |
|
||||
| Move the cursor one word to the right. | `Ctrl + Right Arrow`<br />`Alt + Right Arrow`<br />`Alt + F` |
|
||||
|
||||
#### Editing
|
||||
|
||||
@@ -35,12 +35,12 @@ available combinations.
|
||||
| Delete from the cursor to the end of the line. | `Ctrl + K` |
|
||||
| Delete from the cursor to the start of the line. | `Ctrl + U` |
|
||||
| Clear all text in the input field. | `Ctrl + C` |
|
||||
| Delete the previous word. | `Ctrl + Backspace`<br />`Cmd + Backspace`<br />`Ctrl + W` |
|
||||
| Delete the next word. | `Ctrl + Delete`<br />`Cmd + Delete` |
|
||||
| Delete the previous word. | `Ctrl + Backspace`<br />`Alt + Backspace`<br />`Ctrl + W` |
|
||||
| Delete the next word. | `Ctrl + Delete`<br />`Alt + Delete` |
|
||||
| Delete the character to the left. | `Backspace`<br />`Ctrl + H` |
|
||||
| Delete the character to the right. | `Delete`<br />`Ctrl + D` |
|
||||
| Undo the most recent text edit. | `Ctrl + Z (no Shift)` |
|
||||
| Redo the most recent undone text edit. | `Ctrl + Shift + Z` |
|
||||
| Redo the most recent undone text edit. | `Shift + Ctrl + Z` |
|
||||
|
||||
#### Scrolling
|
||||
|
||||
@@ -84,12 +84,12 @@ available combinations.
|
||||
|
||||
#### Text Input
|
||||
|
||||
| Action | Keys |
|
||||
| ---------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| Submit the current prompt. | `Enter (no Ctrl, no Shift, no Cmd)` |
|
||||
| Insert a newline without submitting. | `Ctrl + Enter`<br />`Cmd + Enter`<br />`Shift + Enter`<br />`Ctrl + J` |
|
||||
| Open the current prompt in an external editor. | `Ctrl + X` |
|
||||
| Paste from the clipboard. | `Ctrl + V`<br />`Cmd + V` |
|
||||
| Action | Keys |
|
||||
| ---------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| Submit the current prompt. | `Enter (no Shift, Alt, Ctrl, Cmd)` |
|
||||
| Insert a newline without submitting. | `Ctrl + Enter`<br />`Cmd + Enter`<br />`Alt + Enter`<br />`Shift + Enter`<br />`Ctrl + J` |
|
||||
| Open the current prompt in an external editor. | `Ctrl + X` |
|
||||
| Paste from the clipboard. | `Ctrl + V`<br />`Cmd + V`<br />`Alt + V` |
|
||||
|
||||
#### App Controls
|
||||
|
||||
@@ -98,7 +98,7 @@ available combinations.
|
||||
| Toggle detailed error information. | `F12` |
|
||||
| Toggle the full TODO list. | `Ctrl + T` |
|
||||
| Show IDE context details. | `Ctrl + G` |
|
||||
| Toggle Markdown rendering. | `Cmd + M` |
|
||||
| Toggle Markdown rendering. | `Alt + M` |
|
||||
| Toggle copy mode when in alternate buffer mode. | `Ctrl + S` |
|
||||
| Toggle YOLO (auto-approval) mode for tool calls. | `Ctrl + Y` |
|
||||
| Cycle through approval modes: default (prompt), auto_edit (auto-approve edits), and plan (read-only). | `Shift + Tab` |
|
||||
|
||||
@@ -33,14 +33,17 @@ describe('keyBindings config', () => {
|
||||
expect(binding.key.length).toBeGreaterThan(0);
|
||||
|
||||
// Modifier properties should be boolean or undefined
|
||||
if (binding.ctrl !== undefined) {
|
||||
expect(typeof binding.ctrl).toBe('boolean');
|
||||
}
|
||||
if (binding.shift !== undefined) {
|
||||
expect(typeof binding.shift).toBe('boolean');
|
||||
}
|
||||
if (binding.command !== undefined) {
|
||||
expect(typeof binding.command).toBe('boolean');
|
||||
if (binding.alt !== undefined) {
|
||||
expect(typeof binding.alt).toBe('boolean');
|
||||
}
|
||||
if (binding.ctrl !== undefined) {
|
||||
expect(typeof binding.ctrl).toBe('boolean');
|
||||
}
|
||||
if (binding.cmd !== undefined) {
|
||||
expect(typeof binding.cmd).toBe('boolean');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,12 +90,14 @@ export enum Command {
|
||||
export interface KeyBinding {
|
||||
/** The key name (e.g., 'a', 'return', 'tab', 'escape') */
|
||||
key: string;
|
||||
/** Control key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */
|
||||
ctrl?: boolean;
|
||||
/** Shift key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */
|
||||
shift?: boolean;
|
||||
/** Command/meta key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */
|
||||
command?: boolean;
|
||||
/** Alt/Option key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */
|
||||
alt?: boolean;
|
||||
/** Control key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */
|
||||
ctrl?: boolean;
|
||||
/** Command/Windows/Super key requirement: true=must be pressed, false=must not be pressed, undefined=ignore */
|
||||
cmd?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,51 +121,54 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
// Cursor Movement
|
||||
[Command.HOME]: [
|
||||
{ key: 'a', ctrl: true },
|
||||
{ key: 'home', ctrl: false, shift: false },
|
||||
{ key: 'home', shift: false, ctrl: false },
|
||||
],
|
||||
[Command.END]: [
|
||||
{ key: 'e', ctrl: true },
|
||||
{ key: 'end', ctrl: false, shift: false },
|
||||
{ key: 'end', shift: false, ctrl: false },
|
||||
],
|
||||
[Command.MOVE_UP]: [
|
||||
{ key: 'up', shift: false, alt: false, ctrl: false, cmd: false },
|
||||
],
|
||||
[Command.MOVE_DOWN]: [
|
||||
{ key: 'down', shift: false, alt: false, ctrl: false, cmd: false },
|
||||
],
|
||||
[Command.MOVE_UP]: [{ key: 'up', ctrl: false, command: false }],
|
||||
[Command.MOVE_DOWN]: [{ key: 'down', ctrl: false, command: false }],
|
||||
[Command.MOVE_LEFT]: [
|
||||
{ key: 'left', ctrl: false, command: false },
|
||||
{ key: 'left', shift: false, alt: false, ctrl: false, cmd: false },
|
||||
{ key: 'b', ctrl: true },
|
||||
],
|
||||
[Command.MOVE_RIGHT]: [
|
||||
{ key: 'right', ctrl: false, command: false },
|
||||
{ key: 'right', shift: false, alt: false, ctrl: false, cmd: false },
|
||||
{ key: 'f', ctrl: true },
|
||||
],
|
||||
[Command.MOVE_WORD_LEFT]: [
|
||||
{ key: 'left', ctrl: true },
|
||||
{ key: 'left', command: true },
|
||||
{ key: 'b', command: true },
|
||||
{ key: 'left', alt: true },
|
||||
{ key: 'b', alt: true },
|
||||
],
|
||||
[Command.MOVE_WORD_RIGHT]: [
|
||||
{ key: 'right', ctrl: true },
|
||||
{ key: 'right', command: true },
|
||||
{ key: 'f', command: true },
|
||||
{ key: 'right', alt: true },
|
||||
{ key: 'f', alt: true },
|
||||
],
|
||||
|
||||
// Editing
|
||||
[Command.KILL_LINE_RIGHT]: [{ key: 'k', ctrl: true }],
|
||||
[Command.KILL_LINE_LEFT]: [{ key: 'u', ctrl: true }],
|
||||
[Command.CLEAR_INPUT]: [{ key: 'c', ctrl: true }],
|
||||
// Added command (meta/alt/option) for mac compatibility
|
||||
[Command.DELETE_WORD_BACKWARD]: [
|
||||
{ key: 'backspace', ctrl: true },
|
||||
{ key: 'backspace', command: true },
|
||||
{ key: 'backspace', alt: true },
|
||||
{ key: 'w', ctrl: true },
|
||||
],
|
||||
[Command.DELETE_WORD_FORWARD]: [
|
||||
{ key: 'delete', ctrl: true },
|
||||
{ key: 'delete', command: true },
|
||||
{ key: 'delete', alt: true },
|
||||
],
|
||||
[Command.DELETE_CHAR_LEFT]: [{ key: 'backspace' }, { key: 'h', ctrl: true }],
|
||||
[Command.DELETE_CHAR_RIGHT]: [{ key: 'delete' }, { key: 'd', ctrl: true }],
|
||||
[Command.UNDO]: [{ key: 'z', ctrl: true, shift: false }],
|
||||
[Command.REDO]: [{ key: 'z', ctrl: true, shift: true }],
|
||||
[Command.UNDO]: [{ key: 'z', shift: false, ctrl: true }],
|
||||
[Command.REDO]: [{ key: 'z', shift: true, ctrl: true }],
|
||||
|
||||
// Scrolling
|
||||
[Command.SCROLL_UP]: [{ key: 'up', shift: true }],
|
||||
@@ -180,10 +185,9 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
[Command.PAGE_DOWN]: [{ key: 'pagedown' }],
|
||||
|
||||
// History & Search
|
||||
[Command.HISTORY_UP]: [{ key: 'p', ctrl: true, shift: false }],
|
||||
[Command.HISTORY_DOWN]: [{ key: 'n', ctrl: true, shift: false }],
|
||||
[Command.HISTORY_UP]: [{ key: 'p', shift: false, ctrl: true }],
|
||||
[Command.HISTORY_DOWN]: [{ key: 'n', shift: false, ctrl: true }],
|
||||
[Command.REVERSE_SEARCH]: [{ key: 'r', ctrl: true }],
|
||||
// Note: original logic ONLY checked ctrl=false, ignored meta/shift/paste
|
||||
[Command.SUBMIT_REVERSE_SEARCH]: [{ key: 'return', ctrl: false }],
|
||||
[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]: [{ key: 'tab' }],
|
||||
|
||||
@@ -203,14 +207,13 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
|
||||
// Suggestions & Completions
|
||||
[Command.ACCEPT_SUGGESTION]: [{ key: 'tab' }, { key: 'return', ctrl: false }],
|
||||
// Completion navigation (arrow or Ctrl+P/N)
|
||||
[Command.COMPLETION_UP]: [
|
||||
{ key: 'up', shift: false },
|
||||
{ key: 'p', ctrl: true, shift: false },
|
||||
{ key: 'p', shift: false, ctrl: true },
|
||||
],
|
||||
[Command.COMPLETION_DOWN]: [
|
||||
{ key: 'down', shift: false },
|
||||
{ key: 'n', ctrl: true, shift: false },
|
||||
{ key: 'n', shift: false, ctrl: true },
|
||||
],
|
||||
[Command.EXPAND_SUGGESTION]: [{ key: 'right' }],
|
||||
[Command.COLLAPSE_SUGGESTION]: [{ key: 'left' }],
|
||||
@@ -220,30 +223,31 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
[Command.SUBMIT]: [
|
||||
{
|
||||
key: 'return',
|
||||
ctrl: false,
|
||||
command: false,
|
||||
shift: false,
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
cmd: false,
|
||||
},
|
||||
],
|
||||
// Split into multiple data-driven bindings
|
||||
// Now also includes shift+enter for multi-line input
|
||||
[Command.NEWLINE]: [
|
||||
{ key: 'return', ctrl: true },
|
||||
{ key: 'return', command: true },
|
||||
{ key: 'return', cmd: true },
|
||||
{ key: 'return', alt: true },
|
||||
{ key: 'return', shift: true },
|
||||
{ key: 'j', ctrl: true },
|
||||
],
|
||||
[Command.OPEN_EXTERNAL_EDITOR]: [{ key: 'x', ctrl: true }],
|
||||
[Command.PASTE_CLIPBOARD]: [
|
||||
{ key: 'v', ctrl: true },
|
||||
{ key: 'v', command: true },
|
||||
{ key: 'v', cmd: true },
|
||||
{ key: 'v', alt: true },
|
||||
],
|
||||
|
||||
// App Controls
|
||||
[Command.SHOW_ERROR_DETAILS]: [{ key: 'f12' }],
|
||||
[Command.SHOW_FULL_TODOS]: [{ key: 't', ctrl: true }],
|
||||
[Command.SHOW_IDE_CONTEXT_DETAIL]: [{ key: 'g', ctrl: true }],
|
||||
[Command.TOGGLE_MARKDOWN]: [{ key: 'm', command: true }],
|
||||
[Command.TOGGLE_MARKDOWN]: [{ key: 'm', alt: true }],
|
||||
[Command.TOGGLE_COPY_MODE]: [{ key: 's', ctrl: true }],
|
||||
[Command.TOGGLE_YOLO]: [{ key: 'y', ctrl: true }],
|
||||
[Command.CYCLE_APPROVAL_MODE]: [{ key: 'tab', shift: true }],
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -154,9 +154,10 @@ function formatBindings(bindings: readonly KeyBinding[]): string[] {
|
||||
|
||||
function formatBinding(binding: KeyBinding): string {
|
||||
const modifiers: string[] = [];
|
||||
if (binding.ctrl) modifiers.push('Ctrl');
|
||||
if (binding.command) modifiers.push('Cmd');
|
||||
if (binding.shift) modifiers.push('Shift');
|
||||
if (binding.alt) modifiers.push('Alt');
|
||||
if (binding.ctrl) modifiers.push('Ctrl');
|
||||
if (binding.cmd) modifiers.push('Cmd');
|
||||
|
||||
const keyName = formatKeyName(binding.key);
|
||||
if (!keyName) {
|
||||
@@ -167,12 +168,13 @@ function formatBinding(binding: KeyBinding): string {
|
||||
let combo = segments.join(' + ');
|
||||
|
||||
const restrictions: string[] = [];
|
||||
if (binding.ctrl === false) restrictions.push('no Ctrl');
|
||||
if (binding.shift === false) restrictions.push('no Shift');
|
||||
if (binding.command === false) restrictions.push('no Cmd');
|
||||
if (binding.shift === false) restrictions.push('Shift');
|
||||
if (binding.alt === false) restrictions.push('Alt');
|
||||
if (binding.ctrl === false) restrictions.push('Ctrl');
|
||||
if (binding.cmd === false) restrictions.push('Cmd');
|
||||
|
||||
if (restrictions.length > 0) {
|
||||
combo = `${combo} (${restrictions.join(', ')})`;
|
||||
combo = `${combo} (no ${restrictions.join(', ')})`;
|
||||
}
|
||||
|
||||
return combo ? `\`${combo}\`` : '';
|
||||
|
||||
@@ -36,7 +36,7 @@ describe('generate-keybindings-doc', () => {
|
||||
},
|
||||
{
|
||||
description: 'Submit with Enter if no modifiers are held.',
|
||||
bindings: [{ key: 'return', ctrl: false, shift: false }],
|
||||
bindings: [{ key: 'return', shift: false, ctrl: false }],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -47,7 +47,7 @@ describe('generate-keybindings-doc', () => {
|
||||
description: 'Move up through results.',
|
||||
bindings: [
|
||||
{ key: 'up', shift: false },
|
||||
{ key: 'p', ctrl: true, shift: false },
|
||||
{ key: 'p', shift: false, ctrl: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -59,7 +59,7 @@ describe('generate-keybindings-doc', () => {
|
||||
expect(markdown).toContain('Trigger custom action.');
|
||||
expect(markdown).toContain('`Ctrl + X`');
|
||||
expect(markdown).toContain('Submit with Enter if no modifiers are held.');
|
||||
expect(markdown).toContain('`Enter (no Ctrl, no Shift)`');
|
||||
expect(markdown).toContain('`Enter (no Shift, Ctrl)`');
|
||||
expect(markdown).toContain('#### Navigation');
|
||||
expect(markdown).toContain('Move up through results.');
|
||||
expect(markdown).toContain('`Up Arrow (no Shift)`');
|
||||
|
||||
Reference in New Issue
Block a user