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. 添加右键菜单
// 用于控制切换该菜单键的显示
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[];
/**
* @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. 设置快捷键
-
数字0:
monaco.KeyCode.Digit0
-
字母A:
monaco.KeyCode.KeyA
-
F1:
monaco.KeyCode.F1
-
Tab:
monaco.KeyCode.Tab
-
Delete:
monaco.KeyCode.Delete
-
Alt:
monaco.KeyMod.Alt
-
Ctrl:
monaco.KeyMod.CtrlCmd
-
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. 滚动到指定位置
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. 代码补全建议提示
/* 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());