一、前言

前段时间碰到了一个 Keybinding 相关的问题,于是探究了一番,首先大家可能会有两个问题:Monaco Editor 是啥?Keybinding 又是啥?

  • Monaco Editor

    微软开源的一个代码编辑器,为 VS Code 的编辑器提供支持,Monaco Editor 核心代码与 VS Code 是共用的(都在 VS Code github 仓库中)。
  • Keybinding

    Monaco Editor 中实现快捷键功能的机制(其实准确来说,应该是部分机制),可以使得通过快捷键来执行操作,例如打开命令面板、切换主题以及编辑器中的一些快捷操作等。

本文主要是针对 Monaco Editor 的 Keybinding 机制进行介绍,由于源码完整的逻辑比较庞杂,所以本文中的展示的源码以及流程会有一定的简化。

文中使用的代码版本:

Monaco Editor:0.30.1

VS Code:1.62.1

二、举个

这里使用 monaco-editor 创建了一个简单的例子,后文会基于这个例子来进行介绍。

  1. import React, { useRef, useEffect, useState } from "react";
  2. import * as monaco from "monaco-editor";
  3. import { codeText } from "./help";
  4. const Editor = () => {
  5. const domRef = useRef<HTMLDivElement>(null);
  6. const [actionDispose, setActionDispose] = useState<monaco.IDisposable>();
  7. useEffect(() => {
  8. const editorIns = monaco.editor.create(domRef.current!, {
  9. value: codeText,
  10. language: "typescript",
  11. theme: "vs-dark",
  12. });
  13. const action = {
  14. id: 'test',
  15. label: 'test',
  16. precondition: 'isChrome == true',
  17. keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL],
  18. run: () => {
  19. window.alert('chrome: cmd + k');
  20. },
  21. };
  22. setActionDispose(editorIns.addAction(action));
  23. editorIns.focus();
  24. return () => {
  25. editorIns.dispose();
  26. };
  27. }, []);
  28. const onClick = () => {
  29. actionDispose?.dispose();
  30. window.alert('已卸载');
  31. };
  32. return (
  33. <div>
  34. <div ref={domRef} className='editor-container' />
  35. <button className='cancel-button' onClick={onClick}>卸载keybinding</button>
  36. </div>
  37. );
  38. };
  39. export default Editor;

三、原理机制

1. 概览

根据上面的例子,Keybinding 机制的总体流程可以简单的分为以下几步:

  • 初始化:主要是初始化服务以及给 dom 添加监听事件
  • 注册:注册 keybinding 和 command
  • 执行:通过按快捷键触发执行对应的 keybinding 和 command
  • 卸载:清除注册的 keybinding 和 command

2. 初始化

回到上面例子中创建 editor 的代码:

  1. const editorIns = monaco.editor.create(domRef.current!, {
  2. value: codeText,
  3. language: "typescript",
  4. theme: "vs-dark",
  5. });

初始化过程如下:

创建 editor 之前会先初始化 services,通过实例化 DynamicStandaloneServices 类创建服务:

  1. let services = new DynamicStandaloneServices(domElement, override);

在 constructor 函数中会执行以下代码注册 keybindingService:

  1. let keybindingService = ensure(IKeybindingService, () =>
  2. this._register(
  3. new StandaloneKeybindingService(
  4. contextKeyService,
  5. commandService,
  6. telemetryService,
  7. notificationService,
  8. logService,
  9. domElement
  10. )
  11. )
  12. );

其中 this._register 方法和 ensure 方法会分别将 StandaloneKeybindingServices 实例保存到 disposable 对象(用于卸载)和 this._serviceCollection 中(用于执行过程查找keybinding)。

实例化 StandaloneKeybindingService,在 constructor 函数中添加 DOM 监听事件:

  1. this._register(
  2. dom.addDisposableListener(
  3. domNode,
  4. dom.EventType.KEY_DOWN,
  5. (e: KeyboardEvent) => {
  6. const keyEvent = new StandardKeyboardEvent(e);
  7. const shouldPreventDefault = this._dispatch(
  8. keyEvent,
  9. keyEvent.target
  10. );
  11. if (shouldPreventDefault) {
  12. keyEvent.preventDefault();
  13. keyEvent.stopPropagation();
  14. }
  15. }
  16. )
  17. );

以上代码中的 dom.addDisposableListener 方法,会通过 addEventListener 的方式,在 domNode 上添加一个 keydown 事件的监听函数,并且返回一个 DomListener 的实例,该实例包含一个用于移除事件监听的 dispose 方法。然后通过 this._register 方法将 DomListener 的实例保存起来。

