记录Monaco Editor 插件在vue中的使用

2,006 阅读7分钟

前言

本文主要是记录monaco editor 的使用,方便后期自己回顾和复习。安装不同的版本的接口可能会有细微的差别,我这里主要使用的版本是1.0.10版本。

安装环境

npm install monaco-editor --save


//这个必须安装,
npm install monaco-editor-webpack-plugin --save 

开始使用

在vue的temeplate 中使用:

<MonacoEditor
  class="monaco-editor" 
  language="myLanguage" 
  theme="myTheme" 
  :editorMounted="onEditorMounted" 
  :options="optionSetting" // 编辑器的配置数据
  @change="onChange" 
  id="containerRef"
></MonacoEditor>

在vue的scrpit 中添加:

    import MonacoEditor from 'monaco-editor-vue';
    import * as monaco from 'monaco-editor'
    import 'monaco-editor/esm/vs/basic-languages/html/html.contribution'
data() {
    return {
      optionSetting:{
        value: '', // 编辑器初始显示文字
        automaticLayout: true, // 自动布局
        overviewRulerBorder: false, // 不要滚动条的边框
        foldingStrategy: 'indentation', // 代码可分小段折叠
        tabSize: 0, // tab 缩进长度
        autoClosingBrackets: 'always', // 是否自动添加结束括号(包括中括号) "always" | "languageDefined" | "beforeWhitespace" | "never"
        // autoClosingDelete: 'always', // 是否自动删除结束括号(包括中括号) "always" | "never" | "auto"
        // autoClosingQuotes: 'always', // 是否自动添加结束的单引号 双引号 "always" | "languageDefined" | "beforeWhitespace" | "never"
        autoIndent: 'None', // 控制编辑器在用户键入、粘贴、移动或缩进行时是否应自动调整缩进
        autoClosingBrackets: true ,
        comments: {
            ignoreEmptyLines: true, // 插入行注释时忽略空行。默认为真。
            insertSpace: true // 在行注释标记之后和块注释标记内插入一个空格。默认为真。
        }, // 注释配置
        cursorBlinking: 'Solid', // 光标动画样式
        cursorSmoothCaretAnimation: true, // 是否启用光标平滑插入动画  当你在快速输入文字的时候 光标是直接平滑的移动还是直接"闪现"到当前文字所处位置
        cursorSurroundingLines: 0, // 光标环绕行数 当文字输入超过屏幕时 可以看见右侧滚动条中光标所处位置是在滚动条中间还是顶部还是底部 即光标环绕行数 环绕行数越大 光标在滚动条中位置越居中
        cursorSurroundingLinesStyle: 'all', // "default" | "all" 光标环绕样式
        cursorWidth: 2, // <=25 光标宽度
        diagnostics: true,
        minimap: { // 关闭代码缩略图
          enabled: false // 是否启用预览图
        },
        overviewRulerBorder: false, // 是否应围绕概览标尺绘制边框
        wordWrap:'on', // 文本自动换行
        folding: true, // 是否启用代码折叠
        scrollBeyondLastLine: false, // 设置编辑器是否可以滚动到最后一行之后
        renderLineHighlight: 'all', // 当前行突出显示方式  "all" | "line" | "none" | "gutter"
        theme: 'vs',// 官方自带三种主题vs, hc-black, or vs-dark
        formatOnPaste: true, //是否粘贴自动格式化
        renderValidationDecorations: "on",
        hover: {
          enabled: true,
          delay: 500,
        },
      },      
      // 编辑器
      monacoEditor:null,
      // 编辑器的内容
      monacoObject:null,
      // 编辑器的指针注入
      monacoProject:null,
      monacoProject1:null,
    };
  },

    // 组件中页面的默认挂载在页面的时间的方法
      onEditorMounted(editor, monaco) {
          // window 是全局挂载,其他组件也可以直接使用editor.setValue()等方法
          window.editor = editor
          window.monaco = monaco
          
          // 只能在当前组件取值和使用(根据情况自行选择)
          // this.monacoEditor = editor
          // this.monacoObject = monaco
    },

