Monaco Editor 中的 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",});初始化过程如下:
Monaco Editor 中的 Keybinding 机制

文章插图
创建 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 的实例保存起来 。

经验总结扩展阅读