3. 注册 keybindings

回到例子中的代码:

  1. const action = {
  2. id: 'test',
  3. label: 'test',
  4. precondition: 'isChrome == true',
  5. keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL],
  6. run: () => {
  7. window.alert('chrome: cmd + k');
  8. },
  9. };
  10. setActionDispose(editorIns.addAction(action));

注册过程如下:

当通过 editorIns.addAction 来注册 keybinding 时,会调用 StandaloneKeybindingServices 实例的 addDynamicKeybinding 方法来注册 keybinding。

  1. public addDynamicKeybinding(
  2. commandId: string,
  3. _keybinding: number,
  4. handler: ICommandHandler,
  5. when: ContextKeyExpression | undefined
  6. ): IDisposable {
  7. const keybinding = createKeybinding(_keybinding, OS);
  8. const toDispose = new DisposableStore();
  9. if (keybinding) {
  10. this._dynamicKeybindings.push({
  11. keybinding: keybinding.parts,
  12. command: commandId,
  13. when: when,
  14. weight1: 1000,
  15. weight2: 0,
  16. extensionId: null,
  17. isBuiltinExtension: false,
  18. });
  19. toDispose.add(
  20. toDisposable(() => {
  21. for (let i = 0; i < this._dynamicKeybindings.length; i++) {
  22. let kb = this._dynamicKeybindings[i];
  23. if (kb.command === commandId) {
  24. this._dynamicKeybindings.splice(i, 1);
  25. this.updateResolver({
  26. source: KeybindingSource.Default,
  27. });
  28. return;
  29. }
  30. }
  31. })
  32. );
  33. }
  34. toDispose.add(CommandsRegistry.registerCommand(commandId, handler));
  35. this.updateResolver({ source: KeybindingSource.Default });
  36. return toDispose;
  37. }

会先根据传入的 _keybinding 创建 keybinding 实例,然后连同 command、when 等其他信息存入_dynamicKeybindings 数组中,同时会注册对应的 command,当后面触发 keybinding 时便执行对应的 command。返回的 toDispose 实例则用于取消对应的 keybinding 和 command。

回到上面代码中创建 keybinding 实例的地方,createKeybinding 方法会根据传入的 _keybinding 数字和 OS 类型得到实例,大致结构如下(已省略部分属性):

  1. {
  2. parts: [
  3. {
  4. ctrlKey: boolean,
  5. shiftKey: boolean,
  6. altKey: boolean,
  7. metaKey: boolean,
  8. keyCode: KeyCode,
  9. }
  10. ],
  11. }

那么,是怎么通过一个 number 得到所有按键信息的呢?往下看↓↓↓

4. key的转换

先看看一开始传入的 keybinding 是什么:

  1. const action = {
  2. id: 'test',
  3. label: 'test',
  4. precondition: 'isChrome == true',
  5. keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL],
  6. run: () => {
  7. window.alert('chrome: cmd + k');
  8. },
  9. };

传入的 keybinding 就是上面代码中的 keybindings 数组中的元素,monaco.KeyMod.CtrlCmd = 2048,monaco.KeyCode.KeyL = 42,对应的数字是 monaco-editor 中定义的枚举值,与真实的 keyCode 存在对应关系。所以注册时传入的 keybinding 参数为: 2048 | 42 = 2090

先简单了解下 JS 中的位运算(操作的是32位带符号的二进制整数,下面例子中只用8位简单表示):

按位与(AND)&

对应的位都为1则返回1,否则返回0

例如:

00001010 // 10

00000110 // 6

------

00000010 // 2

按位或(OR)|

对应的位,只要有一个为1则返回1,否则返回0

00001010 // 10

00000110 // 6

-------

00001110 // 14

左移(Left shift)<<

将二进制数每一位向左移动指定位数,左侧移出的位舍弃,右侧补0

00001010 // 10

------- // 10 << 2

00101000 // 40

右移 >>

将二进制数每位向右移动指定位数,右侧移出的位舍弃,左侧用原来最左边的数补齐

00001010 // 10

------- // 10 >> 2

00000010 // 2

无符号右移 >>>

将二进制数每位向右移动指定位数,右侧移出的位舍弃,左侧补0

00001010 // 10

------- // 10 >> 2

00000010 // 2

