浅记Monaco-editor 初体验

体验背景

公司内部数据开发平台,诸如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);
    }
};

到这里就差不多了,先记录到这了~~~

相关推荐
做你的猫几秒前
深入剖析:基于Vue 3与Three.js的3D知识图谱实现与优化
前端·javascript·vue.js
渊不语5 分钟前
富文本编辑器自定义图片等工具栏-完整开发文档
前端
用户23971282248705 分钟前
taro+vue3+vite项目 tailwind 踩坑记,附修复后的模板源码地址
前端
做你的猫9 分钟前
深入剖析:基于Vue 3的高性能AI聊天组件设计与实现
前端·javascript·vue.js
G佳伟12 分钟前
vue拖动排序,vue使用 HTML5 的draggable拖放 API实现内容拖并排序,并更新数组数据
前端·vue.js·html5
Bling_Bling_117 分钟前
ES6新语法特性(第二篇)
开发语言·前端·es6
石小石Orz28 分钟前
妙啊!Js的对象属性居然还能用这么写
前端
成熟的API调用专家34 分钟前
cesium 获取鼠标点击位置的经度纬度海拔高度
前端
前端无冕之王39 分钟前
分享 HTML 邮件开发的 15 个踩坑实录
前端·html
dreams_dream40 分钟前
vue2实现背景颜色渐变
前端·javascript·css