refactor(ui): unify keybinding infrastructure and support string initialization (#21776)

This commit is contained in:
Tommaso Sciortino
2026-03-09 23:26:33 +00:00
committed by GitHub
parent b89944c3a3
commit 215f8f3f15
53 changed files with 523 additions and 410 deletions
-77
View File
@@ -1,77 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { Key } from '../contexts/KeypressContext.js';
export type { Key };
/**
* Translates a Key object into its corresponding ANSI escape sequence.
* This is useful for sending control characters to a pseudo-terminal.
*
* @param key The Key object to translate.
* @returns The ANSI escape sequence as a string, or null if no mapping exists.
*/
export function keyToAnsi(key: Key): string | null {
if (key.ctrl) {
// Ctrl + letter
if (key.name >= 'a' && key.name <= 'z') {
return String.fromCharCode(
key.name.charCodeAt(0) - 'a'.charCodeAt(0) + 1,
);
}
// Other Ctrl combinations might need specific handling
switch (key.name) {
case 'c':
return '\x03'; // ETX (End of Text), commonly used for interrupt
// Add other special ctrl cases if needed
default:
break;
}
}
// Arrow keys and other special keys
switch (key.name) {
case 'up':
return '\x1b[A';
case 'down':
return '\x1b[B';
case 'right':
return '\x1b[C';
case 'left':
return '\x1b[D';
case 'escape':
return '\x1b';
case 'tab':
return '\t';
case 'backspace':
return '\x7f';
case 'delete':
return '\x1b[3~';
case 'home':
return '\x1b[H';
case 'end':
return '\x1b[F';
case 'pageup':
return '\x1b[5~';
case 'pagedown':
return '\x1b[6~';
default:
break;
}
// Enter/Return
if (key.name === 'return') {
return '\r';
}
// If it's a simple character, return it.
if (!key.ctrl && !key.cmd && key.sequence) {
return key.sequence;
}
return null;
}
@@ -11,7 +11,7 @@ import {
getAdminErrorMessage,
} from '@google/gemini-cli-core';
import { useKeypress } from './useKeypress.js';
import { Command } from '../keyMatchers.js';
import { Command } from '../key/keyMatchers.js';
import { useKeyMatchers } from './useKeyMatchers.js';
import type { HistoryItemWithoutId } from '../types.js';
import { MessageType } from '../types.js';
+2 -2
View File
@@ -5,8 +5,8 @@
*/
import { useMemo } from 'react';
import type { KeyMatchers } from '../keyMatchers.js';
import { defaultKeyMatchers } from '../keyMatchers.js';
import type { KeyMatchers } from '../key/keyMatchers.js';
import { defaultKeyMatchers } from '../key/keyMatchers.js';
/**
* Hook to retrieve the currently active key matchers.
@@ -6,7 +6,7 @@
import { useReducer, useRef, useEffect, useCallback } from 'react';
import { useKeypress, type Key } from './useKeypress.js';
import { Command } from '../keyMatchers.js';
import { Command } from '../key/keyMatchers.js';
import { debugLogger } from '@google/gemini-cli-core';
import { useKeyMatchers } from './useKeyMatchers.js';
+2 -2
View File
@@ -29,8 +29,8 @@ import {
cleanupTerminalOnExit,
terminalCapabilityManager,
} from '../utils/terminalCapabilityManager.js';
import { formatCommand } from '../utils/keybindingUtils.js';
import { Command } from '../../config/keyBindings.js';
import { formatCommand } from '../key/keybindingUtils.js';
import { Command } from '../key/keyBindings.js';
vi.mock('@google/gemini-cli-core', async () => {
const actual = await vi.importActual('@google/gemini-cli-core');
+2 -2
View File
@@ -20,8 +20,8 @@ import {
terminalCapabilityManager,
} from '../utils/terminalCapabilityManager.js';
import { WARNING_PROMPT_DURATION_MS } from '../constants.js';
import { formatCommand } from '../utils/keybindingUtils.js';
import { Command } from '../../config/keyBindings.js';
import { formatCommand } from '../key/keybindingUtils.js';
import { Command } from '../key/keyBindings.js';
interface UseSuspendProps {
handleWarning: (message: string) => void;
@@ -9,18 +9,12 @@ import { act } from 'react';
import { renderHook } from '../../test-utils/render.js';
import { useTabbedNavigation } from './useTabbedNavigation.js';
import { useKeypress } from './useKeypress.js';
import { useKeyMatchers } from './useKeyMatchers.js';
import type { KeyMatchers } from '../keyMatchers.js';
import type { Key, KeypressHandler } from '../contexts/KeypressContext.js';
vi.mock('./useKeypress.js', () => ({
useKeypress: vi.fn(),
}));
vi.mock('./useKeyMatchers.js', () => ({
useKeyMatchers: vi.fn(),
}));
const createKey = (partial: Partial<Key>): Key => ({
name: partial.name || '',
sequence: partial.sequence || '',
@@ -32,27 +26,10 @@ const createKey = (partial: Partial<Key>): Key => ({
...partial,
});
const mockKeyMatchers = {
'cursor.left': vi.fn((key) => key.name === 'left'),
'cursor.right': vi.fn((key) => key.name === 'right'),
'dialog.next': vi.fn((key) => key.name === 'tab' && !key.shift),
'dialog.previous': vi.fn((key) => key.name === 'tab' && key.shift),
} as unknown as KeyMatchers;
vi.mock('../keyMatchers.js', () => ({
Command: {
MOVE_LEFT: 'cursor.left',
MOVE_RIGHT: 'cursor.right',
DIALOG_NEXT: 'dialog.next',
DIALOG_PREV: 'dialog.previous',
},
}));
describe('useTabbedNavigation', () => {
let capturedHandler: KeypressHandler;
beforeEach(() => {
vi.mocked(useKeyMatchers).mockReturnValue(mockKeyMatchers);
vi.mocked(useKeypress).mockImplementation((handler) => {
capturedHandler = handler;
});
@@ -6,7 +6,7 @@
import { useReducer, useCallback, useEffect, useRef } from 'react';
import { useKeypress, type Key } from './useKeypress.js';
import { Command } from '../keyMatchers.js';
import { Command } from '../key/keyMatchers.js';
import { useKeyMatchers } from './useKeyMatchers.js';
/**
+1 -1
View File
@@ -9,7 +9,7 @@ import type { Key } from './useKeypress.js';
import type { TextBuffer } from '../components/shared/text-buffer.js';
import { useVimMode } from '../contexts/VimModeContext.js';
import { debugLogger } from '@google/gemini-cli-core';
import { Command } from '../keyMatchers.js';
import { Command } from '../key/keyMatchers.js';
import { useKeyMatchers } from './useKeyMatchers.js';
export type VimMode = 'NORMAL' | 'INSERT';