在theia或vscode中都內置了很多快捷鍵朴爬,可以很方便的用于觸發(fā)一個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
,在我們繼續(xù)往下看之前莱衩,我們先來看下這個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:表示快捷鍵需要觸發(fā)的命令的id
keybinding:表示快捷鍵的組合方式
context:表示快捷鍵運行的上下文(已經廢棄了,用when
取代)
when:表示快捷鍵執(zhí)行的時機笨蚁,與vscode的快捷鍵里的when定義是一樣的睹晒,可以參考https://code.visualstudio.com/docs/getstarted/keybindings#_when-clause-contexts
args:傳給command的參數
注冊
接下來我們繼續(xù)看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表示工作區(qū)的快捷鍵岔激,END只用于遍歷勒极。
優(yōu)先級: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中的位置,長度越大的優(yōu)先級越低例如:如果同時注冊了ctrl+k enter匾七,和ctrl+k絮短,這樣能確保ctrl+k要優(yōu)先執(zhí)行。長度一樣的則插入到該長度序列中的首個位置昨忆,因為越往前優(yōu)先級越高丁频。
概括來說就是:在同一個scope中,前面的優(yōu)先級要大于后面的邑贴,長度短的要大于長度長的席里。
到此就完成了快捷鍵的注冊流程。
總結
整個流程也比較簡單拢驾,就是通過調用registerKeybinding
方法奖磁,將binding
參數中的keybinding
字符串解析為KeyCode
實例,并緩存在binding.resolved
字段中繁疤,然后再將binding
根據scope
存入到keymaps
數組中咖为。
后續(xù)文章會繼續(xù)講解KeySequence.parse
函數,看是如何將字符串解析為KeyCode
實例的稠腊,敬請期待躁染。
關注公眾號: 前端家園