返回顶部
首页 > 资讯 > 前端开发 > JavaScript >详解Monaco Editor中的Keybinding机制
  • 825
分享到

详解Monaco Editor中的Keybinding机制

2024-04-02 19:04:59 825人浏览 安东尼
摘要

目录一、前言二、举个?三、原理机制1. 概览2. 初始化3. 注册 keybindings4. key的转换5.执行6.卸载四、结语一、前言 前段时间碰到了一个 Keybinding

一、前言

前段时间碰到了一个 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 创建了一个简单的例子,后文会基于这个例子来进行介绍。

import React, { useRef, useEffect, useState } from "react";
import * as monaco from "monaco-editor";
import { codeText } from "./help";
const Editor = () => {
    const domRef = useRef<htmlDivElement>(null);
    const [actionDispose, setActionDispose] = useState<monaco.IDisposable>();
    useEffect(() => {
        const editorIns = monaco.editor.create(domRef.current!, {
            value: codeText,
            language: "typescript",
            theme: "vs-dark",
        });
        const action = {
            id: 'test',
            label: 'test',
            precondition: 'isChrome == true',
            keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL],
            run: () => {
                window.alert('chrome: cmd + k');
            },
        };
        setActionDispose(editorIns.addAction(action));
        editorIns.focus();
        return () => {
            editorIns.dispose();
        };
    }, []);
    const onClick = () => {
        actionDispose?.dispose();
        window.alert('已卸载');
    };
    return (
        <div>
            <div ref={domRef} className='editor-container' />
            <button className='cancel-button' onClick={onClick}>卸载keybinding</button>
        </div>
    );
};
export default Editor;

三、原理机制

1. 概览

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

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

2. 初始化

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

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

初始化过程如下:

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

let services = new DynamicStandaloneServices(domElement, override);

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

let keybindingService = ensure(IKeybindingService, () =>
    this._reGISter(
        new StandaloneKeybindingService(
            contexTKEyService,
            commandService,
            telemetryService,
            notificationService,
            logService,
            domElement
        )
    )
);

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

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

this._register(
    dom.aDDDisposableListener(
        domnode,
        dom.EventType.KEY_DOWN,
        (e: KeyboardEvent) => {
            const keyEvent = new StandardKeyboardEvent(e);
            const shouldPreventDefault = this._dispatch(
                keyEvent,
                keyEvent.target
            );
            if (shouldPreventDefault) {
                keyEvent.preventDefault();
                keyEvent.stopPropagation();
            }
        }
    )
);

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

3. 注册 keybindings

回到例子中的代码:

const action = {
    id: 'test',
    label: 'test',
    precondition: 'isChrome == true',
    keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL],
    run: () => {
        window.alert('chrome: cmd + k');
    },
};
setActionDispose(editorIns.addAction(action));

注册过程如下:

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

public addDynamicKeybinding(
    commandId: string,
    _keybinding: number,
    handler: ICommandHandler,
    when: ContextKeyExpression | undefined
): IDisposable {
    const keybinding = createKeybinding(_keybinding, OS);
    const toDispose = new DisposableStore();
    if (keybinding) {
        this._dynamicKeybindings.push({
            keybinding: keybinding.parts,
            command: commandId,
            when: when,
            weight1: 1000,
            weight2: 0,
            extensionId: null,
            isBuiltinExtension: false,
        });
        toDispose.add(
            toDisposable(() => {
                for (let i = 0; i < this._dynamicKeybindings.length; i++) {
                    let kb = this._dynamicKeybindings[i];
                    if (kb.command === commandId) {
                        this._dynamicKeybindings.splice(i, 1);
                        this.updateResolver({
                            source: KeybindingSource.Default,
                        });
                        return;
                    }
                }
            })
        );
    }
    toDispose.add(CommandsRegistry.registerCommand(commandId, handler));
    this.updateResolver({ source: KeybindingSource.Default });
    return toDispose;
}

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

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

{
    parts: [
        {
            ctrlKey: boolean,
            shiftKey: boolean,
            altKey: boolean,
            metaKey: boolean,
            keyCode: KeyCode,
        }
    ],
}

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

4. key的转换

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

const action = {
    id: 'test',
    label: 'test',
    precondition: 'isChrome == true',
    keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyL],
    run: () => {
        window.alert('chrome: cmd + k');
    },
};

传入的 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 实例:

export function createKeybinding(keybinding: number, OS: OperatingSystem): Keybinding | null {
    if (keybinding === 0) {
        return null;
    }
    const firstPart = (keybinding & 0x0000FFFF) >>> 0;
    // 处理分两步的keybinding,例如:shift shift,若无第二部分,则chordPart = 0
    const chordPart = (keybinding & 0xFFFF0000) >>> 16;
    if (chordPart !== 0) {
        return new ChordKeybinding([
            createSimpleKeybinding(firstPart, OS),
            createSimpleKeybinding(chordPart, OS)
        ]);
    }
    return new ChordKeybinding([createSimpleKeybinding(firstPart, OS)]);
}