接下来看下是怎么根据一个数字,创建出对应的 keybinding 实例:

  1. export function createKeybinding(keybinding: number, OS: OperatingSystem): Keybinding | null {
  2. if (keybinding === 0) {
  3. return null;
  4. }
  5. const firstPart = (keybinding & 0x0000FFFF) >>> 0;
  6. // 处理分两步的keybinding,例如:shift shift,若无第二部分,则chordPart = 0
  7. const chordPart = (keybinding & 0xFFFF0000) >>> 16;
  8. if (chordPart !== 0) {
  9. return new ChordKeybinding([
  10. createSimpleKeybinding(firstPart, OS),
  11. createSimpleKeybinding(chordPart, OS)
  12. ]);
  13. }
  14. return new ChordKeybinding([createSimpleKeybinding(firstPart, OS)]);
  15. }

看下 createSimpleKeybinding 方法做了什么

  1. const enum BinaryKeybindingsMask {
  2. CtrlCmd = (1 << 11) >>> 0, // 2048
  3. Shift = (1 << 10) >>> 0, // 1024
  4. Alt = (1 << 9) >>> 0, // 512
  5. WinCtrl = (1 << 8) >>> 0, // 256
  6. KeyCode = 0x000000FF // 255
  7. }
  8. export function createSimpleKeybinding(keybinding: number, OS: OperatingSystem): SimpleKeybinding {
  9. const ctrlCmd = (keybinding & BinaryKeybindingsMask.CtrlCmd ? true : false);
  10. const winCtrl = (keybinding & BinaryKeybindingsMask.WinCtrl ? true : false);
  11. const ctrlKey = (OS === OperatingSystem.Macintosh ? winCtrl : ctrlCmd);
  12. const shiftKey = (keybinding & BinaryKeybindingsMask.Shift ? true : false);
  13. const altKey = (keybinding & BinaryKeybindingsMask.Alt ? true : false);
  14. const metaKey = (OS === OperatingSystem.Macintosh ? ctrlCmd : winCtrl);
  15. const keyCode = (keybinding & BinaryKeybindingsMask.KeyCode);
  16. return new SimpleKeybinding(ctrlKey, shiftKey, altKey, metaKey, keyCode);
  17. }

拿上面的例子:keybinding = monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL,即 keybinding = 2048 | 42 = 2090,然后看上面代码中的:

const ctrlCmd = (keybinding & BinaryKeybindingsMask.CtrlCmd ? true : false);

运算如下:

100000101010 // 2090 -> keybinding

100000000000 // 2048 -> CtrlCmd

----------- // &

100000000000 // 2048 -> CtrlCmd

再看keyCode的运算:

const keyCode = (keybinding & BinaryKeybindingsMask.KeyCode)

100000101010 // 2090 -> keybinding

000011111111 // 255 -> KeyCode

----------- // &

000000101010 // 42 -> KeyL

于是便得到了 ctrlKey,shiftKey,altKey,metaKey,keyCode 这些值,接下来便由这些值生成SimpleKeybinding实例,该实例包含了上面的这些按键信息以及一些操作方法。

至此,已经完成了 keybinding 的注册,将 keybinding 实例及相关信息存入了 StandaloneKeybindingService 实例的 _dynamicKeybindings 数组中,对应的 command 也注册到了 CommandsRegistry 中。

5.执行

当用户在键盘上按下快捷键时,便会触发 keybinding 对应 command 的执行,执行过程如下:

回到 StandaloneKeybindingServices 初始化的时候,在 domNode 上绑定了 keydown 事件监听函数:

  1. (e: KeyboardEvent) => {
  2. const keyEvent = new StandardKeyboardEvent(e);
  3. const shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target);
  4. if (shouldPreventDefault) {
  5. keyEvent.preventDefault();
  6. keyEvent.stopPropagation();
  7. }
  8. };

当 keydown 事件触发后,便会执行这个监听函数,首先会实例化一个 StandardKeyboardEvent 实例,该实例包含了一些按键信息和方法,大致结构如下(已省略部分属性):

  1. {
  2. target: HTMLElement,
  3. ctrlKey: boolean,
  4. shiftKey: boolean,
  5. altKey: boolean,
  6. metaKey: boolean,
  7. keyCode: KeyCode,
  8. }

