Monaco Editor 的使用与二次开发

目录

前言

Monaco Editor 中文官网

一、快速上手笔记

安装 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 必须唯一
  • 不支持文件夹结构,需要你自己写树结构管理
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
相关推荐
hawk2014bj1 年前
Monaco 多行提示的实现方式
reactjs·monaco
violet-jack2 年前
使用 Monaco Editor 开发 SQL 编辑器
数据库·sql·编辑器·monaco
要成为大神的小菜鸟Simon2 年前
Vue整合Monaco组件报错
前端·javascript·vue.js·前端框架·monaco
SanOrintea2 年前
monaco脚本编辑器 在无界中使用 鼠标点击不到
无界·monaco