看下 createSimpleKeybinding 方法做了什么

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

拿上面的例子:

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 事件监听函数:

(e: KeyboardEvent) => {
    const keyEvent = new StandardKeyboardEvent(e);
    const shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target);
    if (shouldPreventDefault) {
        keyEvent.preventDefault();
        keyEvent.stopPropagation();
    }
};

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

{
    target: HTMLElement,
    ctrlKey: boolean,
    shiftKey: boolean,
    altKey: boolean,
    metaKey: boolean,
    keyCode: KeyCode,
}

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

function extractKeyCode(e: KeyboardEvent): KeyCode {
    if (e.charCode) {
        // "keypress" events mostly
        let char = String.fromCharCode(e.charCode).toUpperCase();
        return KeyCodeUtils.fromString(char);
    }
    const keyCode = e.keyCode;
    // browser quirks
    if (keyCode === 3) {
        return KeyCode.PauseBreak;
    } else if (browser.isFirefox) {
        if (keyCode === 59) {
            return KeyCode.Semicolon;
        } else if (keyCode === 107) {
            return KeyCode.Equal;
        } else if (keyCode === 109) {
            return KeyCode.Minus;
        } else if (platfORM.isMacintosh && keyCode === 224) {
            return KeyCode.Meta;
        }
    } else if (browser.isWEBKit) {
        if (keyCode === 91) {
            return KeyCode.Meta;
        } else if (platform.isMacintosh && keyCode === 93) {
            // the two meta keys in the Mac have different key codes (91 and 93)
            return KeyCode.Meta;
        } else if (!platform.isMacintosh && keyCode === 92) {
            return KeyCode.Meta;
        }
    }
    // cross browser keycodes:
    return EVENT_KEY_CODE_MAP[keyCode] || KeyCode.Unknown;
}

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

protected _dispatch(
    e: IKeyboardEvent,
    target: IContextKeyServiceTarget
): boolean {
    return this._doDispatch(
        this.resolveKeyboardEvent(e),
        target,
         false
    );
}

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

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

private _doDispatch(
    keybinding: ResolvedKeybinding,
    target: IContextKeyServiceTarget,
    isSingleModiferChord = false
): boolean {
    const resolveResult = this._getResolver().resolve(
        contextValue,
        currentChord,
        firstPart
    );
    if (resolveResult && resolveResult.commandId) {
        if (typeof resolveResult.commandArgs === 'undefined') {
            this._commandService
                .executeCommand(resolveResult.commandId)
                .then(undefined, (err) =>
                    this._notificationService.warn(err)
                );
        } else {
            this._commandService
                .executeCommand(
                    resolveResult.commandId,
                    resolveResult.commandArgs
                )
                .then(undefined, (err) =>
                    this._notificationService.warn(err)
                );
        }
    }
}

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

6.卸载

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

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

卸载过程如下:

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

四、结语

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

以上就是详解Monaco Editor中的Keybinding机制的详细内容,更多关于Monaco Editor Keybinding的资料请关注编程网其它相关文章!

--结束END--

本文标题: 详解Monaco Editor中的Keybinding机制

