zoukankan      html  css  js  c++  java
  • monaco editor各种功能实现总结

    我使用的vue,以下是Editor.vue部分代码,只显示了初始化部分。monaco.editor.create方法生成了一个新的编辑器对象,第一个参数是html对象,第二个是options,里面有很多参数,这里只随便设置了两个:主题和自适应layout,接下来将使用这里定义的this.editor对象进行操作,下面提到的方法都定义在methods对象里面(注意由于定义在对象里面,所以下面的所有方法都没有function标志), css式样都定义在<style></style>里面。

    <template>
    <div ref="main" style=" 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);
     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);
     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的话,欢迎提出啊。

  • 相关阅读:
    基础总结篇之一:Activity生命周期
    浅析Android中的消息机制
    详解Android中AsyncTask的使用
    URLConnection的连接、超时、关闭用法总结
    使用Open Flash Chart(OFC)制作图表(Struts2处理)
    用dTree组件生成无限级导航树
    jQuery插件Jeditable的使用(Struts2处理)
    Python Day 56 Django框架学习、学生管理系统迭代二、前后端交互数据传输方式、前台两种跳转方式、form表单详解
    Python Day 55 Django框架、利用学生管理系统来学习Django框架 (版本一)、数据库封装成类、单表操作新url方式、模态对话框
    Python Day 54 Django框架、web请求流程、状态码、自定义web框架
  • 原文地址:https://www.cnblogs.com/onesea/p/15409858.html
Copyright © 2011-2022 走看看