暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

SQL 在线编辑器实践(1)

DBA札记 2025-02-13
197

DBA 组在实现 SQL 审核功能时,有一个场景是期望在 Web 页面实现 SQL 编辑器的效果(用户使用更友好),比如代码高亮、格式化以及关键字自动补全等。经调研,发现monaco-editor
较为符合需求,它是微软开源的一款 vscode 全功能代码编辑器,功能强大、配置灵活,且支持各种自定义的扩展。以下记录一下使用方法。

一:初始化编辑器

  • 用 Vite 快速启动一个 Vue 项目,创建单文件组件
pnpm create vite@latest
// 安装需要的包
pnpm install ant-design-vue@latest --save 
pnpm install unplugin-vue-components -D
pnpm install monaco-editor 

  • 参考 monaco-editor
    官方文档,初始化一个 代码编辑器
<template>
  <div class="editor"></div>
</template>

<script setup>
import * as monaco from 'monaco-editor'
import { onMounted} from 'vue'

/
/ 初始化编辑器
const editor = (element) => monaco.editor.create(element, {
  /
/ 默认值
  value: 'MySQL SQL Editor', 
  /
/ 指定支持的编程语言
  language: 'sql',
  theme: 'vs', 
  fontSize: 18,
  folding: true, 
  readOnly: false
})

onMounted(() => {
  const elements = document.getElementsByClassName('editor')
  elements.length > 0 && editorCreate(elements[0])
})
</
script>

<style scoped>
/* 编辑器样式 */
.editor {
  width: auto;
  height300px;
  border1px solid #f1eeeeea
}
</style>


  • 配置 vue 路由 访问 web 编辑器

自此我们已有了一个在线 SQL 编辑器。不过功能还比较弱,比如不支持语法补全,自定义关键字、主题设置等。

二:编辑器设置

2.1 关键字补全

参考官方示例:Playground

查看示例代码,关键字方法 registerCompletionItemProvider 需要一个 createDependencyProposals(range) 这样的类型:(所以我们只需要按 createDependencyProposals(range) 的方式提供关键词即可)


return {suggestions: createDependencyProposals(range)}; // 关键点

[
  {
      label'"lodash"',
      kind: monaco.languages.CompletionItemKind.Function,
      documentation"The Lodash library exported as Node.js modules.",
      insertText'"lodash": "*"',
      range: range,
  },
  {
      label'"express"',
      kind: monaco.languages.CompletionItemKind.Function,
      documentation"Fast, unopinionated, minimalist web framework",
      insertText'"express": "*"',
      range: range,
  }
 ]

monaco-editor
已经内置了非常丰富的代码关键字库,java,sql,c, json, javascript 等 ,vscode 支持的它均能支持。这里我们直接引入内置的 sql 关键字库,组装数据。

