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 } // 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, // 别名 -> 表名 currentTable: string | null, // 当前正在输入的表名(用于 table. 场景) referencedTables: string[], // 已引用的表名 lastWord: string, // 最后一个单词 } { const text = textBeforeCursor.toUpperCase() const tableAlias = new Map() 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['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(null) const monacoRef = useRef(null) const disposableRef = useRef(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 ( 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', }} /> ) }