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 的实例保存起来 。
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));注册过程如下:
Monaco Editor 中的 Keybinding 机制

文章插图
当通过 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;}

推荐阅读