编辑器主题和设置文字的颜色

设置编辑器的主题主要使用defineThemesetTheme

     monaco.editor.defineTheme('myTheme', {
          base: 'vs', // vs、vs-dark、hc-black // (三种自定义的主题)
          inherit: true,
          rules: [
              // 这里是自己设置的文字颜色,monaco editor 中也有默认配置的颜色(如下图)
              // token 是设置颜色的关键字,foreground 是十六进制的颜色
              { token: 'custom-number', foreground: '#7944F8' },
              { token: 'custom-string', foreground: '#0081ff' },
              { token: 'custom-sys', foreground: '#13ce66' },
              { token: 'custom-let', foreground: '#8ec2f2' },
              { token: 'custom-oper', foreground: '#f95e13' },
          ],
          colors: {
              'editor.background': '#e6ebf5', //编辑器背景颜色
              'editorLineNumber.foreground': '#0081ff', //行号颜色
              'editorLineNumber.activeForeground': '#7944F8', //当前行号颜色
              'editor.lineHighlightBackground': '#e6ebff', // 当前行背景色
              'editorGutter.background': '#e6ebf5', //行号背景色
          }
      });
      monaco.editor.setTheme("myTheme");

image.png

定义主题和文字之后,需要注册我们自定义的语言,和页面中的language="myLanguage"相对应

api setMonarchTokensProvider(languageId, languageDef)的参数如下:

  • 参数一:编辑器的语言种类,可以是sql,html,css等,也可以是自定义的语言(如下的myLanguage)
  • 参数二:设置不同关键字颜色配置(前面必须是正则,后面则是我们设置的自定义的颜色)
monaco.languages.register({ id: 'myLanguage' });
monaco.languages.setMonarchTokensProvider('myLanguage', {
    ignoreCase: false, //忽略大小写
    tokenizer: {
      root: [
          // 内置的颜色
          [/CHILDPOINT1|CHILDPOINT2/, 'custom-number'],
          //  方法的颜色(这里的keyword 就是内置颜色)
          [/[$]/,{token:'keyword'} ],
          // 操作符的颜色
          [/[+]|[-]|[*]|[/]|[%]|[>]|[<]|[=]|[!]|[:]|[&&][||]/,{ token: 'custom-oper' }],
          // 括号的颜色
          [/[(]|[)]/, { token: 'custom-let' }], 
      ],
    }
});

注: 官方内置颜色可点击链接

自定义代码提示(代码补全)

代码提示主要使用的api:registerCompletionItemProvider(languageSelector, provider)的参数如下:

  • 参数一: 编辑器的语言种类,标识字符串;例如:“myLanguage”,用于指定这个代码提示(自动补全功能)应该使用语言。
  • 参数二:provider:一个对象或函数,定义了补全项的提供逻辑。这个对象或函数需要实现特定的接口,主要包括:
    • triggerCharacters:触发元素,我们可以指定特点的触发字符,比如我们写代码的时候输入list.后面自动出来 length 等属性或者方法。

    • provideCompletionItems(model, position): CompletionList | Thenable: 这个方法是核心,当用户触发补全时被调用,model是当前编辑器模型,position是光标的位置。它应该返回一个CompletionList对象或一个Promise,其中包含补全建议列表。

      • label:每个补全项通常包括(显示的文本)
      • kind(类型标识,如函数、变量、类等),
      • insertText(实际插入到文档的文本),以及其他可选属性。
      // 取 data 中的数据需要 用_this 的内容
      const _this = this;
      const commonUse = [];
      window.provider= monaco.languages.registerCompletionItemProvider('myLanguage', {
        provideCompletionItems:  function (model, position) {
            // 判第一个语言注册是否已经注册过了,如果注册过了就销毁,为了解决代码重复提示的问题 
            // window.provider1?.dispose();
            let textUnitPosition = model.getValueInRange({
                startLineNumber: position.lineNumber,
                startColumn: 1,
                endLineNumber: position.lineNumber,
                endColumn: position.column
            });
            
            let match = textUnitPosition.match(/(\S+)$/);
            if (!match) return [];
            match = match[0].toUpperCase();
            
            let suggestions = [];
            const handleSuggestions = (arr, type, detail) => {
                arr.forEach(async item => {
                  let insertText = `${item.name}` 
                    // 判断对象有多少个属性,并依次把他推进数组中 list 是需要和后端配合需要参数的,自动加() noList 是前端写死,不需要参数,也不需要括号
                  let list = [1,2,3,4,5,6,7,8,9]
                  let noList = [98,99]
                  if(list.includes(item.type)){
                    insertText =`${item.name}()`
                  }
                  suggestions.push({
                      label: item.name, // 提示文本
                      kind:item.kind, //提示类型
                      // insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
                      sortText: commonUse.includes(item.name) ? '0' : '1', //排序,可以将常用提示放在前面
                      detail:item.remark, // 提示解释文本
                      insertText:insertText //插入内容,此处可以使用 $数值 来确定插入后光标位置,例如:'<if test="$0">\n\t\n</if>'
                    })                
                  });
              }
              handleSuggestions(_this.allHintList, 'Field', '库表信息');
              handleSuggestions(_this.groupHintList, 'Field', '属性组');
              handleSuggestions(_this.fieldHintList, 'Field', '属性组字段');
            
            
          return {
            incomplete: true,
            suggestions:suggestions
          }
        }
      })

