这篇文章是系列文章《Web 应用快捷键支持》的第三篇。前两篇文章中介绍了如何正确处理浏览器 Keyboard Event。今天我们一起来看看,如何把正确解析的键盘事件,映射到对应的命令上。

在 VS Code 中,用户有两种方式自定义快捷键。第一种是使用快捷键编辑器,也就是以图形化的方式修改快捷键到命令的绑定。第二种,也是 VS Code 从第一个版本就支持的方式,直接修改 JSON 格式的快捷键定义。一个快捷键到命令的定义,以 JSON 形式的来表示是

{
"key": "cmd+/",
"command": "editor.action.commentLine",

"when": "editorTextFocus && !editorReadonly"

}

when语句里可以组合使用各种 context key ,来决定这个绑定什么时候生效。比如上面的例子里,当用户把光标放到编辑器里,同时编辑器里的文件不是只读的(readonly),那么按下cmd+/就是把当前行的代码注释掉。

你可以简单的把 VS Code 的快捷键服务存储着上面这种 JSON 格式的快捷键绑定定义。同时,VS Code 在整个 container 上监听 keyboard event,对于每个 keyboard event,在快捷键服务里进行查询,如果能够找到对应的命令,则执行命令,然后preventDefault()。相信你对此已经比较有经验了,不多加赘述。

Key/Command lookup

一个应用程序里的快捷键最多也就上百个,所以数据结构基本不是问题,你只需要两个 Map

_keyToKeybindings: Map<string, Keybinding[]>;
_commandTokeybindings: Map<string, Keybinding[]>

第一个 Map 是从 Keybinding stringify 的结果到 Keybinding 本身的映射。比如

“cmd+/”: [
{
command: “editor.action.commentLine”,
when: “editorTextFocus && !editorReadonly”,
...
},
{
command: ...
},

...

}

第二个 Map 则是从 Command Id 到 Keybinding 本身

“editor.action.commentLine”: {
command: “editor.action.commentLine”,
when: “editorTextFocus && !editorReadonly”,

...

}

简单的 Map 就能够避免每次都进行暴力搜索了,而且在这几百个快捷键的 scale下,Map 足够好了。

同时你也注意到了,第一个 Map 的值是一个 Array。这是因为真实环境下,是会出现 conflicts。比如应用已经使用了一个快捷键组合,然后用户将这个快捷键组合绑定到了另一个命令上。而我们并不希望出现 data loss,所以我们需要将它们都储存起来,然后执行合适的命令。

那么当快捷键组合出现 conflict 的时候vscode快捷键,我们怎么决定哪条命令呢?有两个方法,我们先看简单的一个。

Ordering

最简单的方法就是给每个快捷键添加一个新的属性 order,用于排序。Order 值更高的就拥有更高的优先级。

VS Code 在使用了这个方法,不过是却是隐式的。VS Code 在启动时,会先注册默认的快捷键,接着是加载插件注册的快捷键,最后加载用户修改的快捷键。然后遵循后注册优先级更高的方式进行覆盖。所以,如果用户将cmd+/绑定给了其他的命令,那么 Toggle Line Comment 就不会被执行了。

要注意的是,如果要显式的使用 order 属性,只能在内部进行使用,但是不能面向用户,否则就会像 CSS 中的z-index一样,仍然可能存在 conflict。

When

第二种方法,就是给快捷键绑定加上限定条件。Toggle Line Comment 只能在编辑器中使用vscode快捷键,那么当用户在使用其他模块时,cmd+/理当不被占用才对。应用可以定义好足够多同时通用的 context key,用于描述应用所处的状态,比如

上面的listFocus是一个通用的 context key,VS Code 中有非常多的 list,如果为每个 list 都指定一个 context key,然后绑定上同一套快捷键,那就太麻烦了。

Context Key Expression

有了 context key,接下来就是看如何让用户自组合使用 context key 来精确的指定快捷键绑定。我们依然是由简入繁。

第一步就是只支持单个 boolean 类型的 context key。那么只需直接拿when的值进行查询就行了。

第二步支持单个 string 或者 number 类型的 context key。这一步就需要对when语句进行解析了,因为我们得在when语句中使用诸如==,!=,>,