其中 keyCode 是经过处理后得到的,由原始键盘事件的 keyCode 转换为 monoco-editor 中的 keyCode,转换过程主要就是兼容一些不同的浏览器,并根据映射关系得到最终的 keyCode。准换方法如下:

  1. function extractKeyCode(e: KeyboardEvent): KeyCode {
  2. if (e.charCode) {
  3. // "keypress" events mostly
  4. let char = String.fromCharCode(e.charCode).toUpperCase();
  5. return KeyCodeUtils.fromString(char);
  6. }
  7. const keyCode = e.keyCode;
  8. // browser quirks
  9. if (keyCode === 3) {
  10. return KeyCode.PauseBreak;
  11. } else if (browser.isFirefox) {
  12. if (keyCode === 59) {
  13. return KeyCode.Semicolon;
  14. } else if (keyCode === 107) {
  15. return KeyCode.Equal;
  16. } else if (keyCode === 109) {
  17. return KeyCode.Minus;
  18. } else if (platform.isMacintosh && keyCode === 224) {
  19. return KeyCode.Meta;
  20. }
  21. } else if (browser.isWebKit) {
  22. if (keyCode === 91) {
  23. return KeyCode.Meta;
  24. } else if (platform.isMacintosh && keyCode === 93) {
  25. // the two meta keys in the Mac have different key codes (91 and 93)
  26. return KeyCode.Meta;
  27. } else if (!platform.isMacintosh && keyCode === 92) {
  28. return KeyCode.Meta;
  29. }
  30. }
  31. // cross browser keycodes:
  32. return EVENT_KEY_CODE_MAP[keyCode] || KeyCode.Unknown;
  33. }

得到了 keyEvent 实例对象后,便通过 this._dispatch(keyEvent, keyEvent.target) 执行。

  1. protected _dispatch(
  2. e: IKeyboardEvent,
  3. target: IContextKeyServiceTarget
  4. ): boolean {
  5. return this._doDispatch(
  6. this.resolveKeyboardEvent(e),
  7. target,
  8. /*isSingleModiferChord*/ false
  9. );
  10. }

直接调用了 this._doDispatch 方法,通过 this.resolveKeyboardEvent(e) 方法处理传入的 keyEvent,得到一个包含了许多 keybinding 操作方法的实例。

接下来主要看下 _doDispatch 方法主要干了啥(以下仅展示了部分代码):

  1. private _doDispatch(
  2. keybinding: ResolvedKeybinding,
  3. target: IContextKeyServiceTarget,
  4. isSingleModiferChord = false
  5. ): boolean {
  6. const resolveResult = this._getResolver().resolve(
  7. contextValue,
  8. currentChord,
  9. firstPart
  10. );
  11. if (resolveResult && resolveResult.commandId) {
  12. if (typeof resolveResult.commandArgs === 'undefined') {
  13. this._commandService
  14. .executeCommand(resolveResult.commandId)
  15. .then(undefined, (err) =>
  16. this._notificationService.warn(err)
  17. );
  18. } else {
  19. this._commandService
  20. .executeCommand(
  21. resolveResult.commandId,
  22. resolveResult.commandArgs
  23. )
  24. .then(undefined, (err) =>
  25. this._notificationService.warn(err)
  26. );
  27. }
  28. }
  29. }

主要是找到 keybinding 对应的 command 并执行,_getResolver 方法会拿到已注册的 keybinding,然后通过 resolve 方法找到对应的 keybinding 及 command 信息。而执行 command 则会从 CommandsRegistry 中找到对应已注册的 command,然后执行 command 的 handler 函数(即keybinding 的回调函数)。

6.卸载

先看看一开始的例子中的代码:

  1. const onClick = () => {
  2. actionDispose?.dispose();
  3. window.alert('已卸载');
  4. };

卸载过程如下:

回到刚开始注册时:setActionDispose(editorIns.addAction(action)),addAction 方法会返回一个 disposable 对象,setActionDispose 将该对象保存了起来。通过调用该对象的 dispose 方法:actionDispose.dispose(),便可卸载该 action,对应的 command 和 keybinding 便都会被卸载。

四、结语

对 Monaco Editor 的 Keybinding 机制进行简单描述,就是通过监听用户的键盘输入,找到对应注册的 keybinding 和 command,然后执行对应的回调函数。但仔细探究的话,每个过程都有很多处理逻辑,本文也只是对其做了一个大体的介绍,实际上还有许多相关的细节没有讲到,感兴趣的同学可以探索探索。