// 关键字补全
monaco.languages.registerCompletionItemProvider("sql", {
  provideCompletionItemsfunction (model, position{
    let suggestions = []
    const keywords = language.keywords
    // console.log('sql keywords:', language.keywords) 

    // 确保自动补全能够准确地插入到当前光标位置处用
    var word = model.getWordUntilPosition(position)
    var range = {
      startLineNumber: position.lineNumber,
      endLineNumber: position.lineNumber,
      startColumn: word.startColumn,
      endColumn: word.endColumn,
    }

    keywords.map(item => {
      if(item) {
        suggestions.push({
          // 显示的文本
          label: item, 
          // 文本类型, 对 类、函数、变量、进行分类
          kind: monaco.languages.CompletionItemKind['Keyword'], 
          // 插入关键字后添加空格
          insertText: item + ' ',
          // 关键字备注
          documentation'DBA 的关键字库',
          range: range
        })
      }
    })

    return {
      suggestions: suggestions,
    }
  },
})

测试 SQL 代码补全功能,可见我们常用的 SQL 语法关键字已能够自动提示。

2.2 主题设置

美观的主题使代码看起来更有个性 ,当然 Monaco Editor 是支持的。以下定义一个名为 myCustomTheme 的主题。

// 自定义主题
monaco.editor.defineTheme('myCustomTheme', {
  // 基于 vs-dark 主题
  base'vs-dark',
  inherittrue
  rules: [
    { token''background'#3A006F' }, // 缩略图背景色
    //{ token: 'keyword', foreground: '#C586C0' }, // 关键字颜色
    //{ token: 'string', foreground: '#6959CD' }, // 字符串颜色
  ],
  colors: {
    'editor.background''#000000'// 编辑器背景色
  }
})

const editor = (element) => monaco.editor.create(element, {
  // ...
  theme'myCustomTheme',   // 引用自定义主题
  // ...
})

暗黑主题测试

2.3 其他设置 (提升用户体验)

列编辑、代码搜索、批量替换、缩略图、代码折叠、鼠标滚轮缩放字体、开启右键菜单、粘贴代码格式化等实用的的功能也给他开启一下。

const editor = (element) => monaco.editor.create(element, {
  // 默认值
  value'MySQL SQL Editor'
  language'sql',
  theme'myCustomTheme'
  fontSize18,
  // 允许代码折叠
  foldingtrue
  // 代码引用显示
  codeLenstrue
  // 自动布局
  automaticLayouttrue
  // 开启缩略图功能
  minimap: { enabledtrue },
  // 粘贴时自动格式化 
  formatOnPastetrue
  // 鼠标滚轮缩放
  mouseWheelZoomtrue
  // 禁用层次提示
  disableLayerHintingtrue
  // 接受输入建议
  acceptSuggestionOnEnter'on',
  // 开启辅助功能 
  accessibilitySupport'on',
  // 开启右键菜单 
  contextmenutrue
  // 允许列编辑
  columnSelectiontrue
  readOnlyfalse
})

三:自定义关键字

内置的语法关键字库存在部分缺失(例如 auto_increment、varchar2 等);此外,库名、表名、字段名、索引名等元数据均存储于后端数据库。要实现 DBeaver、Navicat 自动补全的效果,需要自定义关键字。eg:(在 2.1 中已有答案,参考 createDependencyProposals 方法即可)

// 定义两个 Keywords
const customKeywords = (range) => {
  return [
    {
      label'auto_increment',
      kind: monaco.languages.CompletionItemKind.Keyword,
      documentation"MySQL 自增属性.",
      insertText' ',
      range: range,
    },
    {
      label'DHGate DMS()',
      kind: monaco.languages.CompletionItemKind.Keyword,
      documentation"欢迎使用 DHGate DMS",
      insertText' ',
      range: range,
    }
  ]
}

// 扩充内置的关键字库
monaco.languages.registerCompletionItemProvider("sql", {
  provideCompletionItemsfunction (model, position{
    // ... 
    let suggestions = []
    // 添加自定义的关键字
    const cts = customKeywords(range)
    return {
      suggestions: [...suggestions, ...cts],
    }
  },
})

// 以上基于内置词库扩容,当然也可以独立创建一套词库

上面自定义的关键字是静态形式,若要加载 DB 元数据,则需与后端相结合以动态扩充 suggestions 词库。方法同上 ,本文不再赘述。

四:获取编辑器内容

SQL审核需要获取 web 编辑器中的内容发送给后端处理,获取 model 内容用 onDidChangeModelContent(), getValue() 方法。eg:

<template>
  <div class="editor"></div>
  <!-- 点击按钮获取编辑器内容 -->
  <a-button type="primary" @click="getContent">getContent</a-button>
  <!-- 响应式 -->
  <a-comment>
    <span style="color: blue; font-weight: bold;font-size: 22px;">newContent: </span> 
    {{ editorContent }}
  </a-comment>

</template>

<script setup>
/
/ ...
const editorCreate = (element) => {
  editor = monaco.editor.create(element, {
  /
/ ...
  })
  
  /
/ 编辑器内容变化,回调 onDidChangeModelContent
  /
/ getValue(): Get value of the current model attached to this editor.
  editor.onDidChangeModelContent(() => {
    editorContent.value = editor.getValue()
  })
}

/
/ getContent 获取编辑器中的内容
const getContent = () => {
  /
/ implement it ...
  /
/ 请求 API ,发送 SQL 代码
  console.log('newContent:', editorContent.value)
}
/
/ ...
</
script>


五:新增上下文菜单项

新增一个上下文菜单动作,用于扩展编辑器功能。

<script setup>
// ...
const editorCreate = (element) => {
  editor = monaco.editor.create(element, {
  // ...
  })
  
   // 新增上下文菜单项
  editor.addAction({
    id'dhcoder',
    label'DHcoder',
    keybindings: monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyU,
    preconditionnull
    keybindingContext'editorTextFocus'
    contextMenuGroupId'DHcoder'
    contextMenuOrder1.2
    runfunction(ed{
      alert(ed.getValue())
      return null
    }
  })

</script>

六:组件封装

封装组件,方便复用 SQL Editor。 example:

<template>
  <div class="editor"></div>
  <!-- 点击获取编辑器内容 -->
</template>

<script setup>
import * as monaco from 'monaco-editor'
import { onMounted, ref } from 'vue'
import { language } from 'monaco-editor/
esm/vs/basic-languages/sql/sql.js'

let editor = null
const editorContent = ref()
const emits = defineEmits(['
editorContent'])

// 初始化编辑器
const editorCreate = (element) => {
  editor = monaco.editor.create(element, {
    // 默认值
    value: '
MySQL SQL Editor', 
    language: '
sql',
    theme: '
myCustomTheme', 
    fontSize: 18,
    // 允许代码折叠
    folding: true, 
    // 代码引用显示
    codeLens: true, 
    // 自动布局
    automaticLayout: true, 
    // 开启缩略图功能
    minimap: { enabled: true },
    // 粘贴时自动格式化 
    formatOnPaste: true, 
    // 鼠标滚轮缩放
    mouseWheelZoom: true, 
    // 禁用层次提示
    disableLayerHinting: true, 
    // 接受输入建议
    acceptSuggestionOnEnter: '
on',
    // 开启辅助功能 
    accessibilitySupport: '
on',
    // 开启右键菜单 
    contextmenu: true, 
    // 允许列编辑
    columnSelection: true, 
    readOnly: false
  })
  
  // 编辑器内容变化,回调 onDidChangeModelContent
  // getValue(): Get value of the current model attached to this editor.
  editor.onDidChangeModelContent(() => {
    editorContent.value = editor.getValue()
    emits('
editorContent', editorContent.value)
  })

  // 新增上下文菜单项
  editor.addAction({
    id: '
dhcoder',
    label: '
DHcoder',
    keybindings: monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyU,
    precondition: null, 
    keybindingContext: '
editorTextFocus', 
    contextMenuGroupId: '
DHcoder', 
    contextMenuOrder: 1.2, 
    run: function(ed) {
      alert(ed.getValue())
      return null
    }
  })
}

// getContent 获取编辑器中的内容
const getContent = () => {
  console.log('
newContent:', editorContent.value)
}

// 关键字补全
monaco.languages.registerCompletionItemProvider("sql", {
  provideCompletionItems: function (model, position) {
    let suggestions = []
    const keywords = language.keywords
    // console.log('
sql keywords:', language.keywords) 

    // 确保自动补全建议能够准确地插入到当前光标位置处用
    var word = model.getWordUntilPosition(position)
    var range = {
      startLineNumber: position.lineNumber,
      endLineNumber: position.lineNumber,
      startColumn: word.startColumn,
      endColumn: word.endColumn,
    }

    keywords.map(item => {
      if(item) {
        suggestions.push({
          // 显示的文本
          label: item, 
          // 文本类型, 对 类、函数、变量、进行分类
          kind: monaco.languages.CompletionItemKind['
Keyword'], 
          // 插入关键字后添加空格
          insertText: item + '
 ',
          // 关键字备注
          documentation: '
DBA 的关键字库',
          range: range
        })
      }
    })
    
    const cts = customKeywords(range)
    return {
      suggestions: [...suggestions, ...cts],
    }
  },
})

const customKeywords = (range) => {
  // 定义两个 Keywords
  return [
    {
      label: '
auto_increment',
      kind: monaco.languages.CompletionItemKind.Keyword,
      documentation: "MySQL 自增属性.",
      insertText: '
 ',
      range: range,
    },
    {
      label: '
DHGate DMS()',
      kind: monaco.languages.CompletionItemKind.Keyword,
      documentation: "欢迎使用 DHGate DMS",
      insertText: '
 ',
      range: range,
    }
  ]
}

// 自定义主题
monaco.editor.defineTheme('
myCustomTheme', {
  // 基于 vs-dark 主题
  base: '
vs-dark',
  inherit: true, 
  rules: [
    { token: '
', background: '#3A006F' }, // 缩略图背景色
    //{ token: '
keyword', foreground: '#C586C0' }, // 关键字颜色
    //{ token: '
string', foreground: '#6959CD' }, // 字符串颜色
  ],
  colors: {
    '
editor.background': '#000000', // 编辑器背景色
  }
})

onMounted(() => {
  const elements = document.getElementsByClassName('
editor')
  elements.length > 0 && editorCreate(elements[0])
})
</script>

<style scoped>
/* 编辑器样式 */
.editor {
  width: auto;
  height: 500px;
  border: 1px solid #f1eeeeea
}
</style>


组件使用示例:

<template>
  <label for="tags-select">Database: </label>
  <a-select
    v-model:value="value"
    style="width: 20%"
    allowClear  
    placeholder="Tags Mode"
    :options="options">

  </a-select>

  <!-- 编辑器组件 -->
  <Editor @editorContent="handleEditorContent" style="margin-top: 10px;"/>
</template>
<script setup>
import { ref, watch } from 'vue';

import Editor from '../
components/editor.vue';

const value = ref([])
const options = [...Array(25)].map((_, i) => ({
  value: ('
172.16.10.10') +  (i + 1) + ('/schema1') + (i + 1),
}))

// 编辑器内容处理
const newContent = ref('
')
const handleEditorContent = (text) => {
    newContent.value = text
    // console.log(text)
}

watch(newContent, (newVal, oldVal) => {
  // 处理编辑器内容变化
  console.log('
editorContent changed from', oldVal, 'to', newVal)
})

</script>
<style scoped>
</style>

【实践2 中 我们将介绍如何接入大模型,实现web 在线编辑器自动补全代码的功能】

References

  • https://cn.vuejs.org/guide/essentials/component-basics.html

  • https://www.antdv.com/components/overview-cn/

  • https://vitejs.cn/vite3-cn/guide/using-plugins.html#adding-a-plugin

  • https://microsoft.github.io/monaco-editor/playground.html?source=v0.52.2#example-creating-the-editor-web-component

  • https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.ICodeEditor.html#getValue

  • https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.ICodeEditor.html#onDidChangeModelContent

  • https://github.com/cookieY/gemini-next/blob/next/src/components/editor/editor.vue

  • https://juejin.cn/post/7126852961245855775

关于 monaco-editor 的更多功能需求,请参考:https://microsoft.github.io/monaco-editor/docs.html


文章转载自DBA札记,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论