mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
refactor: migrate to useKeyMatchers hook (#21753)
This commit is contained in:
committed by
GitHub
parent
e406dcc249
commit
ab64b15d51
@@ -119,7 +119,7 @@ import { type InitializationResult } from '../core/initializer.js';
|
|||||||
import { useFocus } from './hooks/useFocus.js';
|
import { useFocus } from './hooks/useFocus.js';
|
||||||
import { useKeypress, type Key } from './hooks/useKeypress.js';
|
import { useKeypress, type Key } from './hooks/useKeypress.js';
|
||||||
import { KeypressPriority } from './contexts/KeypressContext.js';
|
import { KeypressPriority } from './contexts/KeypressContext.js';
|
||||||
import { keyMatchers, Command } from './keyMatchers.js';
|
import { Command } from './keyMatchers.js';
|
||||||
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
|
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
|
||||||
import { useShellInactivityStatus } from './hooks/useShellInactivityStatus.js';
|
import { useShellInactivityStatus } from './hooks/useShellInactivityStatus.js';
|
||||||
import { useFolderTrust } from './hooks/useFolderTrust.js';
|
import { useFolderTrust } from './hooks/useFolderTrust.js';
|
||||||
@@ -164,7 +164,7 @@ import { NewAgentsChoice } from './components/NewAgentsNotification.js';
|
|||||||
import { isSlashCommand } from './utils/commandUtils.js';
|
import { isSlashCommand } from './utils/commandUtils.js';
|
||||||
import { useTerminalTheme } from './hooks/useTerminalTheme.js';
|
import { useTerminalTheme } from './hooks/useTerminalTheme.js';
|
||||||
import { useTimedMessage } from './hooks/useTimedMessage.js';
|
import { useTimedMessage } from './hooks/useTimedMessage.js';
|
||||||
import { shouldDismissShortcutsHelpOnHotkey } from './utils/shortcutsHelp.js';
|
import { useIsHelpDismissKey } from './utils/shortcutsHelp.js';
|
||||||
import { useSuspend } from './hooks/useSuspend.js';
|
import { useSuspend } from './hooks/useSuspend.js';
|
||||||
import { useRunEventNotifications } from './hooks/useRunEventNotifications.js';
|
import { useRunEventNotifications } from './hooks/useRunEventNotifications.js';
|
||||||
import { isNotificationsEnabled } from '../utils/terminalNotifications.js';
|
import { isNotificationsEnabled } from '../utils/terminalNotifications.js';
|
||||||
@@ -205,6 +205,7 @@ import {
|
|||||||
useVisibilityToggle,
|
useVisibilityToggle,
|
||||||
APPROVAL_MODE_REVEAL_DURATION_MS,
|
APPROVAL_MODE_REVEAL_DURATION_MS,
|
||||||
} from './hooks/useVisibilityToggle.js';
|
} from './hooks/useVisibilityToggle.js';
|
||||||
|
import { useKeyMatchers } from './hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The fraction of the terminal width to allocate to the shell.
|
* The fraction of the terminal width to allocate to the shell.
|
||||||
@@ -219,6 +220,8 @@ const SHELL_WIDTH_FRACTION = 0.89;
|
|||||||
const SHELL_HEIGHT_PADDING = 10;
|
const SHELL_HEIGHT_PADDING = 10;
|
||||||
|
|
||||||
export const AppContainer = (props: AppContainerProps) => {
|
export const AppContainer = (props: AppContainerProps) => {
|
||||||
|
const isHelpDismissKey = useIsHelpDismissKey();
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const { config, initializationResult, resumedSessionData } = props;
|
const { config, initializationResult, resumedSessionData } = props;
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const { reset } = useOverflowActions()!;
|
const { reset } = useOverflowActions()!;
|
||||||
@@ -1654,7 +1657,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
debugLogger.log('[DEBUG] Keystroke:', JSON.stringify(key));
|
debugLogger.log('[DEBUG] Keystroke:', JSON.stringify(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shortcutsHelpVisible && shouldDismissShortcutsHelpOnHotkey(key)) {
|
if (shortcutsHelpVisible && isHelpDismissKey(key)) {
|
||||||
setShortcutsHelpVisible(false);
|
setShortcutsHelpVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1848,6 +1851,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
settings.merged.general.devtools,
|
settings.merged.general.devtools,
|
||||||
showErrorDetails,
|
showErrorDetails,
|
||||||
triggerExpandHint,
|
triggerExpandHint,
|
||||||
|
keyMatchers,
|
||||||
|
isHelpDismissKey,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import { useTextBuffer } from '../components/shared/text-buffer.js';
|
|||||||
import { useUIState } from '../contexts/UIStateContext.js';
|
import { useUIState } from '../contexts/UIStateContext.js';
|
||||||
import { clearApiKey, debugLogger } from '@google/gemini-cli-core';
|
import { clearApiKey, debugLogger } from '@google/gemini-cli-core';
|
||||||
import { useKeypress } from '../hooks/useKeypress.js';
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
|
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
interface ApiAuthDialogProps {
|
interface ApiAuthDialogProps {
|
||||||
onSubmit: (apiKey: string) => void;
|
onSubmit: (apiKey: string) => void;
|
||||||
@@ -28,6 +29,7 @@ export function ApiAuthDialog({
|
|||||||
error,
|
error,
|
||||||
defaultValue = '',
|
defaultValue = '',
|
||||||
}: ApiAuthDialogProps): React.JSX.Element {
|
}: ApiAuthDialogProps): React.JSX.Element {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const { terminalWidth } = useUIState();
|
const { terminalWidth } = useUIState();
|
||||||
const viewportWidth = terminalWidth - 8;
|
const viewportWidth = terminalWidth - 8;
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import { Box, Text } from 'ink';
|
|||||||
import { theme } from '../semantic-colors.js';
|
import { theme } from '../semantic-colors.js';
|
||||||
import { useKeypress } from '../hooks/useKeypress.js';
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
import { useUIActions } from '../contexts/UIActionsContext.js';
|
import { useUIActions } from '../contexts/UIActionsContext.js';
|
||||||
import { Command, keyMatchers } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
|
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
export const AdminSettingsChangedDialog = () => {
|
export const AdminSettingsChangedDialog = () => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const { handleRestart } = useUIActions();
|
const { handleRestart } = useUIActions();
|
||||||
|
|
||||||
useKeypress(
|
useKeypress(
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { BaseSelectionList } from './shared/BaseSelectionList.js';
|
|||||||
import type { SelectionListItem } from '../hooks/useSelectionList.js';
|
import type { SelectionListItem } from '../hooks/useSelectionList.js';
|
||||||
import { TabHeader, type Tab } from './shared/TabHeader.js';
|
import { TabHeader, type Tab } from './shared/TabHeader.js';
|
||||||
import { useKeypress, type Key } from '../hooks/useKeypress.js';
|
import { useKeypress, type Key } from '../hooks/useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
import { checkExhaustive } from '@google/gemini-cli-core';
|
import { checkExhaustive } from '@google/gemini-cli-core';
|
||||||
import { TextInput } from './shared/TextInput.js';
|
import { TextInput } from './shared/TextInput.js';
|
||||||
import { formatCommand } from '../utils/keybindingUtils.js';
|
import { formatCommand } from '../utils/keybindingUtils.js';
|
||||||
@@ -36,6 +36,7 @@ import { RenderInline } from '../utils/InlineMarkdownRenderer.js';
|
|||||||
import { MaxSizedBox } from './shared/MaxSizedBox.js';
|
import { MaxSizedBox } from './shared/MaxSizedBox.js';
|
||||||
import { UIStateContext } from '../contexts/UIStateContext.js';
|
import { UIStateContext } from '../contexts/UIStateContext.js';
|
||||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||||
|
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
/** Padding for dialog content to prevent text from touching edges. */
|
/** Padding for dialog content to prevent text from touching edges. */
|
||||||
const DIALOG_PADDING = 4;
|
const DIALOG_PADDING = 4;
|
||||||
@@ -208,6 +209,7 @@ const ReviewView: React.FC<ReviewViewProps> = ({
|
|||||||
progressHeader,
|
progressHeader,
|
||||||
extraParts,
|
extraParts,
|
||||||
}) => {
|
}) => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const unansweredCount = questions.length - Object.keys(answers).length;
|
const unansweredCount = questions.length - Object.keys(answers).length;
|
||||||
const hasUnanswered = unansweredCount > 0;
|
const hasUnanswered = unansweredCount > 0;
|
||||||
|
|
||||||
@@ -288,6 +290,7 @@ const TextQuestionView: React.FC<TextQuestionViewProps> = ({
|
|||||||
progressHeader,
|
progressHeader,
|
||||||
keyboardHints,
|
keyboardHints,
|
||||||
}) => {
|
}) => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const isAlternateBuffer = useAlternateBuffer();
|
const isAlternateBuffer = useAlternateBuffer();
|
||||||
const prefix = '> ';
|
const prefix = '> ';
|
||||||
const horizontalPadding = 1; // 1 for cursor
|
const horizontalPadding = 1; // 1 for cursor
|
||||||
@@ -325,7 +328,7 @@ const TextQuestionView: React.FC<TextQuestionViewProps> = ({
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
[buffer, textValue],
|
[buffer, textValue, keyMatchers],
|
||||||
);
|
);
|
||||||
|
|
||||||
useKeypress(handleExtraKeys, { isActive: true, priority: true });
|
useKeypress(handleExtraKeys, { isActive: true, priority: true });
|
||||||
@@ -487,6 +490,7 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
|||||||
progressHeader,
|
progressHeader,
|
||||||
keyboardHints,
|
keyboardHints,
|
||||||
}) => {
|
}) => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const isAlternateBuffer = useAlternateBuffer();
|
const isAlternateBuffer = useAlternateBuffer();
|
||||||
const numOptions =
|
const numOptions =
|
||||||
(question.options?.length ?? 0) + (question.type !== 'yesno' ? 1 : 0);
|
(question.options?.length ?? 0) + (question.type !== 'yesno' ? 1 : 0);
|
||||||
@@ -680,6 +684,7 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
|||||||
customBuffer,
|
customBuffer,
|
||||||
onEditingCustomOption,
|
onEditingCustomOption,
|
||||||
customOptionText,
|
customOptionText,
|
||||||
|
keyMatchers,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -950,6 +955,7 @@ export const AskUserDialog: React.FC<AskUserDialogProps> = ({
|
|||||||
availableHeight: availableHeightProp,
|
availableHeight: availableHeightProp,
|
||||||
extraParts,
|
extraParts,
|
||||||
}) => {
|
}) => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const uiState = useContext(UIStateContext);
|
const uiState = useContext(UIStateContext);
|
||||||
const availableHeight =
|
const availableHeight =
|
||||||
availableHeightProp ??
|
availableHeightProp ??
|
||||||
@@ -999,7 +1005,7 @@ export const AskUserDialog: React.FC<AskUserDialogProps> = ({
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
[onCancel, submitted, isEditingCustomOption],
|
[onCancel, submitted, isEditingCustomOption, keyMatchers],
|
||||||
);
|
);
|
||||||
|
|
||||||
useKeypress(handleCancel, {
|
useKeypress(handleCancel, {
|
||||||
@@ -1032,7 +1038,7 @@ export const AskUserDialog: React.FC<AskUserDialogProps> = ({
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
[questions.length, submitted, goToNextTab, goToPrevTab],
|
[questions.length, submitted, goToNextTab, goToPrevTab, keyMatchers],
|
||||||
);
|
);
|
||||||
|
|
||||||
useKeypress(handleNavigation, {
|
useKeypress(handleNavigation, {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { cpLen, cpSlice, getCachedStringWidth } from '../utils/textUtils.js';
|
import { cpLen, cpSlice, getCachedStringWidth } from '../utils/textUtils.js';
|
||||||
import { type BackgroundShell } from '../hooks/shellCommandProcessor.js';
|
import { type BackgroundShell } from '../hooks/shellCommandProcessor.js';
|
||||||
import { Command, keyMatchers } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
import { useKeypress } from '../hooks/useKeypress.js';
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
import { formatCommand } from '../utils/keybindingUtils.js';
|
import { formatCommand } from '../utils/keybindingUtils.js';
|
||||||
import {
|
import {
|
||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
RadioButtonSelect,
|
RadioButtonSelect,
|
||||||
type RadioSelectItem,
|
type RadioSelectItem,
|
||||||
} from './shared/RadioButtonSelect.js';
|
} from './shared/RadioButtonSelect.js';
|
||||||
|
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
interface BackgroundShellDisplayProps {
|
interface BackgroundShellDisplayProps {
|
||||||
shells: Map<number, BackgroundShell>;
|
shells: Map<number, BackgroundShell>;
|
||||||
@@ -60,6 +61,7 @@ export const BackgroundShellDisplay = ({
|
|||||||
isFocused,
|
isFocused,
|
||||||
isListOpenProp,
|
isListOpenProp,
|
||||||
}: BackgroundShellDisplayProps) => {
|
}: BackgroundShellDisplayProps) => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const {
|
const {
|
||||||
dismissBackgroundShell,
|
dismissBackgroundShell,
|
||||||
setActiveBackgroundShellPid,
|
setActiveBackgroundShellPid,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { renderWithProviders } from '../../test-utils/render.js';
|
|||||||
import { waitFor } from '../../test-utils/async.js';
|
import { waitFor } from '../../test-utils/async.js';
|
||||||
import { ExitPlanModeDialog } from './ExitPlanModeDialog.js';
|
import { ExitPlanModeDialog } from './ExitPlanModeDialog.js';
|
||||||
import { useKeypress } from '../hooks/useKeypress.js';
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
import {
|
import {
|
||||||
ApprovalMode,
|
ApprovalMode,
|
||||||
validatePlanContent,
|
validatePlanContent,
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
type FileSystemService,
|
type FileSystemService,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
|
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
vi.mock('../utils/editorUtils.js', () => ({
|
vi.mock('../utils/editorUtils.js', () => ({
|
||||||
openFileInEditor: vi.fn(),
|
openFileInEditor: vi.fn(),
|
||||||
@@ -402,6 +403,7 @@ Implement a comprehensive authentication system with multiple providers.
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
useKeypress(
|
useKeypress(
|
||||||
(key) => {
|
(key) => {
|
||||||
if (keyMatchers[Command.QUIT](key)) {
|
if (keyMatchers[Command.QUIT](key)) {
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ import { useConfig } from '../contexts/ConfigContext.js';
|
|||||||
import { AskUserDialog } from './AskUserDialog.js';
|
import { AskUserDialog } from './AskUserDialog.js';
|
||||||
import { openFileInEditor } from '../utils/editorUtils.js';
|
import { openFileInEditor } from '../utils/editorUtils.js';
|
||||||
import { useKeypress } from '../hooks/useKeypress.js';
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
import { formatCommand } from '../utils/keybindingUtils.js';
|
import { formatCommand } from '../utils/keybindingUtils.js';
|
||||||
|
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
export interface ExitPlanModeDialogProps {
|
export interface ExitPlanModeDialogProps {
|
||||||
planPath: string;
|
planPath: string;
|
||||||
@@ -147,6 +148,7 @@ export const ExitPlanModeDialog: React.FC<ExitPlanModeDialogProps> = ({
|
|||||||
width,
|
width,
|
||||||
availableHeight,
|
availableHeight,
|
||||||
}) => {
|
}) => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const { stdin, setRawMode } = useStdin();
|
const { stdin, setRawMode } = useStdin();
|
||||||
const planState = usePlanContent(planPath, config);
|
const planState = usePlanContent(planPath, config);
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ import { theme } from '../semantic-colors.js';
|
|||||||
import { useSettingsStore } from '../contexts/SettingsContext.js';
|
import { useSettingsStore } from '../contexts/SettingsContext.js';
|
||||||
import { useUIState } from '../contexts/UIStateContext.js';
|
import { useUIState } from '../contexts/UIStateContext.js';
|
||||||
import { useKeypress, type Key } from '../hooks/useKeypress.js';
|
import { useKeypress, type Key } from '../hooks/useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
import { FooterRow, type FooterRowItem } from './Footer.js';
|
import { FooterRow, type FooterRowItem } from './Footer.js';
|
||||||
import { ALL_ITEMS, resolveFooterState } from '../../config/footerItems.js';
|
import { ALL_ITEMS, resolveFooterState } from '../../config/footerItems.js';
|
||||||
import { SettingScope } from '../../config/settings.js';
|
import { SettingScope } from '../../config/settings.js';
|
||||||
import { BaseSelectionList } from './shared/BaseSelectionList.js';
|
import { BaseSelectionList } from './shared/BaseSelectionList.js';
|
||||||
import type { SelectionListItem } from '../hooks/useSelectionList.js';
|
import type { SelectionListItem } from '../hooks/useSelectionList.js';
|
||||||
import { DialogFooter } from './shared/DialogFooter.js';
|
import { DialogFooter } from './shared/DialogFooter.js';
|
||||||
|
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
interface FooterConfigDialogProps {
|
interface FooterConfigDialogProps {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
@@ -82,6 +83,7 @@ function footerConfigReducer(
|
|||||||
export const FooterConfigDialog: React.FC<FooterConfigDialogProps> = ({
|
export const FooterConfigDialog: React.FC<FooterConfigDialogProps> = ({
|
||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const { settings, setSetting } = useSettingsStore();
|
const { settings, setSetting } = useSettingsStore();
|
||||||
const { constrainHeight, terminalHeight, staticExtraHeight } = useUIState();
|
const { constrainHeight, terminalHeight, staticExtraHeight } = useUIState();
|
||||||
const [state, dispatch] = useReducer(footerConfigReducer, undefined, () =>
|
const [state, dispatch] = useReducer(footerConfigReducer, undefined, () =>
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { useState, useMemo } from 'react';
|
|||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { theme } from '../semantic-colors.js';
|
import { theme } from '../semantic-colors.js';
|
||||||
import { useKeypress } from '../hooks/useKeypress.js';
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
|
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook entry type matching HookRegistryEntry from core
|
* Hook entry type matching HookRegistryEntry from core
|
||||||
@@ -49,6 +50,7 @@ export const HooksDialog: React.FC<HooksDialogProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
maxVisibleHooks = DEFAULT_MAX_VISIBLE_HOOKS,
|
maxVisibleHooks = DEFAULT_MAX_VISIBLE_HOOKS,
|
||||||
}) => {
|
}) => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const [scrollOffset, setScrollOffset] = useState(0);
|
const [scrollOffset, setScrollOffset] = useState(0);
|
||||||
|
|
||||||
// Flatten hooks with their event names for easier scrolling
|
// Flatten hooks with their event names for easier scrolling
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import { terminalCapabilityManager } from '../utils/terminalCapabilityManager.js
|
|||||||
import type { UIState } from '../contexts/UIStateContext.js';
|
import type { UIState } from '../contexts/UIStateContext.js';
|
||||||
import { isLowColorDepth } from '../utils/terminalUtils.js';
|
import { isLowColorDepth } from '../utils/terminalUtils.js';
|
||||||
import { cpLen } from '../utils/textUtils.js';
|
import { cpLen } from '../utils/textUtils.js';
|
||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { defaultKeyMatchers, Command } from '../keyMatchers.js';
|
||||||
import type { Key } from '../hooks/useKeypress.js';
|
import type { Key } from '../hooks/useKeypress.js';
|
||||||
import {
|
import {
|
||||||
appEvents,
|
appEvents,
|
||||||
@@ -197,7 +197,7 @@ describe('InputPrompt', () => {
|
|||||||
visualCursor: [0, 0],
|
visualCursor: [0, 0],
|
||||||
visualScrollRow: 0,
|
visualScrollRow: 0,
|
||||||
handleInput: vi.fn((key: Key) => {
|
handleInput: vi.fn((key: Key) => {
|
||||||
if (keyMatchers[Command.CLEAR_INPUT](key)) {
|
if (defaultKeyMatchers[Command.CLEAR_INPUT](key)) {
|
||||||
if (mockBuffer.text.length > 0) {
|
if (mockBuffer.text.length > 0) {
|
||||||
mockBuffer.setText('');
|
mockBuffer.setText('');
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import {
|
|||||||
} from '../hooks/useCommandCompletion.js';
|
} from '../hooks/useCommandCompletion.js';
|
||||||
import type { Key } from '../hooks/useKeypress.js';
|
import type { Key } from '../hooks/useKeypress.js';
|
||||||
import { useKeypress } from '../hooks/useKeypress.js';
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
import { formatCommand } from '../utils/keybindingUtils.js';
|
import { formatCommand } from '../utils/keybindingUtils.js';
|
||||||
import type { CommandContext, SlashCommand } from '../commands/types.js';
|
import type { CommandContext, SlashCommand } from '../commands/types.js';
|
||||||
import type { Config } from '@google/gemini-cli-core';
|
import type { Config } from '@google/gemini-cli-core';
|
||||||
@@ -72,8 +72,9 @@ import { useMouseClick } from '../hooks/useMouseClick.js';
|
|||||||
import { useMouse, type MouseEvent } from '../contexts/MouseContext.js';
|
import { useMouse, type MouseEvent } from '../contexts/MouseContext.js';
|
||||||
import { useUIActions } from '../contexts/UIActionsContext.js';
|
import { useUIActions } from '../contexts/UIActionsContext.js';
|
||||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||||
import { shouldDismissShortcutsHelpOnHotkey } from '../utils/shortcutsHelp.js';
|
import { useIsHelpDismissKey } from '../utils/shortcutsHelp.js';
|
||||||
import { useRepeatedKeyPress } from '../hooks/useRepeatedKeyPress.js';
|
import { useRepeatedKeyPress } from '../hooks/useRepeatedKeyPress.js';
|
||||||
|
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns if the terminal can be trusted to handle paste events atomically
|
* Returns if the terminal can be trusted to handle paste events atomically
|
||||||
@@ -207,6 +208,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||||||
suggestionsPosition = 'below',
|
suggestionsPosition = 'below',
|
||||||
setBannerVisible,
|
setBannerVisible,
|
||||||
}) => {
|
}) => {
|
||||||
|
const isHelpDismissKey = useIsHelpDismissKey();
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const { stdout } = useStdout();
|
const { stdout } = useStdout();
|
||||||
const { merged: settings } = useSettings();
|
const { merged: settings } = useSettings();
|
||||||
const kittyProtocol = useKittyKeyboardProtocol();
|
const kittyProtocol = useKittyKeyboardProtocol();
|
||||||
@@ -737,7 +740,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shortcutsHelpVisible && shouldDismissShortcutsHelpOnHotkey(key)) {
|
if (shortcutsHelpVisible && isHelpDismissKey(key)) {
|
||||||
setShortcutsHelpVisible(false);
|
setShortcutsHelpVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1265,6 +1268,8 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
|||||||
shouldShowSuggestions,
|
shouldShowSuggestions,
|
||||||
isShellSuggestionsVisible,
|
isShellSuggestionsVisible,
|
||||||
forceShowShellSuggestions,
|
forceShowShellSuggestions,
|
||||||
|
keyMatchers,
|
||||||
|
isHelpDismissKey,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import { theme } from '../semantic-colors.js';
|
|||||||
import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
|
import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
|
||||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||||
import { useKeypress } from '../hooks/useKeypress.js';
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
|
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
export enum PolicyUpdateChoice {
|
export enum PolicyUpdateChoice {
|
||||||
ACCEPT = 'accept',
|
ACCEPT = 'accept',
|
||||||
@@ -34,6 +35,7 @@ export const PolicyUpdateDialog: React.FC<PolicyUpdateDialogProps> = ({
|
|||||||
request,
|
request,
|
||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const isProcessing = useRef(false);
|
const isProcessing = useRef(false);
|
||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
|
|||||||
import type { FileChangeStats } from '../utils/rewindFileOps.js';
|
import type { FileChangeStats } from '../utils/rewindFileOps.js';
|
||||||
import { useKeypress } from '../hooks/useKeypress.js';
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
import { formatTimeAgo } from '../utils/formatters.js';
|
import { formatTimeAgo } from '../utils/formatters.js';
|
||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
|
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
export enum RewindOutcome {
|
export enum RewindOutcome {
|
||||||
RewindAndRevert = 'rewind_and_revert',
|
RewindAndRevert = 'rewind_and_revert',
|
||||||
@@ -58,6 +59,7 @@ export const RewindConfirmation: React.FC<RewindConfirmationProps> = ({
|
|||||||
terminalWidth,
|
terminalWidth,
|
||||||
timestamp,
|
timestamp,
|
||||||
}) => {
|
}) => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
||||||
useKeypress(
|
useKeypress(
|
||||||
(key) => {
|
(key) => {
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ import { useKeypress } from '../hooks/useKeypress.js';
|
|||||||
import { useRewind } from '../hooks/useRewind.js';
|
import { useRewind } from '../hooks/useRewind.js';
|
||||||
import { RewindConfirmation, RewindOutcome } from './RewindConfirmation.js';
|
import { RewindConfirmation, RewindOutcome } from './RewindConfirmation.js';
|
||||||
import { stripReferenceContent } from '../utils/formatters.js';
|
import { stripReferenceContent } from '../utils/formatters.js';
|
||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
import { CliSpinner } from './CliSpinner.js';
|
import { CliSpinner } from './CliSpinner.js';
|
||||||
import { ExpandableText } from './shared/ExpandableText.js';
|
import { ExpandableText } from './shared/ExpandableText.js';
|
||||||
|
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
interface RewindViewerProps {
|
interface RewindViewerProps {
|
||||||
conversation: ConversationRecord;
|
conversation: ConversationRecord;
|
||||||
@@ -48,6 +49,7 @@ export const RewindViewer: React.FC<RewindViewerProps> = ({
|
|||||||
onExit,
|
onExit,
|
||||||
onRewind,
|
onRewind,
|
||||||
}) => {
|
}) => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const [isRewinding, setIsRewinding] = useState(false);
|
const [isRewinding, setIsRewinding] = useState(false);
|
||||||
const { terminalWidth, terminalHeight } = useUIState();
|
const { terminalWidth, terminalHeight } = useUIState();
|
||||||
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import { useKeypress } from '../hooks/useKeypress.js';
|
|||||||
import { ShellExecutionService } from '@google/gemini-cli-core';
|
import { ShellExecutionService } from '@google/gemini-cli-core';
|
||||||
import { keyToAnsi, type Key } from '../hooks/keyToAnsi.js';
|
import { keyToAnsi, type Key } from '../hooks/keyToAnsi.js';
|
||||||
import { ACTIVE_SHELL_MAX_LINES } from '../constants.js';
|
import { ACTIVE_SHELL_MAX_LINES } from '../constants.js';
|
||||||
import { Command, keyMatchers } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
|
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
export interface ShellInputPromptProps {
|
export interface ShellInputPromptProps {
|
||||||
activeShellPtyId: number | null;
|
activeShellPtyId: number | null;
|
||||||
@@ -23,6 +24,7 @@ export const ShellInputPrompt: React.FC<ShellInputPromptProps> = ({
|
|||||||
focus = true,
|
focus = true,
|
||||||
scrollPageSize = ACTIVE_SHELL_MAX_LINES,
|
scrollPageSize = ACTIVE_SHELL_MAX_LINES,
|
||||||
}) => {
|
}) => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const handleShellInputSubmit = useCallback(
|
const handleShellInputSubmit = useCallback(
|
||||||
(input: string) => {
|
(input: string) => {
|
||||||
if (activeShellPtyId) {
|
if (activeShellPtyId) {
|
||||||
@@ -73,7 +75,13 @@ export const ShellInputPrompt: React.FC<ShellInputPromptProps> = ({
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
[focus, handleShellInputSubmit, activeShellPtyId, scrollPageSize],
|
[
|
||||||
|
focus,
|
||||||
|
handleShellInputSubmit,
|
||||||
|
activeShellPtyId,
|
||||||
|
scrollPageSize,
|
||||||
|
keyMatchers,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
useKeypress(handleInput, { isActive: focus });
|
useKeypress(handleInput, { isActive: focus });
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import {
|
|||||||
type ValidationIntent,
|
type ValidationIntent,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { useKeypress } from '../hooks/useKeypress.js';
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
|
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
interface ValidationDialogProps {
|
interface ValidationDialogProps {
|
||||||
validationLink?: string;
|
validationLink?: string;
|
||||||
@@ -32,6 +33,7 @@ export function ValidationDialog({
|
|||||||
learnMoreUrl,
|
learnMoreUrl,
|
||||||
onChoice,
|
onChoice,
|
||||||
}: ValidationDialogProps): React.JSX.Element {
|
}: ValidationDialogProps): React.JSX.Element {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const [state, setState] = useState<DialogState>('choosing');
|
const [state, setState] = useState<DialogState>('choosing');
|
||||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../semantic-colors.js';
|
||||||
import { useSettings } from '../../contexts/SettingsContext.js';
|
import { useSettings } from '../../contexts/SettingsContext.js';
|
||||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
import { Command } from '../../keyMatchers.js';
|
||||||
import { formatCommand } from '../../utils/keybindingUtils.js';
|
import { formatCommand } from '../../utils/keybindingUtils.js';
|
||||||
import { AskUserDialog } from '../AskUserDialog.js';
|
import { AskUserDialog } from '../AskUserDialog.js';
|
||||||
import { ExitPlanModeDialog } from '../ExitPlanModeDialog.js';
|
import { ExitPlanModeDialog } from '../ExitPlanModeDialog.js';
|
||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
toUnicodeUrl,
|
toUnicodeUrl,
|
||||||
type DeceptiveUrlDetails,
|
type DeceptiveUrlDetails,
|
||||||
} from '../../utils/urlSecurityUtils.js';
|
} from '../../utils/urlSecurityUtils.js';
|
||||||
|
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
export interface ToolConfirmationMessageProps {
|
export interface ToolConfirmationMessageProps {
|
||||||
callId: string;
|
callId: string;
|
||||||
@@ -67,6 +68,7 @@ export const ToolConfirmationMessage: React.FC<
|
|||||||
availableTerminalHeight,
|
availableTerminalHeight,
|
||||||
terminalWidth,
|
terminalWidth,
|
||||||
}) => {
|
}) => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const { confirm, isDiffingEnabled } = useToolActions();
|
const { confirm, isDiffingEnabled } = useToolActions();
|
||||||
const [mcpDetailsExpansionState, setMcpDetailsExpansionState] = useState<{
|
const [mcpDetailsExpansionState, setMcpDetailsExpansionState] = useState<{
|
||||||
callId: string;
|
callId: string;
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ import { TextInput } from './TextInput.js';
|
|||||||
import type { TextBuffer } from './text-buffer.js';
|
import type { TextBuffer } from './text-buffer.js';
|
||||||
import { cpSlice, cpLen, cpIndexToOffset } from '../../utils/textUtils.js';
|
import { cpSlice, cpLen, cpIndexToOffset } from '../../utils/textUtils.js';
|
||||||
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
import { Command } from '../../keyMatchers.js';
|
||||||
import { useSettingsNavigation } from '../../hooks/useSettingsNavigation.js';
|
import { useSettingsNavigation } from '../../hooks/useSettingsNavigation.js';
|
||||||
import { useInlineEditBuffer } from '../../hooks/useInlineEditBuffer.js';
|
import { useInlineEditBuffer } from '../../hooks/useInlineEditBuffer.js';
|
||||||
import { formatCommand } from '../../utils/keybindingUtils.js';
|
import { formatCommand } from '../../utils/keybindingUtils.js';
|
||||||
|
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a single item in the settings dialog.
|
* Represents a single item in the settings dialog.
|
||||||
@@ -136,6 +137,7 @@ export function BaseSettingsDialog({
|
|||||||
availableHeight,
|
availableHeight,
|
||||||
footer,
|
footer,
|
||||||
}: BaseSettingsDialogProps): React.JSX.Element {
|
}: BaseSettingsDialogProps): React.JSX.Element {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
// Calculate effective max items and scope visibility based on terminal height
|
// Calculate effective max items and scope visibility based on terminal height
|
||||||
const { effectiveMaxItemsToShow, finalShowScopeSelector } = useMemo(() => {
|
const { effectiveMaxItemsToShow, finalShowScopeSelector } = useMemo(() => {
|
||||||
const initialShowScope = showScopeSelector;
|
const initialShowScope = showScopeSelector;
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
|||||||
import { useScrollable } from '../../contexts/ScrollProvider.js';
|
import { useScrollable } from '../../contexts/ScrollProvider.js';
|
||||||
import { useAnimatedScrollbar } from '../../hooks/useAnimatedScrollbar.js';
|
import { useAnimatedScrollbar } from '../../hooks/useAnimatedScrollbar.js';
|
||||||
import { useBatchedScroll } from '../../hooks/useBatchedScroll.js';
|
import { useBatchedScroll } from '../../hooks/useBatchedScroll.js';
|
||||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
import { Command } from '../../keyMatchers.js';
|
||||||
import { useOverflowActions } from '../../contexts/OverflowContext.js';
|
import { useOverflowActions } from '../../contexts/OverflowContext.js';
|
||||||
|
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
interface ScrollableProps {
|
interface ScrollableProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@@ -45,6 +46,7 @@ export const Scrollable: React.FC<ScrollableProps> = ({
|
|||||||
flexGrow,
|
flexGrow,
|
||||||
reportOverflow = false,
|
reportOverflow = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const [scrollTop, setScrollTop] = useState(0);
|
const [scrollTop, setScrollTop] = useState(0);
|
||||||
const viewportRef = useRef<DOMElement | null>(null);
|
const viewportRef = useRef<DOMElement | null>(null);
|
||||||
const contentRef = useRef<DOMElement | null>(null);
|
const contentRef = useRef<DOMElement | null>(null);
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import { useScrollable } from '../../contexts/ScrollProvider.js';
|
|||||||
import { Box, type DOMElement } from 'ink';
|
import { Box, type DOMElement } from 'ink';
|
||||||
import { useAnimatedScrollbar } from '../../hooks/useAnimatedScrollbar.js';
|
import { useAnimatedScrollbar } from '../../hooks/useAnimatedScrollbar.js';
|
||||||
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
import { Command } from '../../keyMatchers.js';
|
||||||
|
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
const ANIMATION_FRAME_DURATION_MS = 33;
|
const ANIMATION_FRAME_DURATION_MS = 33;
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ function ScrollableList<T>(
|
|||||||
props: ScrollableListProps<T>,
|
props: ScrollableListProps<T>,
|
||||||
ref: React.Ref<ScrollableListRef<T>>,
|
ref: React.Ref<ScrollableListRef<T>>,
|
||||||
) {
|
) {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const { hasFocus, width } = props;
|
const { hasFocus, width } = props;
|
||||||
const virtualizedListRef = useRef<VirtualizedListRef<T>>(null);
|
const virtualizedListRef = useRef<VirtualizedListRef<T>>(null);
|
||||||
const containerRef = useRef<DOMElement>(null);
|
const containerRef = useRef<DOMElement>(null);
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import { useSelectionList } from '../../hooks/useSelectionList.js';
|
|||||||
import { TextInput } from './TextInput.js';
|
import { TextInput } from './TextInput.js';
|
||||||
import type { TextBuffer } from './text-buffer.js';
|
import type { TextBuffer } from './text-buffer.js';
|
||||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
import { Command } from '../../keyMatchers.js';
|
||||||
|
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic interface for items in a searchable list.
|
* Generic interface for items in a searchable list.
|
||||||
@@ -85,6 +86,7 @@ export function SearchableList<T extends GenericListItem>({
|
|||||||
onSearch,
|
onSearch,
|
||||||
resetSelectionOnItemsChange = false,
|
resetSelectionOnItemsChange = false,
|
||||||
}: SearchableListProps<T>): React.JSX.Element {
|
}: SearchableListProps<T>): React.JSX.Element {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const { filteredItems, searchBuffer, maxLabelWidth } = useSearch({
|
const { filteredItems, searchBuffer, maxLabelWidth } = useSearch({
|
||||||
items,
|
items,
|
||||||
onSearch,
|
onSearch,
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import { theme } from '../../semantic-colors.js';
|
|||||||
import type { TextBuffer } from './text-buffer.js';
|
import type { TextBuffer } from './text-buffer.js';
|
||||||
import { expandPastePlaceholders } from './text-buffer.js';
|
import { expandPastePlaceholders } from './text-buffer.js';
|
||||||
import { cpSlice, cpIndexToOffset } from '../../utils/textUtils.js';
|
import { cpSlice, cpIndexToOffset } from '../../utils/textUtils.js';
|
||||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
import { Command } from '../../keyMatchers.js';
|
||||||
|
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
export interface TextInputProps {
|
export interface TextInputProps {
|
||||||
buffer: TextBuffer;
|
buffer: TextBuffer;
|
||||||
@@ -31,6 +32,7 @@ export function TextInput({
|
|||||||
onCancel,
|
onCancel,
|
||||||
focus = true,
|
focus = true,
|
||||||
}: TextInputProps): React.JSX.Element {
|
}: TextInputProps): React.JSX.Element {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const {
|
const {
|
||||||
text,
|
text,
|
||||||
handleInput,
|
handleInput,
|
||||||
@@ -55,7 +57,7 @@ export function TextInput({
|
|||||||
const handled = handleInput(key);
|
const handled = handleInput(key);
|
||||||
return handled;
|
return handled;
|
||||||
},
|
},
|
||||||
[handleInput, onCancel, onSubmit, text, buffer.pastedContent],
|
[handleInput, onCancel, onSubmit, text, buffer.pastedContent, keyMatchers],
|
||||||
);
|
);
|
||||||
|
|
||||||
useKeypress(handleKeyPress, { isActive: focus, priority: true });
|
useKeypress(handleKeyPress, { isActive: focus, priority: true });
|
||||||
|
|||||||
@@ -25,11 +25,12 @@ import {
|
|||||||
} from '../../utils/textUtils.js';
|
} from '../../utils/textUtils.js';
|
||||||
import { parsePastedPaths } from '../../utils/clipboardUtils.js';
|
import { parsePastedPaths } from '../../utils/clipboardUtils.js';
|
||||||
import type { Key } from '../../contexts/KeypressContext.js';
|
import type { Key } from '../../contexts/KeypressContext.js';
|
||||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
import { Command } from '../../keyMatchers.js';
|
||||||
import type { VimAction } from './vim-buffer-actions.js';
|
import type { VimAction } from './vim-buffer-actions.js';
|
||||||
import { handleVimAction } from './vim-buffer-actions.js';
|
import { handleVimAction } from './vim-buffer-actions.js';
|
||||||
import { LRU_BUFFER_PERF_CACHE_LIMIT } from '../../constants.js';
|
import { LRU_BUFFER_PERF_CACHE_LIMIT } from '../../constants.js';
|
||||||
import { openFileInEditor } from '../../utils/editorUtils.js';
|
import { openFileInEditor } from '../../utils/editorUtils.js';
|
||||||
|
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
export const LARGE_PASTE_LINE_THRESHOLD = 5;
|
export const LARGE_PASTE_LINE_THRESHOLD = 5;
|
||||||
export const LARGE_PASTE_CHAR_THRESHOLD = 500;
|
export const LARGE_PASTE_CHAR_THRESHOLD = 500;
|
||||||
@@ -2708,6 +2709,7 @@ export function useTextBuffer({
|
|||||||
singleLine = false,
|
singleLine = false,
|
||||||
getPreferredEditor,
|
getPreferredEditor,
|
||||||
}: UseTextBufferProps): TextBuffer {
|
}: UseTextBufferProps): TextBuffer {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const initialState = useMemo((): TextBufferState => {
|
const initialState = useMemo((): TextBufferState => {
|
||||||
const lines = initialText.split('\n');
|
const lines = initialText.split('\n');
|
||||||
const [initialCursorRow, initialCursorCol] = calculateInitialCursorPosition(
|
const [initialCursorRow, initialCursorCol] = calculateInitialCursorPosition(
|
||||||
@@ -3270,6 +3272,7 @@ export function useTextBuffer({
|
|||||||
text,
|
text,
|
||||||
visualCursor,
|
visualCursor,
|
||||||
visualLines,
|
visualLines,
|
||||||
|
keyMatchers,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import Spinner from 'ink-spinner';
|
|||||||
import type { Config } from '@google/gemini-cli-core';
|
import type { Config } from '@google/gemini-cli-core';
|
||||||
import { debugLogger, spawnAsync, LlmRole } from '@google/gemini-cli-core';
|
import { debugLogger, spawnAsync, LlmRole } from '@google/gemini-cli-core';
|
||||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
import { Command } from '../../keyMatchers.js';
|
||||||
|
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
interface Issue {
|
interface Issue {
|
||||||
number: number;
|
number: number;
|
||||||
@@ -106,6 +107,7 @@ export const TriageDuplicates = ({
|
|||||||
onExit: () => void;
|
onExit: () => void;
|
||||||
initialLimit?: number;
|
initialLimit?: number;
|
||||||
}) => {
|
}) => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const [state, setState] = useState<TriageState>({
|
const [state, setState] = useState<TriageState>({
|
||||||
status: 'loading',
|
status: 'loading',
|
||||||
issues: [],
|
issues: [],
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import Spinner from 'ink-spinner';
|
|||||||
import type { Config } from '@google/gemini-cli-core';
|
import type { Config } from '@google/gemini-cli-core';
|
||||||
import { debugLogger, spawnAsync, LlmRole } from '@google/gemini-cli-core';
|
import { debugLogger, spawnAsync, LlmRole } from '@google/gemini-cli-core';
|
||||||
import { useKeypress } from '../../hooks/useKeypress.js';
|
import { useKeypress } from '../../hooks/useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
import { Command } from '../../keyMatchers.js';
|
||||||
import { TextInput } from '../shared/TextInput.js';
|
import { TextInput } from '../shared/TextInput.js';
|
||||||
import { useTextBuffer } from '../shared/text-buffer.js';
|
import { useTextBuffer } from '../shared/text-buffer.js';
|
||||||
|
import { useKeyMatchers } from '../../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
interface Issue {
|
interface Issue {
|
||||||
number: number;
|
number: number;
|
||||||
@@ -69,6 +70,7 @@ export const TriageIssues = ({
|
|||||||
initialLimit?: number;
|
initialLimit?: number;
|
||||||
until?: string;
|
until?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const [state, setState] = useState<TriageState>({
|
const [state, setState] = useState<TriageState>({
|
||||||
status: 'loading',
|
status: 'loading',
|
||||||
issues: [],
|
issues: [],
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
getAdminErrorMessage,
|
getAdminErrorMessage,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { useKeypress } from './useKeypress.js';
|
import { useKeypress } from './useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
|
import { useKeyMatchers } from './useKeyMatchers.js';
|
||||||
import type { HistoryItemWithoutId } from '../types.js';
|
import type { HistoryItemWithoutId } from '../types.js';
|
||||||
import { MessageType } from '../types.js';
|
import { MessageType } from '../types.js';
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ export function useApprovalModeIndicator({
|
|||||||
isActive = true,
|
isActive = true,
|
||||||
allowPlanMode = false,
|
allowPlanMode = false,
|
||||||
}: UseApprovalModeIndicatorArgs): ApprovalMode {
|
}: UseApprovalModeIndicatorArgs): ApprovalMode {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const currentConfigValue = config.getApprovalMode();
|
const currentConfigValue = config.getApprovalMode();
|
||||||
const [showApprovalMode, setApprovalMode] = useState(currentConfigValue);
|
const [showApprovalMode, setApprovalMode] = useState(currentConfigValue);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import type { KeyMatchers } from '../keyMatchers.js';
|
||||||
|
import { defaultKeyMatchers } from '../keyMatchers.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to retrieve the currently active key matchers.
|
||||||
|
* This prepares the codebase for dynamic or custom key bindings in the future.
|
||||||
|
*/
|
||||||
|
export function useKeyMatchers(): KeyMatchers {
|
||||||
|
return useMemo(() => defaultKeyMatchers, []);
|
||||||
|
}
|
||||||
@@ -6,8 +6,9 @@
|
|||||||
|
|
||||||
import { useReducer, useRef, useEffect, useCallback } from 'react';
|
import { useReducer, useRef, useEffect, useCallback } from 'react';
|
||||||
import { useKeypress, type Key } from './useKeypress.js';
|
import { useKeypress, type Key } from './useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
import { debugLogger } from '@google/gemini-cli-core';
|
import { debugLogger } from '@google/gemini-cli-core';
|
||||||
|
import { useKeyMatchers } from './useKeyMatchers.js';
|
||||||
|
|
||||||
export interface SelectionListItem<T> {
|
export interface SelectionListItem<T> {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -290,6 +291,7 @@ export function useSelectionList<T>({
|
|||||||
focusKey,
|
focusKey,
|
||||||
priority,
|
priority,
|
||||||
}: UseSelectionListOptions<T>): UseSelectionListResult {
|
}: UseSelectionListOptions<T>): UseSelectionListResult {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const baseItems = toBaseItems(items);
|
const baseItems = toBaseItems(items);
|
||||||
|
|
||||||
const [state, dispatch] = useReducer(selectionListReducer, {
|
const [state, dispatch] = useReducer(selectionListReducer, {
|
||||||
@@ -460,7 +462,7 @@ export function useSelectionList<T>({
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
[dispatch, itemsLength, showNumbers],
|
[dispatch, itemsLength, showNumbers, keyMatchers],
|
||||||
);
|
);
|
||||||
|
|
||||||
useKeypress(handleKeypress, {
|
useKeypress(handleKeypress, {
|
||||||
|
|||||||
@@ -9,12 +9,18 @@ import { act } from 'react';
|
|||||||
import { renderHook } from '../../test-utils/render.js';
|
import { renderHook } from '../../test-utils/render.js';
|
||||||
import { useTabbedNavigation } from './useTabbedNavigation.js';
|
import { useTabbedNavigation } from './useTabbedNavigation.js';
|
||||||
import { useKeypress } from './useKeypress.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';
|
import type { Key, KeypressHandler } from '../contexts/KeypressContext.js';
|
||||||
|
|
||||||
vi.mock('./useKeypress.js', () => ({
|
vi.mock('./useKeypress.js', () => ({
|
||||||
useKeypress: vi.fn(),
|
useKeypress: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('./useKeyMatchers.js', () => ({
|
||||||
|
useKeyMatchers: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
const createKey = (partial: Partial<Key>): Key => ({
|
const createKey = (partial: Partial<Key>): Key => ({
|
||||||
name: partial.name || '',
|
name: partial.name || '',
|
||||||
sequence: partial.sequence || '',
|
sequence: partial.sequence || '',
|
||||||
@@ -26,13 +32,14 @@ const createKey = (partial: Partial<Key>): Key => ({
|
|||||||
...partial,
|
...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', () => ({
|
vi.mock('../keyMatchers.js', () => ({
|
||||||
keyMatchers: {
|
|
||||||
'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),
|
|
||||||
},
|
|
||||||
Command: {
|
Command: {
|
||||||
MOVE_LEFT: 'cursor.left',
|
MOVE_LEFT: 'cursor.left',
|
||||||
MOVE_RIGHT: 'cursor.right',
|
MOVE_RIGHT: 'cursor.right',
|
||||||
@@ -45,6 +52,7 @@ describe('useTabbedNavigation', () => {
|
|||||||
let capturedHandler: KeypressHandler;
|
let capturedHandler: KeypressHandler;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
vi.mocked(useKeyMatchers).mockReturnValue(mockKeyMatchers);
|
||||||
vi.mocked(useKeypress).mockImplementation((handler) => {
|
vi.mocked(useKeypress).mockImplementation((handler) => {
|
||||||
capturedHandler = handler;
|
capturedHandler = handler;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
|
|
||||||
import { useReducer, useCallback, useEffect, useRef } from 'react';
|
import { useReducer, useCallback, useEffect, useRef } from 'react';
|
||||||
import { useKeypress, type Key } from './useKeypress.js';
|
import { useKeypress, type Key } from './useKeypress.js';
|
||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
|
import { useKeyMatchers } from './useKeyMatchers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for the useTabbedNavigation hook.
|
* Options for the useTabbedNavigation hook.
|
||||||
@@ -147,6 +148,7 @@ export function useTabbedNavigation({
|
|||||||
isActive = true,
|
isActive = true,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
}: UseTabbedNavigationOptions): UseTabbedNavigationResult {
|
}: UseTabbedNavigationOptions): UseTabbedNavigationResult {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const [state, dispatch] = useReducer(tabbedNavigationReducer, {
|
const [state, dispatch] = useReducer(tabbedNavigationReducer, {
|
||||||
currentIndex: Math.max(0, Math.min(initialIndex, tabCount - 1)),
|
currentIndex: Math.max(0, Math.min(initialIndex, tabCount - 1)),
|
||||||
tabCount,
|
tabCount,
|
||||||
@@ -231,6 +233,7 @@ export function useTabbedNavigation({
|
|||||||
goToNextTab,
|
goToNextTab,
|
||||||
goToPrevTab,
|
goToPrevTab,
|
||||||
isNavigationBlocked,
|
isNavigationBlocked,
|
||||||
|
keyMatchers,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import type { Key } from './useKeypress.js';
|
|||||||
import type { TextBuffer } from '../components/shared/text-buffer.js';
|
import type { TextBuffer } from '../components/shared/text-buffer.js';
|
||||||
import { useVimMode } from '../contexts/VimModeContext.js';
|
import { useVimMode } from '../contexts/VimModeContext.js';
|
||||||
import { debugLogger } from '@google/gemini-cli-core';
|
import { debugLogger } from '@google/gemini-cli-core';
|
||||||
import { keyMatchers, Command } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
|
import { useKeyMatchers } from './useKeyMatchers.js';
|
||||||
|
|
||||||
export type VimMode = 'NORMAL' | 'INSERT';
|
export type VimMode = 'NORMAL' | 'INSERT';
|
||||||
|
|
||||||
@@ -152,6 +153,7 @@ const vimReducer = (state: VimState, action: VimAction): VimState => {
|
|||||||
* @returns Object with vim state and input handler
|
* @returns Object with vim state and input handler
|
||||||
*/
|
*/
|
||||||
export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
|
export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
|
||||||
|
const keyMatchers = useKeyMatchers();
|
||||||
const { vimEnabled, vimMode, setVimMode } = useVimMode();
|
const { vimEnabled, vimMode, setVimMode } = useVimMode();
|
||||||
const [state, dispatch] = useReducer(vimReducer, initialVimState);
|
const [state, dispatch] = useReducer(vimReducer, initialVimState);
|
||||||
|
|
||||||
@@ -439,7 +441,7 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
|
|||||||
|
|
||||||
return buffer.handleInput(normalizedKey);
|
return buffer.handleInput(normalizedKey);
|
||||||
},
|
},
|
||||||
[buffer, dispatch, updateMode, onSubmit, checkDoubleEscape],
|
[buffer, dispatch, updateMode, onSubmit, checkDoubleEscape, keyMatchers],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1202,6 +1204,7 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) {
|
|||||||
executeCommand,
|
executeCommand,
|
||||||
updateMode,
|
updateMode,
|
||||||
checkDoubleEscape,
|
checkDoubleEscape,
|
||||||
|
keyMatchers,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { keyMatchers, Command, createKeyMatchers } from './keyMatchers.js';
|
import {
|
||||||
|
defaultKeyMatchers,
|
||||||
|
Command,
|
||||||
|
createKeyMatchers,
|
||||||
|
} from './keyMatchers.js';
|
||||||
import type { KeyBindingConfig } from '../config/keyBindings.js';
|
import type { KeyBindingConfig } from '../config/keyBindings.js';
|
||||||
import { defaultKeyBindings } from '../config/keyBindings.js';
|
import { defaultKeyBindings } from '../config/keyBindings.js';
|
||||||
import type { Key } from './hooks/useKeypress.js';
|
import type { Key } from './hooks/useKeypress.js';
|
||||||
@@ -422,14 +426,14 @@ describe('keyMatchers', () => {
|
|||||||
it(`should match ${command} correctly`, () => {
|
it(`should match ${command} correctly`, () => {
|
||||||
positive.forEach((key) => {
|
positive.forEach((key) => {
|
||||||
expect(
|
expect(
|
||||||
keyMatchers[command](key),
|
defaultKeyMatchers[command](key),
|
||||||
`Expected ${command} to match ${JSON.stringify(key)}`,
|
`Expected ${command} to match ${JSON.stringify(key)}`,
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
negative.forEach((key) => {
|
negative.forEach((key) => {
|
||||||
expect(
|
expect(
|
||||||
keyMatchers[command](key),
|
defaultKeyMatchers[command](key),
|
||||||
`Expected ${command} to NOT match ${JSON.stringify(key)}`,
|
`Expected ${command} to NOT match ${JSON.stringify(key)}`,
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ export function createKeyMatchers(
|
|||||||
/**
|
/**
|
||||||
* Default key binding matchers using the default configuration
|
* Default key binding matchers using the default configuration
|
||||||
*/
|
*/
|
||||||
export const keyMatchers: KeyMatchers = createKeyMatchers(defaultKeyBindings);
|
export const defaultKeyMatchers: KeyMatchers =
|
||||||
|
createKeyMatchers(defaultKeyBindings);
|
||||||
|
|
||||||
// Re-export Command for convenience
|
// Re-export Command for convenience
|
||||||
export { Command };
|
export { Command };
|
||||||
|
|||||||
@@ -4,9 +4,12 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Command, keyMatchers } from '../keyMatchers.js';
|
import { Command } from '../keyMatchers.js';
|
||||||
import type { Key } from '../hooks/useKeypress.js';
|
import type { Key } from '../hooks/useKeypress.js';
|
||||||
|
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
||||||
|
|
||||||
export function shouldDismissShortcutsHelpOnHotkey(key: Key): boolean {
|
export function useIsHelpDismissKey(): (key: Key) => boolean {
|
||||||
return Object.values(Command).some((command) => keyMatchers[command](key));
|
const keyMatchers = useKeyMatchers();
|
||||||
|
return (key: Key) =>
|
||||||
|
Object.values(Command).some((command) => keyMatchers[command](key));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user