詳解Monaco?Editor中的Keybinding機(jī)制
一、前言
前段時(shí)間碰到了一個(gè) Keybinding 相關(guān)的問(wèn)題,于是探究了一番,首先大家可能會(huì)有兩個(gè)問(wèn)題:Monaco Editor 是啥?Keybinding 又是啥?
- Monaco Editor: 微軟開(kāi)源的一個(gè)代碼編輯器,為 VS Code 的編輯器提供支持,Monaco Editor 核心代碼與 VS Code 是共用的(都在 VS Code github 倉(cāng)庫(kù)中)。
- Keybinding: Monaco Editor 中實(shí)現(xiàn)快捷鍵功能的機(jī)制(其實(shí)準(zhǔn)確來(lái)說(shuō),應(yīng)該是部分機(jī)制),可以使得通過(guò)快捷鍵來(lái)執(zhí)行操作,例如打開(kāi)命令面板、切換主題以及編輯器中的一些快捷操作等。
本文主要是針對(duì) Monaco Editor 的 Keybinding 機(jī)制進(jìn)行介紹,由于源碼完整的邏輯比較龐雜,所以本文中的展示的源碼以及流程會(huì)有一定的簡(jiǎn)化。
文中使用的代碼版本:
Monaco Editor:0.30.1
VS Code:1.62.1
二、舉個(gè)??
這里使用 monaco-editor 創(chuàng)建了一個(gè)簡(jiǎn)單的例子,后文會(huì)基于這個(gè)例子來(lái)進(jìn)行介紹。
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;
三、原理機(jī)制
1. 概覽
根據(jù)上面的例子,Keybinding 機(jī)制的總體流程可以簡(jiǎn)單的分為以下幾步:
- 初始化:主要是初始化服務(wù)以及給 dom 添加監(jiān)聽(tīng)事件
- 注冊(cè):注冊(cè) keybinding 和 command
- 執(zhí)行:通過(guò)按快捷鍵觸發(fā)執(zhí)行對(duì)應(yīng)的 keybinding 和 command
- 卸載:清除注冊(cè)的 keybinding 和 command
2. 初始化
回到上面例子中創(chuàng)建 editor 的代碼:
const editorIns = monaco.editor.create(domRef.current!, { value: codeText, language: "typescript", theme: "vs-dark", });
初始化過(guò)程如下:
創(chuàng)建 editor 之前會(huì)先初始化 services,通過(guò)實(shí)例化 DynamicStandaloneServices 類創(chuàng)建服務(wù):
let services = new DynamicStandaloneServices(domElement, override);
在 constructor 函數(shù)中會(huì)執(zhí)行以下代碼注冊(cè) keybindingService:
let keybindingService = ensure(IKeybindingService, () => this._register( new StandaloneKeybindingService( contextKeyService, commandService, telemetryService, notificationService, logService, domElement ) ) );
其中 this._register 方法和 ensure 方法會(huì)分別將 StandaloneKeybindingServices 實(shí)例保存到 disposable 對(duì)象(用于卸載)和 this._serviceCollection 中(用于執(zhí)行過(guò)程查找keybinding)。
實(shí)例化 StandaloneKeybindingService,在 constructor 函數(shù)中添加 DOM 監(jiān)聽(tīng)事件:
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 方法,會(huì)通過(guò) addEventListener 的方式,在 domNode 上添加一個(gè) keydown 事件的監(jiān)聽(tīng)函數(shù),并且返回一個(gè) DomListener 的實(shí)例,該實(shí)例包含一個(gè)用于移除事件監(jiān)聽(tīng)的 dispose 方法。然后通過(guò) this._register 方法將 DomListener 的實(shí)例保存起來(lái)。
3. 注冊(cè) 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));
注冊(cè)過(guò)程如下:
當(dāng)通過(guò) editorIns.addAction 來(lái)注冊(cè) keybinding 時(shí),會(huì)調(diào)用 StandaloneKeybindingServices 實(shí)例的 addDynamicKeybinding 方法來(lái)注冊(cè) 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; }
會(huì)先根據(jù)傳入的 _keybinding 創(chuàng)建 keybinding 實(shí)例,然后連同 command、when 等其他信息存入_dynamicKeybindings 數(shù)組中,同時(shí)會(huì)注冊(cè)對(duì)應(yīng)的 command,當(dāng)后面觸發(fā) keybinding 時(shí)便執(zhí)行對(duì)應(yīng)的 command。返回的 toDispose 實(shí)例則用于取消對(duì)應(yīng)的 keybinding 和 command。
回到上面代碼中創(chuàng)建 keybinding 實(shí)例的地方,createKeybinding 方法會(huì)根據(jù)傳入的 _keybinding 數(shù)字和 OS 類型得到實(shí)例,大致結(jié)構(gòu)如下(已省略部分屬性):
{ parts: [ { ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean, keyCode: KeyCode, } ], }
那么,是怎么通過(guò)一個(gè) number 得到所有按鍵信息的呢?往下看↓↓↓
4. key的轉(zhuǎn)換
先看看一開(kāi)始傳入的 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 數(shù)組中的元素,monaco.KeyMod.CtrlCmd = 2048,monaco.KeyCode.KeyL = 42,對(duì)應(yīng)的數(shù)字是 monaco-editor 中定義的枚舉值,與真實(shí)的 keyCode 存在對(duì)應(yīng)關(guān)系。所以注冊(cè)時(shí)傳入的 keybinding 參數(shù)為: 2048 | 42 = 2090
先簡(jiǎn)單了解下 JS 中的位運(yùn)算(操作的是32位帶符號(hào)的二進(jìn)制整數(shù),下面例子中只用8位簡(jiǎn)單表示):
按位與(AND)&
對(duì)應(yīng)的位都為1則返回1,否則返回0
例如:
00001010 // 10
00000110 // 6
------
00000010 // 2
按位或(OR)|
對(duì)應(yīng)的位,只要有一個(gè)為1則返回1,否則返回0
00001010 // 10
00000110 // 6
-------
00001110 // 14
左移(Left shift)<<
將二進(jìn)制數(shù)每一位向左移動(dòng)指定位數(shù),左側(cè)移出的位舍棄,右側(cè)補(bǔ)0
00001010 // 10
------- // 10 << 2
00101000 // 40
右移 >>
將二進(jìn)制數(shù)每位向右移動(dòng)指定位數(shù),右側(cè)移出的位舍棄,左側(cè)用原來(lái)最左邊的數(shù)補(bǔ)齊
00001010 // 10
------- // 10 >> 2
00000010 // 2
無(wú)符號(hào)右移 >>>
將二進(jìn)制數(shù)每位向右移動(dòng)指定位數(shù),右側(cè)移出的位舍棄,左側(cè)補(bǔ)0
00001010 // 10
------- // 10 >> 2
00000010 // 2
接下來(lái)看下是怎么根據(jù)一個(gè)數(shù)字,創(chuàng)建出對(duì)應(yīng)的 keybinding 實(shí)例:
export function createKeybinding(keybinding: number, OS: OperatingSystem): Keybinding | null { if (keybinding === 0) { return null; } const firstPart = (keybinding & 0x0000FFFF) >>> 0; // 處理分兩步的keybinding,例如:shift shift,若無(wú)第二部分,則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);
運(yùn)算如下:
100000101010 // 2090 -> keybinding
100000000000 // 2048 -> CtrlCmd
----------- // &
100000000000 // 2048 -> CtrlCmd
再看keyCode的運(yùn)算:
const keyCode = (keybinding & BinaryKeybindingsMask.KeyCode)
100000101010 // 2090 -> keybinding
000011111111 // 255 -> KeyCode
----------- // &
000000101010 // 42 -> KeyL
于是便得到了 ctrlKey,shiftKey,altKey,metaKey,keyCode 這些值,接下來(lái)便由這些值生成SimpleKeybinding實(shí)例,該實(shí)例包含了上面的這些按鍵信息以及一些操作方法。
至此,已經(jīng)完成了 keybinding 的注冊(cè),將 keybinding 實(shí)例及相關(guān)信息存入了 StandaloneKeybindingService 實(shí)例的 _dynamicKeybindings 數(shù)組中,對(duì)應(yīng)的 command 也注冊(cè)到了 CommandsRegistry 中。
5.執(zhí)行
當(dāng)用戶在鍵盤上按下快捷鍵時(shí),便會(huì)觸發(fā) keybinding 對(duì)應(yīng) command 的執(zhí)行,執(zhí)行過(guò)程如下:
回到 StandaloneKeybindingServices 初始化的時(shí)候,在 domNode 上綁定了 keydown 事件監(jiān)聽(tīng)函數(shù):
(e: KeyboardEvent) => { const keyEvent = new StandardKeyboardEvent(e); const shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target); if (shouldPreventDefault) { keyEvent.preventDefault(); keyEvent.stopPropagation(); } };
當(dāng) keydown 事件觸發(fā)后,便會(huì)執(zhí)行這個(gè)監(jiān)聽(tīng)函數(shù),首先會(huì)實(shí)例化一個(gè) StandardKeyboardEvent 實(shí)例,該實(shí)例包含了一些按鍵信息和方法,大致結(jié)構(gòu)如下(已省略部分屬性):
{ target: HTMLElement, ctrlKey: boolean, shiftKey: boolean, altKey: boolean, metaKey: boolean, keyCode: KeyCode, }
其中 keyCode 是經(jīng)過(guò)處理后得到的,由原始鍵盤事件的 keyCode 轉(zhuǎn)換為 monoco-editor 中的 keyCode,轉(zhuǎn)換過(guò)程主要就是兼容一些不同的瀏覽器,并根據(jù)映射關(guān)系得到最終的 keyCode。準(zhǔn)換方法如下:
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 實(shí)例對(duì)象后,便通過(guò) this._dispatch(keyEvent, keyEvent.target) 執(zhí)行。
protected _dispatch( e: IKeyboardEvent, target: IContextKeyServiceTarget ): boolean { return this._doDispatch( this.resolveKeyboardEvent(e), target, /*isSingleModiferChord*/ false ); }
直接調(diào)用了 this._doDispatch 方法,通過(guò) this.resolveKeyboardEvent(e) 方法處理傳入的 keyEvent,得到一個(gè)包含了許多 keybinding 操作方法的實(shí)例。
接下來(lái)主要看下 _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 對(duì)應(yīng)的 command 并執(zhí)行,_getResolver 方法會(huì)拿到已注冊(cè)的 keybinding,然后通過(guò) resolve 方法找到對(duì)應(yīng)的 keybinding 及 command 信息。而執(zhí)行 command 則會(huì)從 CommandsRegistry 中找到對(duì)應(yīng)已注冊(cè)的 command,然后執(zhí)行 command 的 handler 函數(shù)(即keybinding 的回調(diào)函數(shù))。
6.卸載
先看看一開(kāi)始的例子中的代碼:
const onClick = () => { actionDispose?.dispose(); window.alert('已卸載'); };
卸載過(guò)程如下:
回到剛開(kāi)始注冊(cè)時(shí):setActionDispose(editorIns.addAction(action)),addAction 方法會(huì)返回一個(gè) disposable 對(duì)象,setActionDispose 將該對(duì)象保存了起來(lái)。通過(guò)調(diào)用該對(duì)象的 dispose 方法:actionDispose.dispose(),便可卸載該 action,對(duì)應(yīng)的 command 和 keybinding 便都會(huì)被卸載。
四、結(jié)語(yǔ)
對(duì) Monaco Editor 的 Keybinding 機(jī)制進(jìn)行簡(jiǎn)單描述,就是通過(guò)監(jiān)聽(tīng)用戶的鍵盤輸入,找到對(duì)應(yīng)注冊(cè)的 keybinding 和 command,然后執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù)。但仔細(xì)探究的話,每個(gè)過(guò)程都有很多處理邏輯,本文也只是對(duì)其做了一個(gè)大體的介紹,實(shí)際上還有許多相關(guān)的細(xì)節(jié)沒(méi)有講到,感興趣的同學(xué)可以探索探索。
以上就是詳解Monaco Editor中的Keybinding機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于Monaco Editor Keybinding的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
AngularJS中$http服務(wù)常用的應(yīng)用及參數(shù)
大家都知道,AngularJS中的$http有很多參數(shù)和調(diào)用方法,所以本文只記錄了比較常用的應(yīng)用及參數(shù),方便大家以后使用的時(shí)候參考學(xué)習(xí),下面一起來(lái)看看吧。2016-08-08angularjs 中$apply,$digest,$watch詳解
這篇文章主要介紹了angularjs 中$apply,$digest,$watch詳解的相關(guān)資料,需要的朋友可以參考下2016-10-10基于AngularJs select綁定數(shù)字類型的問(wèn)題
今天小編就為大家分享一篇基于AngularJs select綁定數(shù)字類型的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-10-10angularjs請(qǐng)求數(shù)據(jù)的方法示例
這篇文章主要給大家介紹了關(guān)于angularjs請(qǐng)求數(shù)據(jù)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用angularjs具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08Angular(5.2->6.1)升級(jí)小結(jié)
今天小編就為大家分享一篇關(guān)于Angular(5.2->6.1)升級(jí)小結(jié),小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-12-12Angular4實(shí)現(xiàn)動(dòng)態(tài)添加刪除表單輸入框功能
這篇文章主要介紹了Angular4實(shí)現(xiàn)動(dòng)態(tài)添加刪除表單輸入框功能,需要的朋友可以參考下2017-08-08基于Angularjs實(shí)現(xiàn)分頁(yè)功能
這篇文章主要介紹了基于Angularjs實(shí)現(xiàn)分頁(yè)功能的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-05-05Angular4綁定html內(nèi)容出現(xiàn)警告的處理方法
這篇文章主要給大家介紹了關(guān)于Angular4綁定html內(nèi)容出現(xiàn)警告的處理方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11AngularJS ng-repeat指令及Ajax的應(yīng)用實(shí)例分析
這篇文章主要介紹了AngularJS ng-repeat指令及Ajax的應(yīng)用,結(jié)合實(shí)例形式分析了ng-repeat指令的功能及ajax請(qǐng)求交互相關(guān)操作技巧,需要的朋友可以參考下2017-07-07