下面这段代码是当输入特定字符(我这是输入$ 和 .)的代码提示

    window.provider1 = monaco.languages.registerCompletionItemProvider('myLanguage',{
          triggerCharacters: ['.','$','('], //输入$和.时触发
          provideCompletionItems: (model, position) => {
            // 判第一个语言注册是否已经注册过了,如果注册过了就销毁,为了解决代码重复提示的问题 
            // window.provider?.dispose();
            // this.monacoProject?.dispose(); 
            //  console.log("model---",editor.getModel());
            const line = position.lineNumber
            // console.log("line-----",line);
            // 获取当前列数
            const column = position.column
            // console.log("column-----",column);
            // 获取当前行的所有内容
            const content = model.getLineContent(line)
            //  console.log("content-----",content);
            // 通过下标来取当前输入行的内容,刚刚输入的内容
            const sys = content[column - 2]
            // console.log("sys-----",sys);
            // 当前光标所在位置的行的全部内容
            const codePre = model.getValueInRange({
              startLineNumber: position.lineNumber,
              startColumn: 1,
              endLineNumber: position.lineNumber,
              endColumn: position.column,
            });
            // console.log("codePre-------",codePre);
            const word = model.getWordUntilPosition(position);
            // console.log("word----",word);
           let suggestions = []
            if(sys == '$'){
              // 这里是要匹配要提示的内容,
              let arr = this.allHintList.filter(val=>{
                let list = [98,99]
                if(list.includes(val.type)){
                  val.kind = monaco.languages.CompletionItemKind.Variable
                    return list.includes(val.type)
                }
              })
              // console.log("varList--",arr);
              arr.forEach(item =>{
                suggestions.push({
                  label:item.name,
                  // kind 代码提示的默认类型  代码提示前面的icon
                  kind: item.kind,
                  insertText:item.name.substring(1), //  因为输入了$才提示,所以再取值的时候不应该再取$要截去
                  // insertTextRules: monaco.languages.CompletionItemInsertTextRule.None,
                  detial:item.remark,
                  range: {
                      startLineNumber: position.lineNumber,
                      endLineNumber: position.lineNumber,
                      startColumn: word.startColumn,
                      endColumn: word.endColumn,
                  },
                })
                
              })
            }else if(sys == '.'){
                // 当输入.的时候的提示的方法       
              let pointList = this.handleInputPointToHint()
              console.log("输入了点",pointList);
              pointList.forEach(item =>{
                let insertText = `${item.name}` 
                let list = [1,2,3,4,5,6,7,8,9]
                if(list.includes(item.type)){
                  insertText =`${item.name}()`
                }
                suggestions.push({
                  label:item.name,
                  // kind 代码提示的默认类型  代码提示前面的icon
                  kind: item.kind,
                  insertText:insertText, 
                  // insertTextRules: monaco.languages.CompletionItemInsertTextRule.None,
                  detial:item.remark,
                  // 提示的指定位置
                  range: {
                      startLineNumber: position.lineNumber,
                      endLineNumber: position.lineNumber,
                      startColumn: word.startColumn,
                      endColumn: word.endColumn,
                  },
                })
                
              })
            }else{
              // 有其他默认提示
              suggestions.push[{
                  label:word.word,
                  // 前面的方法或者变量名的数据内容
                  kind: monaco.languages.CompletionItemKind.Function,
                  insertText: word.word,
                  insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
                  range: {
                      startLineNumber: position.lineNumber,
                      endLineNumber: position.lineNumber,
                      startColumn: word.startColumn,
                      endColumn: word.endColumn,
                  },
              }]
            }
            return {
                suggestions: suggestions
            }
          },
      });

