在theia或vscode中都内置了很多快捷键,可以很方便的用于触发一个command(命令),我们今天来学习下theia快捷键的注册原理。
使用
任何东西我们在学习其原理之前,都需要先知道怎么使用,不然就会一头雾水,不知从何看起。
首先我们看下在theia中如何注册一个快捷键,例如注册一个剪切的快捷键:
registry.registerKeybinding({
command: CommonCommands.CUT.id,
keybinding: 'ctrlcmd+x'
});
可以看到注册快捷键的主入口函数是registerKeybinding
,那么我们再来看下这个函数的定义:
/**
* Register a default keybinding to the registry.
*
* Keybindings registered later have higher priority during evaluation.
*
* @param binding the keybinding to be registered
*/
registerKeybinding(binding: common.Keybinding): Disposable {
return this.doRegisterKeybinding(binding);
}
可以看到这里又调用了另一个方法doRegisterKeybinding
,在我们继续往下看之前,我们先来看下这个binding
参数主要定义了哪些内容。
定义
Keybinding
的定义如下:
export interface Keybinding {
/**
* Unique command identifier of the command to be triggered by this keybinding.
*/
command: string;
/**
* The key sequence for the keybinding as defined in packages/keymaps/README.md.
*/
keybinding: string;
/**
* The optional keybinding context where this binding belongs to.
* If not specified, then this keybinding context belongs to the NOOP
* keybinding context.
*
* @deprecated use `when` closure instead
*/
context?: string;
/**
* An optional clause defining the condition when the keybinding is active, e.g. based on the current focus.
* See {@link https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts} for more details.
*/
when?: string;
/**
* Optional arguments that will be passed to the command when it gets triggered via this keybinding.
* Needs to be specified when the triggered command expects arguments to be passed to the command handler.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
args?: any;
}
command:表示快捷键需要触发的命令的id
keybinding:表示快捷键的组合方式
context:表示快捷键运行的上下文(已经废弃了,用when
取代)
when:表示快捷键执行的时机,与vscode的快捷键里的when定义是一样的,可以参考https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts
args:传给command的参数
注册
接下来我们继续看doRegisterKeybinding
的实现逻辑:
protected doRegisterKeybinding(binding: common.Keybinding, scope: KeybindingScope = KeybindingScope.DEFAULT): Disposable {
try {
this.resolveKeybinding(binding);
const scoped = Object.assign(binding, { scope });
this.insertBindingIntoScope(scoped, scope);
return Disposable.create(() => {
const index = this.keymaps[scope].indexOf(scoped);
if (index !== -1) {
this.keymaps[scope].splice(index, 1);
}
});
} catch (error) {
this.logger.warn(`Could not register keybinding:\n ${common.Keybinding.stringify(binding)}\n${error}`);
return Disposable.NULL;
}
}
主要逻辑就是根据scope
在keymaps
中将快捷键存储起来。
keymaps
根据scope
生成的用于存储快捷键的二维数组
protected readonly keymaps: ScopedKeybinding[][] = [...Array(KeybindingScope.length)].map(() => []);
scope
scope用于表示快捷键的作用域:
export enum KeybindingScope {
DEFAULT,
USER,
WORKSPACE,
END
}
DEFAULT表示内置的快捷键,USER表示用户定义的快捷键,WORKSPACE表示工作区的快捷键,END只用于遍历。
优先级:WORKSPACE > USER > DEFAULT
resolveKeybinding
/**
* Ensure that the `resolved` property of the given binding is set by calling the KeyboardLayoutService.
*/
resolveKeybinding(binding: ResolvedKeybinding): KeyCode[] {
if (!binding.resolved) {
const sequence = KeySequence.parse(binding.keybinding);
binding.resolved = sequence.map(code => this.keyboardLayoutService.resolveKeyCode(code));
}
return binding.resolved;
}
binding.resolved
用于缓存解析结果,如果之前解析过则直接返回,这函数主要是两个功能:
- 解析keybinding为
KeyCode
实例 - 根据当前键盘布局返回
KeyCode
,主要是为了兼容其他非标准布局的键盘类型
KeyCode
用于表示快捷键信息,核心代码如下:
/**
* Representation of a pressed key combined with key modifiers.
*/
export class KeyCode {
public readonly key: Key | undefined;
public readonly ctrl: boolean;
public readonly shift: boolean;
public readonly alt: boolean;
public readonly meta: boolean;
public readonly character: string | undefined;
public constructor(schema: KeyCodeSchema) {
const key = schema.key;
if (key) {
if (key.code && key.keyCode && key.easyString) {
this.key = key as Key;
} else if (key.code) {
this.key = Key.getKey(key.code);
} else if (key.keyCode) {
this.key = Key.getKey(key.keyCode);
}
}
this.ctrl = !!schema.ctrl;
this.shift = !!schema.shift;
this.alt = !!schema.alt;
this.meta = !!schema.meta;
this.character = schema.character;
}
...
}
insertBindingIntoScope
/**
* Ensures that keybindings are inserted in order of increasing length of binding to ensure that if a
* user triggers a short keybinding (e.g. ctrl+k), the UI won't wait for a longer one (e.g. ctrl+k enter)
*/
protected insertBindingIntoScope(item: common.Keybinding & { scope: KeybindingScope; }, scope: KeybindingScope): void {
const scopedKeymap = this.keymaps[scope];
const getNumberOfKeystrokes = (binding: common.Keybinding): number => (binding.keybinding.trim().match(/\s/g)?.length ?? 0) + 1;
const numberOfKeystrokesInBinding = getNumberOfKeystrokes(item);
const indexOfFirstItemWithEqualStrokes = scopedKeymap.findIndex(existingBinding => getNumberOfKeystrokes(existingBinding) === numberOfKeystrokesInBinding);
if (indexOfFirstItemWithEqualStrokes > -1) {
scopedKeymap.splice(indexOfFirstItemWithEqualStrokes, 0, item);
} else {
scopedKeymap.push(item);
}
}
根据快捷键的组合长度来决定插入快捷键到keymaps中的位置,长度越大的优先级越低例如:如果同时注册了ctrl+k enter,和ctrl+k,这样能确保ctrl+k要优先执行。长度一样的则插入到该长度序列中的首个位置,因为越往前优先级越高。
概括来说就是:在同一个scope中,前面的优先级要大于后面的,长度短的要大于长度长的。
到此就完成了快捷键的注册流程。
总结
整个流程也比较简单,就是通过调用registerKeybinding
方法,将binding
参数中的keybinding
字符串解析为KeyCode
实例,并缓存在binding.resolved
字段中,然后再将binding
根据scope
存入到keymaps
数组中。
后续文章会继续讲解KeySequence.parse
函数,看是如何将字符串解析为KeyCode
实例的,敬请期待。
关注公众号: 前端家园