monaco-editor初体验

前言

最近新接手的后台管理系统需要一个在线编辑代码的工具,主要是SQLPython。经过查找,发现了monaco-editor这么一个开源工具,可以说是VS Code的web版本。

使用

因为中文文档没找到齐全的资料,只能从搜索引擎提供的博客中查找。当然,少不了GitHub——monaco-editor。官方已经提供了齐全的demo。
第一步是npm安装,然后引入页面。

$ npm install monaco-editor
import * as monaco from "monaco-editor"

第二步是提供一个DOM元素,然后实例化monaco-editor

this.monacoInstance = monaco.editor.create( this.$refs["container"], { value: this.value, ...this.options });

到这一步,核心代码已经完成,简单。

功能追加

然后,我们的编辑器需要几个功能:

  • 自定义语言
  • 自定义样式
  • 自定义关键字提示

还有其他辅助功能:

  • 同步取值
  • 离开销毁

API文档已经提供了接口:

  • language: String
  • theme: String
  • value: String

同步取值:

this.monacoInstance.onDidChangeModelContent(
() => {
        this.$emit("contentChange",
        this.monacoInstance.getValue());
      });

离开销毁:

if (this.monacoInstance) {
    if (this.monacoInstance.getModel()) {
        this.monacoInstance.getModel().dispose();
    }        
    this.monacoInstance.dispose();
    this.monacoInstance = null;
    if(this.provider){
      this.provider.dispose();
      this.provider = null
    }
}

唯一比较麻烦的是自定义关键字提示:

this.provider = monaco.languages.registerCompletionItemProvider("sql", {
    provideCompletionItems(model, position) {
      var textUntilPosition = model.getValueInRange({
        startLineNumber: position.lineNumber,
        startColumn: 1,
        endLineNumber: position.lineNumber,
        endColumn: position.column
      });
      var suggestions = createCompleters(textUntilPosition);
      return {
        suggestions: suggestions
      };
    }
});

这是对sql语言的关键字提示注册,provideCompletionItems方法的返回值就是提示的内容,如果没有返回值,默认在编辑器的内容区域内匹配。如果有返回值,提示返回值。也就是说,做不到即使用自定义关键字,又能自动匹配文本输入区域的内容
关键字提示的核心代码在于createCompleters方法:

let createCompleters = textUntilPosition => {
        //过滤特殊字符
        let _textUntilPosition = textUntilPosition
          .replace(/[\*\[\]@\$\(\)]/g, "")
          .replace(/(\s+|\.)/g, " ");
        //切割成数组
        let arr = _textUntilPosition.split(" ");
        //取当前输入值
        let activeStr = arr[arr.length - 1];
        //获得输入值的长度
        let len = activeStr.length;

        //获得编辑区域内已经存在的内容
        let rexp = new RegExp('([^\\w]|^)'+activeStr+'\\w*', "gim");
        let match = that.value.match(rexp);
        let _hints = !match ? [] : match.map(ele => {
          let rexp = new RegExp(activeStr, "gim");
          let search = ele.search(rexp);
          return ele.substr(search)
        })
        
        //查找匹配当前输入值的元素
        let hints = Array.from(new Set([...that.hints, ..._hints])).sort().filter(ele => {
          let rexp = new RegExp(ele.substr(0, len), "gim");
          return match && match.length === 1 && ele === activeStr || ele.length === 1
            ? false
            : activeStr.match(rexp);
        });
        //添加内容提示
        let res = hints.map(ele => {
          return {
            label: ele,
            kind: that.hints.indexOf(ele) > -1 ? monaco.languages.CompletionItemKind.Keyword : monaco.languages.CompletionItemKind.Text,
            documentation: ele,
            insertText: ele
          };
        });
        return res;
      };

目前写的方法,能够做到两个功能:

  • 取到当前输入的单词,匹配自定义的关键字hints
  • 取到当前输入的单词,匹配编辑区域内容_hints

