官方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。