初始化
我使用的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、添加删除断点
需要注意的是,删除断点的操作我之前不是这么写的,而是在添加断点的操作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
}
这段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>
这段代码是为了解决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)
}
}
}
})
})
最后的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()
}
})
isJsEditor () {
return this.editor.getModel().getLanguageIdentifier().language === 'javascript'
}
上述的代码最下面的部分设置位置那部分,其实和设置断点没有关系,我只是觉得,点击的时候会改变鼠标的位置特别不科学,于是自己处理了一下位置,可以删除的。 另外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, [])
}
这个是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>
最后添加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()
}
})
好吧,大概就可以用了,实际使用可能会有更多问题,具体问题具体分析,慢慢解决吧,我真的觉得这个部分简直全是问题,,,,添加个断点真不容易,其实我推荐自己做断点,不用它的破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()
}
}
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)
}
举个例子,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
}
}
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
}
}
initGoToDefinitionCrossModels这个方法需要在mounted里面调用一下,不然什么都不会发生。然后创建editor的方法也要修改一下:
//第三个参数表示使用指定的service替换默认的
this.editor = monaco.editor.create(this.$refs.main, {
theme: this.curTheme,
automaticLayout: true
}, {
textModelService: this.getTextModelService()
})
之前网上有推荐使用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
}
上面的方法返回的是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)
}
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
})
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')
},
tools.js文件:
let toolUtls = {
/**
* convert obj to string
*/
toString (obj) {}
}
至于添加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
}
}
})
}
对了为了可以顺利的找到worker,需要在webpack的配置文件里面添加const MonacoWebpackPlugin = require(‘monaco-editor-webpack-plugin’)定义,在plugins里面添加new MonacoWebpackPlugin(),这个其实支持参数设置的,我设置失败了,emmm,网上的解决方案都没能解决问题,好在删除参数的话,啥事儿没有,所以就这么用了。
本来还打算实现refactor功能,不过由于没有时间,这个功能无线搁置了,如果有谁实现了,欢迎分享啊。另外,上述的实现都是我自己研究的,不排除有bug,发现bug的话,欢迎提出啊。