import { useEffect, useRef, useCallback } from "react"; import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightActiveLine, drawSelection, dropCursor } from "@codemirror/view"; import { EditorState, Compartment } from "@codemirror/state"; import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands"; import { sql, MySQL, PostgreSQL, StandardSQL, MSSQL, SQLite } from "@codemirror/lang-sql"; import { oneDark } from "@codemirror/theme-one-dark"; import { bracketMatching, indentOnInput, syntaxHighlighting, defaultHighlightStyle, foldGutter, foldKeymap } from "@codemirror/language"; import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap } from "@codemirror/autocomplete"; import { lintKeymap } from "@codemirror/lint"; interface SqlEditorProps { value: string; onChange: (value: string) => void; onAnalyze: () => void; dialect: string; } const dialectMap: Record = { mysql: MySQL, postgres: PostgreSQL, tsql: MSSQL, sqlite: SQLite, ansi: StandardSQL, }; const dialectCompartment = new Compartment(); function getDialectExtension(dialect: string) { const schema = dialectMap[dialect] ?? StandardSQL; // eslint-disable-next-line @typescript-eslint/no-explicit-any return sql({ dialect: schema as any }); } export function SqlEditor({ value, onChange, onAnalyze, dialect }: SqlEditorProps) { const containerRef = useRef(null); const viewRef = useRef(null); const onAnalyzeRef = useRef(onAnalyze); onAnalyzeRef.current = onAnalyze; // Create editor once useEffect(() => { if (!containerRef.current) return; const analyzeKeymap = keymap.of([ { key: "Ctrl-Enter", mac: "Cmd-Enter", run: () => { onAnalyzeRef.current(); return true; }, }, ]); const state = EditorState.create({ doc: value, extensions: [ lineNumbers(), highlightActiveLineGutter(), highlightActiveLine(), history(), drawSelection(), dropCursor(), indentOnInput(), bracketMatching(), closeBrackets(), autocompletion(), foldGutter(), syntaxHighlighting(defaultHighlightStyle, { fallback: true }), dialectCompartment.of(getDialectExtension(dialect)), oneDark, keymap.of([ ...closeBracketsKeymap, ...defaultKeymap, ...historyKeymap, ...foldKeymap, ...completionKeymap, ...lintKeymap, indentWithTab, ]), analyzeKeymap, EditorView.updateListener.of((update) => { if (update.docChanged) { onChange(update.state.doc.toString()); } }), EditorView.theme({ "&": { height: "100%", backgroundColor: "oklch(0.10 0.008 264)", }, ".cm-content": { padding: "12px 0", caretColor: "oklch(0.68 0.16 210)", }, ".cm-line": { padding: "0 16px 0 8px", }, ".cm-cursor": { borderLeftColor: "oklch(0.68 0.16 210)", borderLeftWidth: "2px", }, }), ], }); const view = new EditorView({ state, parent: containerRef.current }); viewRef.current = view; return () => { view.destroy(); viewRef.current = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Sync dialect changes useEffect(() => { const view = viewRef.current; if (!view) return; view.dispatch({ effects: dialectCompartment.reconfigure(getDialectExtension(dialect)), }); }, [dialect]); // Sync external value changes (e.g. "apply formatted") const lastValueRef = useRef(value); useEffect(() => { const view = viewRef.current; if (!view) return; const current = view.state.doc.toString(); if (current !== value && value !== lastValueRef.current) { view.dispatch({ changes: { from: 0, to: current.length, insert: value }, }); } lastValueRef.current = value; }, [value]); // Jump to line const jumpToLine = useCallback((lineNo: number) => { const view = viewRef.current; if (!view) return; const line = view.state.doc.line(Math.max(1, Math.min(lineNo, view.state.doc.lines))); view.dispatch({ selection: { anchor: line.from }, scrollIntoView: true, }); view.focus(); }, []); // Expose jumpToLine via a custom event useEffect(() => { const el = containerRef.current; if (!el) return; const handler = (e: Event) => { const lineNo = (e as CustomEvent).detail; jumpToLine(lineNo); }; el.addEventListener("jump-to-line", handler); return () => el.removeEventListener("jump-to-line", handler); }, [jumpToLine]); return (
); }