import { useCallback, useEffect, useMemo, useRef } from 'react'; import Editor, { type BeforeMount, type OnMount } from '@monaco-editor/react'; interface TableDesignerSqlPreviewProps { sql: string; darkMode?: boolean; height?: string | number; } export type SqlChangeHighlightKind = | 'add' | 'comment' | 'constraint' | 'create' | 'drop' | 'modify' | 'rename'; export interface SqlChangeHighlight { line: string; lineNumber: number; kind: SqlChangeHighlightKind; label: string; } const SQL_PREVIEW_LIGHT_THEME = 'gonavi-sql-preview-light'; const SQL_PREVIEW_DARK_THEME = 'gonavi-sql-preview-dark'; const CHANGE_LINE_RULES: Array<{ kind: SqlChangeHighlightKind; label: string; pattern: RegExp; }> = [ { kind: 'rename', label: '重命名变更', pattern: /\b(RENAME\s+COLUMN|CHANGE\s+COLUMN|RENAME\s+TO|SP_RENAME)\b/i }, { kind: 'add', label: '新增变更', pattern: /\b(ADD\s+COLUMN|ADD\s+PRIMARY\s+KEY)\b/i }, { kind: 'drop', label: '删除变更', pattern: /\b(DROP\s+COLUMN|DROP\s+PRIMARY\s+KEY)\b/i }, { kind: 'modify', label: '字段属性变更', pattern: /\b(MODIFY\s+COLUMN|ALTER\s+COLUMN|SET\s+DATA\s+TYPE|SET\s+DEFAULT|DROP\s+DEFAULT|SET\s+NOT\s+NULL|DROP\s+NOT\s+NULL)\b/i }, { kind: 'constraint', label: '约束变更', pattern: /\b(ADD\s+CONSTRAINT|DROP\s+CONSTRAINT)\b/i }, { kind: 'comment', label: '备注变更', pattern: /\b(COMMENT\s+ON\s+COLUMN|COMMENT\s+ON\s+TABLE)\b/i }, ]; const CREATE_TABLE_PATTERN = /^\s*CREATE\s+TABLE\b/i; const getCreateTableLineHighlight = (line: string, lineNumber: number): SqlChangeHighlight | null => { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('--')) return null; return { line, lineNumber, kind: 'create', label: '新建表结构', }; }; const getAlterLineHighlight = (line: string, lineNumber: number): SqlChangeHighlight | null => { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('--')) return null; const matchedRule = CHANGE_LINE_RULES.find((rule) => rule.pattern.test(trimmed)); if (!matchedRule) return null; return { line, lineNumber, kind: matchedRule.kind, label: matchedRule.label, }; }; export const resolveSqlChangeHighlights = (sql: string): SqlChangeHighlight[] => { const lines = sql.split(/\r?\n/); const isCreateTableSql = lines.some((line) => CREATE_TABLE_PATTERN.test(line)); return lines .map((line, index) => ( isCreateTableSql ? getCreateTableLineHighlight(line, index + 1) : getAlterLineHighlight(line, index + 1) )) .filter((highlight): highlight is SqlChangeHighlight => Boolean(highlight)); }; const registerSqlPreviewThemes: BeforeMount = (monaco) => { monaco.editor.defineTheme(SQL_PREVIEW_LIGHT_THEME, { base: 'vs', inherit: true, rules: [ { token: 'keyword', foreground: '006C9C', fontStyle: 'bold' }, { token: 'operator', foreground: '8250DF' }, { token: 'number', foreground: 'B45309' }, { token: 'string', foreground: '15803D' }, { token: 'comment', foreground: '64748B', fontStyle: 'italic' }, { token: 'predefined', foreground: '0F766E' }, ], colors: { 'editor.background': '#00000000', 'editor.lineHighlightBackground': '#0F172A0A', 'editorGutter.background': '#00000000', 'editorLineNumber.foreground': '#94A3B8', }, }); monaco.editor.defineTheme(SQL_PREVIEW_DARK_THEME, { base: 'vs-dark', inherit: true, rules: [ { token: 'keyword', foreground: '7DD3FC', fontStyle: 'bold' }, { token: 'operator', foreground: 'C4B5FD' }, { token: 'number', foreground: 'FDBA74' }, { token: 'string', foreground: '86EFAC' }, { token: 'comment', foreground: '94A3B8', fontStyle: 'italic' }, { token: 'predefined', foreground: '5EEAD4' }, ], colors: { 'editor.background': '#00000000', 'editor.lineHighlightBackground': '#FFFFFF12', 'editorGutter.background': '#00000000', 'editorLineNumber.foreground': '#64748B', }, }); }; const getLineDecorationClassName = (kind: SqlChangeHighlightKind): string => `gonavi-sql-preview-change-line gonavi-sql-preview-change-line-${kind}`; const getLineDecorationMarkerClassName = (kind: SqlChangeHighlightKind): string => `gonavi-sql-preview-change-marker gonavi-sql-preview-change-marker-${kind}`; const TableDesignerSqlPreview: React.FC = ({ sql, darkMode = false, height = '360px', }) => { const decorationIdsRef = useRef([]); const editorRef = useRef(null); const monacoRef = useRef(null); const changeHighlights = useMemo(() => resolveSqlChangeHighlights(sql), [sql]); const applyChangeDecorations = useCallback(() => { const editor = editorRef.current; const monaco = monacoRef.current; const model = editor?.getModel?.(); if (!editor || !monaco || !model) return; const lineCount = model.getLineCount(); const decorations = changeHighlights .filter((highlight) => highlight.lineNumber <= lineCount) .map((highlight) => { const endColumn = Math.max(1, model.getLineMaxColumn(highlight.lineNumber)); return { range: new monaco.Range(highlight.lineNumber, 1, highlight.lineNumber, endColumn), options: { className: getLineDecorationClassName(highlight.kind), hoverMessage: { value: highlight.label }, isWholeLine: true, linesDecorationsClassName: getLineDecorationMarkerClassName(highlight.kind), }, }; }); decorationIdsRef.current = editor.deltaDecorations(decorationIdsRef.current, decorations); }, [changeHighlights]); const handleEditorMount: OnMount = (editor, monaco) => { editorRef.current = editor; monacoRef.current = monaco; applyChangeDecorations(); }; useEffect(() => { applyChangeDecorations(); }, [applyChangeDecorations, sql]); return (
); }; export default TableDesignerSqlPreview;