注:

 在这里有我遇到的问题,有时候会有重复数据提示,因为注册语言注册了2次(俩次使用接口registerCompletionItemProvider),所以在自定义提示(或者输入$ 的时候,会有重复的代码提示)的时候显示2次代码(如下图)
// 解决办法如下 判第一个语言注册是否已经注册过了,如果注册过了就销毁,为了解决代码重复提示的问题 
//  window.provider1?.dispose();  this.monacoProject?.dispose();  

image.png

鼠标悬停hover的提示内容

鼠标悬停提示主要使用的api:registerHoverProvider(languageSelector, provider)的参数如下:

  • 参数一:编辑器的语言种类,标识字符串;例如:“myLanguage”,用于指定这个悬停代码提示应该使用语言。

  • 参数二:provider:一个对象或函数,定义了悬停的提示信息的提供逻辑。这个对象或函数需要实现特定的接口,主要包括:

    • provideHover(model, position): Hover | Thenable: 当用户在编辑器内某个位置悬停时调用
    • model代表当前编辑器模型
    • position是光标悬停的位置。

    此方法应返回一个Hover对象或一个Promise,该对象包含要显示的悬停内容,比如一段描述性文本、markdown格式的内容或者可能包含标记和链接的富文本。

    window.hoverCode = monaco.languages.registerHoverProvider('myLanguage', {
        provideHover: function(model, position) {
          const line = position.lineNumber
          // 获取当前行的所有内容
          const content = model.getLineContent(line)
          // 当前光标所在位置的行的全部内容
          const codePre = model.getValueInRange({
            startLineNumber: position.lineNumber,
            startColumn: 1,
            endLineNumber: position.lineNumber,
            endColumn: position.column,
          });
          let contents = []
          let obj = {}
          let valueStr = ''
          let valueDesc = ''
          let objDesc ={}
          const handleSuggestions = (arr, type, detail) => {
            contents = []
            let list = [1,2,3,4,5,6,7,8,9]
            arr.forEach(async item => {
              if(list.includes(item.type) && content.substring(0,content.length -2) == item.name){
                //  取出参数
                if(item.param != null || item.param != undefined){
                  obj = {}
                  valueStr = ''
                  let pKeys = Object.keys(item.param).sort()
                  // console.log("是否相等---",pKeys);
                  pKeys.forEach((p,index) =>{
                    if(index == (pKeys.length -1)){
                      valueStr +=`参数${index +1}:***${item.param[p]}*** `
                    }else{
                      valueStr +=`参数${index +1}:***${item.param[p]}*** | `
                    }
                    obj = {value:valueStr}
                  })
                  contents.push(obj)   
                }
                // 取出参数描述
                if(item.paramDesc != null || item.paramDesc != undefined){
                  valueDesc = ''
                  objDesc ={}
                  let dKeys = Object.keys(item.paramDesc).sort()
                  dKeys.forEach((d,index) =>{
                    if(index == (dKeys.length -1)){
                      valueDesc +=`参数${index +1}:***${item.paramDesc[d]}*** `
                    }else{
                      valueDesc +=`参数${index +1}:***${item.paramDesc[d]}*** | `
                    }
                    objDesc = {value:valueDesc}
                  })
                  contents.push(objDesc)
                }
              }     
              });
          }   
          handleSuggestions(_this.allHintList, 'Field', '库表信息');
          return {
              range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column),
              contents:contents
          };
        }
      });

