概述
Monaco Editor 是一款由微软开发的高性能、功能丰富的基于浏览器的代码编辑器,它是Visual Studio Code源生编辑器的核心组件,专为Web环境设计。Monaco Editor以其强大的代码编辑功能著称,包括语法高亮、代码自动完成(智能提示)、代码片段、代码折叠、错误标记、以及多种编程语言的支持等,为用户提供接近桌面级IDE的在线编码体验。
实现
技术栈为vue3 vite typescript monaco-editor
安装依赖
# 安装 monaco-editor
pnpm install monaco-editor
版本号
"monaco-editor": "^0.48.0",
创建组件
-
增加锚点
<div ref="codeEditBox" class="codeEditBox"></div> -
导入worker和monaco-editor
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"; import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"; import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; import EditorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; import * as monaco from "monaco-editor"; -
创建 monaco-editor 环境
self.MonacoEnvironment = { getWorker(_: string, label: string) { if (label === "json") { return new jsonWorker(); } if (["css", "scss", "less"].includes(label)) { return new cssWorker(); } if (["html", "handlebars", "razor"].includes(label)) { return new htmlWorker(); } if (["typescript", "javascript"].includes(label)) { return new tsWorker(); } return new EditorWorker(); }, }; -
创建editor
editor = monaco.editor.create(codeEditBox.value, { value: monacoEditorValue.value, language: props.language, theme: props.editorProps.theme, ...props.editorProps.options, }); -
内容改变抛出事件
editor.onDidChangeModelContent(() => { const value = editor.getValue(); emit("update:modelValue", value); emit("change", value); }); -
添加事件,主要是为了格式化代码
editor.addAction({ id: "formatDocument", label: "Format Document", keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F9], run: (ed: monaco.editor.ICodeEditor) => { ed.getAction("editor.action.formatDocument")?.run(); }, }); -
配置智能化提示
provider = monaco.languages.registerCompletionItemProvider(props.language, { provideCompletionItems: function (model, position) { // 获取范围 const range = getRange(model, position); const suggestions = props.suggestions.map((s: ISuggestions) => ({ // 显示的提示内容 label: s.label, // 用来显示提示内容后的不同的图标 kind: monaco.languages.CompletionItemKind[s.kind], // 选择后粘贴到编辑器中的文字 insertText: s.insertText, // 提示内容后的说明 detail: s.detail, range: range, })); if (model.uri.toString() === editor.getModel()!.uri.toString()) { return { suggestions, }; } }, });
以上是关键代码
详细代码
- 组件文件
<template> <div ref="codeEditBox" class="codeEditBox"></div> </template> <script setup lang="ts"> import { defaultEditorProps, type IEditorProps, type ISuggestions, } from "./monaco-editor-type"; import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"; import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"; import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; import EditorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; import * as monaco from "monaco-editor"; import { useVModel, watchImmediate, watchDeep } from "@vueuse/core"; import { onBeforeUnmount, onMounted, ref } from "vue"; const props = withDefaults(defineProps<IEditorProps>(), { editorProps: defaultEditorProps, registerLanguage: null, modelValue: "", width: "100%", height: "100%", language: "javascript", suggestions: () => [], }); const emit = defineEmits(["update:modelValue", "change", "editor-mounted"]); const monacoEditorValue = useVModel(props, "modelValue", emit); self.MonacoEnvironment = { getWorker(_: string, label: string) { if (label === "json") { return new jsonWorker(); } if (["css", "scss", "less"].includes(label)) { return new cssWorker(); } if (["html", "handlebars", "razor"].includes(label)) { return new htmlWorker(); } if (["typescript", "javascript"].includes(label)) { return new tsWorker(); } return new EditorWorker(); }, }; let editor: monaco.editor.IStandaloneCodeEditor; const codeEditBox = ref(); let provider:any = null const init = () => { registerLanguage() editor = monaco.editor.create(codeEditBox.value, { value: monacoEditorValue.value, language: props.language, theme: props.editorProps.theme, ...props.editorProps.options, }); editor.onDidChangeModelContent(() => { const value = editor.getValue(); emit("update:modelValue", value); emit("change", value); }); editor.addAction({ id: "formatDocument", label: "Format Document", keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.F9], run: (ed: monaco.editor.ICodeEditor) => { ed.getAction("editor.action.formatDocument")?.run(); }, }); provider = monaco.languages.registerCompletionItemProvider(props.language, { provideCompletionItems: function (model, position) { // 获取范围 const range = getRange(model, position); const suggestions = props.suggestions.map((s: ISuggestions) => ({ // 显示的提示内容 label: s.label, // 用来显示提示内容后的不同的图标 kind: monaco.languages.CompletionItemKind[s.kind], // 选择后粘贴到编辑器中的文字 insertText: s.insertText, // 提示内容后的说明 detail: s.detail, range: range, })); if (model.uri.toString() === editor.getModel()!.uri.toString()) { return { suggestions, }; } }, }); emit("editor-mounted", editor); }; const getRange = ( model: monaco.editor.ITextModel, position: monaco.Position ) => { // 获取当前行数 当前列数 const [line, column] = [position.lineNumber, position.column]; // 获取当前输入行的所有内容 const content = model.getLineContent(line); // 通过下标来获取当前光标后一个内容,即为刚输入的内容 const sym = content[column - 2]; const word = model.getWordUntilPosition(position); return { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: word.startColumn, endColumn: word.endColumn, }; }; const registerLanguage = () => { if(props.registerLanguage) { monaco.languages.register({ id: props.language }) monaco.languages.setMonarchTokensProvider(props.language, props.registerLanguage.tokens) } } watchImmediate(monacoEditorValue, (newValue) => { if (editor) { const value = editor.getValue(); if (newValue !== value) { editor.setValue(newValue); } } }); const reload = () => { editor.dispose(); provider.dispose() init(); }; watchDeep( () => props.language, (newValue) => { monaco.editor.setModelLanguage(editor.getModel()!, newValue); reload(); } ); onBeforeUnmount(() => { editor.dispose(); }); onMounted(() => { init(); }); </script> <style lang="scss" scoped> .codeEditBox { height: 100%; } </style> - 类型文件
import { type PropType } from "vue"; export type Theme = "vs" | "hc-black" | "vs-dark"; export type FoldingStrategy = "auto" | "indentation"; export type RenderLineHighlight = "all" | "line" | "none" | "gutter"; export interface IEditorProps { editorProps?: any; registerLanguage?: any; modelValue: string; width?: string | number; height?: string | number; language: string; suggestions?: ISuggestions[]; } export interface ISuggestions { label: string; kind: string; insertText: string; detail: string; range?: {}; } export interface Options { automaticLayout?: boolean; foldingStrategy?: FoldingStrategy; renderLineHighlight?: RenderLineHighlight; selectOnLineNumbers?: boolean; minimap?: { enabled: boolean; }; readOnly?: boolean; fontSize?: number; scrollBeyondLastLine?: boolean; overviewRulerBorder?: boolean; } export interface IEditor { theme: Theme; options: Options; } export const defaultEditorProps: IEditor = { theme: 'vs-dark', options: { automaticLayout: true, foldingStrategy: "indentation", renderLineHighlight: "all", selectOnLineNumbers: true, minimap: { enabled: true, }, readOnly: false, fontSize: 16, scrollBeyondLastLine: false, overviewRulerBorder: false, } }; - 使用
vue文件
<script lang="ts" setup>
import MonacoEditor from "./components/monaco-editor/index.vue";
import { type ISuggestions } from "./components/monaco-editor/monaco-editor-type";
import { onMounted, ref } from "vue";
import {
showJsonValue,
showJavaScriptValue,
javaScriptSuggestions,
jsonSuggestions,
showCssValue,
cssSuggestions,
showGroovyValue,
groovySuggestions,
editorProps,
registerLanguage,
options
} from "./app-type.ts";
const language = ref("javascript");
const showValue = ref(showJsonValue);
const change = () => {
switch (language.value) {
case "javascript":
showValue.value = showJavaScriptValue;
suggestions.value = javaScriptSuggestions;
break;
case "json":
showValue.value = showJsonValue;
suggestions.value = jsonSuggestions;
break;
case "css":
showValue.value = showCssValue;
suggestions.value = cssSuggestions;
break;
case "groovy":
showValue.value = showGroovyValue;
suggestions.value = groovySuggestions;
break;
default:
break;
}
};
const suggestions = ref<ISuggestions[] | undefined>([]);
onMounted(() => {
change();
});
</script>
<template>
<div class="container">
<monaco-editor
v-model="showValue"
:language="language"
:editor-props=editorProps
:suggestions="suggestions"
:register-language="registerLanguage"
></monaco-editor>
<el-select
style="margin-top: 16px"
v-model="language"
class="m-2"
placeholder="Select"
size="large"
@change="change"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</template>
<style lang="scss" scoped>
.container {
height: 500px;
}
</style>
涉及的变量
import type { IEditor } from "./components/monaco-editor/monaco-editor-type";
export const showJsonValue = `{
"star_male": [
{
"name": "鹿晗",
"age": 26
},
{
"name": "李易峰",
"age": 29
},
{
"name": "陈赫",
"age": 31
}
]
}`;
export const showJavaScriptValue = `function test() {
console.log('hello world')
}`;
export const showCssValue = `body {
background-color: #f0f0f0;
}`;
export const showGroovyValue = `pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'echo "Hello World"'
}
}
}
}`;
export const options = [
{
value: "javascript",
label: "JavaScript",
},
{
value: "json",
label: "JSON",
},
{
value: "css",
label: "CSS",
},
{
value: "groovy",
label: "Groovy",
},
];
export const jsonSuggestions = [
{
label: "images:17",
kind: "Value",
insertText: "image: nginx: 1.17.3",
detail: "提供的默认镜像 1.17.3",
},
{
label: "images:18",
kind: "Value",
insertText: "image: nginx: 1.18.3",
detail: "提供的默认镜像 1.18.3",
},
{
label: "images:19",
kind: "Value",
insertText: "image: nginx: 1.19.3",
detail: "提供的默认镜像 1.19.3",
},
];
export const javaScriptSuggestions = [
{ label: "vue", kind: "Value", insertText: "vue: 2", detail: "vue 版本号 2" },
{
label: "html",
kind: "Value",
insertText: "html: 5",
detail: "html 版本号 5",
},
{ label: "jsp", kind: "Value", insertText: "jsp: 2", detail: "jsp 版本号 2" },
];
export const cssSuggestions = [
{
label: "css1",
kind: "Value",
insertText: "css: 1",
detail: "css 版本号 1",
},
{
label: "css2",
kind: "Value",
insertText: "css: 2",
detail: "css 版本号 2",
},
{
label: "css3",
kind: "Value",
insertText: "css: 3",
detail: "css 版本号 3",
},
];
export const groovySuggestions = [
{
label: "pipeline",
kind: "Keyword",
insertText: "pipeline",
detail: "关键字 pipeline",
},
{
label: "agent",
kind: "Keyword",
insertText: "agent",
detail: "关键字 agent",
},
{
label: "node",
kind: "Keyword",
insertText: "node",
detail: "关键字 node",
},
{
label: "label",
kind: "Keyword",
insertText: "label",
detail: "关键字 label",
},
{
label: "stages",
kind: "Keyword",
insertText: "stages",
detail: "关键字 stages",
},
{
label: "container",
kind: "Keyword",
insertText: `container {
}`,
detail: "关键字 container",
},
];
export const registerLanguage = {
language: "groovy",
tokens: {
tokenizer: {
root: [
[
/\b(?:pipeline|agent|node|label|stages|stage|steps|git|url|credentialsId|branch|changelog|poll|container|withSonarQubeEnv|sh|timeout|time|unit|waitForQualityGate)\b/,
"keyword",
],
[
/\b(?:if|else|for|while|switch|case|break|continue|return|try|catch|finally|throw|throws|private|protected|public|static|class|interface|extends|implements|import|package|void|new|instanceof|this|super|true|false|null)\b/,
"keyword",
],
[
/\b(?:def|boolean|byte|char|short|int|long|float|double|void|Boolean|Byte|Character|Short|Integer|Long|Float|Double|String|Object|void)\b/,
"keyword",
],
[/\b(?:true|false|null)\b/, "keyword"],
[/\b(?:String|List|Map|Set|ArrayList|HashMap|HashSet)\b/, "keyword"],
[
/\b(?:println|print|printf|sprintf|format|assert|delete|remove|add|contains|size|length|charAt|indexOf|lastIndexOf|substring|split|join|replace|replaceAll|replaceFirst|matches|contains|startsWith|endsWith|toLowerCase|toUpperCase|trim|valueOf|parseInt|parseLong|parseDouble|parseBoolean|toString|getClass|wait|notify|notifyAll|clone|equals|hashCode|finalize|getClass|notify|toString|wait)\b/,
"keyword",
],
[/\b(?:null|true|false)\b/, "keyword"],
[/\b(?:it|args|this|super)\b/, "keyword"],
[/\b(?:import|package)\b/, "keyword"],
[
/\b(?:class|interface|enum|trait|extends|implements|static|public|protected|private|abstract|final|native|synchronized|transient|volatile|strictfp)\b/,
"keyword",
],
],
},
},
};
export const editorProps: IEditor = {
theme: "vs-dark",
options: {
minimap: {
enabled: false,
},
},
};
结语
Monaco Editor是一个强大且灵活的解决方案,适用于那些需要在Web应用中提供代码编辑能力的场景,比如在线代码编辑器、教育平台、云IDE等。