本文链接: https://lsjlt.com/news/168767.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 详解Monaco Editor中的Keybinding机制
    目录一、前言二、举个三、原理机制1. 概览2. 初始化3. 注册 keybindings4. key的转换5.执行6.卸载四、结语一、前言 前段时间碰到了一个 Keybinding ...
    99+
    2024-04-02
  • monaco editor在Angular的使用详解
    目录正文安装依赖使用正文 本篇文章主要记录下最近的一次业务中用到的 monaco-editor 在 angular 中的使用 安装依赖 在 angular12 及之前你可以选择 mo...
    99+
    2024-04-02
  • vue2.x中monaco-editor的使用说明
    目录vue2中使用monaco-editor安装配置创建MonacoEditor公共组件使用vue-monaco-editor遇到的坑编辑器重复加载上次编辑器中的内容,不会被新的内容...
    99+
    2022-11-13
    vue-monaco-editor monaco-editor的使用 vue2中monaco-editor使用
  • Monaco Editor开发SQL代码提示编辑器实例详解
    目录安装简易 SQL 编辑器相关功能获取选中代码替换选中代码处理光标位置自定义 SQL 库表提示,并保留原有 SQL 提示编辑器 resize编辑器设置主题SQL 代码格式化右键菜单...
    99+
    2022-11-13
    Monaco Editor开发SQL编辑器 Monaco Editor代码提示
  • Android中的binder机制详解
    目录前言 1、Binder是什么? 2、Android系统框架3、Binder通信 4、Binder框架 5、Binder中使用的设计模式 6、Binder与内存映射mmap 参考文...
    99+
    2024-04-02
  • node.js中watch机制详解
    几乎所有构建系统都选择使用watch机制来解决开发过程中需要反复生成构建后文件的问题,但在watch机制下,长期以来我们必须忍受修改完代码,保存完代码必须喝口茶才能刷新看看效果的问题。在这里我们尝试探讨为什...
    99+
    2022-06-04
    详解 机制 node
  • Python中import机制详解
    Python语言中import的使用很简单,直接使用 import module_name 语句导入即可。这里我主要写一下"import"的本质。 Python官方 定义:Python code in on...
    99+
    2022-06-04
    详解 机制 Python
  • 详解Android中AsyncTask机制
    在Android当中,提供了两种方式来解决线程直接的通信问题,一种是通过Handler的机制,还有一种就是今天要详细讲解的 AsyncTask 机制。 AsyncTask&n...
    99+
    2022-06-06
    asynctask Android
  • Android 中Looper机制详解
    版本基于:Android R 0. 前言 在《Android 基于Handler 剖析消息机制》一文中,以 Handler 类为起点详细分析了异步通信,分析了Java 端 Handler 与Looper、MessageQueue、Mes...
    99+
    2023-09-09
    android Looper epoll
  • 详解Node.js中的事件机制
    前言 在前端编程中,事件的应用十分广泛,DOM上的各种事件。在Ajax大规模应用之后,异步请求更得到广泛的认同,而Ajax亦是基于事件机制的。 通常js给我们的第一印象就是运行在客户端浏览器上面的脚本,通过...
    99+
    2022-06-04
    详解 机制 事件
  • Java中的反射机制详解
    目录一、什么是反射?二、为什么要用反射三、Class类四、获取Class类对象的四种方式五.通过反射构造一个类的实例①使用Class.newInstance②通过反射先获取构造方法再...
    99+
    2024-04-02
  • 详解bash中的初始化机制
    Bash初始化文件 交互式login shell 在下列情况下,我们可以获得一个login shell: uFJHrKxWJu 登录系统时获得的顶层shell,无论是通过本地终端登录,还是通过网络ssh登录。这种情...
    99+
    2022-06-04
    bash 初始化 bash 初始化机制
  • Pytorch中的广播机制详解(Broadcast)
    目录1. Pytorch中的广播机制2. 广播机制的理解3. 两个张量进行广播机制的条件4. 当两个张量满足可广播条件后5. 从空间上理解广播机制总结1. Pytorch中的广播机制...
    99+
    2023-01-03
    Pytorch广播机制 Pytorch Broadcast 广播机制
  • 详解Rust中的所有权机制
    目录Rust中的所有权机制什么是所有权所有权规则String类型内存与分配移动克隆所有权与函数引用与借用可变引用悬垂引用引用的规则Slice类型(切片)字符串切片其他类型的 slic...
    99+
    2022-11-13
    Rust所有权机制 Rust所有权
  • 详解java中各类锁的机制
    目录前言1. 乐观锁与悲观锁2. 公平锁与非公平锁3. 可重入锁4. 读写锁(共享锁与独占锁)6. 自旋锁7. 无锁 / 偏向锁 / 轻量级锁 / 重量级锁前言 总结java常见的锁...
    99+
    2024-04-02
  • Go语言中的GC机制详解
    标题:Go语言中的GC机制详解 Go语言作为一种现代化且高效的编程语言,其垃圾回收(Garbage Collection,GC)机制一直是其亮点之一。GC机制的设计使得开发者可以更加专...
    99+
    2024-04-02
  • Go语言中的锁机制详解
    标题:Go语言中的锁机制详解 Go语言中的锁机制是一种用于并发编程的重要工具,通过锁机制可以保护共享资源,避免多个goroutine同时访问导致的数据竞争问题。在本文中,我们将深入探讨...
    99+
    2024-04-02
  • Android View 绘制机制的详解
    View 绘制机制一、 View 树的绘图流程当 Activity 接收到焦点的时候,它会被请求绘制布局,该请求由 Android framework 处理.绘制是从根节点开始,对布局树进行 measure 和 draw。整个 View 树...
    99+
    2023-05-31
    android view 绘制
  • mysql的锁机制详解
    这段时间一直在学习mysql数据库。项目组一直用的是oracle,所以对mysql的了解也不深。本文主要是对mysql锁的总结。 Mysql的锁主要分为3大类:    表级锁:存储引擎为Myisam。锁住整个表,特点是开销小,加锁快,锁定力...
    99+
    2021-08-13
    mysql的锁机制详解
  • 详解 MySQL的FreeList机制
    一、前言 MySQL启动后,BufferPool就会被初始化,在你没有执行任何查询操作之前,BufferPool中的缓存页都是一块块空的内存,未被使用过也没有任何数据保存在里面。 而且你也知道了通过缓冲页的描述信息可...
    99+
    2022-05-13
    MySQL FreeList机制 MySQL FreeList
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作