目录
- 前言
- 一、快速上手笔记
- [二、常用配置项(最常用 20 个)](#二、常用配置项(最常用 20 个))
- [三、常用 API](#三、常用 API)
-
- [1、更新 options(不会渲染抖动)](#1、更新 options(不会渲染抖动))
- 2、设置主题
- 3、内容与编辑功能
-
- (1)、获取/设置内容
- (2)、监听内容变化
- (3)、插入文本
- (4)、操作选区
- (5)、滚动到某行
- (6)、格式化代码
- [(7)、撤销 / 重做](#(7)、撤销 / 重做)
- 4、快捷键
- [5、搜索 & 查找](#5、搜索 & 查找)
- 6、自定义语言
-
- (1)、注册语言
- [(2)、定义词法高亮(Tokens Provider)](#(2)、定义词法高亮(Tokens Provider))
- (3)、提示(Completion)
- [(4)、语法诊断(Error Markers)](#(4)、语法诊断(Error Markers))
- [(5)、悬浮提示(Hover Provider)](#(5)、悬浮提示(Hover Provider))
- [(6)、定义跳转(Definition Provider)](#(6)、定义跳转(Definition Provider))
- 7、视图行为
-
- (1)、自动换行
- [(2)、启动 minimap(代码缩略图)](#(2)、启动 minimap(代码缩略图))
- (3)、显示行号/隐藏行号
- (4)、光标样式
- [8、模型 (Model) 操作](#8、模型 (Model) 操作)
- 四、编辑器事件
- 五、扩展功能(高级)
-
- [1、Diff Editor](#1、Diff Editor)
- 2、代码折叠
- 3、Outline(文档结构)
- [六、Bundler 中的 Monaco 优化](#六、Bundler 中的 Monaco 优化)
-
- [1、Vite 中使用(最推荐)](#1、Vite 中使用(最推荐))
- 2、Webpack(老项目)
- [七、真实项目中的 12 个常见坑](#七、真实项目中的 12 个常见坑)
- [八、项目中使用 Monaco Editor](#八、项目中使用 Monaco Editor)
- [九、深度二次开发 Monaco Editor(进阶)](#九、深度二次开发 Monaco Editor(进阶))
-
- 1、常用功能(项目里用得最多)
-
- (1)、自定义补全(IntelliSense)
- [(2)、自定义语法高亮(Tokens Provider)](#(2)、自定义语法高亮(Tokens Provider))
- [(3)、自定义语法错误提示 Diagnostics](#(3)、自定义语法错误提示 Diagnostics)
- (4)、自定义主题
- [(5)、嵌入多文件(Virtual File System)](#(5)、嵌入多文件(Virtual File System))
- [(6)、自定义右键菜单(Context Menu)](#(6)、自定义右键菜单(Context Menu))
- [(7)、双栏 Diff 编辑器](#(7)、双栏 Diff 编辑器)
- 2、二次开发需要掌握的内部机制
-
- [(1)、Monaco = 编辑器 + Model(数据) + Tokenizer + LSP(可选)](#(1)、Monaco = 编辑器 + Model(数据) + Tokenizer + LSP(可选))
- (2)、内容流动路径(你做定制要按这个流程走)
- 3、项目里的最佳实践结构
前言
一、快速上手笔记
安装 Monaco Editor:
typescript
npm i -S monaco-editor
核心代码模板:
typescript
import * as monaco from "monaco-editor";
const editor = monaco.editor.create(document.getElementById("container")!, {
value: "",
language: "javascript",
theme: "vs-dark",
fontSize: 14,
minimap: { enabled: false },
automaticLayout: true
});
二、常用配置项(最常用 20 个)
1、显示与布局
| 配置项 | 作用 |
|---|---|
automaticLayout |
自动布局(强烈建议开启) |
minimap.enabled |
迷你地图 |
fontSize / fontFamily |
字体控制 |
lineNumbers |
"on/off/relative" |
tabSize / insertSpaces |
Tab 行为 |
2、编辑行为
| 配置项 | 作用 |
|---|---|
readOnly |
只读模式 |
wordWrap |
自动换行 |
scrollBeyondLastLine |
滚动区域 |
suggestOnTriggerCharacters |
自动触发补全 |
quickSuggestions |
即时提示开关 |
3、高亮与语言功能
| 配置项 | 作用 |
|---|---|
language |
语言 |
theme |
主题 |
formatOnPaste / formatOnType |
格式化 |
三、常用 API
1、更新 options(不会渲染抖动)
typescript
editor.updateOptions({ readOnly: true });
2、设置主题
(1)、内置主题
- vs:亮色
- vs-dark:暗色
- hc-black:高对比度
typescript
monaco.editor.setTheme('vs-dark')
(2)、自定义主题
typescript
monaco.editor.defineTheme('myTheme', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'comment', foreground: '5c6370' }
],
colors: {
'editor.background': '#1e1e1e'
}
})
monaco.editor.setTheme('myTheme')
3、内容与编辑功能
(1)、获取/设置内容
typescript
editor.getValue()
editor.setValue("new code here")
(2)、监听内容变化
typescript
editor.onDidChangeModelContent(e => {
console.log(editor.getValue());
});
(3)、插入文本
typescript
editor.executeEdits('insert', [
{
range: editor.getSelection(),
text: 'Hello',
forceMoveMarkers: true
}
])
(4)、操作选区
typescript
const sel = editor.getSelection()
editor.setSelection(new monaco.Range(1, 1, 1, 5))
(5)、滚动到某行
typescript
editor.revealLineInCenter(200);
(6)、格式化代码
Monaco 内置:JS、TS、JSON
其他语言依赖语言包
typescript
editor.getAction('editor.action.formatDocument').run()
(7)、撤销 / 重做
typescript
editor.trigger('keyboard', 'undo', null)
editor.trigger('keyboard', 'redo', null)
4、快捷键
(1)、注册命令
typescript
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
console.log('Ctrl+S pressed')
})
(2)、配置快捷键
typescript
monaco.editor.addKeybindingRules([
{
keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyD,
command: 'editor.action.deleteLines'
}
])
5、搜索 & 查找
(1)、打开查找框
typescript
editor.getAction('actions.find').run()
(2)、查找结果事件(监听)
typescript
editor.onDidChangeCursorPosition(...)
editor.onDidChangeCursorSelection(...)
6、自定义语言
(1)、注册语言
typescript
monaco.languages.register({ id: 'myLang' })
(2)、定义词法高亮(Tokens Provider)
typescript
monaco.languages.setMonarchTokensProvider('myLang', {
tokenizer: {
root: [
[/[a-z_$][\w$]*/, 'identifier'],
[/".*?"/, 'string']
]
}
})
(3)、提示(Completion)
typescript
monaco.languages.registerCompletionItemProvider('javascript', {
provideCompletionItems(model, position) {
return {
suggestions: [
{
label: 'log',
kind: monaco.languages.CompletionItemKind.Function,
insertText: 'console.log()'
}
]
}
}
})
(4)、语法诊断(Error Markers)
typescript
monaco.editor.setModelMarkers(editor.getModel(), 'owner', [
{
message: 'Error text',
severity: monaco.MarkerSeverity.Error,
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 10
}
])
(5)、悬浮提示(Hover Provider)
typescript
monaco.languages.registerHoverProvider('javascript', {
provideHover(model, position) {
return {
contents: [{ value: '**Hello** Hover Text' }]
}
}
})
(6)、定义跳转(Definition Provider)
typescript
monaco.languages.registerDefinitionProvider('javascript', {
provideDefinition(model, pos) {
return [{
range: new monaco.Range(1, 1, 1, 10),
uri: model.uri
}]
}
})
7、视图行为
(1)、自动换行
typescript
editor.updateOptions({ wordWrap: 'on' })
(2)、启动 minimap(代码缩略图)
typescript
editor.updateOptions({
minimap: { enabled: true }
})
(3)、显示行号/隐藏行号
typescript
editor.updateOptions({ lineNumbers: 'on' })
editor.updateOptions({ lineNumbers: 'off' })
(4)、光标样式
typescript
editor.updateOptions({ cursorStyle: 'line' })
8、模型 (Model) 操作
(1)、创建新模型
typescript
const model = monaco.editor.createModel('code here', 'javascript')
(2)、多文件支持
typescript
const modelA = monaco.editor.createModel(codeA, 'javascript', monaco.Uri.parse('file://a.js'))
const modelB = monaco.editor.createModel(codeB, 'javascript', monaco.Uri.parse('file://b.js'))
editor.setModel(modelB)
(3)、监听 model 变化
typescript
editor.onDidChangeModelContent((e) => {})
(4)、保存功能
typescript
const value = editor.getValue()
// 自己写保存逻辑 (API)
自动保存:
typescript
editor.onDidChangeModelContent(debounce(() => {
save(editor.getValue())
}, 800))
四、编辑器事件
| 事件 | 说明 |
|---|---|
onDidChangeModelContent |
文本变化事件 |
onDidChangeCursorPosition |
光标更改 |
onDidChangeCursorSelection |
用户选中区域更改 |
onDidBlurEditorText |
失焦 |
onDidFocusEditorText |
获取焦点 |
onDidDispose |
Editor销毁 |
示例:
typescript
editor.onDidChangeModelContent(() => {
console.log('content changed')
})
五、扩展功能(高级)
1、Diff Editor
typescript
const diff = monaco.editor.createDiffEditor(...)
diff.setModel({
original: monaco.editor.createModel(oldCode, 'javascript'),
modified: monaco.editor.createModel(newCode, 'javascript')
})
2、代码折叠
typescript
editor.getAction('editor.foldAll').run()
3、Outline(文档结构)
如果语言支持,会自动出现。
六、Bundler 中的 Monaco 优化
1、Vite 中使用(最推荐)
使用 vite-plugin-monaco-editor。
typescript
import monacoEditorPlugin from 'vite-plugin-monaco-editor'
export default {
plugins: [monacoEditorPlugin()]
}
2、Webpack(老项目)
使用 monaco-editor-webpack-plugin。
七、真实项目中的 12 个常见坑
| 位置 | 问题 | 解决方案 |
|---|---|---|
| Model | 未销毁导致内存泄漏 | model.dispose() |
| Token | 染色异常 | Tokenizer 顺序不正确 |
| 补全 | provider 被多次触发 | 用 debounce 限制 |
| 高亮 | Token 名称不匹配 | 确保 tokensProvider 返回一致 |
| 主题 | 无效果 | forgot inherit:true |
| 多文件 | 切换模型丢失光标 | 先保存 selection 再切换 |
| 性能 | 大文件卡顿 | 使用 differential decorations |
| 右键菜单 | Action 重复注册 | 判断 editor._actions 是否已存在 |
| diff | model 未复用 | 导致内存暴涨 |
| 事件 | onDidChangeModelContent 触发太频繁 | debounce + delta diff |
| 滚动 | revealLine 无效 | 需使用 revealLineInCenterIfOutsideViewport |
| 打包 | monaco 编辑器体积太大 | 使用 monaco-editor-webpack-plugin 进行裁剪 |
八、项目中使用 Monaco Editor
在 Vue + TypeScript + NaiveUI 项目里使用 Monaco Editor。
monaco-editor.tsx:
typescript
import {
defineComponent,
onMounted,
onUnmounted,
PropType,
ref,
watch
} from 'vue'
import { NSelect } from 'naive-ui'
import { useFormItem } from 'naive-ui/es/_mixins'
import { call } from 'naive-ui/es/_utils'
import { useThemeStore } from '@/store/theme/theme'
import * as monaco from 'monaco-editor'
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
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 Styles from './styles.scss'
import type {
MaybeArray,
OnUpdateValue,
OnUpdateValueImpl,
monaco as Monaco
} from './types'
const props = {
value: {
type: String as PropType<string>,
default: ''
},
defaultValue: {
type: String as PropType<string>
},
'onUpdate:value': [Function, Array] as PropType<MaybeArray<OnUpdateValue>>,
onUpdateValue: [Function, Array] as PropType<MaybeArray<OnUpdateValue>>,
height: {
type: String,
default: '100%'
},
options: {
type: Object as PropType<
Partial<Monaco.editor.IStandaloneEditorConstructionOptions> & {
isSelectLanguage?: boolean
editorTheme?: string
}
>,
default: () => ({
readOnly: false,
isSelectLanguage: false,
language: 'javascript',
editorTheme: 'vs'
})
}
}
window.MonacoEnvironment = {
getWorker(_: any, 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()
}
}
export default defineComponent({
name: 'MonacoEditor',
props,
emits: ['change', 'focus', 'blur'],
setup(props, ctx) {
let editor = null as monaco.editor.IStandaloneCodeEditor | null
const themeStore = useThemeStore()
const monacoEditorThemeRef = ref(
props.options.editorTheme || (themeStore.darkTheme ? 'vs-dark' : 'vs')
)
const editorRef = ref()
const formItem = useFormItem({})
const languageValue = ref(props.options.language)
const languageList: any[] = [
{
label: 'JavaScript',
value: 'javascript'
},
{
label: 'TypeScript',
value: 'typescript'
},
{
label: 'Python',
value: 'python'
},
{
label: 'PHP',
value: 'php'
},
{
label: 'Java',
value: 'java'
},
{
label: '...',
value: '...'
}
]
const initMonacoEditor = () => {
const dom = editorRef.value
if (dom) {
editor = monaco.editor.create(dom, {
...props.options,
readOnly: formItem.mergedDisabledRef.value || props.options?.readOnly,
value: props.defaultValue ?? props.value,
automaticLayout: true,
theme: monacoEditorThemeRef.value,
scrollbar: {
alwaysConsumeMouseWheel: false
}
})
editor.onDidChangeModelContent(() => {
const { onUpdateValue, 'onUpdate:value': _onUpdateValue } = props
const value = editor?.getValue() || ''
if (onUpdateValue) call(onUpdateValue as OnUpdateValueImpl, value)
if (_onUpdateValue) call(_onUpdateValue as OnUpdateValueImpl, value)
ctx.emit('change', value)
formItem.nTriggerFormChange()
formItem.nTriggerFormInput()
})
editor.onDidBlurEditorWidget(() => {
ctx.emit('blur')
formItem.nTriggerFormBlur()
})
editor.onDidFocusEditorWidget(() => {
ctx.emit('focus')
formItem.nTriggerFormFocus()
})
}
}
/**
* 获取编辑器内容
* @returns 编辑器内容
*/
const getValue = () => editor?.getValue()
/**
* 插入代码片段
* @param {string} snippet 代码片段
*/
const insertCode = (snippet: any) => {
const position = editor!.getPosition() // 获取当前光标位置
if (editor && position) {
editor.executeEdits('', [
{
// 执行插入操作
range: new monaco.Range(
position.lineNumber,
position.column,
position.lineNumber,
position.column
),
text: snippet,
forceMoveMarkers: true
}
])
}
}
/**
* 改变编辑器的语言
* @param value 语言
*/
const updateLanguage = (
value: string,
option: SelectBaseOption | null | SelectBaseOption[]
): void => {
if (editor) {
monaco.editor.setModelLanguage(editor.getModel() as any, value)
}
}
onMounted(() => {
initMonacoEditor()
})
onUnmounted(() => {
editor?.dispose()
})
watch(
() => props.value,
(val) => {
if (val !== getValue()) {
editor?.setValue(val)
}
}
)
watch(
() => formItem.mergedDisabledRef.value,
(value) => {
editor?.updateOptions({ readOnly: value })
}
)
watch(
() => themeStore.darkTheme,
() => {
editor?.dispose()
monacoEditorThemeRef.value = themeStore.darkTheme ? 'vs-dark' : 'vs'
initMonacoEditor()
}
)
ctx.expose({ getValue, insertCode })
return { editorRef, languageValue, languageList, updateLanguage }
},
render() {
return (
<div class={Styles.editorBox}>
{this.$props.options.isSelectLanguage && (
<div>
<NSelect
size='tiny'
class={Styles.select}
v-model:value={this.languageValue}
disabled={true}
options={this.languageList}
onUpdateValue={this.updateLanguage}
/>
</div>
)}
<div
ref='editorRef'
class={Styles.editor}
style={{
height: this.$props.height
}}
/>
</div>
)
}
})
styles.scss:
css
.editorBox {
display: flex;
flex-flow: column;
width: 100%;
height: 100%;
.select {
width: 150px;
}
.editor {
width: 100%;
height: 100%;
border: 1px solid #eee;
}
}
types.ts:
typescript
import type { MaybeArray } from 'naive-ui/es/_utils'
import type monaco from 'monaco-editor'
type OnUpdateValue = <T extends string>(value: T) => void
type OnUpdateValueImpl = (value: string) => void
export { MaybeArray, OnUpdateValue, OnUpdateValueImpl, monaco }
九、深度二次开发 Monaco Editor(进阶)
1、常用功能(项目里用得最多)
(1)、自定义补全(IntelliSense)
typescript
monaco.languages.registerCompletionItemProvider("javascript", {
provideCompletionItems(model, position) {
return {
suggestions: [
{
label: "print",
kind: monaco.languages.CompletionItemKind.Function,
insertText: "console.log($1)",
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
}
]
};
}
});
难点:
- 你需要理解 model.getValueInRange 来获取上下文
- Snippet 的 1 2 占位符写法很多人不熟
- 多语言补全需要注册多个 provider
(2)、自定义语法高亮(Tokens Provider)
typescript
monaco.languages.setMonarchTokensProvider("dsl", {
tokenizer: {
root: [
[/print/, "keyword"],
[/\d+/, "number"],
[/".*?"/, "string"],
]
}
});
难点:
- Tokenizer 的优先级顺序非常关键
- Regex 要写得小心,否则出现"整行染色"问题
(3)、自定义语法错误提示 Diagnostics
typescript
monaco.editor.setModelMarkers(model, "owner", [
{
severity: monaco.MarkerSeverity.Error,
message: "你写错啦!",
startLineNumber: 3,
startColumn: 1,
endLineNumber: 3,
endColumn: 10
}
]);
难点:
- 标记位置经常偏移(必须用 model.getLineContent 分析)
- 与异步语法校验结合时需要去重复、去抖
(4)、自定义主题
typescript
monaco.editor.defineTheme("myTheme", {
base: "vs-dark",
inherit: true,
rules: [
{ token: "keyword", foreground: "ff557f" },
{ token: "number", foreground: "ffaa00" }
],
colors: {
"editor.background": "#1e1d2d"
}
});
难点:
- Token 名称要与 Monarch provider 返回的 token 一致
- UI 颜色表有上百个,可参考 VSCode theme JSON
(5)、嵌入多文件(Virtual File System)
Monaco 自身只支持单文件,你要自己处理:
typescript
monaco.editor.createModel(content, language, monaco.Uri.parse("file:///src/a.ts"));
monaco.editor.createModel(content, language, monaco.Uri.parse("file:///src/b.ts"));
editor.setModel(monaco.editor.getModel(monaco.Uri.parse("file:///src/a.ts")));
难点:
- Model 生命周期管理(不销毁会内存泄漏)
- URI 必须唯一
- 不支持文件夹结构,需要你自己写树结构管理
(6)、自定义右键菜单(Context Menu)
typescript
editor.addAction({
id: "format-doc",
label: "格式化文档",
contextMenuGroupId: "navigation",
run() { console.log("format"); }
});
难点:
- Action 分组难以控制,会和默认 VSCode 菜单冲突
- 多 Action 要注意排序
(7)、双栏 Diff 编辑器
typescript
monaco.editor.createDiffEditor(container, {
enableSplitViewResizing: true
});
diffEditor.setModel({
original: monaco.editor.createModel(oldCode, language),
modified: monaco.editor.createModel(newCode, language)
});
难点:
- diffEditor 无法直接复用普通 editor 的 options,需要单独处理
- 左右内容同步位置要自己写事件监听
2、二次开发需要掌握的内部机制
必须理解的 "Monaco 的底层结构",不然做不了深度定制:
- Monaco = 编辑器 + Model(数据) + Tokenizer + LSP(可选)
- 内容流动路径(你做定制要按这个流程走)
(1)、Monaco = 编辑器 + Model(数据) + Tokenizer + LSP(可选)
- Editor 只是 UI
- Model 才是存储内容的核心
- 修改内容是对 Model 操作
- 高亮基于 Tokenizer
- 补全/诊断 基于 Provider
(2)、内容流动路径(你做定制要按这个流程走)
内容输入 → Model 更改 → Tokenizer 重新解析 → Provider 触发 → UI 更新(Editor)
这就是为什么:
- Model 改了会导致补全/高亮/错误重新触发
- 你做语法校验不能太频繁(否则阻塞 UI)
3、项目里的最佳实践结构
bash
src/monaco/
setup/
load.ts # worker 加载
language.ts # 注册语言
theme.ts # 主题
shortcuts.ts # 快捷键
features/
completion.ts # 智能提示
hover.ts # hover 提示
diagnostics.ts # 语法错误提示
actions.ts # 右键菜单
modelManager.ts # 模型多文件管理
utils/
uri.ts # uri 辅助
debounce.ts