前言
最近新接手的后台管理系统需要一个在线编辑代码的工具,主要是SQL
和Python
。经过查找,发现了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
,这算是一个额外的用户体验优化。
效果如下:
花了一天,大概的需求已经实现,后期继续优化,基本都是设计上的问题了。
完整的代码如下:CodeEditor.vue
SQLEditor.vue
SQL Editor
后记
前面的代码是没问题的,但是有一个小小的不足:自定义语言,没有变色。虽然可以代码提示,但不是关键字。
经过查找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);