添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

monaco-editor 编辑器

英 [ˈmɒnəkəʊ] 美 [ˈmɑnəˌkoʊ]

微软之前有个项目叫做 Monaco Workbench,后来这个项目变成了VSCode,而 Monaco Editor 就是从这个项目中成长出来的一个web编辑器,他们很大一部分的代码(monaco-editor-core)都是共用的,所以monaco和VSCode在编辑代码,交互以及UI上几乎是一摸一样的,有点不同的是,两者的平台不一样,monaco基于浏览器,而VSCode基于electron,所以功能上VSCode更加健全,并且性能比较强大。

yarn add monaco-editor

本文中使用的 monaco-editor 版本是 ^0.34.1,sql-formatter 版本需要 ^3.1.0


Using Vite

Import monaco-editor using Vite 2 · Discussion #1791 · vitejs/vite



i. 创建编辑器

<div id="editor"></div>

create(domElement, options) 创建编辑器

import { editor as MonacoEditor } from 'monaco-editor';
import 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution';
const editor = MonacoEditor.create('#editor', {
    /* 一些配置, 比如说主题,语言类型,等等 */
    theme: 'vs',
    /* 语言 */
    language: 'sql',
    /* 是否只读 */
    readOnly: false,
    /* 初始值 */
    value: '',
    /* 自动布局 */
    automaticLayout: true,
    /* 阻止编辑器滚动到最后一行之外 */
    scrollBeyondLastLine: true,



ii. 销毁编辑器

// 前面已经定义了 edior
editor.dispose();



iii. 切换主题

setTheme(themeName) 切换主题

monaco.editor.setTheme('vs');



iv. 格式化代码

格式化代码,比如说要格式化 sql

安装:yarn add sql-formatter@3.1.0

版本必须高于 3.1.0,低于这个版本格式化注释会出现问题

import { format } from 'sql-formatter';
// 22.1 已经定义 editor
editor.setValue(format(editor.getValue())); // 建议使用序号 v 的方式



v. 设置值能回退

然而,setValue 设置的值并不能“回退”(Ctrl+Z)

[Bug] when trigger setValue() ,ctrl+z doesn't undo · Issue #3358 · microsoft/monaco-editor

const setEditorValue = (val) => {
    editor.pushUndoStop();
    editor.executeEdits('name-of-edit', [
            range: editor.getModel().getFullModelRange(), // full range
            text: val, // target value here
    editor.pushUndoStop();



vi. 修改 options

创建 editor 的时候,第二个参数就是 options。比如说,现在有个需求需要来回切换 readOnly 状态

// 前面已经定义了 editor
editor.updateOptions({ readOnly: true });



vii. 添加右键菜单

issues#724

// 用于控制切换该菜单键的显示
const shouldShowSqlRunnerAction = editor.createContextKey('shouldShowSqlRunnerAction', false);
// 前面已经定义了 editor
// 添加 action
editor.addAction({
    // id
    id: 'sql-runner',
    // 该菜单键显示文本
    label: '运行选中SQL语句',
    // 控制该菜单键显示
    precondition: 'shouldShowSqlRunnerAction',
    // 该菜单键位置
    contextMenuGroupId: 'navigation',
    contextMenuOrder: 1.5,
    // 点击该菜单键后运行
    run: (event) => {
        // 光标位置
        alert(event.getPosition())
// 隐藏
shouldShowSqlRunnerAction.set(false);
// 显示
shouldShowSqlRunnerAction.set(true);




viii. 高亮显示报错行

/*-- website\typedoc\monaco.d.ts --*/
 * All decorations added through this call will get the ownerId of this editor.
 * @deprecated
deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[];

Monaco Editor Playground

/**
 * @description 高亮显示报错行
 * @param decorations 
 * @param position 
 * @returns decorations
const handleErrorMark = ({ editor, decorations, position: { rowStart, columnStart, rowEnd, columnEnd } }) => {
    return editor.deltaDecorations(
        [...decorations],  // oldDecorations 每次清空上次标记的
                range: new monaco.Range(rowStart, columnStart, rowEnd, columnEnd),  // rowStart, columnStart, rowEnd, columnEnd 
                options: {
                    isWholeLine: true,
                    className: 'myContentClass',  // 代码行样式类名
                    glyphMarginClassName: 'myGlyphMarginClass'   // 行数前面小块标记样式类名
        ],  // 如果需要清空所有标记,将 newDecorations 设为空数组即可

注意,样式这里是 background-color

.myGlyphMarginClass {
    background: red;
.myContentClass {
    background: red;
}



ix. 全屏编辑切换

目测官方没提供方法,因此手动实现

  • 样式
[data-fullscreen=true]{
    position: fixed !important;
    top: 0 !important;
    left: 0 !important;
    height: 100vh !important;
    width: 100vw !important;
    z-index: 999 !important;
}
  • 操作
/**
 * @description 退出全屏编辑
 * @return { void }
const handleExitFullEdit = (e) => {
    // 按 ESC 键退出全屏编辑
    const ESC_KEY_CODE = 27;
    if (ESC_KEY_CODE !== e.keyCode) return;
    const target = document.getElementById('editor');
    target.setAttribute('data-fullscreen', false);
    window.removeEventListener('keyup', handleExitFullExit);
 * @description 全屏编辑
 * @return { void }
const handleFullEdit = () => {
    const target = document.getElementById('editor');
    target.setAttribute('data-fullscreen', true);
    window.addEventListener('keyup', handleExitFullExit);
/* 提示:如果你用 Vue,可以直接写 :data-fullScreen="isFullScreen",这两个事件只需要更新状态值 isFullScreen 即可 */



x. 设置快捷键

  1. 数字0: monaco.KeyCode.Digit0
  2. 字母A: monaco.KeyCode.KeyA
  3. F1: monaco.KeyCode.F1
  4. Tab: monaco.KeyCode.Tab
  5. Delete: monaco.KeyCode.Delete
  6. Alt: monaco.KeyMod.Alt
  7. Ctrl: monaco.KeyMod.CtrlCmd
  8. Shift: monaco.KeyMod.Shift
/* Alt + Delete 清除代码 */
editor.addCommand(monaco.KeyMod.Alt | monaco.KeyCode.Delete, () => { /* 清除代码操作 */ }, condition);
/* condition:condition === true 时,按下快捷键才有效 */
var condition = editor.createContextKey(/*key name*/ 'condition ', /*default value*/ false);



xi. 汉化编辑器

安装 yarn add monaco-editor-nls

使用

import { setLocaleData } from 'monaco-editor-nls';
import zh from 'monaco-editor-nls/locale/zh-hans';
setLocaleData(zh);



xii. 光标位置

1. 获取光标位置

editor.getPosition()

2. 设置光标位置

editor.setPosition({ column, lineNumber })



xiii. 布局

重新布局。如果在创建实例时设置了自动布局 automaticLayout: true 则不需要用此方法

editor.layout()



xiv. 指定行列添加代码

/* InsertCode 实例;在鼠标按下时创建实例,在鼠标弹起时进行销毁。 */
var insertCode = null;
 * @description list-item mousedown event
 * @return { void }
const handleCopy = (data) => {
    /* 设置 cursor */
    document.body.style.cssText += `cursor: copy;`;
    insertCode = new insertCode({
        /* 传入实例 */
        instance: editor,
        /* 需要插入的代码 */
        code: data.code,
 * @description list-item mousedown event
 * @return { void }
const handleMouseUp = () => {
    /* 设置 cursor */
    document.body.style.cssText += `cursor: auto;`;
    insertCode?.dispose();
    insertCode = null;

然后绑定在对应的事件上即可

/* 对应列表的操作 #list */
document.querySelector('#list').addEventListener('mousedown', (event) => {
	/* 根据事件冒泡,判断对应的 #list-item 并传参 */
	/* handleCopy(data); */
/* 挂载后 - 绑定 */
window.addEventListener('mouseup', handleMouseUp);
/* 销毁前 - 移除 */
window.removeEventListener('mouseup', handleMouseUp);

为了这一个功能高可用性,我将这个操作写成了一个类 InsertCode;在鼠标按下时创建实例,在鼠标弹起时进行销毁。

/**
 * @description 插入代码至指定的行
export class InsertCode {
     * @description 构造函数
     * @param { object } [obj]
     * @param { object } [obj.instance] 编辑器实例
     * @param { object } [obj.name] 编辑器名称
     * @param { string } [obj.code] 需要被插入的代码
     * @return { void }
    constructor({ instance, name, code } = { name: '', code: '' }) {
        if (!instance) {
            throw new Error('必须传入编辑器实例');
        this.name = name;
        this.instance = instance;
        this.code = code;
        this.instanceMouseMove = null;
        this.instanceMouseUp = null;
        this._register();
     * @description 注册事件
     * @return { void }
    _register() {
        /* instance 鼠标进入事件 */
        this.instanceMouseMove = this.instance.onMouseMove(e => {
            /* 当前鼠标位置 */
            const position = e.target.position;
            /* 设置 instance 光标位置 */
            this.instance.setPosition({ ...position });
            /* 设置 instance 聚焦 */
            this.instance.focus();
        /* instance 鼠标松开事件 */
        this.instanceMouseUp = this.instance.onMouseUp(e => {
            /* 当前鼠标位置 */
            const position = e.target.position;
            /* range */
            const range = e.target.range;
            /* 插入代码 */
            this.instance.executeEdits(this.name, [
                    /* 位置 */
                    range,
                    /* 代码内容 */
                    text: this.code,
            /* 设置 instance 光标位置 */
            this.instance.setPosition({ ...position });
            /* 设置 instance 聚焦 */
            this.instance.focus();
     * @description 卸载/销毁
     * @return { void }
    dispose() {
        if (this.instance) {
            this.instanceMouseMove?.dispose();
            this.instanceMouseMove = null;
            this.instanceMouseUp?.dispose();
            this.instanceMouseUp = null;
            this.instance = null;



xv. 获取选中内容

getSelection

/**
 * @description 获取 monaco-editor 选中代码内容
 * @param { object } [obj] 
 * @param { object } [obj.instance] monaco.create 编辑器实例
 * @returns value 代码内容
function getSelectionValue({ instance}) {
	const selection = instance.getSelection(); // 获取光标选中的值
	const { startLineNumber, endLineNumber, startColumn, endColumn } = selection;
	const model = instance.getModel();
	return model.getValueInRange({
		startLineNumber,
		startColumn,
		endLineNumber,
		endColumn,

xvi. 获取当前行代码

const model = editor.getModel();
/* 获取光标位置 */
const position = model.getPositionAt();
/* 获取代码 */
const code = model.getLineContent(position.lineNumber);



xvii. 滚动到指定位置

Revealing a position

editor.revealPositionInCenter({ lineNumber: 50, column: 120 });
// Also see:
// - editor.revealLine
// - editor.revealLineInCenter
// - editor.revealLineInCenterIfOutsideViewport
// - editor.revealLines
// - editor.revealLinesInCenter
// - editor.revealLinesInCenterIfOutsideViewport
// - editor.revealPosition
// - editor.revealPositionInCenter
// - editor.revealPositionInCenterIfOutsideViewport
// - editor.revealRange
// - editor.revealRangeInCenter
// - editor.revealRangeInCenterIfOutsideViewport



xviii. 代码补全建议提示

怎么使用Monaco Editor开发SQL代码提示编辑器

/* utils/RegisterCompletion.js */
import { languages as MonacoLanguages } from "monaco-editor";
import { language } from "monaco-editor/esm/vs/basic-languages/sql/sql";
import _ from 'lodash-es';
const { keywords } = language;
 * @description monaco-editor 自定义代码补全建议
export default class RegisterCompletion {
    constructor(language, hintData) {
        this._language = language;
        this._hintData = hintData;
        this._disposable = null;
        this.registerCompletion();
     * @description 销毁
    dispose() {
        this._disposable.dispose?.();
     * @description 注册
     * @returns dispose
    registerCompletion() {
        this._disposable = MonacoLanguages.registerCompletionItemProvider(this._language, {
            triggerCharacters: [".", ...keywords],
            provideCompletionItems: (model, position) => {
                let suggestions = [];
                const { lineNumber, column } = position;
                /* 获取当前光标所在行的文本 */
                const beforeEditingText = model.getValueInRange({
                    startLineNumber: lineNumber,
                    startColumn: 0,
                    endLineNumber: lineNumber,
                    endColumn: column,
                /* 正在编辑的单词 */
                const editingWord = _.last(beforeEditingText.trim().split(/\s+/));
                /* .结尾 */
                if (editingWord.endsWith(".")) {
                    const wordNoDot = editingWord.slice(0, editingWord.length - 1);
                    if (Object.keys(this._hintData).includes(wordNoDot)) {
                        suggestions = [...this.getTableSuggest(wordNoDot)];
                /* .开头 */
                else if (editingWord === ".") {
                    suggestions = [];
                else {
                    suggestions = [
                        ...this.getDBSuggest(),
                        ...this.getSQLSuggest(),
                return {
                    suggestions,
    // 获取 SQL 语法提示
    getSQLSuggest() {
        return keywords.map((key) => ({
            label: key,
            kind: MonacoLanguages.CompletionItemKind.Enum,
            insertText: key,
    // 数据库名(hintData 的 key 值)
    getDBSuggest() {
        return Object.keys(this._hintData).map((key) => ({
            label: key,
            kind: MonacoLanguages.CompletionItemKind.Constant,
            insertText: key,
    // 表名(hintData 的 value 值)
    getTableSuggest(dbName) {
        const tableNames = this._hintData[dbName];
        if (!tableNames) {
            return [];
        return tableNames.map((name) => ({
            label: name,
            kind: MonacoLanguages.CompletionItemKind.Constant,
            insertText: name,

在页面组件生命周期末尾一定要“销毁自定义的代码补全建议”,否则会重复

/* 使用 */
import RegisterCompletion from '@/utils/RegisterCompletion.js';
const hintData = {
    // 数据库 adbs 下有两张表,分别是 dim_realtime_recharge_paycfg_range、dim_realtime_recharge_range
    adbs: ['dim_realtime_recharge_paycfg_range', 'dim_realtime_recharge_range'],
    dimi: ['ads_adid', 'ads_spec_adid_category'],
// 注册
const registerCompletionInstance = new RegisterCompletion('sql', hintData);
// 销毁
registerCompletionInstance.dispose();



xix. 触发 action

monaco 定义了很多 action,但可能没有设置快捷键,这个时候我们需要自定快捷键去触发 action

interface IEditor {
    trigger(source: string | null | undefined, handlerId: string, payload: any): void;

如下图所示,现在需要对 Transform to Lowercase 和 Transform to Uppercase 添加自定义快捷键

// Alt + L -- transform to lowercase
editor.addCommand(monaco.KeyMod.Alt | monaco.KeyCode.KeyL, () => {
    editor.trigger('', 'editor.action.transformToLowercase');
// Alt + U -- transform to uppercase
editor.addCommand(monaco.KeyMod.Alt | monaco.KeyCode.KeyU, () => {
    editor.trigger('', 'editor.action.transformToUppercase');


xx. 获取 actions

可以查看 monaco 支持哪些 actions

editor.getSupportedActions();


xxi. 快捷键


xxii. 注册新语言

需求:注册 FlinkSQL 语言。

分析:关键字相关信息与通用 SQL 有差异 Flink 1.14 Keywords ,需要对关键字等进行处理。

可以基于 monaco-editor 提供的 sql 进行改造

monaco.languages.register({ id: "flink" });
monaco.languages.setMonarchTokensProvider("flink", {
  // monaco-editor/esm/vs/basic-languages/sql/sql.js
monaco.editor.defineTheme("flink-dark", {
  // monaco-editor/esm/editor/standalone/common/theme.js

使用在创建实例的时候,指定 theme 与 language 为自己创建的即可

monaco.editor.create(document.getElementById("container"), {
  theme: 'flink-dark',
  value: '',
  language: 'flink',


xxiii. 为新语言注册快捷键

// 定义注释文本格式
monaco.languages.setLanguageConfiguration('flink', {
  comments: {
    lineComment: '--',
    blockComment: ['/*', '*/']
class FlinkActionProvider {
  provideCodeActions(model, range, context, token) {
    const commentLineAction = {
      title: 'Toggle Line Comment',
      command: 'editor.action.commentLine',
      arguments: [{ forceMoveMarkers: true }]
    const actions = [commentLineAction];
    return { actions: actions };
// 定义注释、取消注释命令
monaco.languages.registerCodeActionProvider('flink', new FlinkActionProvider());