注册签名帮助(自定义方法的参数提示内容)

签名帮助提供器的api:registerSignatureHelpProvider(languageSelector, provider)的参数如下:

  • 参数一:编辑器的语言种类,标识字符串;例如:“myLanguage”,用于指定这个签名帮助提供器应该使用语言。

  • 参数二:provider:一个对象,定义了签名帮助的提供逻辑。这个对象需要实现以下方法:

    • provideSignatureHelp(model, position): 当用户可能需要函数或方法调用签名帮助时调用,model代表当前编辑器模型,position是光标所在的位置。返回一个SignatureHelpResult对象或Promise,该对象包含可能的签名信息,如函数名、参数列表、每个参数的描述等。
    window.signatureText =  monaco.languages.languages.registerSignatureHelpProvider("myLanguage", {
        // 重要说明 如果不使用特定字符来激活函数,
        // 可以使用editor.trigger('keyboard', 'editor.action.triggerParameterHints');来激活
        // keyboard表示触发来源为键盘事件,editor.action.triggerParameterHints是一个内置命令
        // 用于显示参数提示和签名帮助信息
        signatureHelpTriggerCharacters: ["(", ",","()",")"],
        //  当在编辑器中输入括号或者都好的时候触发签名提示内容
        provideSignatureHelp: (model, position, token) => {
          const line = position.lineNumber
          // 获取当前行的所有内容
          const content = model.getLineContent(line)
          // 找字符串中最后一个.
          //  console.log("参数提示conetnt---",content);
           let p =/\b(\w+)\s*\(/
          //  取表达中最后一个点和( 之间的内容,如果没有 .则只取 ( 前的内容
          let pointPos = content.lastIndexOf('.')
          let leftBacketPos = content.lastIndexOf('(')
          let rightBacketPos = content.lastIndexOf(')')
          let newLastContent = ''
          
          if(pointPos != -1 && pointPos < leftBacketPos){
            newLastContent = content.substring(pointPos+1,leftBacketPos)
          }else{
            newLastContent = content.substring(0,leftBacketPos)
          }
          // console.log("数据内容----",newLastContent,pointPos,leftBacketPos);
          let signatures = []
           // 取出参数提示
           let parameters = []
          let currentItem =  this.allHintList.find(item =>item.name === newLastContent)
          // console.log("currentItem",currentItem);
          if(currentItem && currentItem.param && currentItem.paramDesc){
            // 取出当前数据的参数
            let pkeys = Object.keys(currentItem.param).sort()
            let paramList = []
            pkeys.map((k,index) =>{
              // paramList.push(currentItem.param[k])
              // 后面拼凑index 是为了解决 参数相同的时候,直接在最后一个画线 
              // 因为 Monaco editor将会将这些参数视为同一个参数,并且只在签名帮助信息中显示一次
              paramList.push(`${currentItem.param[k]} $param${index+1}`)
            })
            let paramStr = paramList.join(',')
           
            let dKeys = Object.keys(currentItem.paramDesc).sort()
            parameters=pkeys.map((param, index) =>{
              return {
                label:`${currentItem.param[param]} $param${index+1}`,
                documentation: currentItem.paramDesc[dKeys[index]] ,
              }
            })
            signatures =[{
              label:`${currentItem.name}(${paramStr})`,
              parameters:parameters
            }]
          }
          // 动态设置参数位置
          // 确定参数的位置--- 比较参数括号和括号中的逗号 douhaoNum == parameterNum -1
          // 但数字函数中输入. 截取就会不准确-- 只截取( 之前的.  括号中的. 不截
          let paramContent = ''
          if(leftBacketPos != -1 && rightBacketPos != -1){
            paramContent = content.substring(leftBacketPos+1,rightBacketPos)
          }
          let activeParameter = 0
          let activeSignature = 0
          let count = paramContent.split(',').length 
          // console.log("参数:",count,paramContent,parameters.length);
          if(count > 0 && count <= parameters.length){
            activeParameter = count -1
            monaco.editor.getModelMarkers({owner: "owner"}).forEach(marker => {
              if (marker.severity === monaco.MarkerSeverity.Error || marker.severity === monaco.MarkerSeverity.Warning) {  
                 monaco.editor.setModelMarkers(model, "owner", [])  
               } 
             })   
          }else{
            // 当参数输入超过本来的参数 提示错误
             monaco.editor.setModelMarkers(editor.getModel(), 'owner', [{
                 startLineNumber: position.lineNumber,
                 startColumn: 1,
                 endLineNumber: position.lineNumber,
                 endColumn: position.column,
                 message:'参数数量过多', // 提示文案
                 severity: monaco.MarkerSeverity.Error, // 提示的类型
               },
             ])
          }
          return {
            dispose: () => {},
            value: {
              activeParameter: activeParameter,
              activeSignature: 0,
              signatures:signatures,
            },
          };
        },  
      });

注册快捷指令

在编辑器中也内置了我们快捷指令,像我们常用的ctrl+c 和ctrl+v ,在编辑器中使用apiaddAction(descriptor)可以添加自定义的操作,比如:快捷键绑定或者菜单项,其参数如下:

  • action (IActionDescriptor): 一个对象,描述了要添加的动作。这个对象可以包含以下属性:

    • id (string): 动作的唯一标识符,用于识别和引用该动作。
    • label (string): 动作的标签,通常会在菜单或提示中显示。
    • alias (string | string[]): 可选,动作的别名,用于命令面板搜索。
    • iconClass (string): 可选,图标类名,用于装饰动作项。
    • run: (editor: ICodeEditor, args?: any) => any): 必需,当动作被触发时执行的函数。editor参数是当前编辑器实例,args是可选的额外参数,这取决于触发情境。
    • contextMenuId: (string | string[]): 可选,定义动作在哪些菜单中出现的ID列表。例如:2_customCommand:自定义指令,1_modification:修改组,9_cutcopypaste:剪切复制粘贴组
    • keybindings: (Keybinding[] | { primary: Keybinding[], secondary?: Keybinding[] }): 可选,定义触发动作的快捷键绑定。Keybinding是一个包含修饰键(如Ctrl、Alt、Shift)和KeyCode(按键码)的对象。
    • precondition: (ContextKeyExpr): 可选,一个条件表达式,决定动作是否可用。
    • kinds: (string[]): 可选,动作的种类,如"导航"、"修改"等。
    //  注册快捷命令
    handleRegistureCommand(){ 
      // 在某个地方触发执行命令
      // this.editor.getAction(['editor.action.formateCodeForce' ])._run()
      editor.addAction({
        id: 'formateCodeForce',
        label: '强制格式化',
        keybindings: [monaco.KeyMod.CtrlCmd|monaco.KeyMod.Alt|monaco.KeyCode.KeyF],
        contextMenuGroupId: '9_cutcopypaste', // 2_customCommand  自定义指令
        run(ed, opt) {
          let a = editor.getValue()
          let b = beautify_js(a)
          // editor.setValue(b)
          editor.executeEdits("", [
            {
              range: new monaco.Range(1, 1, editor.getModel().getLineCount() + 1, 1),
              text: b
            }
          ])
        }
      })
    },
    
    // 一键清除全部的代码
    handleDeleteAll(){
      editor.addAction({
        id: 'deleteAllText',
        label: '一键清空',
        keybindings: [monaco.KeyMod.Alt|monaco.KeyCode.Delete],
        contextMenuGroupId: '9_cutcopypaste', //navigation:该组是右键的第一栏,1_modification:修改组,9_cutcopypaste:剪切复制粘贴组
        run(ed, opt) {
          editor.executeEdits("", [
            {
              range: new monaco.Range(1, 1, editor.getModel().getLineCount() + 1, 1),
              text: "",
            }
          ])
        }
      })
    },

结尾

以此来自己记录这个知识点,也希望可以帮助更多的人。。。

如果对您有用,希望您留下评论/👍/收藏。