mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-25 20:44:46 -07:00
feat(cli): customizable keyboard shortcuts (#21945)
This commit is contained in:
committed by
GitHub
parent
657f19c1f3
commit
daf3701194
@@ -4,14 +4,18 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { KeyBindingConfig } from './keyBindings.js';
|
||||
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 {
|
||||
Command,
|
||||
commandCategories,
|
||||
commandDescriptions,
|
||||
defaultKeyBindings,
|
||||
defaultKeyBindingConfig,
|
||||
KeyBinding,
|
||||
loadCustomKeybindings,
|
||||
} from './keyBindings.js';
|
||||
|
||||
describe('KeyBinding', () => {
|
||||
@@ -104,26 +108,11 @@ describe('KeyBinding', () => {
|
||||
});
|
||||
|
||||
describe('keyBindings config', () => {
|
||||
describe('defaultKeyBindings', () => {
|
||||
it('should have bindings for all commands', () => {
|
||||
const commands = Object.values(Command);
|
||||
|
||||
for (const command of commands) {
|
||||
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();
|
||||
});
|
||||
it('should have bindings for all commands', () => {
|
||||
for (const command of Object.values(Command)) {
|
||||
expect(defaultKeyBindingConfig.has(command)).toBe(true);
|
||||
expect(defaultKeyBindingConfig.get(command)?.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
describe('command metadata', () => {
|
||||
@@ -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'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user