mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 12:40:44 +00:00
Sync upstream Gemini-CLI v0.8.2 (#838)
This commit is contained in:
parent
096fabb5d6
commit
eb95c131be
644 changed files with 70389 additions and 23709 deletions
|
|
@ -9,7 +9,15 @@ import { renderHook, act, waitFor } from '@testing-library/react';
|
|||
import type { Mock } from 'vitest';
|
||||
import { vi } from 'vitest';
|
||||
import type { Key } from './KeypressContext.js';
|
||||
import { KeypressProvider, useKeypressContext } from './KeypressContext.js';
|
||||
import {
|
||||
KeypressProvider,
|
||||
useKeypressContext,
|
||||
DRAG_COMPLETION_TIMEOUT_MS,
|
||||
// CSI_END_O,
|
||||
// SS3_END,
|
||||
SINGLE_QUOTE,
|
||||
DOUBLE_QUOTE,
|
||||
} from './KeypressContext.js';
|
||||
import { useStdin } from 'ink';
|
||||
import { EventEmitter } from 'node:events';
|
||||
|
||||
|
|
@ -351,6 +359,25 @@ describe('KeypressContext - Kitty Protocol', () => {
|
|||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should recognize Ctrl+Backspace in kitty protocol', async () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Modifier 5 is Ctrl
|
||||
act(() => {
|
||||
stdin.sendKittySequence(`\x1b[127;5u`);
|
||||
});
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'backspace',
|
||||
kittyProtocol: true,
|
||||
ctrl: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('paste mode', () => {
|
||||
|
|
@ -1232,10 +1259,13 @@ describe('KeypressContext - Kitty Protocol', () => {
|
|||
'[DEBUG] Kitty buffer accumulating:',
|
||||
expect.stringContaining('\x1b[27u'),
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
'[DEBUG] Kitty sequence parsed successfully:',
|
||||
expect.stringContaining('\x1b[27u'),
|
||||
const parsedCall = consoleLogSpy.mock.calls.find(
|
||||
(args) =>
|
||||
typeof args[0] === 'string' &&
|
||||
args[0].includes('[DEBUG] Kitty sequence parsed successfully'),
|
||||
);
|
||||
expect(parsedCall).toBeTruthy();
|
||||
expect(parsedCall?.[1]).toEqual(expect.stringContaining('\x1b[27u'));
|
||||
});
|
||||
|
||||
it('should log kitty buffer overflow when debugKeystrokeLogging is true', async () => {
|
||||
|
|
@ -1365,4 +1395,345 @@ describe('KeypressContext - Kitty Protocol', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Parameterized functional keys', () => {
|
||||
it.each([
|
||||
// Parameterized
|
||||
{ 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[3~`, expected: { name: 'delete' } },
|
||||
{ sequence: `\x1b[5~`, expected: { name: 'pageup' } },
|
||||
{ sequence: `\x1b[6~`, expected: { name: 'pagedown' } },
|
||||
{ sequence: `\x1b[1~`, expected: { name: 'home' } },
|
||||
{ sequence: `\x1b[4~`, expected: { name: 'end' } },
|
||||
{ sequence: `\x1b[2~`, expected: { name: 'insert' } },
|
||||
// Legacy Arrows
|
||||
{
|
||||
sequence: `\x1b[A`,
|
||||
expected: { name: 'up', ctrl: false, meta: false, shift: false },
|
||||
},
|
||||
{
|
||||
sequence: `\x1b[B`,
|
||||
expected: { name: 'down', ctrl: false, meta: false, shift: false },
|
||||
},
|
||||
{
|
||||
sequence: `\x1b[C`,
|
||||
expected: { name: 'right', ctrl: false, meta: false, shift: false },
|
||||
},
|
||||
{
|
||||
sequence: `\x1b[D`,
|
||||
expected: { name: 'left', ctrl: false, meta: false, shift: false },
|
||||
},
|
||||
// Legacy Home/End
|
||||
{
|
||||
sequence: `\x1b[H`,
|
||||
expected: { name: 'home', ctrl: false, meta: false, shift: false },
|
||||
},
|
||||
{
|
||||
sequence: `\x1b[F`,
|
||||
expected: { name: 'end', ctrl: false, meta: false, shift: false },
|
||||
},
|
||||
])(
|
||||
'should recognize sequence "$sequence" as $expected.name',
|
||||
({ sequence, expected }) => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => stdin.sendKittySequence(sequence));
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining(expected),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('Shift+Tab forms', () => {
|
||||
it.each([
|
||||
{ sequence: `\x1b[Z`, description: 'legacy reverse Tab' },
|
||||
{ sequence: `\x1b[1;2Z`, description: 'parameterized reverse Tab' },
|
||||
])(
|
||||
'should recognize $description "$sequence" as Shift+Tab',
|
||||
({ sequence }) => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => stdin.sendKittySequence(sequence));
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'tab', shift: true }),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('Double-tap and batching', () => {
|
||||
it('should emit two delete events for double-tap CSI[3~', async () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => stdin.sendKittySequence(`\x1b[3~`));
|
||||
act(() => stdin.sendKittySequence(`\x1b[3~`));
|
||||
|
||||
expect(keyHandler).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ name: 'delete' }),
|
||||
);
|
||||
expect(keyHandler).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({ name: 'delete' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should parse two concatenated tilde-coded sequences in one chunk', async () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
act(() => stdin.sendKittySequence(`\x1b[3~\x1b[5~`));
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'delete' }),
|
||||
);
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'pageup' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should ignore incomplete CSI then parse the next complete sequence', async () => {
|
||||
const keyHandler = vi.fn();
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
act(() => result.current.subscribe(keyHandler));
|
||||
|
||||
// Incomplete ESC sequence then a complete Delete
|
||||
act(() => {
|
||||
// Provide an incomplete ESC sequence chunk with a real ESC character
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
sequence: '\x1b[1;',
|
||||
});
|
||||
});
|
||||
act(() => stdin.sendKittySequence(`\x1b[3~`));
|
||||
|
||||
expect(keyHandler).toHaveBeenCalledTimes(1);
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ name: 'delete' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Drag and Drop Handling', () => {
|
||||
let stdin: MockStdin;
|
||||
const mockSetRawMode = vi.fn();
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<KeypressProvider kittyProtocolEnabled={true}>{children}</KeypressProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
stdin = new MockStdin();
|
||||
(useStdin as Mock).mockReturnValue({
|
||||
stdin,
|
||||
setRawMode: mockSetRawMode,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('drag start by quotes', () => {
|
||||
it('should start collecting when single quote arrives and not broadcast immediately', async () => {
|
||||
const keyHandler = vi.fn();
|
||||
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: SINGLE_QUOTE,
|
||||
});
|
||||
});
|
||||
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should start collecting when double quote arrives and not broadcast immediately', async () => {
|
||||
const keyHandler = vi.fn();
|
||||
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: DOUBLE_QUOTE,
|
||||
});
|
||||
});
|
||||
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('drag collection and completion', () => {
|
||||
it('should collect single character inputs during drag mode', async () => {
|
||||
const keyHandler = vi.fn();
|
||||
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
|
||||
// Start by single quote
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: SINGLE_QUOTE,
|
||||
});
|
||||
});
|
||||
|
||||
// Send single character
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: 'a',
|
||||
});
|
||||
});
|
||||
|
||||
// Character should not be immediately broadcast
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
|
||||
// Fast-forward to completion timeout
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(DRAG_COMPLETION_TIMEOUT_MS + 10);
|
||||
});
|
||||
|
||||
// Should broadcast the collected path as paste (includes starting quote)
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: '',
|
||||
paste: true,
|
||||
sequence: `${SINGLE_QUOTE}a`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should collect multiple characters and complete on timeout', async () => {
|
||||
const keyHandler = vi.fn();
|
||||
|
||||
const { result } = renderHook(() => useKeypressContext(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.subscribe(keyHandler);
|
||||
});
|
||||
|
||||
// Start by single quote
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: SINGLE_QUOTE,
|
||||
});
|
||||
});
|
||||
|
||||
// Send multiple characters
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: 'p',
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: 'a',
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: 't',
|
||||
});
|
||||
});
|
||||
|
||||
act(() => {
|
||||
stdin.pressKey({
|
||||
name: undefined,
|
||||
ctrl: false,
|
||||
meta: false,
|
||||
shift: false,
|
||||
paste: false,
|
||||
sequence: 'h',
|
||||
});
|
||||
});
|
||||
|
||||
// Characters should not be immediately broadcast
|
||||
expect(keyHandler).not.toHaveBeenCalled();
|
||||
|
||||
// Fast-forward to completion timeout
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(DRAG_COMPLETION_TIMEOUT_MS + 10);
|
||||
});
|
||||
|
||||
// Should broadcast the collected path as paste (includes starting quote)
|
||||
expect(keyHandler).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: '',
|
||||
paste: true,
|
||||
sequence: `${SINGLE_QUOTE}path`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue