monaco editor各种功能实现总结
我使用的vue,以下是Editor.vue部分代码,只显示了初始化部分。monaco.editor.create方法生成了一个新的编辑器对象,第一个参数是html对象,第二个是options,里面有很多参数,这里只随便设置了两个:主题和自适应layout,接下来将使用这里定义的this.editor对象进行操作,下面提到的方法都定义在methods对象里面(注意由于定义在对象里面,所以下面的所有方法都没有function标志), css式样都定义在<style></style>里面。
<template> <div ref="main" style="width: 100%;height: 100%;margin-left: 5px;"></div> </template> <script> import * as monaco from 'monaco-editor/esm/vs/editor/editor.main.js' import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution' import { StandaloneCodeEditorServiceImpl } from 'monaco-editor/esm/vs/editor/standalone/browser/standaloneCodeServiceImpl.js' export default { name: 'Editor', data () { return { editor: null, //黑色主题,vs是白色主题,我喜欢黑色 curTheme: 'vs-dark' } }, methods: {}, mounted () { //注意这个初始化没有指定model,可以自己创建一个model,然后使用this.editor.setModel设置进去 //创建model时指定uri,之后可以通过monaco.editor.getModel(uri)获取指定的model //没有设置model的话,接下来的代码没有办法执行 this.editor = monaco.editor.create(this.$refs.main, {theme: this.curTheme, automaticLayout: true}) } </script> <style> </style> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
1、添加删除断点
需要注意的是,删除断点的操作我之前不是这么写的,而是在添加断点的操作let ids = model.deltaDecorations([], [value])有一个返回值是添加的断点的Id集合,我将该集合按照每个model分类存了起来,然后在删除的时候直接操作model.deltaDecorations(ids, []),刚开始并没有发现问题是好用的,然而,后来发现当删除大段多行的文字,并且这些文字里面包含好几个断点的时候,断点会堆积到最上面,视觉上只有一个断点,但是其实是很多个断点叠加在一起,效果就是运行removeBreakpoint时候没有反应,并且换行的时候,下面一行也会出现断点。后来通过监控model的内容change事件将多余的breakpoint删除了,但是为了防止万一,删除断点的方法也改成了下面这种复杂的方法。
//添加断点 async addBreakPoint (line) { let model = this.editor.getModel() if (!model) return let value = {range: new monaco.Range(line, 1, line, 1), options: { isWholeLine: true, linesDecorationsClassName: 'breakpoints' }} model.deltaDecorations([], [value]) }, //删除断点,如果指定了line,删除指定行的断点,否则删除当前model里面的所有断点 async removeBreakPoint (line) { let model = this.editor.getModel() if (!model) return let decorations let ids = [] if (line !== undefined) { decorations = this.editor.getLineDecorations(line) } else { decorations = this.editor.getAllDecorations() } for (let decoration of decorations) { if (decoration.options.linesDecorationsClassName === 'breakpoints') { ids.push(decoration.id) } } if (ids && ids.length) { model.deltaDecorations(ids, []) } }, //判断该行是否存在断点 hasBreakPoint (line) { let decorations = this.editor.getLineDecorations(line) for (let decoration of decorations) { if (decoration.options.linesDecorationsClassName === 'breakpoints') { return true } } return false } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 这段css是控制breakpoint的样式的,我是个css小白,将就着看吧,,,, <style> .breakpoints{ background: red; background: radial-gradient(circle at 3px 3px, white, red); width: 10px !important; height: 10px !important; left: 0px !important; top: 3px; border-radius: 5px; } </style> 1 2 3 4 5 6 7 8 9 10 11
这段代码是为了解决breakpoint堆积的问题,监听了ChangeModelContent事件,在内容发生改变之后进行相应的处理。(添加在mounted中editor初始化之后)
this.editor.onDidChangeModelContent((e) => {
let model = this.editor.getModel()
//必须在nextTick处理,不然getPosition返回的位置有问题
this.$nextTick(() => {
//获取当前的鼠标位置
let pos = this.editor.getPosition()
if (pos) {
//获取当前的行
let line = pos.lineNumber
//如果当前行的内容为空,删除断点(空行不允许设置断点,我自己规定的,,,)
if (this.editor.getModel().getLineContent(line).trim() === '') { this.removeBreakPoint(line) } else { //如果当前行存在断点,删除多余的断点只保留一个 if (this.hasBreakPoint(line)) { this.removeBreakPoint(line) this.addBreakPoint(line) } } } }) }) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
最后的breakpoint的效果图大概如下:
到之前为止,我们只是定义了添加删除breakpoint的方法,你可以在代码里面调用方法进行添加删除breakpoint的操作,但是实际上大多编辑器都是通过点击指定行的方式添加breakpoint的,为了达到点击添加的目的,我们需要监听一下MouseDown事件,添加相应的操作:
this.editor.onMouseDown(e => {
//我建立了很多不同种类的编辑器js, text等,这里只允许js编辑器添加breakpoint,如果你想在mousedown里面做点别的,放在这个前面啊,否则,return了,,,,
if (!this.isJsEditor()) return
//这里限制了一下点击的位置,只有点击breakpoint应该出现的位置,才会创建,其他位置没反应
if (e.target.detail && e.target.detail.offsetX && e.target.detail.offsetX >= 0 && e.target.detail.offsetX <= 10) {
let line = e.target.position.lineNumber
//空行不创建
if (this.editor.getModel().getLineContent(line).trim() === '') { return }
//如果点击的位置没有的话创建breakpoint,有的话,删除
if (!this.hasBreakPoint(line)) { this.addBreakPoint(line) } else { this.removeBreakPoint(line) }
//如果存在上个位置,将鼠标移到上个位置,否则使editor失去焦点
if (this.lastPosition) { this.editor.setPosition(this.lastPosition) } else { document.activeElement.blur() } }
//更新lastPosition为当前鼠标的位置(只有点击编辑器里面的内容的时候)
if (e.target.type === 6 || e.target.type === 7) { this.lastPosition = this.editor.getPosition() } }) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 isJsEditor () { return this.editor.getModel().getLanguageIdentifier().language === 'javascript' } 1 2 3
上述的代码最下面的部分设置位置那部分,其实和设置断点没有关系,我只是觉得,点击的时候会改变鼠标的位置特别不科学,于是自己处理了一下位置,可以删除的。 另外e.target.type这个主要是判断点击的位置在哪里,这里6,7表示是编辑器里面的内容的位置,具体可以参考官方文档。以下截图是从官方文档截得:
到上面为止,添加断点部分基本上完成了,但是我使用了一下vscode(它使用monaco editor做的编辑器),发现人家在鼠标移动到该出现breakpoint的时候会出现一个半透明的圆点,表示点击这个位置可以出现breakpoint?或者表示breakpoint应该出现在这个位置?不管它什么原因,我觉得我也应该有。
注意啊,这里因为鼠标移开就删除了,所以完全没有删除真的breakpoint时那样麻烦。
//添加一个伪breakpoint
addFakeBreakPoint (line) { if (this.hasBreakPoint(line)) return let value = {range: new monaco.Range(line, 1, line, 1), options: { isWholeLine: true, linesDecorationsClassName: 'breakpoints-fake' }} this.decorations = this.editor.deltaDecorations(this.decorations, [value]) },
//删除所有的伪breakpoint
removeFakeBreakPoint () { this.decorations = this.editor.deltaDecorations(this.decorations, []) } 1 2 3 4 5 6 7 8 9 10
这个是css样式,一个半透明的圆点
<style> .breakpoints-fake{ background: rgba(255, 0, 0, 0.2); width: 10px !important; height: 10px !important; left: 0px !important; top: 3px; border-radius: 5px; } </style> 1 2 3 4 5 6 7 8 9 10
最后添加mouse相关的事件监听:
this.editor.onMouseMove(e => { if (!this.isJsEditor()) return this.removeFakeBreakPoint() if (e.target.detail && e.target.detail.offsetX && e.target.detail.offsetX >= 0 && e.target.detail.offsetX <= 10) { let line = e.target.position.lineNumber this.addFakeBreakPoint(line) } }) this.editor.onMouseLeave(() => { this.removeFakeBreakPoint() }) //这个是因为鼠标放在breakpoint的位置,然后焦点在editor里面,点击enter的话,出现好多伪breakpoint,emmmm,我也不知道怎么回事,没办法,按enter键的话,强制删除所有的伪breakpoint this.editor.onKeyDown(e => { if (e.code === 'Enter') { this.removeFakeBreakPoint() } }) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
好吧,大概就可以用了,实际使用可能会有更多问题,具体问题具体分析,慢慢解决吧,我真的觉得这个部分简直全是问题,,,,添加个断点真不容易,其实我推荐自己做断点,不用它的破decoration,,,,
2、插入文本
在当前鼠标的位置插入指定文本的代码如下,比较麻烦,但是也没有太多代码,如果你已经选定了一段代码的话,应该会替换当前选中的文本。
insertContent (text) { if (this.editor) { let selection = this.editor.getSelection() let range = new monaco.Range(selection.startLineNumber, selection.startColumn, selection.endLineNumber, selection.endColumn) let id = { major: 1, minor: 1 } let op = {identifier: id, range: range, text: text, forceMoveMarkers: true} this.editor.executeEdits(this.root, [op]) this.editor.focus() } } 1 2 3 4 5 6 7 8 9 10
3、手动触发Action
这个方法特别简单也没有,但是关键是你得知道Action的id是什么,,,你问我怎么知道的,我去看的源码。
很坑有没有,不过我通过看源码发现了一个可以调用的方法require('monaco-editor/esm/vs/editor/browser/editorExtensions.js').EditorExtensionsRegistry.getEditorActions()这个结果是一个Action数组,包括注册了的Action的各种信息,当然也包括id。(ps: trigger的第一个参数没发现有什么用,就都用anything代替了)
trigger (id) { if (!this.editor) return this.editor.trigger('anyString', id) } 1 2 3 4
举个例子,format document的Action对象大概就是下面这个样子,我们可以通过trigger('editor.action.formatDocument')触发格式化文件的功能。
{ "id": "editor.action.formatDocument", "precondition": { "key": "editorReadonly" }, "_kbOpts": { "kbExpr": { "key": "editorTextFocus", "_defaultValue": false }, "primary": 1572, "linux": { "primary": 3111 }, "weight": 100 }, "label": "Format Document", "alias": "Format Document", "menuOpts": { "when": { "key": "editorHasDocumentFormattingProvider", "_defaultValue": false }, "group": "1_modification", "order": 1.3 } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
4、多model支持转到定义和查找引用
这个之前出过很多错误,网上的搜到的很多答案根本不好用,为了弄明白为啥不好用我还去阅读了相关的源码,下面说一下好用的版本:
//这个函数是从网上找的,用于自定义一个TextModelService,替换原先的
getTextModelService () { return { createModelReference (uri) { const model = { load () { return Promise.resolve(model) }, dispose () { }, textEditorModel: monaco.editor.getModel(uri) } return Promise.resolve({ object: model, dispose () { } }) } } },
//这个两个方法是为了替换CodeEditorService,可以看出和上面的实现不一样,区别在哪里呢
//本来也是打算按照上面的方法来做的,但是也看到了上面的方法需要定义各种需要用到的方法,你得很理解这个Service才可以自己定义啊
//这个就不需要了,只通过原型修改了两个相关的方法,然后其他的就不需要关心了
//上面的好处是在创建editor的时候使用上面的service代替,只影响替换了的editor,下面这个直接影响了所有的editor
//具体使用什么方法可以自己考量,我这个service采用了这种方法,主要是因为自定义的service各种报错,失败了,,,
initGoToDefinitionCrossModels () { let self = this StandaloneCodeEditorServiceImpl.prototype.findModel = function (editor, resource) { let model = null if (resource !== null) { model = monaco.editor.getModel(resource) } return model } StandaloneCodeEditorServiceImpl.prototype.doOpenEditor = function (editor, input) { //这个this.findModel调用的是StandaloneCodeEditorServiceImpl.prototype.findModel这个方法 let model = this.findModel(editor, input.resource) if (model) { editor.setModel(model) } else { return null } let selection = input.options.selection if (selection) { if (typeof selection.endLineNumber === 'number' && typeof selection.endColumn === 'number') editor.setSelection(selection) editor.revealRangeInCenter(selection, 1 /* Immediate */) } else { let pos = { lineNumber: selection.startLineNumber, column: selection.startColumn } editor.setPosition(pos) editor.revealPositionInCenter(pos, 1 /* Immediate */) } editor.focus() } return editor } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
initGoToDefinitionCrossModels这个方法需要在mounted里面调用一下,不然什么都不会发生。然后创建editor的方法也要修改一下:
//第三个参数表示使用指定的service替换默认的
this.editor = monaco.editor.create(this.$refs.main, { theme: this.curTheme, automaticLayout: true }, { textModelService: this.getTextModelService() }) 1 2 3 4 5 6 7
之前网上有推荐使用new StandaloneCodeEditorServiceImpl()生成一个codeEditorService,然后像替换textModelService一样替换codeEditorService的,亲测不好用,new这个操作里面有一些额外的操作,并不可以,想要替换的话,个人认为应该如textModelService一样,自己定义一个对象(可以读读源码了解一下需要实现的方法)。
完成了以上内容,再执行右键-》go to definition就可以跳到定义了,其他如peek definition和find all references都可以正常执行了。
5、全局搜索
monaco编辑器支持单个model内部的搜索,mac快捷键是cmd+f,没有找到全局的搜索,如果我们想在打开的文件夹下面的每个model里面进行搜索的话,需要自己操作一下:
findAllMatches (searchText) {
let result = {}
if (searchText) {
//注意如果你一个model都没有注册的话,这里什么都拿不到
//举个例子啊,下面将一个路径为filePath,语言为lang,文件内容为fileContent的本地文件注册为model
//monaco.editor.createModel(fileContent, lang, monaco.Uri.file(filePath)) monaco.editor.getModels().forEach(model => { result[model.uri.toString()] = [] for (let match of model.findMatches(searchText)) { result[model.uri.toString()].push({ text: model.getLineContent(match.range.startLineNumber), range: match.range, model: model }) } }) } return result } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
上面的方法返回的是monaco.editor里面注册过的每个model对应的搜索对象,包括当前行的文本,目标对象的范围,和model对象。返回的结果可以用于显示,如果想要点击指定的文本跳到对应的model的话,需要做如下操作:
//这里range和model,对应findAllMatches返回结果集合里面对象的range和model属性
goto (range, model) { //设置model,如果是做编辑器的话,打开了多个文本,还会涉及到标签页的切换等其他细节,这里不考虑这些 this.editor.setModel(model) //选中指定range的文本 this.editor.setSelection(range) //把选中的位置放到中间显示 this.editor.revealRangeInCenter(range) } 1 2 3 4 5 6 7 8 9
6、Git新旧版本比较使用DiffEditor
async showDiffEditor (filePath, language) {
//这个方法是我自己定义的,因为用于显示git的修改对比,所以是使用的git命令获取的相关的原始文本
let oriText = await git.catFile(filePath)
let originalModel = monaco.editor.createModel(oriText, language)
//修改后的文本这里在打开文件之前我都初始化好了,所以可以直接通过该方法获得,没有提前创建好的话,可以参照上面的例子创建
let modifiedModel = monaco.editor.getModel(monaco.Uri.file(filePath)) if (!this.diffEditor) { //创建一个diffEditor,readOnly表示只读,this.$refs.main是html对象 this.diffEditor = monaco.editor.createDiffEditor(this.$refs.main, { enableSplitViewResizing: false, automaticLayout: true, readOnly: true }) } this.diffEditor.setModel({ original: originalModel, modified: modifiedModel }) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 7、添加Completions和Defaults 添加一个default对象,代码是从官方的文档找到的,然后自己改写了下面的引用部分。主要作用是这么做之后,在编辑器里面输入tools.js文件里面定义的toolUtls.之后,将会提示toString这个function,并且显示注释信息。感觉和competition挺像啊。 initDefaults () { // validation settings monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ noSemanticValidation: true, noSyntaxValidation: false }) // compiler options monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ target: monaco.languages.typescript.ScriptTarget.ES6, allowNonTsExtensions: true }) let toolsPath = path.join(__dirname, 'tools.js') let str = require('fs').readFileSync(toolsPath).toString() monaco.languages.typescript.javascriptDefaults.addExtraLib(str, 'tools.js') }, 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 tools.js文件: let toolUtls = { /** * convert obj to string */ toString (obj) {} } 1 2 3 4 5 6
至于添加completion也有官方文档,很容易实现:
addCompletions () {
//keyMap是一个普通对象(比如:let keyMap = {Man: 1, Woman: 2})
//这样做的好处是,假如一个方法需要的参数都是类型,但是类型使用1,2,3,4这种数字表示,你很难记住对应的类型名称
//通过这种方式,你输入Man的时候可以插入1 /*Man*/,参数仍然是数字,但是看起来有意义多了,输入也比较方便
//为了key的提示更清楚,可以使用People_Man,People_Woman这种相同前缀的key值,输入People就会提示各种type了
let suggestions = [] for (let key in keyMap) { suggestions.push({ label: key, kind: monaco.languages.CompletionItemKind.Enum, insertText: keyMap[key].toString() + ` /*${key}*/` }) } monaco.languages.registerCompletionItemProvider('javascript', { provideCompletionItems: () => { return { suggestions: suggestions } } }) } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
对了为了可以顺利的找到worker,需要在webpack的配置文件里面添加const MonacoWebpackPlugin = require(‘monaco-editor-webpack-plugin’)定义,在plugins里面添加new MonacoWebpackPlugin(),这个其实支持参数设置的,我设置失败了,emmm,网上的解决方案都没能解决问题,好在删除参数的话,啥事儿没有,所以就这么用了。
本来还打算实现refactor功能,不过由于没有时间,这个功能无线搁置了,如果有谁实现了,欢迎分享啊。另外,上述的实现都是我自己研究的,不排除有bug,发现bug的话,欢迎提出啊。