官方demojavascript
官方API Doc,但其搜索框不支持模糊匹配html
官方GitHub Issues,可搜索相關問題java
CSDN優秀博客node
帶主題顏色選擇的demopython
在瀏覽器中搭建Monaco Editor,推薦使用ESModule版本+WebPack+npm插件的形式,比較簡單。連接中即爲官方給出的部署樣例。linux
須要注意的是,通過筆者踩坑,推薦的node.js包版本爲:webpack
"dependencies": { "monaco-editor": "=0.19.3", "monaco-editor-webpack-plugin": "=1.9.0", "webpack": "^3.6.0", "webpack-dev-server": "^2.9.1", }
其中,monaco-editor <= 0.19.1
時無換行自動縮進,monaco-editor = 0.20.0
時編輯器有機率在網頁佈局中只佔高度5px。所以推薦使用版本0.19.2或0.19.3。對應的,monaco-editor-webpack-plugin
使用版本1.8.2(對應editor的0.19.2)或1.9.0(對應editor的0.19.3+)。git
在實現IntelliSense時推薦使用webpack v3.x。github
在Monaco Editor中,每一個用戶可見的編輯器均對應一個IStandaloneCodeEditor。在構造時能夠指定一系列選項,如行號、minimap等。
其中,每一個編輯器的代碼內容等信息存儲在ITextModel中。model保存了文檔內容、文檔語言、文檔路徑等一系列信息,當editor關閉後model仍保留在內存中
所以能夠說,editor對應着用戶看到的編輯器界面,是短時間的、暫時的;model對應着當前網頁歷史上打開/建立過的全部代碼文檔,是長期的、保持的。
建立model時每每給出一個URI,如inmemory://model1
、file://a.txt
等。注意到,此處的URI只是一個對model的惟一標識符,不表明在編輯器中作的編輯將會實時自動保存在本地文件a.txt
中!如下爲樣例:
let uri = monaco.Uri.parse("file://" + filePath); var model = monaco.editor.getModel(uri); // 若是該文檔已經建立/打開則直接取得已存在的model if (!model) // 不然建立新的model model = monaco.editor.createModel(code, language, uri); // 如 code="console.log('hello')", language="javascript" // 也能夠不指定uri參數,直接使用model = monaco.editor.createModel(code, language),會自動分配一個uri let editor = monaco.editor.create(document.getElementById(container_id), { model: model, automaticLayout: true, // 構造選項,具體清單見上文連接 glyphMargin: true, lightbulb: { enabled: true } });
其中container_id
爲放置該編輯器界面的HTML div ID(爲支持多編輯器)。一個合理的建立方式在一個共同的editorRoot
下建立多個container
:
let new_container = document.createElement("DIV"); new_container.id = "container-" + fileCounter.toString(10); new_container.className = "container"; document.getElementById("editorRoot").appendChild(new_container); let container_id = new_container.id;
同時在css中設置container
類的樣式等。
獲取與editor或model的相關信息是簡單的,在ITextModel和IStandaloneCodeEditor的API文檔中不難找到。
如下是一些經常使用信息,包括獲取model實例、獲取代碼內容(字符串)、獲取代碼長度、獲取光標位置、跳光標到給定位置、置焦點到某編輯器等。
export function getModel(editor) { return editor.getModel(); } export function getCode(editor) { return editor.getModel().getValue(); } export function getCodeLength(editor) { // chars, including \n, \t !!! return editor.getModel().getValueLength(); } export function getCursorPosition(editor) { let line = editor.getPosition().lineNumber; let column = editor.getPosition().column; return { ln: line, col: column }; } export function setCursorPosition(editor, ln, col) { let pos = { lineNumber: ln, column: col }; editor.setPosition(pos); } export function setFocus(editor) { editor.focus(); }
能夠在這個demo處預覽由brijeshb42/monaco-themes實現的部分主題,經過npm包的形式使用(見前連接中readme)或手動設置:
export function setTheme(themeName) { // 部分json文件的名稱不能直接用於monaco.editor.defineTheme(如含有空格等) fetch('/themes/' + themes[themeName] + '.json') // 可使用一個map進行轉換 .then(data => data.json()) .then(data => { monaco.editor.defineTheme(themeName, data); monaco.editor.setTheme(themeName); }); }
下面是切換顯示行號、切換顯示小地圖、設置字號字體等的實現:
export function setLineNumberOnOff(editor, option) { // option === 'on' / 'off' if (option === 'on' || option === 'off') { editor.updateOptions({ lineNumbers: option }); } } export function setMinimapOnOff(editor, option) { // option === 'on' / 'off' if (option === 'on') { editor.updateOptions({ minimap: { enabled: true } }); } else if (option === 'off') { editor.updateOptions({ minimap: { enabled: false } }); } } export function setFontSize(editor, size) { editor.updateOptions({ fontSize: size }); } export function setFontFamily(editor, family) { editor.updateOptions({ fontFamily: family }); }
在Monaco中,大部分的編輯器行爲(如複製、粘貼、剪切、摺疊、跳轉等)都是一個IEditorAction
。可使用getSupportedActions打印出全部action的ID。
Monaco支持多鍵快捷鍵和組合鍵。前者指形如F5
、Ctrl+S
、Alt+Ctrl+Shift+S
,同時按下以觸發功能的鍵;後者指先按下Ctrl+K
,再按下某(些)鍵以觸發功能的兩次按鍵。其中後者能夠經過editor.addCommand(monaco.KeyMod.chord(chord1, chord2), callBackFunc)
實現,因不太實用故再也不贅述。
下面是爲某些actions指定快捷鍵的實現方式:
function bindKeyWithAction(editor, key, actionID) { editor.addCommand(key, function () { editor.trigger('', actionID); }); } // 使用二進制或符號表示同時按下多個鍵 // 使用monaco.KeyMod.CtrlCmd以確保跨平臺性:macOS下爲command(⌘),win/linux下爲Ctrl // Ctrl/⌘ [ jump to bracket bindKeyWithAction(editor, monaco.KeyMod.CtrlCmd | monaco.KeyCode.US_OPEN_SQUARE_BRACKET, "editor.action.jumpToBracket"); // Ctrl/⌘ + expand bindKeyWithAction(editor, monaco.KeyMod.CtrlCmd | monaco.KeyCode.US_EQUAL, "editor.unfold"); // Ctrl/⌘ - fold bindKeyWithAction(editor, monaco.KeyMod.CtrlCmd | monaco.KeyCode.US_MINUS, "editor.fold"); // Alt Ctrl/⌘ + expand recursively bindKeyWithAction(editor, monaco.KeyMod.Alt | monaco.KeyMod.CtrlCmd | monaco.KeyCode.US_EQUAL, "editor.unfoldRecursively"); // Shift Ctrl/⌘ + expand all bindKeyWithAction(editor, monaco.KeyMod.Shift | monaco.KeyMod.CtrlCmd | monaco.KeyCode.US_EQUAL, "editor.unfoldAll");
在Monaco中右鍵菜單存儲在node modulemonaco-editor
中,但咱們仍然能夠經過指定路徑獲取到。右鍵菜單分爲若干個entries
(能夠理解爲菜單組),每一個組中包含一系列菜單項。每一個菜單項中存儲了將執行的action、菜單項文本、菜單項ID等。所以以過濾右鍵菜單、只保留想留下的若干項、去除不須要的多餘項爲例,能夠經過迭代和比較action進行修改:
var menus = require('monaco-editor/esm/vs/platform/actions/common/actions').MenuRegistry._menuItems; export function removeUnnecessaryMenu() { var stay = [ "editor.action.jumpToBracket", "editor.action.selectToBracket", // ... action IDs ... "editor.action.clipboardCopyAction", "editor.action.clipboardPasteAction", ] for (let [key, menu] of menus.entries()) { if (typeof menu == "undefined") { continue; } for (let index = 0; index < menu.length; index++) { if (typeof menu[index].command == "undefined") { continue; } if (!stay.includes(menu[index].command.id)) { // menu[index].command.id獲取action的ID字符串 menu.splice(index, 1); } } } }
然而因爲右鍵菜單是根據打開的文檔類型、語言動態決定的,所以建立editor後執行一次removeUnnecessaryMenu()
不必定能所有過濾,推薦連續執行三次。
代碼片斷(snippets)是提升代碼編寫效率的重要工具。其表現形式爲,用戶輸入某些字符觸發自動補全提示,若選擇snippet類型的補全則會在光標後添加一段預先設計好的代碼片斷,且部分須要用戶設置的部分(如變量名、初始值等)爲用戶留空,用戶按下tab鍵能夠在各個留空位置直接快速切換。
如如下的snippets可讓用戶在python代碼中快速建立一個初值爲-1的二維數組:
[[${1:0}]*${3:cols} for _ in range(${2:rows})]
其中${1:0}、${2:rows}、${3:cols}
爲用戶可能修改的位置,初始值爲0、rows、cols
。用戶鍵入-1便可將0更改成-1,按下tab再鍵入4便可將rows更改成4。
如下是在Monaco中的實現方法:
monaco.languages.registerCompletionItemProvider('python', { provideCompletionItems: function (model, position) { var word = model.getWordUntilPosition(position); var range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: word.startColumn, endColumn: word.endColumn }; return { suggestions: createDependencyProposals(range, languageService, editor, word) }; } }); function createDependencyProposals(range, languageService = false, editor, curWord) { let snippets = [ { label: 'list2d_basic', // 用戶鍵入list2d_basic的任意前綴便可觸發自動補全,選擇該項便可觸發添加代碼片斷 kind: monaco.languages.CompletionItemKind.Snippet, documentation: "2D-list with built-in basic type elements", insertText: '[[${1:0}]*${3:cols} for _ in range(${2:rows})]', // ${i:j},其中i表示按tab切換的順序編號,j表示默認串 insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, range: range }, ]; return snippets; }
首先須要定義某語言的關鍵詞、內置函數等待補全詞的列表:
var python_keys = [ // python keywords 'and', 'as', ... 'yield', // python built-in functions 'abs', 'sum', ... ];
以後在上文的createDependencyProposals()
中增長對關鍵詞的補全便可。其中monaco.languages.CompletionItemKind.Keyword
能夠換成對應的類型,如Function
、Const
、Class
等,這裏再也不作區分:
function createDependencyProposals(range, languageService = false, editor, curWord) { // snippets的定義同上 // keys(泛指一切待補全的預約義詞彙)的定義: let keys = []; for (const item of python_keys) { keys.push({ label: item, kind: monaco.languages.CompletionItemKind.Keyword, documentation: "", insertText: item, range: range }); } return snippets.concat(keys); }
當上述snippets和keywords均沒有設置時,Monaco Editor會使用當前文檔的全部詞彙進行「代碼補全提示」。但增長任何自定義補全規則後,原來的naive版詞彙補全將會失效,且如今沒有好的辦法能作到既保留原始word-based補全又使自定義規則生效。
Monaco Editor使用Monarch進行代碼parsing,但暫時沒有一個好的接口能直接獲取parse出的當前文檔的全部token。所以咱們能夠經過正則表達式本身進行簡單的parsing,將當前代碼的全部token取出,加入上述createDependencyProposals()
中,從而間接達到基於token的word-based completion。
在Javascript中使用正則表達式進行全局屢次模式匹配:
const identifierPattern = "([a-zA-Z_]\\w*)"; // 正則表達式定義 注意轉義\\w export function getTokens(code) { let identifier = new RegExp(identifierPattern, "g"); // 注意加入參數"g"表示屢次查找 let tokens = []; let array1; while ((array1 = identifier.exec(code)) !== null) { tokens.push(array1[0]); } return Array.from(new Set(tokens)); // 去重 }
再添加到補全規則中便可實現實時更新的token補全:
function createDependencyProposals(range, languageService = false, editor, curWord) { // snippets和keys的定義同上 let words = []; let tokens = getTokens(editor.getModel().getValue()); for (const item of tokens) { if (item != curWord.word) { words.push({ label: item, kind: monaco.languages.CompletionItemKind.Text, // Text 沒有特殊意義 這裏表示基於文本&單詞的補全 documentation: "", insertText: item, range: range }); } } return snippets.concat(keys).concat(words); }
如何使各類類型的IDE/編輯器擁有代碼補全、代碼錯誤檢查、代碼格式化等語言服務一直是一個難題。傳統的方法是爲每一個IDE/編輯器進行每種語言的適配,十分麻煩。因而微軟提出了Language Server Protocol以構建一套通用的server/client語言服務系統。不一樣的IDE/編輯器做爲client只要調用LSP的接口便可獲取代碼操做的結構,可共用相同的server。
筆者使用的Python Language Server Protocol實現是pyls,C/C++ Language Server Protocol實現是MaskRay/ccls。
Monaco端client的接口是monaco-languageclient,遠程主機端server的接口是pyls_jsonrpc。
它們之間經過基於WebSocket的json-rpc進行通訊。
Client端須要創建WebSocket鏈接,並監聽其信息傳輸。
注意python的語言服務因爲多數場景是單文件補全,且在pyls中已經實現了用戶更改實時同步給server,所以沒必要要將全部用戶代碼文件同步到遠程server主機的BASE_DIR目錄下。但C++的語言服務是基於文件夾的,且在ccls中用戶的實時更改沒有經過WebSocket實時同步給server,所以須要額外將文件實時保存在遠程server中。筆者團隊使用http接口進行實時file update。
import * as monaco from 'monaco-editor'; import { listen } from 'vscode-ws-jsonrpc'; import { MonacoLanguageClient, CloseAction, ErrorAction, MonacoServices, createConnection } from 'monaco-languageclient'; const ReconnectingWebSocket = require('reconnecting-websocket'); function getPythonReady(editor, BASE_DIR, url) { // 註冊語言 monaco.languages.register({ id: 'python', extensions: ['.py'], aliases: ['py', 'PY', 'python', 'PYTHON', 'py3', 'PY3', 'python3', 'PYTHON3'], }); // 設置文件目錄。若是server爲遠程主機則須要將文件實時同步到遠程主機的BASE_DIR目錄下(C++須要 Python不須要) MonacoServices.install(editor, { rootUri: BASE_DIR }); // 創建鏈接 建立LSP client if (!connected) { const webSocket = createWebSocket(url); listen({ webSocket, onConnection: connection => { connected = true; // create and start the language client const languageClient = createLanguageClient(connection); const disposable = languageClient.start(); connection.onClose(() => disposable.dispose()); } }); } }
其中createWebSocket()
、createLanguageClient()
等具體實現詳見vLab-Editor/src/language/python.js。
Server端須要創建WebSocket鏈接,轉發命令給具體的LSP進程並轉發結果給client。
可使用tornado實現,將web socket的read、write重定向到LSP進程的標準輸入輸出流中。
import subprocess import threading import argparse import json from tornado import ioloop, process, web, websocket from pyls_jsonrpc import streams class LanguageServerWebSocketHandler(websocket.WebSocketHandler): writer = None def open(self, *args, **kwargs): proc = process.Subprocess( ['pyls', '-v'], # 具體的LSP實現進程,如 'pyls -v'、'ccls --init={"index": {"onChange": true}}'等 stdin=subprocess.PIPE, stdout=subprocess.PIPE ) self.writer = streams.JsonRpcStreamWriter(proc.stdin) def consume(): ioloop.IOLoop() reader = streams.JsonRpcStreamReader(proc.stdout) reader.listen(lambda msg: self.write_message(json.dumps(msg))) thread = threading.Thread(target=consume) thread.daemon = True thread.start() def on_message(self, message): self.writer.write(json.loads(message)) def check_origin(self, origin): return True if __name__ == "__main__": app = web.Application([ (r"/python", LanguageServerWebSocketHandler), ]) app.listen(3000, address="127.0.0.1") # URL = "ws://127.0.0.1:3000/python" ioloop.IOLoop.current().start()
上述的語言服務已經支持了對代碼進行解析、處理和返回結果。然而要想得到完整的、媲美VSCode的用戶交互體驗,還能夠添加自動打開查找到的定義/引用指向的文件。
要想實現Ctrl+單擊打開標識符的定義文件和位置,須要重寫StandaloneCodeEditorServiceImpl.prototype.doOpenEditor()
方法。詳見vLab-Editor/master/src/app.js#L128。
要想實現打開文件(或peek文件),須要在打開和peek動做前加載目標文件的內容。這須要在構造編輯器時重寫textModelService
中的一系列方法。詳見vLab-Editor/master/src/Editor.js#L27。