easysql/src/components/SqlEditor.tsx

752 lines
33 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useRef, useEffect } from 'react'
import Editor, { OnMount, loader } from '@monaco-editor/react'
import * as monaco from 'monaco-editor'
import { TableInfo, ColumnInfo } from '../types'
// 配置 Monaco 使用本地加载(避免 CDN 问题)
loader.config({ monaco })
interface Props {
value: string
onChange: (value: string) => void
onRun: () => void
onSave?: () => void
onOpen?: () => void
onFormat?: () => void
databases: string[]
tables: TableInfo[]
columns: Map<string, ColumnInfo[]>
}
// SQL 关键字分组
const SQL_KEYWORDS = {
// 查询相关
query: ['SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'NOT', 'IN', 'LIKE', 'BETWEEN', 'IS', 'NULL', 'EXISTS', 'ANY', 'SOME'],
// 连接相关
join: ['JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER', 'FULL', 'CROSS', 'ON', 'USING'],
// 分组排序
groupOrder: ['GROUP', 'BY', 'HAVING', 'ORDER', 'ASC', 'DESC', 'LIMIT', 'OFFSET'],
// 数据操作
dml: ['INSERT', 'INTO', 'VALUES', 'UPDATE', 'SET', 'DELETE'],
// 数据定义
ddl: ['CREATE', 'ALTER', 'DROP', 'TABLE', 'DATABASE', 'INDEX', 'VIEW', 'TRIGGER', 'PROCEDURE', 'FUNCTION'],
// 集合操作
set: ['UNION', 'ALL', 'DISTINCT', 'INTERSECT', 'EXCEPT'],
// 条件
conditional: ['AS', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'IF'],
// 约束
constraint: ['PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES', 'UNIQUE', 'CHECK', 'DEFAULT', 'AUTO_INCREMENT', 'NOT', 'CONSTRAINT', 'IDENTITY'],
// 其他
other: ['TRUE', 'FALSE', 'TOP', 'WITH', 'RECURSIVE', 'TEMPORARY', 'TEMP', 'CASCADE', 'RESTRICT']
}
const ALL_KEYWORDS = Object.values(SQL_KEYWORDS).flat()
// SQL 函数分组
const SQL_FUNCTIONS = {
// 聚合函数
aggregate: [
{ name: 'COUNT', desc: '计数', snippet: 'COUNT(${1:*})' },
{ name: 'SUM', desc: '求和', snippet: 'SUM(${1:column})' },
{ name: 'AVG', desc: '平均值', snippet: 'AVG(${1:column})' },
{ name: 'MAX', desc: '最大值', snippet: 'MAX(${1:column})' },
{ name: 'MIN', desc: '最小值', snippet: 'MIN(${1:column})' },
{ name: 'GROUP_CONCAT', desc: '分组连接', snippet: 'GROUP_CONCAT(${1:column} SEPARATOR ${2:\',\'})' },
{ name: 'STRING_AGG', desc: '字符串聚合(SQL Server)', snippet: 'STRING_AGG(${1:column}, ${2:\',\'})' },
],
// 字符串函数
string: [
{ name: 'CONCAT', desc: '连接字符串', snippet: 'CONCAT(${1:str1}, ${2:str2})' },
{ name: 'SUBSTRING', desc: '截取子串', snippet: 'SUBSTRING(${1:str}, ${2:start}, ${3:length})' },
{ name: 'LENGTH', desc: '字符串长度', snippet: 'LENGTH(${1:str})' },
{ name: 'LEN', desc: '字符串长度(SQL Server)', snippet: 'LEN(${1:str})' },
{ name: 'UPPER', desc: '转大写', snippet: 'UPPER(${1:str})' },
{ name: 'LOWER', desc: '转小写', snippet: 'LOWER(${1:str})' },
{ name: 'TRIM', desc: '去除两端空格', snippet: 'TRIM(${1:str})' },
{ name: 'LTRIM', desc: '去除左侧空格', snippet: 'LTRIM(${1:str})' },
{ name: 'RTRIM', desc: '去除右侧空格', snippet: 'RTRIM(${1:str})' },
{ name: 'REPLACE', desc: '替换', snippet: 'REPLACE(${1:str}, ${2:from}, ${3:to})' },
{ name: 'REVERSE', desc: '反转字符串', snippet: 'REVERSE(${1:str})' },
{ name: 'LEFT', desc: '左侧截取', snippet: 'LEFT(${1:str}, ${2:n})' },
{ name: 'RIGHT', desc: '右侧截取', snippet: 'RIGHT(${1:str}, ${2:n})' },
{ name: 'LPAD', desc: '左侧填充', snippet: 'LPAD(${1:str}, ${2:len}, ${3:padstr})' },
{ name: 'RPAD', desc: '右侧填充', snippet: 'RPAD(${1:str}, ${2:len}, ${3:padstr})' },
{ name: 'INSTR', desc: '查找位置', snippet: 'INSTR(${1:str}, ${2:substr})' },
{ name: 'CHARINDEX', desc: '查找位置(SQL Server)', snippet: 'CHARINDEX(${1:substr}, ${2:str})' },
{ name: 'LOCATE', desc: '查找位置', snippet: 'LOCATE(${1:substr}, ${2:str})' },
{ name: 'SPLIT_PART', desc: '分割取部分(PostgreSQL)', snippet: 'SPLIT_PART(${1:str}, ${2:delimiter}, ${3:part})' },
],
// 数值函数
numeric: [
{ name: 'ABS', desc: '绝对值', snippet: 'ABS(${1:num})' },
{ name: 'CEIL', desc: '向上取整', snippet: 'CEIL(${1:num})' },
{ name: 'CEILING', desc: '向上取整', snippet: 'CEILING(${1:num})' },
{ name: 'FLOOR', desc: '向下取整', snippet: 'FLOOR(${1:num})' },
{ name: 'ROUND', desc: '四舍五入', snippet: 'ROUND(${1:num}, ${2:decimals})' },
{ name: 'MOD', desc: '取模', snippet: 'MOD(${1:n}, ${2:m})' },
{ name: 'POWER', desc: '幂运算', snippet: 'POWER(${1:base}, ${2:exp})' },
{ name: 'SQRT', desc: '平方根', snippet: 'SQRT(${1:num})' },
{ name: 'RAND', desc: '随机数', snippet: 'RAND()' },
{ name: 'SIGN', desc: '符号函数', snippet: 'SIGN(${1:num})' },
],
// 日期时间函数
datetime: [
{ name: 'NOW', desc: '当前日期时间', snippet: 'NOW()' },
{ name: 'GETDATE', desc: '当前日期时间(SQL Server)', snippet: 'GETDATE()' },
{ name: 'CURRENT_TIMESTAMP', desc: '当前时间戳', snippet: 'CURRENT_TIMESTAMP' },
{ name: 'CURDATE', desc: '当前日期', snippet: 'CURDATE()' },
{ name: 'CURTIME', desc: '当前时间', snippet: 'CURTIME()' },
{ name: 'DATE', desc: '提取日期', snippet: 'DATE(${1:datetime})' },
{ name: 'TIME', desc: '提取时间', snippet: 'TIME(${1:datetime})' },
{ name: 'YEAR', desc: '提取年份', snippet: 'YEAR(${1:date})' },
{ name: 'MONTH', desc: '提取月份', snippet: 'MONTH(${1:date})' },
{ name: 'DAY', desc: '提取日期', snippet: 'DAY(${1:date})' },
{ name: 'HOUR', desc: '提取小时', snippet: 'HOUR(${1:time})' },
{ name: 'MINUTE', desc: '提取分钟', snippet: 'MINUTE(${1:time})' },
{ name: 'SECOND', desc: '提取秒', snippet: 'SECOND(${1:time})' },
{ name: 'DATE_FORMAT', desc: '格式化日期(MySQL)', snippet: 'DATE_FORMAT(${1:date}, ${2:\'%Y-%m-%d\'})' },
{ name: 'FORMAT', desc: '格式化(SQL Server)', snippet: 'FORMAT(${1:date}, ${2:\'yyyy-MM-dd\'})' },
{ name: 'DATE_ADD', desc: '日期加法', snippet: 'DATE_ADD(${1:date}, INTERVAL ${2:1} ${3:DAY})' },
{ name: 'DATEADD', desc: '日期加法(SQL Server)', snippet: 'DATEADD(${1:day}, ${2:1}, ${3:date})' },
{ name: 'DATE_SUB', desc: '日期减法', snippet: 'DATE_SUB(${1:date}, INTERVAL ${2:1} ${3:DAY})' },
{ name: 'DATEDIFF', desc: '日期差', snippet: 'DATEDIFF(${1:date1}, ${2:date2})' },
{ name: 'TIMESTAMPDIFF', desc: '时间戳差', snippet: 'TIMESTAMPDIFF(${1:SECOND}, ${2:datetime1}, ${3:datetime2})' },
{ name: 'TO_CHAR', desc: '转字符串(PostgreSQL)', snippet: 'TO_CHAR(${1:date}, ${2:\'YYYY-MM-DD\'})' },
{ name: 'TO_DATE', desc: '转日期(PostgreSQL)', snippet: 'TO_DATE(${1:str}, ${2:\'YYYY-MM-DD\'})' },
],
// 条件函数
conditional: [
{ name: 'IF', desc: '条件判断(MySQL)', snippet: 'IF(${1:condition}, ${2:true_value}, ${3:false_value})' },
{ name: 'IIF', desc: '条件判断(SQL Server)', snippet: 'IIF(${1:condition}, ${2:true_value}, ${3:false_value})' },
{ name: 'IFNULL', desc: '空值替换(MySQL)', snippet: 'IFNULL(${1:expr}, ${2:default})' },
{ name: 'ISNULL', desc: '空值替换(SQL Server)', snippet: 'ISNULL(${1:expr}, ${2:default})' },
{ name: 'NULLIF', desc: '相等则返回空', snippet: 'NULLIF(${1:expr1}, ${2:expr2})' },
{ name: 'COALESCE', desc: '返回第一个非空值', snippet: 'COALESCE(${1:expr1}, ${2:expr2}, ${3:default})' },
{ name: 'NVL', desc: '空值替换(Oracle)', snippet: 'NVL(${1:expr}, ${2:default})' },
{ name: 'GREATEST', desc: '返回最大值', snippet: 'GREATEST(${1:val1}, ${2:val2})' },
{ name: 'LEAST', desc: '返回最小值', snippet: 'LEAST(${1:val1}, ${2:val2})' },
],
// 转换函数
conversion: [
{ name: 'CAST', desc: '类型转换', snippet: 'CAST(${1:expr} AS ${2:type})' },
{ name: 'CONVERT', desc: '类型转换', snippet: 'CONVERT(${1:type}, ${2:expr})' },
{ name: 'TRY_CAST', desc: '安全类型转换(SQL Server)', snippet: 'TRY_CAST(${1:expr} AS ${2:type})' },
{ name: 'TRY_CONVERT', desc: '安全类型转换(SQL Server)', snippet: 'TRY_CONVERT(${1:type}, ${2:expr})' },
],
// 窗口函数
window: [
{ name: 'ROW_NUMBER', desc: '行号', snippet: 'ROW_NUMBER() OVER (${1:ORDER BY column})' },
{ name: 'RANK', desc: '排名(有并列)', snippet: 'RANK() OVER (${1:ORDER BY column})' },
{ name: 'DENSE_RANK', desc: '密集排名', snippet: 'DENSE_RANK() OVER (${1:ORDER BY column})' },
{ name: 'NTILE', desc: '分组编号', snippet: 'NTILE(${1:n}) OVER (${2:ORDER BY column})' },
{ name: 'LAG', desc: '前一行值', snippet: 'LAG(${1:column}, ${2:1}) OVER (${3:ORDER BY column})' },
{ name: 'LEAD', desc: '后一行值', snippet: 'LEAD(${1:column}, ${2:1}) OVER (${3:ORDER BY column})' },
{ name: 'FIRST_VALUE', desc: '第一个值', snippet: 'FIRST_VALUE(${1:column}) OVER (${2:ORDER BY column})' },
{ name: 'LAST_VALUE', desc: '最后一个值', snippet: 'LAST_VALUE(${1:column}) OVER (${2:ORDER BY column})' },
{ name: 'SUM', desc: '窗口求和', snippet: 'SUM(${1:column}) OVER (${2:PARTITION BY column})' },
],
}
const ALL_FUNCTIONS = Object.values(SQL_FUNCTIONS).flat()
// 数据类型
const SQL_TYPES = [
'INT', 'INTEGER', 'BIGINT', 'SMALLINT', 'TINYINT',
'DECIMAL', 'NUMERIC', 'FLOAT', 'DOUBLE', 'REAL', 'MONEY',
'VARCHAR', 'NVARCHAR', 'CHAR', 'NCHAR', 'TEXT', 'NTEXT', 'LONGTEXT', 'MEDIUMTEXT', 'TINYTEXT',
'DATE', 'TIME', 'DATETIME', 'DATETIME2', 'TIMESTAMP', 'YEAR', 'SMALLDATETIME',
'BOOLEAN', 'BOOL', 'BIT', 'BLOB', 'BINARY', 'VARBINARY', 'IMAGE',
'JSON', 'JSONB', 'XML', 'UUID', 'UNIQUEIDENTIFIER',
'ENUM', 'SET', 'ARRAY'
]
// 分析 SQL 上下文
function analyzeSqlContext(textBeforeCursor: string): {
context: 'select_columns' | 'from_table' | 'where_condition' | 'join_table' | 'on_condition' | 'order_by' | 'group_by' | 'insert_table' | 'update_table' | 'set_column' | 'values' | 'into_columns' | 'general',
tableAlias: Map<string, string>, // 别名 -> 表名
currentTable: string | null, // 当前正在输入的表名(用于 table. 场景)
referencedTables: string[], // 已引用的表名
lastWord: string, // 最后一个单词
} {
const text = textBeforeCursor.toUpperCase()
const tableAlias = new Map<string, string>()
let currentTable: string | null = null
const referencedTables: string[] = []
// 提取表别名和引用的表 (FROM table AS alias 或 FROM table alias 或 JOIN table alias)
const aliasRegex = /(?:FROM|JOIN|UPDATE)\s+[`\[\"]?(\w+)[`\]\"]?(?:\s+(?:AS\s+)?([A-Z]\w*))?/gi
let match
while ((match = aliasRegex.exec(textBeforeCursor)) !== null) {
const tableName = match[1].toLowerCase()
referencedTables.push(tableName)
if (match[2]) {
tableAlias.set(match[2].toLowerCase(), tableName)
}
}
// 检查是否在 table. 后面(包括正在输入的情况 table.col
const dotMatch = textBeforeCursor.match(/[`\[\"]?(\w+)[`\]\"]?\.(\w*)$/i)
if (dotMatch) {
currentTable = dotMatch[1].toLowerCase()
// 检查是否是别名
if (tableAlias.has(currentTable)) {
currentTable = tableAlias.get(currentTable)!
}
}
// 获取最后一个单词
const lastWordMatch = textBeforeCursor.match(/(\w+)\s*$/i)
const lastWord = lastWordMatch ? lastWordMatch[1].toUpperCase() : ''
// 判断上下文
let context: ReturnType<typeof analyzeSqlContext>['context'] = 'general'
// 检查是否在括号内INSERT INTO table (columns) 的情况)
const lastOpenParen = textBeforeCursor.lastIndexOf('(')
const lastCloseParen = textBeforeCursor.lastIndexOf(')')
const inParentheses = lastOpenParen > lastCloseParen
// 检查 INSERT INTO table ( 后面的上下文
if (inParentheses) {
const beforeParen = textBeforeCursor.substring(0, lastOpenParen)
if (/INSERT\s+INTO\s+\w+\s*$/i.test(beforeParen)) {
context = 'into_columns'
return { context, tableAlias, currentTable, referencedTables, lastWord }
} else if (/VALUES\s*$/i.test(beforeParen)) {
context = 'values'
return { context, tableAlias, currentTable, referencedTables, lastWord }
}
}
// 找出最后一个关键字的位置,确定当前处于哪个子句中
const keywordPositions: { keyword: string; index: number }[] = []
const keywords = [
'SELECT', 'FROM', 'WHERE', 'JOIN', 'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN',
'FULL JOIN', 'CROSS JOIN', 'ON', 'AND', 'OR', 'ORDER BY', 'GROUP BY',
'HAVING', 'INSERT INTO', 'UPDATE', 'SET', 'VALUES', 'LIMIT', 'OFFSET'
]
for (const kw of keywords) {
// 使用更精确的匹配,确保是独立的关键字
const regex = new RegExp(`\\b${kw}\\b`, 'gi')
let m
while ((m = regex.exec(text)) !== null) {
keywordPositions.push({ keyword: kw, index: m.index })
}
}
// 按位置排序,找到最后一个关键字
keywordPositions.sort((a, b) => b.index - a.index)
if (keywordPositions.length > 0) {
const lastKeyword = keywordPositions[0].keyword
const afterKeyword = text.substring(keywordPositions[0].index + lastKeyword.length)
// 检查关键字后面是否有其他关键字(排除当前正在分析的关键字)
const hasSubsequentKeyword = keywordPositions.length > 1 &&
['FROM', 'WHERE', 'JOIN', 'ORDER BY', 'GROUP BY', 'HAVING', 'LIMIT'].some(k =>
text.indexOf(k, keywordPositions[0].index + lastKeyword.length) > -1
)
switch (lastKeyword) {
case 'SELECT':
// SELECT 后面,如果还没有 FROM提示字段
if (!text.includes('FROM')) {
context = 'select_columns'
} else {
context = 'general'
}
break
case 'FROM':
case 'INSERT INTO':
// FROM 或 INSERT INTO 后面,提示表名
// 检查是否已经输入了表名(有空格分隔的后续内容且不是继续输入表名)
if (/^\s+\w+\s+/i.test(afterKeyword)) {
// 已经输入了完整的表名,不再提示
context = 'general'
} else {
context = lastKeyword === 'FROM' ? 'from_table' : 'insert_table'
}
break
case 'UPDATE':
if (/^\s+\w+\s+/i.test(afterKeyword)) {
context = 'general'
} else {
context = 'update_table'
}
break
case 'INNER JOIN':
case 'LEFT JOIN':
case 'RIGHT JOIN':
case 'FULL JOIN':
case 'CROSS JOIN':
case 'JOIN':
if (/^\s+\w+\s+/i.test(afterKeyword)) {
context = 'general'
} else {
context = 'join_table'
}
break
case 'ON':
context = 'on_condition'
break
case 'WHERE':
case 'AND':
case 'OR':
case 'HAVING':
context = 'where_condition'
break
case 'ORDER BY':
context = 'order_by'
break
case 'GROUP BY':
context = 'group_by'
break
case 'SET':
context = 'set_column'
break
case 'VALUES':
context = 'values'
break
default:
context = 'general'
}
}
return { context, tableAlias, currentTable, referencedTables, lastWord }
}
export default function SqlEditor({ value, onChange, onRun, onSave, onOpen, onFormat, databases, tables, columns }: Props) {
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null)
const monacoRef = useRef<typeof monaco | null>(null)
const disposableRef = useRef<monaco.IDisposable | null>(null)
// 使用 ref 保存最新的数据和回调
const dataRef = useRef({ databases, tables, columns })
const callbacksRef = useRef({ onRun, onSave, onOpen, onFormat })
// 更新 ref 中的数据
useEffect(() => {
dataRef.current = { databases, tables, columns }
}, [databases, tables, columns])
// 更新 ref 中的回调
useEffect(() => {
callbacksRef.current = { onRun, onSave, onOpen, onFormat }
}, [onRun, onSave, onOpen, onFormat])
const handleEditorMount: OnMount = (editor, monacoInstance) => {
editorRef.current = editor
monacoRef.current = monacoInstance
// 注册 SQL 语言的自动补全
disposableRef.current = monacoInstance.languages.registerCompletionItemProvider('sql', {
triggerCharacters: ['.', ' ', '`', '[', '"', ','],
provideCompletionItems: (model, position) => {
const word = model.getWordUntilPosition(position)
const range = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
}
// 获取光标前的文本进行上下文分析
const textBeforeCursor = model.getValueInRange({
startLineNumber: 1,
startColumn: 1,
endLineNumber: position.lineNumber,
endColumn: position.column,
})
const { context, tableAlias, currentTable, referencedTables } = analyzeSqlContext(textBeforeCursor)
// 获取最新的数据
const { databases: dbs, tables: tbls, columns: cols } = dataRef.current
const suggestions: monaco.languages.CompletionItem[] = []
// 如果在 table. 后面,只提示该表的字段
if (currentTable) {
// 检查是否有该表的列信息
const tableColumns = cols.get(currentTable) ||
[...cols.entries()].find(([name]) => name.toLowerCase() === currentTable)?.[1]
if (tableColumns) {
// 添加 * 选项在最前
suggestions.push({
label: '*',
kind: monacoInstance.languages.CompletionItemKind.Constant,
insertText: '*',
range,
detail: '所有字段',
sortText: '!0',
})
tableColumns.forEach((col, idx) => {
const isPK = col.key === 'PRI'
suggestions.push({
label: col.name,
kind: monacoInstance.languages.CompletionItemKind.Field,
insertText: col.name,
range,
detail: `${col.type}${isPK ? ' 🔑' : ''}${col.comment ? ' · ' + col.comment : ''}`,
documentation: {
value: `**${currentTable}.${col.name}**\n\n` +
`- 类型: \`${col.type}\`\n` +
`- 可空: ${col.nullable ? '✅ 是' : '❌ 否'}\n` +
(col.key ? `- 键: ${col.key}\n` : '') +
(col.comment ? `- 备注: ${col.comment}` : '')
},
sortText: '!1' + (isPK ? '0' : '1') + String(idx).padStart(3, '0'),
})
})
}
return { suggestions }
}
// 获取当前语句中引用的表的列
const getReferencedTableColumns = () => {
const result: Array<{ tableName: string; col: ColumnInfo }> = []
for (const tableName of referencedTables) {
const tableColumns = cols.get(tableName) ||
[...cols.entries()].find(([name]) => name.toLowerCase() === tableName)?.[1]
if (tableColumns) {
tableColumns.forEach(col => result.push({ tableName, col }))
}
}
return result
}
// 根据上下文提供不同的建议
const addKeywords = (priority: string = '3') => {
ALL_KEYWORDS.forEach(keyword => {
suggestions.push({
label: keyword,
kind: monacoInstance.languages.CompletionItemKind.Keyword,
insertText: keyword,
range,
detail: '关键字',
sortText: priority + keyword,
})
suggestions.push({
label: keyword.toLowerCase(),
kind: monacoInstance.languages.CompletionItemKind.Keyword,
insertText: keyword.toLowerCase(),
range,
detail: '关键字',
sortText: priority + keyword,
})
})
}
const addFunctions = (categories?: (keyof typeof SQL_FUNCTIONS)[], priority: string = '2') => {
const funcsToAdd = categories
? categories.flatMap(cat => SQL_FUNCTIONS[cat] || [])
: ALL_FUNCTIONS
funcsToAdd.forEach(func => {
suggestions.push({
label: func.name,
kind: monacoInstance.languages.CompletionItemKind.Function,
insertText: func.snippet,
insertTextRules: monacoInstance.languages.CompletionItemInsertTextRule.InsertAsSnippet,
range,
detail: `ƒ ${func.desc}`,
sortText: priority + func.name,
})
})
}
const addTables = (priority: string = '0') => {
tbls.forEach(table => {
const isView = table.isView
suggestions.push({
label: table.name,
kind: isView
? monacoInstance.languages.CompletionItemKind.Interface
: monacoInstance.languages.CompletionItemKind.Class,
insertText: table.name,
range,
detail: isView
? `👁️ 视图`
: `📋 表 (${table.rows.toLocaleString()} 行)`,
sortText: priority + (isView ? '1' : '0') + table.name,
})
})
}
const addColumns = (priority: string = '1', withTablePrefix: boolean = false) => {
cols.forEach((colList, tableName) => {
colList.forEach(col => {
const comment = col.comment ? ` - ${col.comment}` : ''
const label = withTablePrefix ? `${tableName}.${col.name}` : col.name
const isPK = col.key === 'PRI'
suggestions.push({
label,
kind: monacoInstance.languages.CompletionItemKind.Field,
insertText: label,
range,
detail: `📌 ${tableName} · ${col.type}${isPK ? ' 🔑' : ''}`,
documentation: {
value: `**${tableName}.${col.name}**\n\n` +
`- 类型: \`${col.type}\`\n` +
`- 可空: ${col.nullable ? '✅ 是' : '❌ 否'}\n` +
(col.key ? `- 键: ${col.key}\n` : '') +
(col.comment ? `- 备注: ${col.comment}` : '')
},
sortText: priority + (isPK ? '0' : '1') + col.name,
})
})
})
}
const addDatabases = (priority: string = '0') => {
dbs.forEach(db => {
suggestions.push({
label: db,
kind: monacoInstance.languages.CompletionItemKind.Module,
insertText: db,
range,
detail: '📁 数据库',
sortText: priority + db,
})
})
}
// 添加当前引用表的字段(优先显示)
const addReferencedColumns = (priority: string = '0') => {
const refCols = getReferencedTableColumns()
if (refCols.length > 0) {
refCols.forEach(({ tableName, col }, idx) => {
const isPK = col.key === 'PRI'
const label = referencedTables.length > 1 ? `${tableName}.${col.name}` : col.name
suggestions.push({
label,
kind: monacoInstance.languages.CompletionItemKind.Field,
insertText: label,
range,
detail: `${col.type}${isPK ? ' 🔑' : ''}${col.comment ? ' · ' + col.comment : ''}`,
documentation: {
value: `**${tableName}.${col.name}**\n\n` +
`- 类型: \`${col.type}\`\n` +
`- 可空: ${col.nullable ? '✅ 是' : '❌ 否'}\n` +
(col.key ? `- 键: ${col.key}\n` : '') +
(col.comment ? `- 备注: ${col.comment}` : '')
},
sortText: priority + (isPK ? '0' : '1') + String(idx).padStart(4, '0'),
})
})
}
}
// 根据上下文添加建议
switch (context) {
case 'select_columns':
// SELECT 后优先提示:* -> 当前表字段 -> 聚合函数 -> 其他表字段
suggestions.push({
label: '*',
kind: monacoInstance.languages.CompletionItemKind.Constant,
insertText: '*',
range,
detail: '所有字段',
sortText: '!00',
})
addReferencedColumns('!1') // 优先显示当前引用表的字段
addFunctions(['aggregate', 'window'], '!2') // 聚合和窗口函数次之
addColumns('2', false) // 其他表字段(不带表名前缀,更简洁)
addFunctions(['string', 'datetime', 'conditional'], '3')
return { suggestions } // 直接返回
case 'from_table':
case 'join_table':
case 'insert_table':
case 'update_table':
// FROM/JOIN/INSERT/UPDATE 后 只 提示表名和数据库,直接返回
addTables('!0') // 表名最优先
addDatabases('1')
return { suggestions } // 直接返回,不添加代码片段
case 'where_condition':
case 'on_condition':
// WHERE/ON 后优先提示当前表字段
addReferencedColumns('!0') // 当前引用表字段最优先
addColumns('2', false) // 其他表字段
addFunctions(['conditional', 'string', 'datetime'], '3')
addKeywords('8')
return { suggestions }
case 'order_by':
case 'group_by':
// ORDER BY/GROUP BY 后优先提示当前表字段
addReferencedColumns('!0')
addColumns('2', false)
if (context === 'group_by') {
addFunctions(['aggregate'], '3')
}
return { suggestions }
case 'set_column':
case 'into_columns':
// SET/INSERT (columns) 后只提示当前表字段
addReferencedColumns('!0')
if (context === 'set_column') {
addFunctions(['conditional', 'string', 'datetime', 'numeric'], '2')
}
return { suggestions }
case 'values':
// VALUES 后提示函数和关键字
addFunctions(['datetime', 'string', 'conditional'], '!0')
addKeywords('2')
return { suggestions }
default:
// 通用情况 - 关键字优先
addKeywords('!0')
addTables('1')
addColumns('2', true)
addFunctions(undefined, '3')
addDatabases('4')
}
// 数据类型
SQL_TYPES.forEach(type => {
suggestions.push({
label: type,
kind: monacoInstance.languages.CompletionItemKind.TypeParameter,
insertText: type,
range,
detail: '数据类型',
sortText: '5' + type,
})
})
// 常用代码片段
const snippets = [
{ label: 'sel', insertText: 'SELECT * FROM ${1:table} WHERE ${2:1=1}', detail: 'SELECT 查询' },
{ label: 'selc', insertText: 'SELECT COUNT(*) as count FROM ${1:table}', detail: 'COUNT 计数' },
{ label: 'selt', insertText: 'SELECT TOP ${1:10} * FROM ${2:table}', detail: 'SELECT TOP (SQL Server)' },
{ label: 'sell', insertText: 'SELECT * FROM ${1:table} LIMIT ${2:10}', detail: 'SELECT LIMIT' },
{ label: 'selp', insertText: 'SELECT * FROM ${1:table} LIMIT ${2:10} OFFSET ${3:0}', detail: 'SELECT 分页' },
{ label: 'seld', insertText: 'SELECT DISTINCT ${1:column} FROM ${2:table}', detail: 'SELECT DISTINCT' },
{ label: 'ins', insertText: 'INSERT INTO ${1:table} (${2:columns})\nVALUES (${3:values})', detail: 'INSERT 插入' },
{ label: 'insm', insertText: 'INSERT INTO ${1:table} (${2:columns})\nVALUES\n (${3:values1}),\n (${4:values2})', detail: 'INSERT 多行' },
{ label: 'upd', insertText: 'UPDATE ${1:table}\nSET ${2:column} = ${3:value}\nWHERE ${4:condition}', detail: 'UPDATE 更新' },
{ label: 'del', insertText: 'DELETE FROM ${1:table}\nWHERE ${2:condition}', detail: 'DELETE 删除' },
{ label: 'crt', insertText: 'CREATE TABLE ${1:table_name} (\n id INT PRIMARY KEY AUTO_INCREMENT,\n ${2:column_name} ${3:VARCHAR(255)} NOT NULL,\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP\n)', detail: 'CREATE TABLE' },
{ label: 'crts', insertText: 'CREATE TABLE ${1:table_name} (\n id INT IDENTITY(1,1) PRIMARY KEY,\n ${2:column_name} ${3:NVARCHAR(255)} NOT NULL,\n created_at DATETIME2 DEFAULT GETDATE()\n)', detail: 'CREATE TABLE (SQL Server)' },
{ label: 'alt', insertText: 'ALTER TABLE ${1:table}\nADD ${2:column} ${3:type}', detail: 'ALTER TABLE 添加列' },
{ label: 'idx', insertText: 'CREATE INDEX ${1:idx_name}\nON ${2:table} (${3:column})', detail: 'CREATE INDEX' },
{ label: 'join', insertText: 'SELECT ${1:t1.*}, ${2:t2.*}\nFROM ${3:table1} t1\nINNER JOIN ${4:table2} t2 ON t1.${5:id} = t2.${6:t1_id}', detail: 'INNER JOIN' },
{ label: 'ljoin', insertText: 'SELECT ${1:t1.*}, ${2:t2.*}\nFROM ${3:table1} t1\nLEFT JOIN ${4:table2} t2 ON t1.${5:id} = t2.${6:t1_id}', detail: 'LEFT JOIN' },
{ label: 'rjoin', insertText: 'SELECT ${1:t1.*}, ${2:t2.*}\nFROM ${3:table1} t1\nRIGHT JOIN ${4:table2} t2 ON t1.${5:id} = t2.${6:t1_id}', detail: 'RIGHT JOIN' },
{ label: 'case', insertText: 'CASE\n WHEN ${1:condition1} THEN ${2:result1}\n WHEN ${3:condition2} THEN ${4:result2}\n ELSE ${5:default}\nEND', detail: 'CASE WHEN' },
{ label: 'cte', insertText: 'WITH ${1:cte_name} AS (\n ${2:SELECT * FROM table}\n)\nSELECT * FROM ${1:cte_name}', detail: 'CTE 公用表表达式' },
{ label: 'sub', insertText: 'SELECT * FROM (\n ${1:SELECT * FROM table}\n) AS ${2:subquery}', detail: '子查询' },
{ label: 'exs', insertText: 'SELECT * FROM ${1:table1} t1\nWHERE EXISTS (\n SELECT 1 FROM ${2:table2} t2\n WHERE t2.${3:t1_id} = t1.${4:id}\n)', detail: 'EXISTS 子查询' },
{ label: 'grp', insertText: 'SELECT ${1:column}, COUNT(*) as count\nFROM ${2:table}\nGROUP BY ${1:column}\nHAVING COUNT(*) > ${3:1}\nORDER BY count DESC', detail: 'GROUP BY 分组' },
{ label: 'pag', insertText: 'SELECT *\nFROM ${1:table}\nORDER BY ${2:id}\nOFFSET ${3:0} ROWS\nFETCH NEXT ${4:10} ROWS ONLY', detail: 'OFFSET FETCH 分页 (SQL Server)' },
]
snippets.forEach(snip => {
suggestions.push({
label: snip.label,
kind: monacoInstance.languages.CompletionItemKind.Snippet,
insertText: snip.insertText,
insertTextRules: monacoInstance.languages.CompletionItemInsertTextRule.InsertAsSnippet,
range,
detail: '📝 ' + snip.detail,
sortText: '6' + snip.label,
})
})
return { suggestions }
}
})
// Ctrl+Enter 执行
editor.addCommand(monacoInstance.KeyMod.CtrlCmd | monacoInstance.KeyCode.Enter, () => {
callbacksRef.current.onRun()
})
// Ctrl+S 保存
editor.addCommand(monacoInstance.KeyMod.CtrlCmd | monacoInstance.KeyCode.KeyS, () => {
callbacksRef.current.onSave?.()
})
// Ctrl+O 打开
editor.addCommand(monacoInstance.KeyMod.CtrlCmd | monacoInstance.KeyCode.KeyO, () => {
callbacksRef.current.onOpen?.()
})
// Ctrl+Shift+F 格式化
editor.addCommand(monacoInstance.KeyMod.CtrlCmd | monacoInstance.KeyMod.Shift | monacoInstance.KeyCode.KeyF, () => {
callbacksRef.current.onFormat?.()
})
// Alt+Shift+F 格式化VSCode 风格)
editor.addCommand(monacoInstance.KeyMod.Alt | monacoInstance.KeyMod.Shift | monacoInstance.KeyCode.KeyF, () => {
callbacksRef.current.onFormat?.()
})
}
// 清理
useEffect(() => {
return () => {
disposableRef.current?.dispose()
}
}, [])
return (
<Editor
height="100%"
language="sql"
value={value}
onChange={(v) => onChange(v || '')}
onMount={handleEditorMount}
theme="vs"
options={{
minimap: { enabled: false },
fontSize: 14,
fontFamily: "'Cascadia Code', 'Consolas', monospace",
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 2,
wordWrap: 'on',
suggestOnTriggerCharacters: true,
quickSuggestions: {
other: true,
comments: false,
strings: true,
},
snippetSuggestions: 'top',
suggest: {
showKeywords: true,
showSnippets: true,
showFunctions: true,
showFields: true,
showClasses: true,
showModules: true,
preview: true,
filterGraceful: true,
},
padding: { top: 10, bottom: 10 },
acceptSuggestionOnEnter: 'on',
}}
/>
)
}