Vue 中使用 Monaco editor

Monaco Editor 是一款开源的在线代码编辑器,是 VSCode 的浏览器版本,随着近年 VSCode 大热,Monaco Editor 也随之走红。本文以 SQL 为例,展示 Monaco Editor 在 Vue 项目中的使用,并分享实际使用过程中的踩坑经历。

Vue 项目中引入 Monaco Editor

  1. 安装 Monaco Editor
1
npm install monaco-editor -D
  1. 安装 Webpack 打包 Monaco Editor 的插件
1
npm install monaco-editor-webpack-plugin -D
  1. 安装 Monaco Editor 汉化包
1
npm install monaco-editor-locales-plugin -D
  1. 配置 vue.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const MonacoEditorWebpackPlugin = require('monaco-editor-webpack-plugin');
const MonacoEditorLocalesPlugin = require('monaco-editor-locales-plugin');

module.exports = {
// ...
configurWebpack: config => {
config.plugins.push(new MonacoEditorWebpackPlugin({
languages: ['sql']
}));
config.plugins.push(new MonacoEditorLocalesPlugin({
languages: ['es', 'zh-cn'],
defaultLanguages: 'zh-cn',
logUnmatched: false,
mapLanguages: {}
}));
}
}

使用 Monaco Editor

1
<div class="u-sql-editor"></div>
1
2
3
4
5
6
7
8
9
10
import * as monaco from 'monaco-editor';

this.editor = monaco.editor.create(this.$el, {
theme: 'vs',
language: 'sql',
value: ''
});

// 销毁
this.editor.dispose();

常见配置项

这里贴一份简单的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
theme: 'vs',
language: 'sql',
readOnly: false,
lineHeight: 24,
fontSize: 12,
fontFamily: 'Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace',
lineNumbersMinChars: 3,
wordWrap: 'on',
renderLineHighlight: 'all',
minimap: {
enabled: false // 编辑器右侧的缩略图
},
contextmenu: false,
automaticLayout: true,
scrollBeyondLastLine: false,
folding: true,
rulers: [100] // 代码分割线
}

常用功能

自定义提示

自定义提示主要通过 registerCompletionItemProvider 来实现

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
// SQL 关键字提示
import { language as sqlLanguage } from '@lib/sql';

monaco.languages.registerCompletionItemProvider('sql', {
provideCompletionItems: (model, position, context, token) => {
const textUntilPosition = model.getValueInRange({
startLineNumber: position.lineNumber,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column
});
const match = textUntilPosition.match(/(\S+)$/);
const suggestions: monaco.languages.CompletionItem[] = [];
if (match) {
const matchStr = match[0].toUpperCase();
sqlLanguage.keywords.forEach((item: string) => {
if (item.startsWith(matchStr)) {
suggestions.push({
label: item,
kind: monaco.languages.CompletionItemKind.Keyword,
insertText: item
} as monaco.languages.CompletionItem);
}
});
sqlLanguage.operators.forEach((item: string) => {
if (item.startsWith(matchStr)) {
suggestions.push({
label: item,
kind: monaco.languages.CompletionItemKind.Operator,
insertText: item
} as monaco.languages.CompletionItem);
}
});
sqlLanguage.builtinFunctions.forEach((item: string) => {
if (item.startsWith(matchStr)) {
suggestions.push({
label: item,
kind: monaco.languages.CompletionItemKind.Function,
insertText: item
} as monaco.languages.CompletionItem);
}
});
}
return {
suggestions: Array.from(new Set(suggestions))
};
}
});

sqlLanguage 内容详见:sql.ts

tips: 这里的 suggestions 针对该 language 是全局唯一的。

格式化 SQL

1
2
3
4
5
6
7
8
9
10
11
12
import sqlFormatter from 'sql-formatter';

// 格式化 SQL
monaco.languages.registerDocumentFormattingEditProvider('sql', {
provideDocumentFormattingEdits(model) {
const formatted = sqlFormatter.format(model.getValue());
return [{
range: model.getFullModelRange(),
text: formatted
}];
}
});

自定义快捷键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
addCommand() {
if (this.editor) {
const saveBinding = this.editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S, () => {
this.$emit('save');
});
this.commandMap['save'] = saveBinding; // Ctrl + s 保存

const formatBinding = this.editor.addCommand(monaco.KeyMod.Alt | monaco.KeyMod.Shift | monaco.KeyCode.KEY_F, () => {
this.formatSql();
});
this.commandMap['format'] = formatBinding; // Alt + shift + f 格式化
}
}

// 手动调用
executeCommand(command: string) {
const cmd = this.commandMap[command];
cmd && this.editor && this.editor._commandService.executeCommand(cmd);
}

监听值变化 onDidChangeModelContent

1
2
3
4
5
this.editor.onDidChangeModelContent(e => {
const val = this.editor.getValue();
this.$emit('update:value', val);
this.$emit('change', val);
});

自定义主题配色

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
monaco.editor.defineTheme('customSql', {
base: 'vs',
inherit: !1,
colors: {
'editorHoverWidget.background': '#FAFAFA',
'editorHoverWidget.border': '#DEDEDE',
'editor.lineHighlightBackground': '#EFF8FF',
'editor.selectionBackground': '#D5D5EF',
'editorLineNumber.foreground': '#999999',
'editorSuggestWidget.background': '#FFFFFF',
'editorSuggestWidget.selectedBackground': '#EFF8FF'
},
rules: [{
token: 'comments',
foreground: '8E908C'
}, {
token: 'keyword',
foreground: '8959A8'
}, {
token: 'predefined',
foreground: '11B7BE'
}, {
token: 'doubleString',
foreground: 'AB1010'
}, {
token: 'singleString',
foreground: 'AB1010'
}, {
token: 'number',
foreground: 'AB1010'
}, {
token: 'string.sql',
foreground: '718C00'
}]
});

tips: 配色的 color 注释参照官方示例 customizing-the-appearence-exposed-colors.html

自定义语言

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
const registerLogLanguage = () => {
monaco.languages.register({ id: 'logLanguage' });

monaco.languages.setMonarchTokensProvider('logLanguage', {
tokenizer: {
root: [
[/INFO.*/, 'custom-info'],
[/ERROR.*/, 'custom-error'],
[/WARN.*/, 'custom-warn'],
[/DEBUG.*/, 'custom-debug'],
[/\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2},\d{3}/, 'custom-date']
]
}
});

const themeData: monaco.editor.IStandaloneThemeData = {
base: 'vs',
inherit: false,
colors: {
// Set here the colors you want... except for the minimap
'editor.background': '#f6f7f8'
},
rules: [
{ token: 'custom-info', foreground: '808080' },
{ token: 'custom-error', foreground: 'ff0000', fontStyle: 'bold' },
{ token: 'custom-warn', foreground: 'ffa500' },
{ token: 'custom-debug', foreground: 'ffa500' },
{ token: 'custom-date', foreground: '008800' },
{ token: '', background: '#f6f7f8' }
]
};
monaco.editor.defineTheme('logTheme', themeData);
};

tips: minimap 的背景色设置:{ token: '', background: '#f6f7f8' }

总结

以上是自己在使用 Monaco Editor 时遇到的一些问题,大家如果有其他问题欢迎留言讨论,我也会持续更新使用过程中踩的一些坑~

最后的最后,大家在遇到问题的时候,可以多看看文档,很多问题其实在文档中都能找到对应的解决方案。

查看评论