通过正则表达式的匹配,同时过滤必要的元字符,查找对应的值,主要是因为用户可能会输入$^*[]()等字符。
不同的匹配来源,使用不同的icon,即monaco.languages.CompletionItemKind,这算是一个额外的用户体验优化。
效果如下:
monaco-editor初体验_第1张图片
花了一天,大概的需求已经实现,后期继续优化,基本都是设计上的问题了。
完整的代码如下:
CodeEditor.vue





SQLEditor.vue







后记

前面的代码是没问题的,但是有一个小小的不足:自定义语言,没有变色。虽然可以代码提示,但不是关键字。
经过查找api,现整理如下:

import * as monaco from "monaco-editor";

let sqlStr = "ADD,EXCEPT,PERCENT,ALL,EXEC,PLAN,ALTER,EXECUTE,PRECISION,AND,EXISTS,PRIMARY,ANY,EXIT,PRINT,AS,FETCH,PROC,ASC,FILE,PROCEDURE,AUTHORIZATION,FILLFACTOR,PUBLIC,BACKUP,FOR,RAISERROR,BEGIN,FOREIGN,READ,BETWEEN,FREETEXT,READTEXT,BREAK,FREETEXTTABLE,RECONFIGURE,BROWSE,FROM,REFERENCES,BULK,FULL,REPLICATION,BY,FUNCTION,RESTORE,CASCADE,GOTO,RESTRICT,CASE,GRANT,RETURN,CHECK,GROUP,REVOKE,CHECKPOINT,HAVING,RIGHT,CLOSE,HOLDLOCK,ROLLBACK,CLUSTERED,IDENTITY,ROWCOUNT,COALESCE,IDENTITY_INSERT,ROWGUIDCOL,COLLATE,IDENTITYCOL,RULE,COLUMN,IF,SAVE,COMMIT,IN,SCHEMA,COMPUTE,INDEX,SELECT,CONSTRAINT,INNER,SESSION_USER,CONTAINS,INSERT,SET,CONTAINSTABLE,INTERSECT,SETUSER,CONTINUE,INTO,SHUTDOWN,CONVERT,IS,SOME,CREATE,JOIN,STATISTICS,CROSS,KEY,SYSTEM_USER,CURRENT,KILL,TABLE,CURRENT_DATE,LEFT,TEXTSIZE,CURRENT_TIME,LIKE,THEN,CURRENT_TIMESTAMP,LINENO,TO,CURRENT_USER,LOAD,TOP,CURSOR,NATIONAL,TRAN,DATABASE,NOCHECK,TRANSACTION,DBCC,NONCLUSTERED,TRIGGER,DEALLOCATE,NOT,TRUNCATE,DECLARE,NULL,TSEQUAL,DEFAULT,NULLIF,UNION,DELETE,OF,UNIQUE,DENY,OFF,UPDATE,DESC,OFFSETS,UPDATETEXT,DISK,ON,USE,DISTINCT,OPEN,USER,DISTRIBUTED,OPENDATASOURCE,VALUES,DOUBLE,OPENQUERY,VARYING,DROP,OPENROWSET,VIEW,DUMMY,OPENXML,WAITFOR,DUMP,OPTION,WHEN,ELSE,OR,WHERE,END,ORDER,WHILE,ERRLVL,OUTER,WITH,ESCAPE,OVER,WRITETEXT,SELECT,INSERT,DELETE,UPDATE,CREATE TABLE,DROP TABLE,ALTER TABLE,CREATE VIEW,DROP VIEW,CREATE INDEX,DROP INDEX,CREATE PROCEDURE,DROP PROCEDURE,CREATE TRIGGER,DROP TRIGGER,CREATE SCHEMA,DROP SCHEMA,CREATE DOMAIN,ALTER DOMAIN,DROP DOMAIN,GRANT,DENY,REVOKE,COMMIT,ROLLBACK,SET TRANSACTION,DECLARE,EXPLAN,OPEN,FETCH,CLOSE,PREPARE,EXECUTE,DESCRIBE,FORM,ORDER BY";
let pythonStr = "False,None,self,True,and,as,assert,break,class,continue,def,del,elif,else,except,finally,for,from,global,if,import,in,is,lambda,nonlocal,not,or,pass,raise,return,try,while,with,yield";
//let ScalaStr = "import,val,def,spark,sqlContext,show";
let ScalaStr = ''
const LoadToken = (Hints, languages) => {
  monaco.languages.setMonarchTokensProvider(languages, {
    keywords: Hints,
    tokenizer: {
      root: [
        // identifiers and keywords
        [/[a-z_$][\w$]*/, {
          cases: {
            '@keywords': 'keyword',
            '@default': 'identifier'
          }
        }],
      ]

    }
  })
}

