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
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user