monaco editor各种功能实现总结

2022-06-18 11:56:44

初始化

我使用的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 monacofrom'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'exportdefault{
  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删除了,但是为了防止万一,删除断点的方法也改成了下面这种复杂的方法。

//添加断点asyncaddBreakPoint(line){let model=this.editor.getModel()if(!model)returnlet value={range:newmonaco.Range(line,1, line,1), options:{ isWholeLine:true, linesDecorationsClassName:'breakpoints'}}
      model.deltaDecorations([],[value])},//删除断点,如果指定了line,删除指定行的断点,否则删除当前model里面的所有断点asyncremoveBreakPoint(line){let model=this.editor.getModel()if(!model)returnlet decorationslet ids=[]if(line!== undefined){
        decorations=this.editor.getLineDecorations(line)}else{
        decorations=this.editor.getAllDecorations()}for(let decorationof 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 decorationof decorations){if(decoration.options.linesDecorationsClassName==='breakpoints'){returntrue}}returnfalse}

这段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(){returnthis.editor.getModel().getLanguageIdentifier().language==='javascript'}

上述的代码最下面的部分设置位置那部分,其实和设置断点没有关系,我只是觉得,点击的时候会改变鼠标的位置特别不科学,于是自己处理了一下位置,可以删除的。 另外e.target.type这个主要是判断点击的位置在哪里,这里6,7表示是编辑器里面的内容的位置,具体可以参考官方文档。以下截图是从官方文档截得:
在这里插入图片描述
到上面为止,添加断点部分基本上完成了,但是我使用了一下vscode(它使用monaco editor做的编辑器),发现人家在鼠标移动到该出现breakpoint的时候会出现一个半透明的圆点,表示点击这个位置可以出现breakpoint?或者表示breakpoint应该出现在这个位置?不管它什么原因,我觉得我也应该有。
注意啊,这里因为鼠标移开就删除了,所以完全没有删除真的breakpoint时那样麻烦。

//添加一个伪breakpointaddFakeBreakPoint(line){if(this.hasBreakPoint(line))returnlet value={range:newmonaco.Range(line,1, line,1), options:{ isWholeLine:true, linesDecorationsClassName:'breakpoints-fake'}}this.decorations=this.editor.deltaDecorations(this.decorations,[value])},//删除所有的伪breakpointremoveFakeBreakPoint(){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())returnthis.removeFakeBreakPoint()if(e.target.detail&& e.target.detail.offsetX&& e.target.detail.offsetX>=0&& e.target.detail.offsetX<=10){let line= e.target.position.lineNumberthis.addFakeBreakPoint(line)}})this.editor.onMouseLeave(()=>{this.removeFakeBreakPoint()})//这个是因为鼠标放在breakpoint的位置,然后焦点在editor里面,点击enter的话,出现好多伪breakpoint,emmmm,我也不知道怎么回事,没办法,按enter键的话,强制删除所有的伪breakpointthis.editor.onKeyDown(e=>{if(e.code==='Enter'){this.removeFakeBreakPoint()}})

好吧,大概就可以用了,实际使用可能会有更多问题,具体问题具体分析,慢慢解决吧,我真的觉得这个部分简直全是问题,,,,添加个断点真不容易,其实我推荐自己做断点,不用它的破decoration,,,,

2、插入文本

在当前鼠标的位置插入指定文本的代码如下,比较麻烦,但是也没有太多代码,如果你已经选定了一段代码的话,应该会替换当前选中的文本。

insertContent(text){if(this.editor){let selection=this.editor.getSelection()let range=newmonaco.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)returnthis.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=nullif(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{returnnull}let selection= input.options.selectionif(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 matchof 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和
  • 作者:gao_grace
  • 原文链接:https://blog.csdn.net/gao_grace/article/details/88890895
    更新时间:2022-06-18 11:56:44