const init = () => {
  let hintsSql = []
  hintsSql = sqlStr.split(",");
  hintsSql = Array.from(new Set([...hintsSql])).sort();

  let hintsPython = []
  hintsPython = pythonStr.split(",");
  hintsPython = Array.from(new Set([...hintsPython])).sort();

  let hintsScala = []
  hintsScala = ScalaStr.split(",");
  hintsScala = Array.from(new Set([...hintsScala])).sort();

  initLanguage(hintsSql, 'sql')
  initLanguage(hintsPython, 'python')
  //initLanguage(hintsScala, 'scala')

  monaco.editor.defineTheme('my-balck', {
    base: 'vs-dark',
    inherit: true,
    rules: [
      {token: 'source.myLang', foreground: '001529'},
      {background: '001529', fontSize: "14px"}
    ],
    colors: {
      'editor.background': '#001529',
      'editor.lineHighlightBorder': '#001529'
    },
  });

  monaco.editor.defineTheme('my-white', {
    base: 'vs',
    inherit: true,
    rules: [
      {token: 'source.myLang', foreground: 'ffffff'},
      {background: 'ffffff', fontSize: "14px"}
    ],
    colors: {
      'editor.background': '#ffffff',
      'editor.lineHighlightBorder': '#ffffff'
    },
  });
}

export const initLanguage = (Hints, languages) => {
  let createCompleters = (textUntilPosition, value) => {
    //过滤特殊字符
    let _textUntilPosition = textUntilPosition
      .replace(/[\*\[\]@\$\(\)]/g, "")
      .replace(/[\s+\.,]/g, " ");
    //切割成数组
    let arr = _textUntilPosition.split(" ");
    //取当前输入值
    let activeStr = arr[arr.length - 1];
    //获得输入值的长度
    let len = activeStr.length;

    //获得编辑区域内已经存在的内容
    let rexp = new RegExp("([^\\w]|^)" + activeStr + "\\w*", "gim");
    let match = value.match(rexp);
    let _hints = !match
      ? []
      : match.map(ele => {
        let rexp = new RegExp(activeStr, "gim");
        let search = ele.search(rexp);
        return ele.substr(search);
      });

    //查找匹配当前输入值的元素
    let hints = Array.from(new Set([...Hints, ..._hints]))
      .sort()
      .filter(ele => {
        let rexp = new RegExp(ele.substr(0, len), "gim");
        return (match && match.length === 1 && ele === activeStr) ||
        ele.length === 1
          ? false
          : activeStr.match(rexp);
      });

    //添加内容提示
    let res = hints.map(ele => {
      return {
        label: ele,
        kind:
          hints.indexOf(ele) > -1
            ? monaco.languages.CompletionItemKind.Keyword
            : monaco.languages.CompletionItemKind.Text,
        documentation: ele,
        insertText: ele
      };
    });
    return res;
  };
  monaco.languages.register({id: languages});
  LoadToken(Hints, languages);
  monaco.languages.registerCompletionItemProvider(languages, {
    provideCompletionItems(model, position) {
      var textUntilPosition = model.getValueInRange({
        startLineNumber: position.lineNumber,
        startColumn: 1,
        endLineNumber: position.lineNumber,
        endColumn: position.column,
      });
      var value = model.getValue();
      var suggestions = createCompleters(textUntilPosition, value);
      return {
        suggestions: suggestions
      };
    }
  });
}
init()


export default monaco

关键代码在于:
monaco.languages.register({id: languages});
LoadToken(Hints, languages);

完~

你可能感兴趣的:(monaco-editor初体验)