体验背景
公司内部数据开发平台,诸如dataworks,云器等ToB产品,商业化产品毕竟比较贵,增加团队成本,无奈之下,选择自研(本着一次开发,终身受用的原则,牛马是最便宜的,哈哈),其核心部分则是支持灵活的sql编写及运行,也就带来了本次monaco-editor编辑器初体验
实现效果预览

功能概览
- 支持快捷保存
- 支持快捷执行sql代码
- 自动选择光标所在处上下文完整sql代码
- 支持快捷格式化代码
- 支持代码提示,并跟进当前环境,实时提示合理的库、表、字段及sql关键词
- 支持圈选固定代码执行
- 支持全局搜索替换
- 嵌入AI助手
- 支持插入变量,以${xxx}形式插入,并在执行时替换
体验开始
安装、引用
js
// 安装,安装方式喜欢就好
pnpm add monaco-editor
/** 引用 **/
import * as monaco from 'monaco-editor'; // 如果不需要花里胡哨的功能,可选择按需引入,可极大缩小打包体积
// 引入 EditorWorker, 可以让编辑器的一些耗时或计算密集型任务置于Web Worker中运行,避免阻塞主流程,造成卡顿
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
// web worker 注册上
self.MonacoEnvironment = {
getWorker() {
return new EditorWorker();
},
};
接下来就是编辑器初始化了,此处贴出我自己的配置,具体支持配置项,还请查阅文档
js
// editorContainer 目标容器
monaco.editor.create(editorContainer, {
value: sqlContent.value, // 编辑器初始内容
language: 'sql', // 语法高亮的语言模式
glyphMargin: true, // 显示行号左侧的空白栏(用于显示断点、小图标)
overviewRulerBorder: false, // 是否显示右侧滚动条边上的边框
lineNumbersMinChars: 3, // 行号栏的最小宽度(字符数)
fontSize: 12, // 编辑器字体大小(px)
minimap: { enabled: false }, // 是否显示右侧代码缩略图(minimap)
automaticLayout: true, // 自动布局,容器大小变化时自动调整编辑器
scrollbar: {
vertical: 'visible', // 垂直滚动条模式:'auto'(需要时显示)|'visible'(总是显示)|'hidden'
useShadows: true, // 滚动时显示阴影
verticalScrollbarSize: 8, // 垂直滚动条的宽度(px)
horizontal: 'hidden', // 水平滚动条隐藏
},
overviewRulerLanes: 1, // 概览标尺(overview ruler)通道数量,默认 3
renderLineHighlight: 'none', // 当前行高亮模式:'none' | 'gutter' | 'line' | 'all'
contextmenu: false, // 是否启用右键菜单
hover: {
enabled: true, // 是否启用鼠标悬停提示
delay: 300, // 悬停延迟(毫秒)
sticky: true, // 鼠标移到提示框上时是否保持显示
},
scrollBeyondLastLine: false, // 是否允许滚动到最后一行以下的空白区域
padding: {
bottom: 10, // 编辑器底部的内边距(px)
},
})
事件监听
监听光标位置变化,通过reason区分触发变化的方式
js
editor.onDidChangeCursorPosition((e) => {
if (e.reason === monaco.editor.CursorChangeReason.Explicit) {
// TODO
}
});
监听输入内容
js
editor.onDidChangeModelContent((e) => {})
编辑框获得/失去焦点
js
editor.onDidFocusEditorText((e) => {})
editor.onDidBlurEditorText(() => {})
监听选择代码
js
editor.onDidChangeCursorSelection(() => {
const selection = editor.getSelection();
if (selection) {
// 手动触发你的选区变化逻辑,如执行选中代码或高亮显示
handleCursorSelectionChange(selection);
}
})
添加右键菜单
js
editor.addAction({
id: 'my-custom-action', // 唯一 ID
label: 'Copilot', // 菜单显示名称
contextMenuGroupId: 'navigation', // 菜单分组 (内置 navigation, 1_modification, etc.)
contextMenuOrder: 1, // 排序,数字越小越靠前
run: () => {
// 菜单项执行逻辑
},
});
监听鼠标点击按钮,如左侧添加自定义执行按钮,点击执行
js
// 点击执行按钮,执行当前选中的 SQL
editor.onMouseDown((e) => {
if (e.target.type === monaco.editor.MouseTargetType.GUTTER_GLYPH_MARGIN) {
submitQuery();
}
});
自定义注册快捷键功能
js
// (Ctrl || Cmd) + Enter 执行
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
submitQuery();
});
// (Ctrl || Cmd) + S 保存
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => {
save();
});
// CtrlCmd + Shift + F 格式化
editor.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyF,
() => {
formatCode();
}
);
// Shift + Tab 默认回收缩进
editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Tab, () => {
editor.getAction('editor.action.outdentLines')?.run();
});
// 自定义Tab键
editor.addCommand(monaco.KeyCode.Tab, () => {
editor.trigger('keyboard', 'tab', null);
});
在光标处从外部插入代码
js
// 在光标处从外部插入一段sql代码
const insertCode = (text) => {
const position = editor.getPosition(); // 获取光标位置
editor.executeEdits('', [
{
range: new monaco.Range(
position?.lineNumber || 1,
position?.column || 1,
position?.lineNumber || 1,
position?.column || 1
),
text,
forceMoveMarkers: true,
},
]);
};
代码智能提示部分
js
// 根据实际情况主动设置当前正确的代码提示列表
const currentSuggestionList = []
// 注册自动提示
// suggestion demo
// {
// label: field.name,
// kind: monaco.languages.CompletionItemKind.Field,
// insertText: field.name,
// detail: field.type,
// sortText: '1',
// command: {
// id: 'completionCommand', // 可通过注册该事件,监听所选择的提示内容,如下demo
// arguments: ['tableField'], // 传递给监听函数的参数,可扩展多个参数
// },
}
const completeProvider = monaco.languages.registerCompletionItemProvider('sql', {
provideCompletionItems: () => {
if (currentSuggestionList.length === 0) {
const suggestController = editor.getContribution(
'editor.contrib.suggestController'
);
suggestController?.cancelSuggestWidget?.();
}
// suggestions 即当前配置的提示信息
return {
suggestions: currentSuggestionList,
};
},
});
// 注册自定义事件,监听所选择的提示内容
monaco.editor.registerCommand('completionCommand',(_, type) => {
console.log(type)
// 输出 tableField
});
// 主动触发代码提示
editor.trigger('keyboard', 'editor.action.triggerSuggest', {});
最终来个代码格式化,依赖sql-formatter库,上代码
js
import { format } from 'sql-formatter';
// sql格式化
const formatCode = () => {
try {
const placeholders: string[] = [];
// 用 __VAR_0__ 这种形式替换掉 ${name}
const replaced = (sqlContent.value || '').replace(
/\$\{[^}]+\}/g,
(match) => {
const index = placeholders.length;
placeholders.push(match); // 保存变量
return `__VAR_${index}__`; // 替换为临时标识符
}
);
// 格式化
let formatted = format(replaced, {
language: 'sql',
tabWidth: 2, // 缩进字符,默认是两个空格
keywordCase: 'upper', // 是否将关键字大写,默认 true
linesBetweenQueries: 2, // 多个 SQL 语句之间空几行,默认 1
});
// 再替换回来
placeholders.forEach((original, index) => {
const regex = new RegExp(`__VAR_${index}__`, 'g');
formatted = formatted.replace(regex, original);
});
sqlContent.value = formatted;
} catch (error) {
console.log(error);
}
};
到这里就差不多了,先记录到这了~~~