Monaco Editor 中的 Keybinding 机制的更多相关文章

  1. 手把手教你实现在Monaco Editor中使用VSCode主题

    背景 笔者开源了一个小项目code-run,类似codepen的一个工具,其中代码编辑器使用的是微软的Monaco Editor,这个库是直接从VSCode的源码中生成的,只不过是做了一点修改让它支持 ...

  2. 【软工】[技术博客] 用Monaco Editor打造接近vscode体验的浏览器IDE

    [技术博客] 用Monaco Editor打造接近vscode体验的浏览器IDE 官方文档与重要参考资料 官方demo 官方API调用样例 Playground 官方API Doc,但其搜索框不支持模 ...

  3. Vue cli2.0 项目中使用Monaco Editor编辑器

    monaco-editor 是微软出的一条开源web在线编辑器支持多种语言,代码高亮,代码提示等功能,与Visual Studio Code 功能几乎相同. 在项目中可能会用带代码编辑功能,或者展示代 ...

  4. js 在浏览器中使用 monaco editor

    <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content ...

  5. Monaco Editor 使用入门

    以前项目是用ace编辑器的,但是总有些不敬人意的地方.前端事件看见的VS Code编辑器Monaco Editor准备更换下,下面介绍一些使用中遇到的一点问题.代码提示 1.项目引用 import * ...

  6. 使用 TypeScript,React,ANTLR 和 Monaco Editor 创建一个自定义 Web 编辑器(二)

    译文来源 欢迎阅读如何使用 TypeScript, React, ANTLR4, Monaco Editor 创建一个自定义 Web 编辑器系列的第二章节, 在这之前建议您阅读使用 TypeScrip ...

  7. Asp.Net Core 使用Monaco Editor 实现代码编辑器

    在项目中经常有代码在线编辑的需求,比如修改基于Xml的配置文件,编辑Json格式的测试数据等.我们可以使用微软开源的在线代码编辑器Monaco Editor实现这些功能.Monaco Editor是著 ...

  8. .Net中Remoting通信机制简单实例

    .Net中Remoting通信机制 前言: 本程序例子实现一个简单的Remoting通信案例 本程序采用语言:c# 编译工具:vs2013工程文件 编译环境:.net 4.0 程序模块: Test测试 ...

  9. Objective-C中的属性机制

    Objective-C 2.0中的属性机制为我们提供了便捷的获取和设置实例变量的方式,也可以说属性为我们提供了一个默认的设置器和访问器的实现.在学习OC中属性之前我们先要知道为什么要为变量实现gett ...

随机推荐

  1. UVA195 Anagram 题解

    To 题目 主要思路:全排列 + 亿点点小技巧. 不会全排列的可以先把这道题过了 \(P1706\). 这道题的难点就在于有重复的单词,只记一次. 第一个想法是将所有以生成的单词记录下来,然后每次判断 ...

  2. Go语言基础三:基本数据类型和运算符

    Go语言数据类型 与其他编程语言一样,Go语言提供了各种数据类型,可分为基本的数据类型和复杂的数据类型.基本的数据类型就是基本的构造块,例如字符串.数字和布尔值.复杂的数据类型是用户自己定义的结构,由 ...

  3. DDL_操作数据库_创建&查询和DDL_操作数据库_修改&删除&使用

    DDL操作数据库.表 1.操作数据库:CRUD C(Create):创建 创建数据库: create database 数据库名称: 创建数据库判断不存在再创建 create database if ...

  4. 哈希-hash

    一. 概念 1.引例 有线性表(1,75,324,43,1353,90,46,-  ) 目的:查找值为90的元素 常见做法: 1.通过一维数组进行遍历查找 (依次比较)( O(n) ) 2.如果关键字 ...

  5. 学会使用MySQL的Explain执行计划,SQL性能调优从此不再困难

    上篇文章讲了MySQL架构体系,了解到MySQL Server端的优化器可以生成Explain执行计划,而执行计划可以帮助我们分析SQL语句性能瓶颈,优化SQL查询逻辑,今天就一块学习Explain执 ...

  6. lamp平台构建

    目录 lamp平台构建 安装httpd 安装mysql 安装php 配置apache 启用代理模块 配置虚拟主机 启用代理模块 验证 lamp平台构建 环境说明: 系统平台 IP 需要安装的服务 ce ...

  7. WPF 实现带蒙版的 MessageBox 消息提示框

    WPF 实现带蒙版的 MessageBox 消息提示框 WPF 实现带蒙版的 MessageBox 消息提示框 作者:WPFDevelopersOrg 原文链接: https://github.com ...

  8. 删除MySQL数据用户

    mysql删除用户的方法: 1.使用"drop user 用户名;"命令删除: 2.使用"delete from user where user='用户名' and ho ...

  9. Fiddler抓包工具下载安装及使用

    一.Fiddler简介 简介: Fiddler是一款强大的Web调试工具,他能记录所有客户端和服务器的HTTP/HTTPS请求 工作原理: Fiddler是以代理web服务器的形式工作的,它使用代理地 ...

  10. 妙啊!纯 CSS 实现拼图游戏

    本文,将向大家介绍一种将多个 CSS 技巧运用到极致的技巧,利用纯 CSS 实现拼图游戏. 本技巧源自于 Temani Afif 的 CodePen CSS Only Puzzle game.一款完全 ...