mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
feat(cli): customizable keyboard shortcuts (#21945)
This commit is contained in:
committed by
GitHub
parent
657f19c1f3
commit
daf3701194
@@ -92,6 +92,8 @@ import { computeTerminalTitle } from './utils/windowTitle.js';
|
|||||||
|
|
||||||
import { SessionStatsProvider } from './ui/contexts/SessionContext.js';
|
import { SessionStatsProvider } from './ui/contexts/SessionContext.js';
|
||||||
import { VimModeProvider } from './ui/contexts/VimModeContext.js';
|
import { VimModeProvider } from './ui/contexts/VimModeContext.js';
|
||||||
|
import { KeyMatchersProvider } from './ui/hooks/useKeyMatchers.js';
|
||||||
|
import { loadKeyMatchers } from './ui/key/keyMatchers.js';
|
||||||
import { KeypressProvider } from './ui/contexts/KeypressContext.js';
|
import { KeypressProvider } from './ui/contexts/KeypressContext.js';
|
||||||
import { useKittyKeyboardProtocol } from './ui/hooks/useKittyKeyboardProtocol.js';
|
import { useKittyKeyboardProtocol } from './ui/hooks/useKittyKeyboardProtocol.js';
|
||||||
import {
|
import {
|
||||||
@@ -208,6 +210,11 @@ export async function startInteractiveUI(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { matchers, errors } = await loadKeyMatchers();
|
||||||
|
errors.forEach((error) => {
|
||||||
|
coreEvents.emitFeedback('warning', error);
|
||||||
|
});
|
||||||
|
|
||||||
const version = await getVersion();
|
const version = await getVersion();
|
||||||
setWindowTitle(basename(workspaceRoot), settings);
|
setWindowTitle(basename(workspaceRoot), settings);
|
||||||
|
|
||||||
@@ -230,9 +237,12 @@ export async function startInteractiveUI(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContext.Provider value={settings}>
|
<SettingsContext.Provider value={settings}>
|
||||||
|
<KeyMatchersProvider value={matchers}>
|
||||||
<KeypressProvider
|
<KeypressProvider
|
||||||
config={config}
|
config={config}
|
||||||
debugKeystrokeLogging={settings.merged.general.debugKeystrokeLogging}
|
debugKeystrokeLogging={
|
||||||
|
settings.merged.general.debugKeystrokeLogging
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<MouseProvider
|
<MouseProvider
|
||||||
mouseEventsEnabled={mouseEventsEnabled}
|
mouseEventsEnabled={mouseEventsEnabled}
|
||||||
@@ -259,6 +269,7 @@ export async function startInteractiveUI(
|
|||||||
</TerminalProvider>
|
</TerminalProvider>
|
||||||
</MouseProvider>
|
</MouseProvider>
|
||||||
</KeypressProvider>
|
</KeypressProvider>
|
||||||
|
</KeyMatchersProvider>
|
||||||
</SettingsContext.Provider>
|
</SettingsContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2026 Google LLC
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import type { KeyMatchers } from '../key/keyMatchers.js';
|
|
||||||
import { defaultKeyMatchers } from '../key/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, []);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type React from 'react';
|
||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
import type { KeyMatchers } from '../key/keyMatchers.js';
|
||||||
|
import { defaultKeyMatchers } from '../key/keyMatchers.js';
|
||||||
|
|
||||||
|
export const KeyMatchersContext =
|
||||||
|
createContext<KeyMatchers>(defaultKeyMatchers);
|
||||||
|
|
||||||
|
export const KeyMatchersProvider = ({
|
||||||
|
children,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
value: KeyMatchers;
|
||||||
|
}): React.JSX.Element => (
|
||||||
|
<KeyMatchersContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</KeyMatchersContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to retrieve the currently active key matchers.
|
||||||
|
* Defaults to defaultKeyMatchers if no provider is present, allowing tests to run without explicit wrappers.
|
||||||
|
*/
|
||||||
|
export function useKeyMatchers(): KeyMatchers {
|
||||||
|
return useContext(KeyMatchersContext);
|
||||||
|
}
|
||||||
@@ -4,14 +4,18 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import type { KeyBindingConfig } from './keyBindings.js';
|
import * as os from 'node:os';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import * as fs from 'node:fs/promises';
|
||||||
|
import { Storage } from '@google/gemini-cli-core';
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
commandCategories,
|
commandCategories,
|
||||||
commandDescriptions,
|
commandDescriptions,
|
||||||
defaultKeyBindings,
|
defaultKeyBindingConfig,
|
||||||
KeyBinding,
|
KeyBinding,
|
||||||
|
loadCustomKeybindings,
|
||||||
} from './keyBindings.js';
|
} from './keyBindings.js';
|
||||||
|
|
||||||
describe('KeyBinding', () => {
|
describe('KeyBinding', () => {
|
||||||
@@ -104,28 +108,13 @@ describe('KeyBinding', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('keyBindings config', () => {
|
describe('keyBindings config', () => {
|
||||||
describe('defaultKeyBindings', () => {
|
|
||||||
it('should have bindings for all commands', () => {
|
it('should have bindings for all commands', () => {
|
||||||
const commands = Object.values(Command);
|
for (const command of Object.values(Command)) {
|
||||||
|
expect(defaultKeyBindingConfig.has(command)).toBe(true);
|
||||||
for (const command of commands) {
|
expect(defaultKeyBindingConfig.get(command)?.length).toBeGreaterThan(0);
|
||||||
expect(defaultKeyBindings[command]).toBeDefined();
|
|
||||||
expect(Array.isArray(defaultKeyBindings[command])).toBe(true);
|
|
||||||
expect(defaultKeyBindings[command]?.length).toBeGreaterThan(0);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should export all required types', () => {
|
|
||||||
// Basic type checks
|
|
||||||
expect(typeof Command.HOME).toBe('string');
|
|
||||||
expect(typeof Command.END).toBe('string');
|
|
||||||
|
|
||||||
// Config should be readonly
|
|
||||||
const config: KeyBindingConfig = defaultKeyBindings;
|
|
||||||
expect(config[Command.HOME]).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('command metadata', () => {
|
describe('command metadata', () => {
|
||||||
const commandValues = Object.values(Command);
|
const commandValues = Object.values(Command);
|
||||||
|
|
||||||
@@ -157,3 +146,92 @@ describe('keyBindings config', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('loadCustomKeybindings', () => {
|
||||||
|
let tempDir: string;
|
||||||
|
let tempFilePath: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
tempDir = await fs.mkdtemp(
|
||||||
|
path.join(os.tmpdir(), 'gemini-keybindings-test-'),
|
||||||
|
);
|
||||||
|
tempFilePath = path.join(tempDir, 'keybindings.json');
|
||||||
|
vi.spyOn(Storage, 'getUserKeybindingsPath').mockReturnValue(tempFilePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true });
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns default bindings when file does not exist', async () => {
|
||||||
|
// We don't write the file.
|
||||||
|
const { config, errors } = await loadCustomKeybindings();
|
||||||
|
|
||||||
|
expect(errors).toHaveLength(0);
|
||||||
|
expect(config.get(Command.RETURN)).toEqual([new KeyBinding('enter')]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges valid custom bindings, prepending them to defaults', async () => {
|
||||||
|
const customJson = JSON.stringify([
|
||||||
|
{ command: Command.RETURN, key: 'ctrl+a' },
|
||||||
|
]);
|
||||||
|
await fs.writeFile(tempFilePath, customJson, 'utf8');
|
||||||
|
|
||||||
|
const { config, errors } = await loadCustomKeybindings();
|
||||||
|
|
||||||
|
expect(errors).toHaveLength(0);
|
||||||
|
expect(config.get(Command.RETURN)).toEqual([
|
||||||
|
new KeyBinding('ctrl+a'),
|
||||||
|
new KeyBinding('enter'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles JSON with comments', async () => {
|
||||||
|
const customJson = `
|
||||||
|
[
|
||||||
|
// This is a comment
|
||||||
|
{ "command": "${Command.QUIT}", "key": "ctrl+x" }
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
await fs.writeFile(tempFilePath, customJson, 'utf8');
|
||||||
|
|
||||||
|
const { config, errors } = await loadCustomKeybindings();
|
||||||
|
|
||||||
|
expect(errors).toHaveLength(0);
|
||||||
|
expect(config.get(Command.QUIT)).toEqual([
|
||||||
|
new KeyBinding('ctrl+x'),
|
||||||
|
new KeyBinding('ctrl+c'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns validation errors for invalid schema', async () => {
|
||||||
|
const invalidJson = JSON.stringify([{ command: 'unknown', key: 'a' }]);
|
||||||
|
await fs.writeFile(tempFilePath, invalidJson, 'utf8');
|
||||||
|
|
||||||
|
const { config, errors } = await loadCustomKeybindings();
|
||||||
|
|
||||||
|
expect(errors.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
expect(errors[0]).toMatch(/error at 0.command: Invalid enum value/);
|
||||||
|
// Should still have defaults
|
||||||
|
expect(config.get(Command.RETURN)).toEqual([new KeyBinding('enter')]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns validation errors for invalid key patterns but loads valid ones', async () => {
|
||||||
|
const mixedJson = JSON.stringify([
|
||||||
|
{ command: Command.RETURN, key: 'super+a' }, // invalid
|
||||||
|
{ command: Command.QUIT, key: 'ctrl+y' }, // valid
|
||||||
|
]);
|
||||||
|
await fs.writeFile(tempFilePath, mixedJson, 'utf8');
|
||||||
|
|
||||||
|
const { config, errors } = await loadCustomKeybindings();
|
||||||
|
|
||||||
|
expect(errors.length).toBe(1);
|
||||||
|
expect(errors[0]).toMatch(/Invalid keybinding/);
|
||||||
|
expect(config.get(Command.QUIT)).toEqual([
|
||||||
|
new KeyBinding('ctrl+y'),
|
||||||
|
new KeyBinding('ctrl+c'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'node:fs/promises';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { parse as parseIgnoringComments } from 'comment-json';
|
||||||
|
import { isNodeError, Storage } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Command enum for all available keyboard shortcuts
|
* Command enum for all available keyboard shortcuts
|
||||||
*/
|
*/
|
||||||
@@ -213,153 +218,172 @@ export class KeyBinding {
|
|||||||
/**
|
/**
|
||||||
* Configuration type mapping commands to their key bindings
|
* Configuration type mapping commands to their key bindings
|
||||||
*/
|
*/
|
||||||
export type KeyBindingConfig = {
|
export type KeyBindingConfig = Map<Command, readonly KeyBinding[]>;
|
||||||
readonly [C in Command]: readonly KeyBinding[];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default key binding configuration
|
* Default key binding configuration
|
||||||
* Matches the original hard-coded logic exactly
|
* Matches the original hard-coded logic exactly
|
||||||
*/
|
*/
|
||||||
export const defaultKeyBindings: KeyBindingConfig = {
|
export const defaultKeyBindingConfig: KeyBindingConfig = new Map([
|
||||||
// Basic Controls
|
// Basic Controls
|
||||||
[Command.RETURN]: [new KeyBinding('enter')],
|
[Command.RETURN, [new KeyBinding('enter')]],
|
||||||
[Command.ESCAPE]: [new KeyBinding('escape'), new KeyBinding('ctrl+[')],
|
[Command.ESCAPE, [new KeyBinding('escape'), new KeyBinding('ctrl+[')]],
|
||||||
[Command.QUIT]: [new KeyBinding('ctrl+c')],
|
[Command.QUIT, [new KeyBinding('ctrl+c')]],
|
||||||
[Command.EXIT]: [new KeyBinding('ctrl+d')],
|
[Command.EXIT, [new KeyBinding('ctrl+d')]],
|
||||||
|
|
||||||
// Cursor Movement
|
// Cursor Movement
|
||||||
[Command.HOME]: [new KeyBinding('ctrl+a'), new KeyBinding('home')],
|
[Command.HOME, [new KeyBinding('ctrl+a'), new KeyBinding('home')]],
|
||||||
[Command.END]: [new KeyBinding('ctrl+e'), new KeyBinding('end')],
|
[Command.END, [new KeyBinding('ctrl+e'), new KeyBinding('end')]],
|
||||||
[Command.MOVE_UP]: [new KeyBinding('up')],
|
[Command.MOVE_UP, [new KeyBinding('up')]],
|
||||||
[Command.MOVE_DOWN]: [new KeyBinding('down')],
|
[Command.MOVE_DOWN, [new KeyBinding('down')]],
|
||||||
[Command.MOVE_LEFT]: [new KeyBinding('left')],
|
[Command.MOVE_LEFT, [new KeyBinding('left')]],
|
||||||
[Command.MOVE_RIGHT]: [new KeyBinding('right'), new KeyBinding('ctrl+f')],
|
[Command.MOVE_RIGHT, [new KeyBinding('right'), new KeyBinding('ctrl+f')]],
|
||||||
[Command.MOVE_WORD_LEFT]: [
|
[
|
||||||
|
Command.MOVE_WORD_LEFT,
|
||||||
|
[
|
||||||
new KeyBinding('ctrl+left'),
|
new KeyBinding('ctrl+left'),
|
||||||
new KeyBinding('alt+left'),
|
new KeyBinding('alt+left'),
|
||||||
new KeyBinding('alt+b'),
|
new KeyBinding('alt+b'),
|
||||||
],
|
],
|
||||||
[Command.MOVE_WORD_RIGHT]: [
|
],
|
||||||
|
[
|
||||||
|
Command.MOVE_WORD_RIGHT,
|
||||||
|
[
|
||||||
new KeyBinding('ctrl+right'),
|
new KeyBinding('ctrl+right'),
|
||||||
new KeyBinding('alt+right'),
|
new KeyBinding('alt+right'),
|
||||||
new KeyBinding('alt+f'),
|
new KeyBinding('alt+f'),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
|
||||||
// Editing
|
// Editing
|
||||||
[Command.KILL_LINE_RIGHT]: [new KeyBinding('ctrl+k')],
|
[Command.KILL_LINE_RIGHT, [new KeyBinding('ctrl+k')]],
|
||||||
[Command.KILL_LINE_LEFT]: [new KeyBinding('ctrl+u')],
|
[Command.KILL_LINE_LEFT, [new KeyBinding('ctrl+u')]],
|
||||||
[Command.CLEAR_INPUT]: [new KeyBinding('ctrl+c')],
|
[Command.CLEAR_INPUT, [new KeyBinding('ctrl+c')]],
|
||||||
[Command.DELETE_WORD_BACKWARD]: [
|
[
|
||||||
|
Command.DELETE_WORD_BACKWARD,
|
||||||
|
[
|
||||||
new KeyBinding('ctrl+backspace'),
|
new KeyBinding('ctrl+backspace'),
|
||||||
new KeyBinding('alt+backspace'),
|
new KeyBinding('alt+backspace'),
|
||||||
new KeyBinding('ctrl+w'),
|
new KeyBinding('ctrl+w'),
|
||||||
],
|
],
|
||||||
[Command.DELETE_WORD_FORWARD]: [
|
],
|
||||||
|
[
|
||||||
|
Command.DELETE_WORD_FORWARD,
|
||||||
|
[
|
||||||
new KeyBinding('ctrl+delete'),
|
new KeyBinding('ctrl+delete'),
|
||||||
new KeyBinding('alt+delete'),
|
new KeyBinding('alt+delete'),
|
||||||
new KeyBinding('alt+d'),
|
new KeyBinding('alt+d'),
|
||||||
],
|
],
|
||||||
[Command.DELETE_CHAR_LEFT]: [
|
|
||||||
new KeyBinding('backspace'),
|
|
||||||
new KeyBinding('ctrl+h'),
|
|
||||||
],
|
],
|
||||||
[Command.DELETE_CHAR_RIGHT]: [
|
[
|
||||||
new KeyBinding('delete'),
|
Command.DELETE_CHAR_LEFT,
|
||||||
new KeyBinding('ctrl+d'),
|
[new KeyBinding('backspace'), new KeyBinding('ctrl+h')],
|
||||||
],
|
],
|
||||||
[Command.UNDO]: [new KeyBinding('cmd+z'), new KeyBinding('alt+z')],
|
[
|
||||||
[Command.REDO]: [
|
Command.DELETE_CHAR_RIGHT,
|
||||||
|
[new KeyBinding('delete'), new KeyBinding('ctrl+d')],
|
||||||
|
],
|
||||||
|
[Command.UNDO, [new KeyBinding('cmd+z'), new KeyBinding('alt+z')]],
|
||||||
|
[
|
||||||
|
Command.REDO,
|
||||||
|
[
|
||||||
new KeyBinding('ctrl+shift+z'),
|
new KeyBinding('ctrl+shift+z'),
|
||||||
new KeyBinding('cmd+shift+z'),
|
new KeyBinding('cmd+shift+z'),
|
||||||
new KeyBinding('alt+shift+z'),
|
new KeyBinding('alt+shift+z'),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
|
||||||
// Scrolling
|
// Scrolling
|
||||||
[Command.SCROLL_UP]: [new KeyBinding('shift+up')],
|
[Command.SCROLL_UP, [new KeyBinding('shift+up')]],
|
||||||
[Command.SCROLL_DOWN]: [new KeyBinding('shift+down')],
|
[Command.SCROLL_DOWN, [new KeyBinding('shift+down')]],
|
||||||
[Command.SCROLL_HOME]: [
|
[
|
||||||
new KeyBinding('ctrl+home'),
|
Command.SCROLL_HOME,
|
||||||
new KeyBinding('shift+home'),
|
[new KeyBinding('ctrl+home'), new KeyBinding('shift+home')],
|
||||||
],
|
],
|
||||||
[Command.SCROLL_END]: [
|
[
|
||||||
new KeyBinding('ctrl+end'),
|
Command.SCROLL_END,
|
||||||
new KeyBinding('shift+end'),
|
[new KeyBinding('ctrl+end'), new KeyBinding('shift+end')],
|
||||||
],
|
],
|
||||||
[Command.PAGE_UP]: [new KeyBinding('pageup')],
|
[Command.PAGE_UP, [new KeyBinding('pageup')]],
|
||||||
[Command.PAGE_DOWN]: [new KeyBinding('pagedown')],
|
[Command.PAGE_DOWN, [new KeyBinding('pagedown')]],
|
||||||
|
|
||||||
// History & Search
|
// History & Search
|
||||||
[Command.HISTORY_UP]: [new KeyBinding('ctrl+p')],
|
[Command.HISTORY_UP, [new KeyBinding('ctrl+p')]],
|
||||||
[Command.HISTORY_DOWN]: [new KeyBinding('ctrl+n')],
|
[Command.HISTORY_DOWN, [new KeyBinding('ctrl+n')]],
|
||||||
[Command.REVERSE_SEARCH]: [new KeyBinding('ctrl+r')],
|
[Command.REVERSE_SEARCH, [new KeyBinding('ctrl+r')]],
|
||||||
[Command.SUBMIT_REVERSE_SEARCH]: [new KeyBinding('enter')],
|
[Command.SUBMIT_REVERSE_SEARCH, [new KeyBinding('enter')]],
|
||||||
[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH]: [new KeyBinding('tab')],
|
[Command.ACCEPT_SUGGESTION_REVERSE_SEARCH, [new KeyBinding('tab')]],
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
[Command.NAVIGATION_UP]: [new KeyBinding('up')],
|
[Command.NAVIGATION_UP, [new KeyBinding('up')]],
|
||||||
[Command.NAVIGATION_DOWN]: [new KeyBinding('down')],
|
[Command.NAVIGATION_DOWN, [new KeyBinding('down')]],
|
||||||
// Navigation shortcuts appropriate for dialogs where we do not need to accept
|
// Navigation shortcuts appropriate for dialogs where we do not need to accept
|
||||||
// text input.
|
// text input.
|
||||||
[Command.DIALOG_NAVIGATION_UP]: [new KeyBinding('up'), new KeyBinding('k')],
|
[Command.DIALOG_NAVIGATION_UP, [new KeyBinding('up'), new KeyBinding('k')]],
|
||||||
[Command.DIALOG_NAVIGATION_DOWN]: [
|
[
|
||||||
new KeyBinding('down'),
|
Command.DIALOG_NAVIGATION_DOWN,
|
||||||
new KeyBinding('j'),
|
[new KeyBinding('down'), new KeyBinding('j')],
|
||||||
],
|
],
|
||||||
[Command.DIALOG_NEXT]: [new KeyBinding('tab')],
|
[Command.DIALOG_NEXT, [new KeyBinding('tab')]],
|
||||||
[Command.DIALOG_PREV]: [new KeyBinding('shift+tab')],
|
[Command.DIALOG_PREV, [new KeyBinding('shift+tab')]],
|
||||||
|
|
||||||
// Suggestions & Completions
|
// Suggestions & Completions
|
||||||
[Command.ACCEPT_SUGGESTION]: [new KeyBinding('tab'), new KeyBinding('enter')],
|
[Command.ACCEPT_SUGGESTION, [new KeyBinding('tab'), new KeyBinding('enter')]],
|
||||||
[Command.COMPLETION_UP]: [new KeyBinding('up'), new KeyBinding('ctrl+p')],
|
[Command.COMPLETION_UP, [new KeyBinding('up'), new KeyBinding('ctrl+p')]],
|
||||||
[Command.COMPLETION_DOWN]: [new KeyBinding('down'), new KeyBinding('ctrl+n')],
|
[Command.COMPLETION_DOWN, [new KeyBinding('down'), new KeyBinding('ctrl+n')]],
|
||||||
[Command.EXPAND_SUGGESTION]: [new KeyBinding('right')],
|
[Command.EXPAND_SUGGESTION, [new KeyBinding('right')]],
|
||||||
[Command.COLLAPSE_SUGGESTION]: [new KeyBinding('left')],
|
[Command.COLLAPSE_SUGGESTION, [new KeyBinding('left')]],
|
||||||
|
|
||||||
// Text Input
|
// Text Input
|
||||||
// Must also exclude shift to allow shift+enter for newline
|
// Must also exclude shift to allow shift+enter for newline
|
||||||
[Command.SUBMIT]: [new KeyBinding('enter')],
|
[Command.SUBMIT, [new KeyBinding('enter')]],
|
||||||
[Command.NEWLINE]: [
|
[
|
||||||
|
Command.NEWLINE,
|
||||||
|
[
|
||||||
new KeyBinding('ctrl+enter'),
|
new KeyBinding('ctrl+enter'),
|
||||||
new KeyBinding('cmd+enter'),
|
new KeyBinding('cmd+enter'),
|
||||||
new KeyBinding('alt+enter'),
|
new KeyBinding('alt+enter'),
|
||||||
new KeyBinding('shift+enter'),
|
new KeyBinding('shift+enter'),
|
||||||
new KeyBinding('ctrl+j'),
|
new KeyBinding('ctrl+j'),
|
||||||
],
|
],
|
||||||
[Command.OPEN_EXTERNAL_EDITOR]: [new KeyBinding('ctrl+x')],
|
],
|
||||||
[Command.PASTE_CLIPBOARD]: [
|
[Command.OPEN_EXTERNAL_EDITOR, [new KeyBinding('ctrl+x')]],
|
||||||
|
[
|
||||||
|
Command.PASTE_CLIPBOARD,
|
||||||
|
[
|
||||||
new KeyBinding('ctrl+v'),
|
new KeyBinding('ctrl+v'),
|
||||||
new KeyBinding('cmd+v'),
|
new KeyBinding('cmd+v'),
|
||||||
new KeyBinding('alt+v'),
|
new KeyBinding('alt+v'),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
|
||||||
// App Controls
|
// App Controls
|
||||||
[Command.SHOW_ERROR_DETAILS]: [new KeyBinding('f12')],
|
[Command.SHOW_ERROR_DETAILS, [new KeyBinding('f12')]],
|
||||||
[Command.SHOW_FULL_TODOS]: [new KeyBinding('ctrl+t')],
|
[Command.SHOW_FULL_TODOS, [new KeyBinding('ctrl+t')]],
|
||||||
[Command.SHOW_IDE_CONTEXT_DETAIL]: [new KeyBinding('ctrl+g')],
|
[Command.SHOW_IDE_CONTEXT_DETAIL, [new KeyBinding('ctrl+g')]],
|
||||||
[Command.TOGGLE_MARKDOWN]: [new KeyBinding('alt+m')],
|
[Command.TOGGLE_MARKDOWN, [new KeyBinding('alt+m')]],
|
||||||
[Command.TOGGLE_COPY_MODE]: [new KeyBinding('ctrl+s')],
|
[Command.TOGGLE_COPY_MODE, [new KeyBinding('ctrl+s')]],
|
||||||
[Command.TOGGLE_YOLO]: [new KeyBinding('ctrl+y')],
|
[Command.TOGGLE_YOLO, [new KeyBinding('ctrl+y')]],
|
||||||
[Command.CYCLE_APPROVAL_MODE]: [new KeyBinding('shift+tab')],
|
[Command.CYCLE_APPROVAL_MODE, [new KeyBinding('shift+tab')]],
|
||||||
[Command.SHOW_MORE_LINES]: [new KeyBinding('ctrl+o')],
|
[Command.SHOW_MORE_LINES, [new KeyBinding('ctrl+o')]],
|
||||||
[Command.EXPAND_PASTE]: [new KeyBinding('ctrl+o')],
|
[Command.EXPAND_PASTE, [new KeyBinding('ctrl+o')]],
|
||||||
[Command.FOCUS_SHELL_INPUT]: [new KeyBinding('tab')],
|
[Command.FOCUS_SHELL_INPUT, [new KeyBinding('tab')]],
|
||||||
[Command.UNFOCUS_SHELL_INPUT]: [new KeyBinding('shift+tab')],
|
[Command.UNFOCUS_SHELL_INPUT, [new KeyBinding('shift+tab')]],
|
||||||
[Command.CLEAR_SCREEN]: [new KeyBinding('ctrl+l')],
|
[Command.CLEAR_SCREEN, [new KeyBinding('ctrl+l')]],
|
||||||
[Command.RESTART_APP]: [new KeyBinding('r'), new KeyBinding('shift+r')],
|
[Command.RESTART_APP, [new KeyBinding('r'), new KeyBinding('shift+r')]],
|
||||||
[Command.SUSPEND_APP]: [new KeyBinding('ctrl+z')],
|
[Command.SUSPEND_APP, [new KeyBinding('ctrl+z')]],
|
||||||
[Command.SHOW_SHELL_INPUT_UNFOCUS_WARNING]: [new KeyBinding('tab')],
|
[Command.SHOW_SHELL_INPUT_UNFOCUS_WARNING, [new KeyBinding('tab')]],
|
||||||
|
|
||||||
// Background Shell Controls
|
// Background Shell Controls
|
||||||
[Command.BACKGROUND_SHELL_ESCAPE]: [new KeyBinding('escape')],
|
[Command.BACKGROUND_SHELL_ESCAPE, [new KeyBinding('escape')]],
|
||||||
[Command.BACKGROUND_SHELL_SELECT]: [new KeyBinding('enter')],
|
[Command.BACKGROUND_SHELL_SELECT, [new KeyBinding('enter')]],
|
||||||
[Command.TOGGLE_BACKGROUND_SHELL]: [new KeyBinding('ctrl+b')],
|
[Command.TOGGLE_BACKGROUND_SHELL, [new KeyBinding('ctrl+b')]],
|
||||||
[Command.TOGGLE_BACKGROUND_SHELL_LIST]: [new KeyBinding('ctrl+l')],
|
[Command.TOGGLE_BACKGROUND_SHELL_LIST, [new KeyBinding('ctrl+l')]],
|
||||||
[Command.KILL_BACKGROUND_SHELL]: [new KeyBinding('ctrl+k')],
|
[Command.KILL_BACKGROUND_SHELL, [new KeyBinding('ctrl+k')]],
|
||||||
[Command.UNFOCUS_BACKGROUND_SHELL]: [new KeyBinding('shift+tab')],
|
[Command.UNFOCUS_BACKGROUND_SHELL, [new KeyBinding('shift+tab')]],
|
||||||
[Command.UNFOCUS_BACKGROUND_SHELL_LIST]: [new KeyBinding('tab')],
|
[Command.UNFOCUS_BACKGROUND_SHELL_LIST, [new KeyBinding('tab')]],
|
||||||
[Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING]: [new KeyBinding('tab')],
|
[Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING, [new KeyBinding('tab')]],
|
||||||
};
|
]);
|
||||||
|
|
||||||
interface CommandCategory {
|
interface CommandCategory {
|
||||||
readonly title: string;
|
readonly title: string;
|
||||||
@@ -593,3 +617,62 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
|
|||||||
[Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING]:
|
[Command.SHOW_BACKGROUND_SHELL_UNFOCUS_WARNING]:
|
||||||
'Show warning when trying to move focus away from background shell.',
|
'Show warning when trying to move focus away from background shell.',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const keybindingsSchema = z.array(
|
||||||
|
z.object({
|
||||||
|
command: z.nativeEnum(Command),
|
||||||
|
key: z.string(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads custom keybindings from the user's keybindings.json file.
|
||||||
|
* Keybindings are merged with the default bindings.
|
||||||
|
*/
|
||||||
|
export async function loadCustomKeybindings(): Promise<{
|
||||||
|
config: KeyBindingConfig;
|
||||||
|
errors: string[];
|
||||||
|
}> {
|
||||||
|
const errors: string[] = [];
|
||||||
|
let config = defaultKeyBindingConfig;
|
||||||
|
|
||||||
|
const userKeybindingsPath = Storage.getUserKeybindingsPath();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(userKeybindingsPath, 'utf8');
|
||||||
|
const parsedJson = parseIgnoringComments(content);
|
||||||
|
const result = keybindingsSchema.safeParse(parsedJson);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
config = new Map(defaultKeyBindingConfig);
|
||||||
|
for (const { command, key } of result.data) {
|
||||||
|
const currentBindings = config.get(command) ?? [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keyBinding = new KeyBinding(key);
|
||||||
|
// Add new binding (prepend so it's the primary one shown in UI)
|
||||||
|
config.set(command, [keyBinding, ...currentBindings]);
|
||||||
|
} catch (e) {
|
||||||
|
errors.push(`Invalid keybinding for command "${command}": ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
errors.push(
|
||||||
|
...result.error.issues.map(
|
||||||
|
(issue) =>
|
||||||
|
`Keybindings file "${userKeybindingsPath}" error at ${issue.path.join('.')}: ${issue.message}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (isNodeError(error) && error.code === 'ENOENT') {
|
||||||
|
// File doesn't exist, use default bindings
|
||||||
|
} else {
|
||||||
|
errors.push(
|
||||||
|
`Error reading keybindings file "${userKeybindingsPath}": ${error}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { config, errors };
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,18 +4,21 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import * as os from 'node:os';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import * as fs from 'node:fs/promises';
|
||||||
|
import { Storage } from '@google/gemini-cli-core';
|
||||||
import {
|
import {
|
||||||
defaultKeyMatchers,
|
defaultKeyMatchers,
|
||||||
Command,
|
Command,
|
||||||
createKeyMatchers,
|
createKeyMatchers,
|
||||||
|
loadKeyMatchers,
|
||||||
} from './keyMatchers.js';
|
} from './keyMatchers.js';
|
||||||
import type { KeyBindingConfig } from './keyBindings.js';
|
import { defaultKeyBindingConfig, KeyBinding } from './keyBindings.js';
|
||||||
import { defaultKeyBindings, KeyBinding } from './keyBindings.js';
|
|
||||||
import type { Key } from '../hooks/useKeypress.js';
|
import type { Key } from '../hooks/useKeypress.js';
|
||||||
|
|
||||||
describe('keyMatchers', () => {
|
const createKey = (name: string, mods: Partial<Key> = {}): Key => ({
|
||||||
const createKey = (name: string, mods: Partial<Key> = {}): Key => ({
|
|
||||||
name,
|
name,
|
||||||
shift: false,
|
shift: false,
|
||||||
alt: false,
|
alt: false,
|
||||||
@@ -24,8 +27,9 @@ describe('keyMatchers', () => {
|
|||||||
insertable: false,
|
insertable: false,
|
||||||
sequence: name,
|
sequence: name,
|
||||||
...mods,
|
...mods,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('keyMatchers', () => {
|
||||||
// Test data for each command with positive and negative test cases
|
// Test data for each command with positive and negative test cases
|
||||||
const testCases = [
|
const testCases = [
|
||||||
// Basic bindings
|
// Basic bindings
|
||||||
@@ -443,10 +447,11 @@ describe('keyMatchers', () => {
|
|||||||
|
|
||||||
describe('Custom key bindings', () => {
|
describe('Custom key bindings', () => {
|
||||||
it('should work with custom configuration', () => {
|
it('should work with custom configuration', () => {
|
||||||
const customConfig: KeyBindingConfig = {
|
const customConfig = new Map(defaultKeyBindingConfig);
|
||||||
...defaultKeyBindings,
|
customConfig.set(Command.HOME, [
|
||||||
[Command.HOME]: [new KeyBinding('ctrl+h'), new KeyBinding('0')],
|
new KeyBinding('ctrl+h'),
|
||||||
};
|
new KeyBinding('0'),
|
||||||
|
]);
|
||||||
|
|
||||||
const customMatchers = createKeyMatchers(customConfig);
|
const customMatchers = createKeyMatchers(customConfig);
|
||||||
|
|
||||||
@@ -460,10 +465,11 @@ describe('keyMatchers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should support multiple key bindings for same command', () => {
|
it('should support multiple key bindings for same command', () => {
|
||||||
const config: KeyBindingConfig = {
|
const config = new Map(defaultKeyBindingConfig);
|
||||||
...defaultKeyBindings,
|
config.set(Command.QUIT, [
|
||||||
[Command.QUIT]: [new KeyBinding('ctrl+q'), new KeyBinding('alt+q')],
|
new KeyBinding('ctrl+q'),
|
||||||
};
|
new KeyBinding('alt+q'),
|
||||||
|
]);
|
||||||
|
|
||||||
const matchers = createKeyMatchers(config);
|
const matchers = createKeyMatchers(config);
|
||||||
expect(matchers[Command.QUIT](createKey('q', { ctrl: true }))).toBe(true);
|
expect(matchers[Command.QUIT](createKey('q', { ctrl: true }))).toBe(true);
|
||||||
@@ -473,10 +479,8 @@ describe('keyMatchers', () => {
|
|||||||
|
|
||||||
describe('Edge Cases', () => {
|
describe('Edge Cases', () => {
|
||||||
it('should handle empty binding arrays', () => {
|
it('should handle empty binding arrays', () => {
|
||||||
const config: KeyBindingConfig = {
|
const config = new Map(defaultKeyBindingConfig);
|
||||||
...defaultKeyBindings,
|
config.set(Command.HOME, []);
|
||||||
[Command.HOME]: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const matchers = createKeyMatchers(config);
|
const matchers = createKeyMatchers(config);
|
||||||
expect(matchers[Command.HOME](createKey('a', { ctrl: true }))).toBe(
|
expect(matchers[Command.HOME](createKey('a', { ctrl: true }))).toBe(
|
||||||
@@ -485,3 +489,44 @@ describe('keyMatchers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('loadKeyMatchers integration', () => {
|
||||||
|
let tempDir: string;
|
||||||
|
let tempFilePath: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
tempDir = await fs.mkdtemp(
|
||||||
|
path.join(os.tmpdir(), 'gemini-keymatchers-test-'),
|
||||||
|
);
|
||||||
|
tempFilePath = path.join(tempDir, 'keybindings.json');
|
||||||
|
vi.spyOn(Storage, 'getUserKeybindingsPath').mockReturnValue(tempFilePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true });
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads matchers from a real file on disk', async () => {
|
||||||
|
const customJson = JSON.stringify([
|
||||||
|
{ command: Command.QUIT, key: 'ctrl+y' },
|
||||||
|
]);
|
||||||
|
await fs.writeFile(tempFilePath, customJson, 'utf8');
|
||||||
|
|
||||||
|
const { matchers, errors } = await loadKeyMatchers();
|
||||||
|
|
||||||
|
expect(errors).toHaveLength(0);
|
||||||
|
// User binding matches
|
||||||
|
expect(matchers[Command.QUIT](createKey('y', { ctrl: true }))).toBe(true);
|
||||||
|
// Default binding still matches as fallback
|
||||||
|
expect(matchers[Command.QUIT](createKey('c', { ctrl: true }))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns errors when the file on disk is invalid', async () => {
|
||||||
|
await fs.writeFile(tempFilePath, 'invalid json {', 'utf8');
|
||||||
|
|
||||||
|
const { errors } = await loadKeyMatchers();
|
||||||
|
|
||||||
|
expect(errors.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -6,7 +6,11 @@
|
|||||||
|
|
||||||
import type { Key } from '../hooks/useKeypress.js';
|
import type { Key } from '../hooks/useKeypress.js';
|
||||||
import type { KeyBindingConfig } from './keyBindings.js';
|
import type { KeyBindingConfig } from './keyBindings.js';
|
||||||
import { Command, defaultKeyBindings } from './keyBindings.js';
|
import {
|
||||||
|
Command,
|
||||||
|
defaultKeyBindingConfig,
|
||||||
|
loadCustomKeybindings,
|
||||||
|
} from './keyBindings.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a key matches any of the bindings for a command
|
* Checks if a key matches any of the bindings for a command
|
||||||
@@ -14,9 +18,11 @@ import { Command, defaultKeyBindings } from './keyBindings.js';
|
|||||||
function matchCommand(
|
function matchCommand(
|
||||||
command: Command,
|
command: Command,
|
||||||
key: Key,
|
key: Key,
|
||||||
config: KeyBindingConfig = defaultKeyBindings,
|
config: KeyBindingConfig = defaultKeyBindingConfig,
|
||||||
): boolean {
|
): boolean {
|
||||||
return config[command].some((binding) => binding.matches(key));
|
const bindings = config.get(command);
|
||||||
|
if (!bindings) return false;
|
||||||
|
return bindings.some((binding) => binding.matches(key));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,7 +41,7 @@ export type KeyMatchers = {
|
|||||||
* Creates key matchers from a key binding configuration
|
* Creates key matchers from a key binding configuration
|
||||||
*/
|
*/
|
||||||
export function createKeyMatchers(
|
export function createKeyMatchers(
|
||||||
config: KeyBindingConfig = defaultKeyBindings,
|
config: KeyBindingConfig = defaultKeyBindingConfig,
|
||||||
): KeyMatchers {
|
): KeyMatchers {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
||||||
const matchers = {} as { [C in Command]: KeyMatcher };
|
const matchers = {} as { [C in Command]: KeyMatcher };
|
||||||
@@ -50,8 +56,23 @@ export function createKeyMatchers(
|
|||||||
/**
|
/**
|
||||||
* Default key binding matchers using the default configuration
|
* Default key binding matchers using the default configuration
|
||||||
*/
|
*/
|
||||||
export const defaultKeyMatchers: KeyMatchers =
|
export const defaultKeyMatchers: KeyMatchers = createKeyMatchers(
|
||||||
createKeyMatchers(defaultKeyBindings);
|
defaultKeyBindingConfig,
|
||||||
|
);
|
||||||
|
|
||||||
// Re-export Command for convenience
|
// Re-export Command for convenience
|
||||||
export { Command };
|
export { Command };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads and creates key matchers including user customizations.
|
||||||
|
*/
|
||||||
|
export async function loadKeyMatchers(): Promise<{
|
||||||
|
matchers: KeyMatchers;
|
||||||
|
errors: string[];
|
||||||
|
}> {
|
||||||
|
const { config, errors } = await loadCustomKeybindings();
|
||||||
|
return {
|
||||||
|
matchers: createKeyMatchers(config),
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
type Command,
|
type Command,
|
||||||
type KeyBinding,
|
type KeyBinding,
|
||||||
type KeyBindingConfig,
|
type KeyBindingConfig,
|
||||||
defaultKeyBindings,
|
defaultKeyBindingConfig,
|
||||||
} from './keyBindings.js';
|
} from './keyBindings.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,10 +97,10 @@ export function formatKeyBinding(
|
|||||||
*/
|
*/
|
||||||
export function formatCommand(
|
export function formatCommand(
|
||||||
command: Command,
|
command: Command,
|
||||||
config: KeyBindingConfig = defaultKeyBindings,
|
config: KeyBindingConfig = defaultKeyBindingConfig,
|
||||||
platform?: string,
|
platform?: string,
|
||||||
): string {
|
): string {
|
||||||
const bindings = config[command];
|
const bindings = config.get(command);
|
||||||
if (!bindings || bindings.length === 0) {
|
if (!bindings || bindings.length === 0) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,10 @@ export class Storage {
|
|||||||
return path.join(Storage.getGlobalGeminiDir(), 'policies');
|
return path.join(Storage.getGlobalGeminiDir(), 'policies');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getUserKeybindingsPath(): string {
|
||||||
|
return path.join(Storage.getGlobalGeminiDir(), 'keybindings.json');
|
||||||
|
}
|
||||||
|
|
||||||
static getUserAgentsDir(): string {
|
static getUserAgentsDir(): string {
|
||||||
return path.join(Storage.getGlobalGeminiDir(), 'agents');
|
return path.join(Storage.getGlobalGeminiDir(), 'agents');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type { KeyBinding } from '../packages/cli/src/ui/key/keyBindings.js';
|
|||||||
import {
|
import {
|
||||||
commandCategories,
|
commandCategories,
|
||||||
commandDescriptions,
|
commandDescriptions,
|
||||||
defaultKeyBindings,
|
defaultKeyBindingConfig,
|
||||||
} from '../packages/cli/src/ui/key/keyBindings.js';
|
} from '../packages/cli/src/ui/key/keyBindings.js';
|
||||||
import {
|
import {
|
||||||
formatWithPrettier,
|
formatWithPrettier,
|
||||||
@@ -82,7 +82,7 @@ export function buildDefaultDocSections(): readonly KeybindingDocSection[] {
|
|||||||
title: category.title,
|
title: category.title,
|
||||||
commands: category.commands.map((command) => ({
|
commands: category.commands.map((command) => ({
|
||||||
description: commandDescriptions[command],
|
description: commandDescriptions[command],
|
||||||
bindings: defaultKeyBindings[command],
|
bindings: defaultKeyBindingConfig.get(command) ?? [],
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user