mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-21 02:24:09 -07:00
fix(cli): support CJK input and full Unicode scalar values in terminal protocols (#22353)
This commit is contained in:
committed by
GitHub
parent
fa024133e6
commit
24933a90d0
@@ -22,7 +22,7 @@ describe('KeyBinding', () => {
|
||||
describe('constructor', () => {
|
||||
it('should parse a simple key', () => {
|
||||
const binding = new KeyBinding('a');
|
||||
expect(binding.key).toBe('a');
|
||||
expect(binding.name).toBe('a');
|
||||
expect(binding.ctrl).toBe(false);
|
||||
expect(binding.shift).toBe(false);
|
||||
expect(binding.alt).toBe(false);
|
||||
@@ -31,45 +31,45 @@ describe('KeyBinding', () => {
|
||||
|
||||
it('should parse ctrl+key', () => {
|
||||
const binding = new KeyBinding('ctrl+c');
|
||||
expect(binding.key).toBe('c');
|
||||
expect(binding.name).toBe('c');
|
||||
expect(binding.ctrl).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse shift+key', () => {
|
||||
const binding = new KeyBinding('shift+z');
|
||||
expect(binding.key).toBe('z');
|
||||
expect(binding.name).toBe('z');
|
||||
expect(binding.shift).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse alt+key', () => {
|
||||
const binding = new KeyBinding('alt+left');
|
||||
expect(binding.key).toBe('left');
|
||||
expect(binding.name).toBe('left');
|
||||
expect(binding.alt).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse cmd+key', () => {
|
||||
const binding = new KeyBinding('cmd+f');
|
||||
expect(binding.key).toBe('f');
|
||||
expect(binding.name).toBe('f');
|
||||
expect(binding.cmd).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle aliases (option/opt/meta)', () => {
|
||||
const optionBinding = new KeyBinding('option+b');
|
||||
expect(optionBinding.key).toBe('b');
|
||||
expect(optionBinding.name).toBe('b');
|
||||
expect(optionBinding.alt).toBe(true);
|
||||
|
||||
const optBinding = new KeyBinding('opt+b');
|
||||
expect(optBinding.key).toBe('b');
|
||||
expect(optBinding.name).toBe('b');
|
||||
expect(optBinding.alt).toBe(true);
|
||||
|
||||
const metaBinding = new KeyBinding('meta+enter');
|
||||
expect(metaBinding.key).toBe('enter');
|
||||
expect(metaBinding.name).toBe('enter');
|
||||
expect(metaBinding.cmd).toBe(true);
|
||||
});
|
||||
|
||||
it('should parse multiple modifiers', () => {
|
||||
const binding = new KeyBinding('ctrl+shift+alt+cmd+x');
|
||||
expect(binding.key).toBe('x');
|
||||
expect(binding.name).toBe('x');
|
||||
expect(binding.ctrl).toBe(true);
|
||||
expect(binding.shift).toBe(true);
|
||||
expect(binding.alt).toBe(true);
|
||||
@@ -78,14 +78,14 @@ describe('KeyBinding', () => {
|
||||
|
||||
it('should be case-insensitive', () => {
|
||||
const binding = new KeyBinding('CTRL+Shift+F');
|
||||
expect(binding.key).toBe('f');
|
||||
expect(binding.name).toBe('f');
|
||||
expect(binding.ctrl).toBe(true);
|
||||
expect(binding.shift).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle named keys with modifiers', () => {
|
||||
const binding = new KeyBinding('ctrl+enter');
|
||||
expect(binding.key).toBe('enter');
|
||||
expect(binding.name).toBe('enter');
|
||||
expect(binding.ctrl).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -144,14 +144,14 @@ export class KeyBinding {
|
||||
]);
|
||||
|
||||
/** The key name (e.g., 'a', 'enter', 'tab', 'escape') */
|
||||
readonly key: string;
|
||||
readonly name: string;
|
||||
readonly shift: boolean;
|
||||
readonly alt: boolean;
|
||||
readonly ctrl: boolean;
|
||||
readonly cmd: boolean;
|
||||
|
||||
constructor(pattern: string) {
|
||||
let remains = pattern.toLowerCase().trim();
|
||||
let remains = pattern.trim();
|
||||
let shift = false;
|
||||
let alt = false;
|
||||
let ctrl = false;
|
||||
@@ -160,31 +160,32 @@ export class KeyBinding {
|
||||
let matched: boolean;
|
||||
do {
|
||||
matched = false;
|
||||
if (remains.startsWith('ctrl+')) {
|
||||
const lowerRemains = remains.toLowerCase();
|
||||
if (lowerRemains.startsWith('ctrl+')) {
|
||||
ctrl = true;
|
||||
remains = remains.slice(5);
|
||||
matched = true;
|
||||
} else if (remains.startsWith('shift+')) {
|
||||
} else if (lowerRemains.startsWith('shift+')) {
|
||||
shift = true;
|
||||
remains = remains.slice(6);
|
||||
matched = true;
|
||||
} else if (remains.startsWith('alt+')) {
|
||||
} else if (lowerRemains.startsWith('alt+')) {
|
||||
alt = true;
|
||||
remains = remains.slice(4);
|
||||
matched = true;
|
||||
} else if (remains.startsWith('option+')) {
|
||||
} else if (lowerRemains.startsWith('option+')) {
|
||||
alt = true;
|
||||
remains = remains.slice(7);
|
||||
matched = true;
|
||||
} else if (remains.startsWith('opt+')) {
|
||||
} else if (lowerRemains.startsWith('opt+')) {
|
||||
alt = true;
|
||||
remains = remains.slice(4);
|
||||
matched = true;
|
||||
} else if (remains.startsWith('cmd+')) {
|
||||
} else if (lowerRemains.startsWith('cmd+')) {
|
||||
cmd = true;
|
||||
remains = remains.slice(4);
|
||||
matched = true;
|
||||
} else if (remains.startsWith('meta+')) {
|
||||
} else if (lowerRemains.startsWith('meta+')) {
|
||||
cmd = true;
|
||||
remains = remains.slice(5);
|
||||
matched = true;
|
||||
@@ -193,15 +194,17 @@ export class KeyBinding {
|
||||
|
||||
const key = remains;
|
||||
|
||||
if ([...key].length !== 1 && !KeyBinding.VALID_LONG_KEYS.has(key)) {
|
||||
const isSingleChar = [...key].length === 1;
|
||||
|
||||
if (!isSingleChar && !KeyBinding.VALID_LONG_KEYS.has(key.toLowerCase())) {
|
||||
throw new Error(
|
||||
`Invalid keybinding key: "${key}" in "${pattern}".` +
|
||||
` Must be a single character or one of: ${[...KeyBinding.VALID_LONG_KEYS].join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
this.key = key;
|
||||
this.shift = shift;
|
||||
this.name = key.toLowerCase();
|
||||
this.shift = shift || (isSingleChar && this.name !== key);
|
||||
this.alt = alt;
|
||||
this.ctrl = ctrl;
|
||||
this.cmd = cmd;
|
||||
@@ -209,7 +212,7 @@ export class KeyBinding {
|
||||
|
||||
matches(key: Key): boolean {
|
||||
return (
|
||||
this.key === key.name &&
|
||||
key.name === this.name &&
|
||||
!!key.shift === !!this.shift &&
|
||||
!!key.alt === !!this.alt &&
|
||||
!!key.ctrl === !!this.ctrl &&
|
||||
@@ -219,7 +222,7 @@ export class KeyBinding {
|
||||
|
||||
equals(other: KeyBinding): boolean {
|
||||
return (
|
||||
this.key === other.key &&
|
||||
this.name === other.name &&
|
||||
this.shift === other.shift &&
|
||||
this.alt === other.alt &&
|
||||
this.ctrl === other.ctrl &&
|
||||
|
||||
@@ -475,6 +475,22 @@ describe('keyMatchers', () => {
|
||||
expect(matchers[Command.QUIT](createKey('q', { ctrl: true }))).toBe(true);
|
||||
expect(matchers[Command.QUIT](createKey('q', { alt: true }))).toBe(true);
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
|
||||
@@ -86,7 +86,7 @@ export function formatKeyBinding(
|
||||
if (binding.shift) parts.push(modMap.shift);
|
||||
if (binding.cmd) parts.push(modMap.cmd);
|
||||
|
||||
const keyName = KEY_NAME_MAP[binding.key] || binding.key.toUpperCase();
|
||||
const keyName = KEY_NAME_MAP[binding.name] || binding.name.toUpperCase();
|
||||
parts.push(keyName);
|
||||
|
||||
return parts.join('+');
|
||||
|
||||
Reference in New Issue
Block a user