2025-08-09 16:03:17 +09:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
2026-03-11 01:05:50 +00:00
|
|
|
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';
|
2026-03-09 20:48:09 +00:00
|
|
|
import {
|
|
|
|
|
defaultKeyMatchers,
|
|
|
|
|
Command,
|
|
|
|
|
createKeyMatchers,
|
2026-03-11 01:05:50 +00:00
|
|
|
loadKeyMatchers,
|
2026-03-09 20:48:09 +00:00
|
|
|
} from './keyMatchers.js';
|
2026-03-11 01:05:50 +00:00
|
|
|
import { defaultKeyBindingConfig, KeyBinding } from './keyBindings.js';
|
2026-03-09 23:26:33 +00:00
|
|
|
import type { Key } from '../hooks/useKeypress.js';
|
2025-08-09 16:03:17 +09:00
|
|
|
|
2026-03-11 01:05:50 +00:00
|
|
|
const createKey = (name: string, mods: Partial<Key> = {}): Key => ({
|
|
|
|
|
name,
|
|
|
|
|
shift: false,
|
|
|
|
|
alt: false,
|
|
|
|
|
ctrl: false,
|
|
|
|
|
cmd: false,
|
|
|
|
|
insertable: false,
|
|
|
|
|
sequence: name,
|
|
|
|
|
...mods,
|
|
|
|
|
});
|
2025-08-09 16:03:17 +09:00
|
|
|
|
2026-03-11 01:05:50 +00:00
|
|
|
describe('keyMatchers', () => {
|
2025-08-09 16:03:17 +09:00
|
|
|
// Test data for each command with positive and negative test cases
|
|
|
|
|
const testCases = [
|
|
|
|
|
// Basic bindings
|
2025-08-10 07:28:28 +09:00
|
|
|
{
|
|
|
|
|
command: Command.RETURN,
|
2026-03-10 02:32:40 +00:00
|
|
|
positive: [createKey('enter')],
|
2025-08-10 07:28:28 +09:00
|
|
|
negative: [createKey('r')],
|
|
|
|
|
},
|
2025-08-09 16:03:17 +09:00
|
|
|
{
|
|
|
|
|
command: Command.ESCAPE,
|
2026-03-05 22:11:53 +00:00
|
|
|
positive: [createKey('escape')],
|
|
|
|
|
negative: [
|
|
|
|
|
createKey('e'),
|
|
|
|
|
createKey('esc'),
|
|
|
|
|
createKey('escape', { ctrl: true }),
|
|
|
|
|
],
|
2025-08-09 16:03:17 +09:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Cursor movement
|
|
|
|
|
{
|
|
|
|
|
command: Command.HOME,
|
2026-01-12 16:28:10 -08:00
|
|
|
positive: [createKey('a', { ctrl: true }), createKey('home')],
|
2025-08-09 16:03:17 +09:00
|
|
|
negative: [
|
|
|
|
|
createKey('a'),
|
|
|
|
|
createKey('a', { shift: true }),
|
|
|
|
|
createKey('b', { ctrl: true }),
|
2026-01-20 18:15:18 -08:00
|
|
|
createKey('home', { ctrl: true }),
|
2025-08-09 16:03:17 +09:00
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.END,
|
2026-01-12 16:28:10 -08:00
|
|
|
positive: [createKey('e', { ctrl: true }), createKey('end')],
|
2025-08-09 16:03:17 +09:00
|
|
|
negative: [
|
|
|
|
|
createKey('e'),
|
|
|
|
|
createKey('e', { shift: true }),
|
|
|
|
|
createKey('a', { ctrl: true }),
|
2026-01-20 18:15:18 -08:00
|
|
|
createKey('end', { ctrl: true }),
|
2025-08-09 16:03:17 +09:00
|
|
|
],
|
|
|
|
|
},
|
2026-01-12 16:28:10 -08:00
|
|
|
{
|
|
|
|
|
command: Command.MOVE_LEFT,
|
2026-01-30 09:53:09 -08:00
|
|
|
positive: [createKey('left')],
|
|
|
|
|
negative: [
|
|
|
|
|
createKey('left', { ctrl: true }),
|
|
|
|
|
createKey('b'),
|
|
|
|
|
createKey('b', { ctrl: true }),
|
|
|
|
|
],
|
2026-01-12 16:28:10 -08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.MOVE_RIGHT,
|
|
|
|
|
positive: [createKey('right'), createKey('f', { ctrl: true })],
|
|
|
|
|
negative: [createKey('right', { ctrl: true }), createKey('f')],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.MOVE_WORD_LEFT,
|
|
|
|
|
positive: [
|
|
|
|
|
createKey('left', { ctrl: true }),
|
2026-01-21 10:13:26 -08:00
|
|
|
createKey('left', { alt: true }),
|
|
|
|
|
createKey('b', { alt: true }),
|
2026-01-12 16:28:10 -08:00
|
|
|
],
|
|
|
|
|
negative: [createKey('left'), createKey('b', { ctrl: true })],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.MOVE_WORD_RIGHT,
|
|
|
|
|
positive: [
|
|
|
|
|
createKey('right', { ctrl: true }),
|
2026-01-21 10:13:26 -08:00
|
|
|
createKey('right', { alt: true }),
|
|
|
|
|
createKey('f', { alt: true }),
|
2026-01-12 16:28:10 -08:00
|
|
|
],
|
|
|
|
|
negative: [createKey('right'), createKey('f', { ctrl: true })],
|
|
|
|
|
},
|
2025-08-09 16:03:17 +09:00
|
|
|
|
|
|
|
|
// Text deletion
|
|
|
|
|
{
|
|
|
|
|
command: Command.KILL_LINE_RIGHT,
|
|
|
|
|
positive: [createKey('k', { ctrl: true })],
|
|
|
|
|
negative: [createKey('k'), createKey('l', { ctrl: true })],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.KILL_LINE_LEFT,
|
|
|
|
|
positive: [createKey('u', { ctrl: true })],
|
|
|
|
|
negative: [createKey('u'), createKey('k', { ctrl: true })],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.CLEAR_INPUT,
|
|
|
|
|
positive: [createKey('c', { ctrl: true })],
|
|
|
|
|
negative: [createKey('c'), createKey('k', { ctrl: true })],
|
|
|
|
|
},
|
2026-01-12 16:28:10 -08:00
|
|
|
{
|
|
|
|
|
command: Command.DELETE_CHAR_LEFT,
|
2026-01-14 12:08:46 -08:00
|
|
|
positive: [createKey('backspace'), createKey('h', { ctrl: true })],
|
2026-01-12 16:28:10 -08:00
|
|
|
negative: [createKey('h'), createKey('x', { ctrl: true })],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.DELETE_CHAR_RIGHT,
|
|
|
|
|
positive: [createKey('delete'), createKey('d', { ctrl: true })],
|
|
|
|
|
negative: [createKey('d'), createKey('x', { ctrl: true })],
|
|
|
|
|
},
|
2025-09-02 21:00:41 -04:00
|
|
|
{
|
|
|
|
|
command: Command.DELETE_WORD_BACKWARD,
|
|
|
|
|
positive: [
|
|
|
|
|
createKey('backspace', { ctrl: true }),
|
2026-01-21 10:13:26 -08:00
|
|
|
createKey('backspace', { alt: true }),
|
2026-01-12 16:28:10 -08:00
|
|
|
createKey('w', { ctrl: true }),
|
2025-09-02 21:00:41 -04:00
|
|
|
],
|
|
|
|
|
negative: [createKey('backspace'), createKey('delete', { ctrl: true })],
|
|
|
|
|
},
|
2026-01-12 16:28:10 -08:00
|
|
|
{
|
|
|
|
|
command: Command.DELETE_WORD_FORWARD,
|
|
|
|
|
positive: [
|
|
|
|
|
createKey('delete', { ctrl: true }),
|
2026-01-21 10:13:26 -08:00
|
|
|
createKey('delete', { alt: true }),
|
2026-02-18 09:19:26 -08:00
|
|
|
createKey('d', { alt: true }),
|
2026-01-12 16:28:10 -08:00
|
|
|
],
|
|
|
|
|
negative: [createKey('delete'), createKey('backspace', { ctrl: true })],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.UNDO,
|
2026-01-28 14:57:27 -08:00
|
|
|
positive: [
|
|
|
|
|
createKey('z', { shift: false, cmd: true }),
|
|
|
|
|
createKey('z', { shift: false, alt: true }),
|
|
|
|
|
],
|
|
|
|
|
negative: [
|
|
|
|
|
createKey('z'),
|
|
|
|
|
createKey('z', { shift: true, cmd: true }),
|
|
|
|
|
createKey('z', { shift: false, ctrl: true }),
|
|
|
|
|
],
|
2026-01-12 16:28:10 -08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.REDO,
|
2026-01-28 14:57:27 -08:00
|
|
|
positive: [
|
|
|
|
|
createKey('z', { shift: true, cmd: true }),
|
|
|
|
|
createKey('z', { shift: true, alt: true }),
|
|
|
|
|
createKey('z', { shift: true, ctrl: true }),
|
|
|
|
|
],
|
|
|
|
|
negative: [createKey('z'), createKey('z', { shift: false, cmd: true })],
|
2026-01-12 16:28:10 -08:00
|
|
|
},
|
2025-08-09 16:03:17 +09:00
|
|
|
|
|
|
|
|
// Screen control
|
|
|
|
|
{
|
|
|
|
|
command: Command.CLEAR_SCREEN,
|
|
|
|
|
positive: [createKey('l', { ctrl: true })],
|
|
|
|
|
negative: [createKey('l'), createKey('k', { ctrl: true })],
|
|
|
|
|
},
|
|
|
|
|
|
2025-11-13 11:16:23 -08:00
|
|
|
// Scrolling
|
|
|
|
|
{
|
|
|
|
|
command: Command.SCROLL_UP,
|
|
|
|
|
positive: [createKey('up', { shift: true })],
|
2026-02-08 00:09:48 -08:00
|
|
|
negative: [createKey('up')],
|
2025-11-13 11:16:23 -08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.SCROLL_DOWN,
|
|
|
|
|
positive: [createKey('down', { shift: true })],
|
2026-02-08 00:09:48 -08:00
|
|
|
negative: [createKey('down')],
|
2025-11-13 11:16:23 -08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.SCROLL_HOME,
|
2026-02-08 00:09:48 -08:00
|
|
|
positive: [
|
|
|
|
|
createKey('home', { ctrl: true }),
|
|
|
|
|
createKey('home', { shift: true }),
|
|
|
|
|
],
|
2026-01-20 18:15:18 -08:00
|
|
|
negative: [createKey('end'), createKey('home')],
|
2025-11-13 11:16:23 -08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.SCROLL_END,
|
2026-02-08 00:09:48 -08:00
|
|
|
positive: [
|
|
|
|
|
createKey('end', { ctrl: true }),
|
|
|
|
|
createKey('end', { shift: true }),
|
|
|
|
|
],
|
2026-01-20 18:15:18 -08:00
|
|
|
negative: [createKey('home'), createKey('end')],
|
2025-11-13 11:16:23 -08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.PAGE_UP,
|
2026-03-05 22:11:53 +00:00
|
|
|
positive: [createKey('pageup')],
|
|
|
|
|
negative: [
|
|
|
|
|
createKey('pagedown'),
|
|
|
|
|
createKey('up'),
|
|
|
|
|
createKey('pageup', { shift: true }),
|
|
|
|
|
],
|
2025-11-13 11:16:23 -08:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.PAGE_DOWN,
|
2026-03-05 22:11:53 +00:00
|
|
|
positive: [createKey('pagedown')],
|
|
|
|
|
negative: [
|
|
|
|
|
createKey('pageup'),
|
|
|
|
|
createKey('down'),
|
|
|
|
|
createKey('pagedown', { ctrl: true }),
|
|
|
|
|
],
|
2025-11-13 11:16:23 -08:00
|
|
|
},
|
|
|
|
|
|
2025-08-09 16:03:17 +09:00
|
|
|
// History navigation
|
|
|
|
|
{
|
|
|
|
|
command: Command.HISTORY_UP,
|
|
|
|
|
positive: [createKey('p', { ctrl: true })],
|
|
|
|
|
negative: [createKey('p'), createKey('up')],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.HISTORY_DOWN,
|
|
|
|
|
positive: [createKey('n', { ctrl: true })],
|
|
|
|
|
negative: [createKey('n'), createKey('down')],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.NAVIGATION_UP,
|
2026-03-05 22:11:53 +00:00
|
|
|
positive: [createKey('up')],
|
|
|
|
|
negative: [
|
|
|
|
|
createKey('p'),
|
|
|
|
|
createKey('u'),
|
|
|
|
|
createKey('up', { ctrl: true }),
|
|
|
|
|
],
|
2025-08-09 16:03:17 +09:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.NAVIGATION_DOWN,
|
2026-03-05 22:11:53 +00:00
|
|
|
positive: [createKey('down')],
|
|
|
|
|
negative: [
|
|
|
|
|
createKey('n'),
|
|
|
|
|
createKey('d'),
|
|
|
|
|
createKey('down', { ctrl: true }),
|
|
|
|
|
],
|
2025-08-09 16:03:17 +09:00
|
|
|
},
|
|
|
|
|
|
2025-11-03 16:22:04 -08:00
|
|
|
// Dialog navigation
|
|
|
|
|
{
|
|
|
|
|
command: Command.DIALOG_NAVIGATION_UP,
|
|
|
|
|
positive: [createKey('up'), createKey('k')],
|
|
|
|
|
negative: [
|
|
|
|
|
createKey('up', { shift: true }),
|
|
|
|
|
createKey('k', { shift: true }),
|
|
|
|
|
createKey('p'),
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.DIALOG_NAVIGATION_DOWN,
|
|
|
|
|
positive: [createKey('down'), createKey('j')],
|
|
|
|
|
negative: [
|
|
|
|
|
createKey('down', { shift: true }),
|
|
|
|
|
createKey('j', { shift: true }),
|
|
|
|
|
createKey('n'),
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
|
2025-08-09 16:03:17 +09:00
|
|
|
// Auto-completion
|
|
|
|
|
{
|
|
|
|
|
command: Command.ACCEPT_SUGGESTION,
|
2026-03-10 02:32:40 +00:00
|
|
|
positive: [createKey('tab'), createKey('enter')],
|
|
|
|
|
negative: [createKey('enter', { ctrl: true }), createKey('space')],
|
2025-08-09 16:03:17 +09:00
|
|
|
},
|
2025-08-10 07:28:28 +09:00
|
|
|
{
|
|
|
|
|
command: Command.COMPLETION_UP,
|
|
|
|
|
positive: [createKey('up'), createKey('p', { ctrl: true })],
|
|
|
|
|
negative: [createKey('p'), createKey('down')],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.COMPLETION_DOWN,
|
|
|
|
|
positive: [createKey('down'), createKey('n', { ctrl: true })],
|
|
|
|
|
negative: [createKey('n'), createKey('up')],
|
|
|
|
|
},
|
2025-08-09 16:03:17 +09:00
|
|
|
|
|
|
|
|
// Text input
|
|
|
|
|
{
|
|
|
|
|
command: Command.SUBMIT,
|
2026-03-10 02:32:40 +00:00
|
|
|
positive: [createKey('enter')],
|
2025-08-09 16:03:17 +09:00
|
|
|
negative: [
|
2026-03-10 02:32:40 +00:00
|
|
|
createKey('enter', { ctrl: true }),
|
|
|
|
|
createKey('enter', { cmd: true }),
|
|
|
|
|
createKey('enter', { alt: true }),
|
2025-08-09 16:03:17 +09:00
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.NEWLINE,
|
|
|
|
|
positive: [
|
2026-03-10 02:32:40 +00:00
|
|
|
createKey('enter', { ctrl: true }),
|
|
|
|
|
createKey('enter', { cmd: true }),
|
|
|
|
|
createKey('enter', { alt: true }),
|
2025-08-09 16:03:17 +09:00
|
|
|
],
|
2026-03-10 02:32:40 +00:00
|
|
|
negative: [createKey('enter'), createKey('n')],
|
2025-08-09 16:03:17 +09:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// External tools
|
|
|
|
|
{
|
|
|
|
|
command: Command.OPEN_EXTERNAL_EDITOR,
|
2026-01-14 15:09:09 -08:00
|
|
|
positive: [createKey('x', { ctrl: true })],
|
2025-08-09 16:03:17 +09:00
|
|
|
negative: [createKey('x'), createKey('c', { ctrl: true })],
|
|
|
|
|
},
|
|
|
|
|
{
|
2025-11-17 15:48:33 -08:00
|
|
|
command: Command.PASTE_CLIPBOARD,
|
2025-08-09 16:03:17 +09:00
|
|
|
positive: [createKey('v', { ctrl: true })],
|
|
|
|
|
negative: [createKey('v'), createKey('c', { ctrl: true })],
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// App level bindings
|
|
|
|
|
{
|
|
|
|
|
command: Command.SHOW_ERROR_DETAILS,
|
2025-10-28 12:10:40 -07:00
|
|
|
positive: [createKey('f12')],
|
2026-01-30 09:53:09 -08:00
|
|
|
negative: [
|
|
|
|
|
createKey('o', { ctrl: true }),
|
|
|
|
|
createKey('b', { ctrl: true }),
|
|
|
|
|
],
|
2025-08-09 16:03:17 +09:00
|
|
|
},
|
2025-10-17 21:10:57 -07:00
|
|
|
{
|
|
|
|
|
command: Command.SHOW_FULL_TODOS,
|
|
|
|
|
positive: [createKey('t', { ctrl: true })],
|
|
|
|
|
negative: [createKey('t'), createKey('e', { ctrl: true })],
|
|
|
|
|
},
|
2025-08-09 16:03:17 +09:00
|
|
|
{
|
2026-01-13 12:07:55 -08:00
|
|
|
command: Command.SHOW_IDE_CONTEXT_DETAIL,
|
2025-08-14 17:50:20 +00:00
|
|
|
positive: [createKey('g', { ctrl: true })],
|
|
|
|
|
negative: [createKey('g'), createKey('t', { ctrl: true })],
|
2025-08-09 16:03:17 +09:00
|
|
|
},
|
2025-10-16 11:23:36 -07:00
|
|
|
{
|
|
|
|
|
command: Command.TOGGLE_MARKDOWN,
|
2026-01-21 10:13:26 -08:00
|
|
|
positive: [createKey('m', { alt: true })],
|
2025-10-16 11:23:36 -07:00
|
|
|
negative: [createKey('m'), createKey('m', { shift: true })],
|
|
|
|
|
},
|
2025-11-03 13:41:58 -08:00
|
|
|
{
|
|
|
|
|
command: Command.TOGGLE_COPY_MODE,
|
|
|
|
|
positive: [createKey('s', { ctrl: true })],
|
2026-01-21 10:13:26 -08:00
|
|
|
negative: [createKey('s'), createKey('s', { alt: true })],
|
2025-11-03 13:41:58 -08:00
|
|
|
},
|
2025-08-09 16:03:17 +09:00
|
|
|
{
|
|
|
|
|
command: Command.QUIT,
|
2025-08-10 07:28:28 +09:00
|
|
|
positive: [createKey('c', { ctrl: true })],
|
2025-08-09 16:03:17 +09:00
|
|
|
negative: [createKey('c'), createKey('d', { ctrl: true })],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.EXIT,
|
2025-08-10 07:28:28 +09:00
|
|
|
positive: [createKey('d', { ctrl: true })],
|
2025-08-09 16:03:17 +09:00
|
|
|
negative: [createKey('d'), createKey('c', { ctrl: true })],
|
|
|
|
|
},
|
2026-02-12 09:55:56 -08:00
|
|
|
{
|
|
|
|
|
command: Command.SUSPEND_APP,
|
2026-03-05 22:11:53 +00:00
|
|
|
positive: [createKey('z', { ctrl: true })],
|
2026-02-12 09:55:56 -08:00
|
|
|
negative: [
|
|
|
|
|
createKey('z'),
|
|
|
|
|
createKey('y', { ctrl: true }),
|
|
|
|
|
createKey('z', { alt: true }),
|
2026-03-05 22:11:53 +00:00
|
|
|
createKey('z', { ctrl: true, shift: true }),
|
2026-02-12 09:55:56 -08:00
|
|
|
],
|
|
|
|
|
},
|
2025-08-09 16:03:17 +09:00
|
|
|
{
|
|
|
|
|
command: Command.SHOW_MORE_LINES,
|
2026-02-12 15:00:13 -08:00
|
|
|
positive: [createKey('o', { ctrl: true })],
|
|
|
|
|
negative: [
|
2026-01-26 18:14:03 -08:00
|
|
|
createKey('s', { ctrl: true }),
|
2026-02-12 15:00:13 -08:00
|
|
|
createKey('s'),
|
|
|
|
|
createKey('l', { ctrl: true }),
|
2026-01-26 18:14:03 -08:00
|
|
|
],
|
2025-08-09 16:03:17 +09:00
|
|
|
},
|
|
|
|
|
// Shell commands
|
|
|
|
|
{
|
|
|
|
|
command: Command.REVERSE_SEARCH,
|
|
|
|
|
positive: [createKey('r', { ctrl: true })],
|
|
|
|
|
negative: [createKey('r'), createKey('s', { ctrl: true })],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.SUBMIT_REVERSE_SEARCH,
|
2026-03-10 02:32:40 +00:00
|
|
|
positive: [createKey('enter')],
|
|
|
|
|
negative: [createKey('enter', { ctrl: true }), createKey('tab')],
|
2025-08-09 16:03:17 +09:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.ACCEPT_SUGGESTION_REVERSE_SEARCH,
|
2026-03-05 22:11:53 +00:00
|
|
|
positive: [createKey('tab')],
|
|
|
|
|
negative: [
|
2026-03-10 02:32:40 +00:00
|
|
|
createKey('enter'),
|
2026-03-05 22:11:53 +00:00
|
|
|
createKey('space'),
|
|
|
|
|
createKey('tab', { ctrl: true }),
|
|
|
|
|
],
|
2025-08-09 16:03:17 +09:00
|
|
|
},
|
2025-09-11 13:27:27 -07:00
|
|
|
{
|
2026-01-13 12:07:55 -08:00
|
|
|
command: Command.FOCUS_SHELL_INPUT,
|
2026-01-12 15:30:12 -08:00
|
|
|
positive: [createKey('tab')],
|
2026-02-12 14:25:24 -05:00
|
|
|
negative: [createKey('f6'), createKey('f', { ctrl: true })],
|
2025-09-11 13:27:27 -07:00
|
|
|
},
|
2026-01-12 14:50:32 -08:00
|
|
|
{
|
|
|
|
|
command: Command.TOGGLE_YOLO,
|
|
|
|
|
positive: [createKey('y', { ctrl: true })],
|
2026-01-21 10:13:26 -08:00
|
|
|
negative: [createKey('y'), createKey('y', { alt: true })],
|
2026-01-12 14:50:32 -08:00
|
|
|
},
|
|
|
|
|
{
|
2026-01-21 10:19:47 -05:00
|
|
|
command: Command.CYCLE_APPROVAL_MODE,
|
2026-01-12 14:50:32 -08:00
|
|
|
positive: [createKey('tab', { shift: true })],
|
|
|
|
|
negative: [createKey('tab')],
|
|
|
|
|
},
|
2026-01-30 09:53:09 -08:00
|
|
|
{
|
|
|
|
|
command: Command.TOGGLE_BACKGROUND_SHELL,
|
|
|
|
|
positive: [createKey('b', { ctrl: true })],
|
|
|
|
|
negative: [createKey('f10'), createKey('b')],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
command: Command.TOGGLE_BACKGROUND_SHELL_LIST,
|
|
|
|
|
positive: [createKey('l', { ctrl: true })],
|
|
|
|
|
negative: [createKey('l')],
|
|
|
|
|
},
|
2025-08-09 16:03:17 +09:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
describe('Data-driven key binding matches original logic', () => {
|
|
|
|
|
testCases.forEach(({ command, positive, negative }) => {
|
|
|
|
|
it(`should match ${command} correctly`, () => {
|
|
|
|
|
positive.forEach((key) => {
|
|
|
|
|
expect(
|
2026-03-09 20:48:09 +00:00
|
|
|
defaultKeyMatchers[command](key),
|
2025-08-09 16:03:17 +09:00
|
|
|
`Expected ${command} to match ${JSON.stringify(key)}`,
|
|
|
|
|
).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
negative.forEach((key) => {
|
|
|
|
|
expect(
|
2026-03-09 20:48:09 +00:00
|
|
|
defaultKeyMatchers[command](key),
|
2025-08-09 16:03:17 +09:00
|
|
|
`Expected ${command} to NOT match ${JSON.stringify(key)}`,
|
|
|
|
|
).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Custom key bindings', () => {
|
|
|
|
|
it('should work with custom configuration', () => {
|
2026-03-11 01:05:50 +00:00
|
|
|
const customConfig = new Map(defaultKeyBindingConfig);
|
|
|
|
|
customConfig.set(Command.HOME, [
|
|
|
|
|
new KeyBinding('ctrl+h'),
|
|
|
|
|
new KeyBinding('0'),
|
|
|
|
|
]);
|
2025-08-09 16:03:17 +09:00
|
|
|
|
|
|
|
|
const customMatchers = createKeyMatchers(customConfig);
|
|
|
|
|
|
|
|
|
|
expect(customMatchers[Command.HOME](createKey('h', { ctrl: true }))).toBe(
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
expect(customMatchers[Command.HOME](createKey('0'))).toBe(true);
|
|
|
|
|
expect(customMatchers[Command.HOME](createKey('a', { ctrl: true }))).toBe(
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should support multiple key bindings for same command', () => {
|
2026-03-11 01:05:50 +00:00
|
|
|
const config = new Map(defaultKeyBindingConfig);
|
|
|
|
|
config.set(Command.QUIT, [
|
|
|
|
|
new KeyBinding('ctrl+q'),
|
|
|
|
|
new KeyBinding('alt+q'),
|
|
|
|
|
]);
|
2025-08-09 16:03:17 +09:00
|
|
|
|
|
|
|
|
const matchers = createKeyMatchers(config);
|
|
|
|
|
expect(matchers[Command.QUIT](createKey('q', { ctrl: true }))).toBe(true);
|
2026-01-21 10:13:26 -08:00
|
|
|
expect(matchers[Command.QUIT](createKey('q', { alt: true }))).toBe(true);
|
2025-08-09 16:03:17 +09:00
|
|
|
});
|
2026-03-13 21:24:26 +00:00
|
|
|
it('should support matching non-ASCII and CJK characters', () => {
|
|
|
|
|
const config = new Map(defaultKeyBindingConfig);
|
|
|
|
|
config.set(Command.QUIT, [new KeyBinding('Å'), new KeyBinding('가')]);
|
|
|
|
|
|
|
|
|
|
const matchers = createKeyMatchers(config);
|
|
|
|
|
|
|
|
|
|
// Å is normalized to å with shift=true by the parser
|
|
|
|
|
expect(matchers[Command.QUIT](createKey('å', { shift: true }))).toBe(
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
expect(matchers[Command.QUIT](createKey('å'))).toBe(false);
|
|
|
|
|
|
|
|
|
|
// CJK characters do not have a lower/upper case
|
|
|
|
|
expect(matchers[Command.QUIT](createKey('가'))).toBe(true);
|
|
|
|
|
expect(matchers[Command.QUIT](createKey('나'))).toBe(false);
|
|
|
|
|
});
|
2025-08-09 16:03:17 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Edge Cases', () => {
|
|
|
|
|
it('should handle empty binding arrays', () => {
|
2026-03-11 01:05:50 +00:00
|
|
|
const config = new Map(defaultKeyBindingConfig);
|
|
|
|
|
config.set(Command.HOME, []);
|
2025-08-09 16:03:17 +09:00
|
|
|
|
|
|
|
|
const matchers = createKeyMatchers(config);
|
|
|
|
|
expect(matchers[Command.HOME](createKey('a', { ctrl: true }))).toBe(
|
|
|
|
|
false,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-03-11 01:05:50 +00:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
});
|