Skip to content

Commit 10387d4

Browse files
committed
Upgrade catalog explore editor to CodeMirror 6 with shared theme system
Replace the plain HTML textarea in the catalog explore SQL editor with a full CodeMirror 6 instance featuring SQL syntax highlighting, line numbers, bracket matching, and undo/redo history. Extract the duplicated Gruvpuccin and Tokyo Night CM theme logic from sandbox-editor and synthesis-output into a shared lib/cm-themes module so all three editors use the same theme infrastructure and palette switching.
1 parent ba226f7 commit 10387d4

File tree

9 files changed

+183
-81
lines changed

9 files changed

+183
-81
lines changed

dashboard/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
],
2828
"scripts": {
2929
"build": "vite build",
30-
"build:static": "vite build && cp -r dist/ ../server/static/",
30+
"build:static": "vite build && cp -r dist/ ../server/dashboard/",
3131
"dev": "vite --port 3001",
3232
"preview": "vite preview --port 3001",
3333
"lint": "biome check",
@@ -42,13 +42,14 @@
4242
"@codemirror/commands": "^6.10.3",
4343
"@codemirror/lang-javascript": "^6.2.5",
4444
"@codemirror/lang-json": "^6.0.2",
45+
"@codemirror/lang-sql": "^6.10.0",
4546
"@codemirror/language": "^6.12.2",
4647
"@codemirror/state": "^6.6.0",
4748
"@codemirror/view": "^6.40.0",
49+
"@flink-reactor/dsl": "0.1.8-rc.3",
4850
"@flink-reactor/instruments-ui": "workspace:*",
4951
"@flink-reactor/ui": "workspace:*",
5052
"@lezer/highlight": "^1.2.3",
51-
"radix-ui": "^1.4.3",
5253
"@shikijs/core": "^4.0.2",
5354
"@shikijs/langs": "^4.0.2",
5455
"@shikijs/themes": "^4.0.2",
@@ -59,10 +60,10 @@
5960
"cmdk": "^1.1.1",
6061
"codemirror": "^6.0.2",
6162
"date-fns": "^4.1.0",
62-
"@flink-reactor/dsl": "0.1.8-rc.3",
6363
"graphql": "^16.13.1",
6464
"lucide-react": "^0.563.0",
6565
"motion": "^12.36.0",
66+
"radix-ui": "^1.4.3",
6667
"react": "^19.2.4",
6768
"react-dom": "^19.2.4",
6869
"react-icons": "^5.6.0",

dashboard/src/components/catalogs/explore-editor.tsx

Lines changed: 100 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,122 @@
11
/**
22
* @module explore-editor
33
*
4-
* SQL editor textarea with run/cancel controls for the catalog explore page.
4+
* CodeMirror 6 SQL editor with run/cancel controls for the catalog explore page.
55
* Reads SQL text and execution status from {@link useCatalogExploreStore}.
6-
* Supports Cmd+Enter keyboard shortcut to execute queries.
6+
* Supports Cmd+Enter keyboard shortcut to execute queries. Uses the same
7+
* Gruvpuccin / Tokyo Night theme system as the sandbox editor.
78
*/
89

10+
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"
11+
import { sql } from "@codemirror/lang-sql"
12+
import { bracketMatching } from "@codemirror/language"
13+
import { EditorState } from "@codemirror/state"
14+
import {
15+
EditorView,
16+
highlightActiveLine,
17+
keymap,
18+
lineNumbers,
19+
placeholder,
20+
} from "@codemirror/view"
921
import { Button, Spinner } from "@flink-reactor/ui"
1022
import { Play, Square } from "lucide-react"
11-
import { useCallback } from "react"
23+
import { useEffect, useRef } from "react"
24+
import {
25+
createThemeCompartment,
26+
getActiveTheme,
27+
useCmPaletteObserver,
28+
} from "@/lib/cm-themes"
1229
import { useCatalogExploreStore } from "@/stores/catalog-explore-store"
1330

31+
const themeCompartment = createThemeCompartment()
32+
1433
/**
1534
* SQL editor with status indicator, run button, and cancel button.
1635
*
1736
* Displays the current query status (idle, submitting, running, completed,
18-
* failed, cancelled) in the toolbar. The textarea supports Cmd+Enter to
19-
* execute and auto-disables controls while a query is in flight.
37+
* failed, cancelled) in the toolbar. The CodeMirror editor supports
38+
* Cmd+Enter to execute and syncs with the catalog explore store.
2039
*/
2140
export function ExploreEditor() {
22-
const sql = useCatalogExploreStore((s) => s.sql)
41+
const sql$ = useCatalogExploreStore((s) => s.sql)
2342
const setSql = useCatalogExploreStore((s) => s.setSql)
2443
const status = useCatalogExploreStore((s) => s.status)
2544
const executeQuery = useCatalogExploreStore((s) => s.executeQuery)
2645
const cancelQuery = useCatalogExploreStore((s) => s.cancelQuery)
2746

2847
const isRunning = status === "submitting" || status === "running"
2948

30-
const handleKeyDown = useCallback(
31-
(e: React.KeyboardEvent) => {
32-
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
33-
e.preventDefault()
34-
if (isRunning) return
35-
executeQuery()
36-
}
37-
},
38-
[executeQuery, isRunning],
39-
)
49+
const containerRef = useRef<HTMLDivElement>(null)
50+
const viewRef = useRef<EditorView | null>(null)
51+
const setSqlRef = useRef(setSql)
52+
const executeQueryRef = useRef(executeQuery)
53+
const isRunningRef = useRef(isRunning)
54+
55+
setSqlRef.current = setSql
56+
executeQueryRef.current = executeQuery
57+
isRunningRef.current = isRunning
58+
59+
// Create the CodeMirror editor on mount
60+
useEffect(() => {
61+
if (!containerRef.current) return
62+
63+
const state = EditorState.create({
64+
doc: sql$,
65+
extensions: [
66+
lineNumbers(),
67+
highlightActiveLine(),
68+
bracketMatching(),
69+
history(),
70+
sql(),
71+
placeholder("SELECT * FROM ..."),
72+
themeCompartment.of(getActiveTheme()),
73+
keymap.of([
74+
...defaultKeymap,
75+
...historyKeymap,
76+
{
77+
key: "Mod-Enter",
78+
run: () => {
79+
if (isRunningRef.current) return true
80+
executeQueryRef.current()
81+
return true
82+
},
83+
},
84+
]),
85+
EditorView.updateListener.of((update) => {
86+
if (update.docChanged) {
87+
setSqlRef.current(update.state.doc.toString())
88+
}
89+
}),
90+
],
91+
})
92+
93+
const view = new EditorView({
94+
state,
95+
parent: containerRef.current,
96+
})
97+
98+
viewRef.current = view
99+
100+
return () => {
101+
view.destroy()
102+
}
103+
// eslint-disable-next-line react-hooks/exhaustive-deps
104+
}, [])
105+
106+
// Sync external value changes (e.g. template selector)
107+
useEffect(() => {
108+
const view = viewRef.current
109+
if (!view) return
110+
const current = view.state.doc.toString()
111+
if (current !== sql$) {
112+
view.dispatch({
113+
changes: { from: 0, to: current.length, insert: sql$ },
114+
})
115+
}
116+
}, [sql$])
117+
118+
// Watch for palette changes and reconfigure the theme compartment
119+
useCmPaletteObserver(viewRef, themeCompartment)
40120

41121
return (
42122
<div className="glass-card overflow-hidden">
@@ -81,21 +161,17 @@ export function ExploreEditor() {
81161
size="sm"
82162
variant="outline"
83163
onClick={executeQuery}
84-
disabled={isRunning || !sql.trim()}
164+
disabled={isRunning || !sql$.trim()}
85165
className="h-7 gap-1.5 text-xs"
86166
>
87167
{isRunning ? <Spinner size="sm" /> : <Play className="size-3" />}
88168
Run
89169
</Button>
90170
</div>
91171
</div>
92-
<textarea
93-
value={sql}
94-
onChange={(e) => setSql(e.target.value)}
95-
onKeyDown={handleKeyDown}
96-
placeholder="SELECT * FROM ..."
97-
spellCheck={false}
98-
className="h-32 w-full resize-y bg-transparent p-3 font-mono text-sm text-zinc-200 placeholder:text-zinc-600 focus:outline-none"
172+
<div
173+
ref={containerRef}
174+
className="[&_.cm-editor]:min-h-32 [&_.cm-editor]:outline-none [&_.cm-scroller]:max-h-96 [&_.cm-scroller]:overflow-y-auto"
99175
/>
100176
</div>
101177
)

dashboard/src/components/sandbox/sandbox-editor.tsx

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"
1313
import { javascript } from "@codemirror/lang-javascript"
1414
import { bracketMatching } from "@codemirror/language"
1515
import {
16-
Compartment,
1716
EditorState,
1817
RangeSet,
1918
StateEffect,
@@ -41,8 +40,11 @@ import {
4140
focusHighlightField,
4241
setFocusLines,
4342
} from "./focus-highlight"
44-
import { gruvpuccinCmTheme } from "./themes/gruvpuccin-cm-theme"
45-
import { tokyoNightCmTheme } from "./themes/tokyo-night-cm-theme"
43+
import {
44+
createThemeCompartment,
45+
getActiveTheme,
46+
useCmPaletteObserver,
47+
} from "@/lib/cm-themes"
4648

4749
// ---------------------------------------------------------------------------
4850
// Gutter marker classes — each instance carries its diagnostic message
@@ -273,14 +275,7 @@ interface SandboxEditorProps {
273275
focusComponents?: string[] | null
274276
}
275277

276-
const themeCompartment = new Compartment()
277-
278-
function getActiveTheme() {
279-
if (typeof document === "undefined") return gruvpuccinCmTheme
280-
return document.documentElement.dataset.palette === "tokyo-night"
281-
? tokyoNightCmTheme
282-
: gruvpuccinCmTheme
283-
}
278+
const themeCompartment = createThemeCompartment()
284279

285280
/**
286281
* CodeMirror editor with TSX/JSX support, DSL autocompletion, diagnostic
@@ -407,22 +402,7 @@ export function SandboxEditor({
407402
}, [focusComponents])
408403

409404
// Watch for palette changes and reconfigure the theme compartment
410-
useEffect(() => {
411-
const observer = new MutationObserver(() => {
412-
const view = viewRef.current
413-
if (!view) return
414-
view.dispatch({
415-
effects: themeCompartment.reconfigure(getActiveTheme()),
416-
})
417-
})
418-
419-
observer.observe(document.documentElement, {
420-
attributes: true,
421-
attributeFilter: ["data-palette"],
422-
})
423-
424-
return () => observer.disconnect()
425-
}, [])
405+
useCmPaletteObserver(viewRef, themeCompartment)
426406

427407
return (
428408
<div

dashboard/src/components/sandbox/synthesis-output.tsx

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
foldGutter,
77
foldService,
88
} from "@codemirror/language"
9-
import { Compartment, EditorState } from "@codemirror/state"
9+
import { EditorState } from "@codemirror/state"
1010
import { EditorView, lineNumbers } from "@codemirror/view"
1111
import {
1212
Button,
@@ -40,21 +40,17 @@ import {
4040
setFocusLines,
4141
} from "./focus-highlight"
4242
import { SynthInspector } from "./synth-inspector"
43-
import { gruvpuccinCmTheme } from "./themes/gruvpuccin-cm-theme"
44-
import { tokyoNightCmTheme } from "./themes/tokyo-night-cm-theme"
43+
import {
44+
createThemeCompartment,
45+
getActiveTheme,
46+
useCmPaletteObserver,
47+
} from "@/lib/cm-themes"
4548

4649
// ---------------------------------------------------------------------------
4750
// Read-only CodeMirror viewer
4851
// ---------------------------------------------------------------------------
4952

50-
const themeCompartment = new Compartment()
51-
52-
function getActiveTheme() {
53-
if (typeof document === "undefined") return gruvpuccinCmTheme
54-
return document.documentElement.dataset.palette === "tokyo-night"
55-
? tokyoNightCmTheme
56-
: gruvpuccinCmTheme
57-
}
53+
const themeCompartment = createThemeCompartment()
5854

5955
/**
6056
* Fold service that tells CM6 where CREATE TABLE folds are possible.
@@ -255,22 +251,7 @@ function CodeViewer({
255251
])
256252

257253
// Watch for palette changes
258-
useEffect(() => {
259-
const observer = new MutationObserver(() => {
260-
const view = viewRef.current
261-
if (!view) return
262-
view.dispatch({
263-
effects: themeCompartment.reconfigure(getActiveTheme()),
264-
})
265-
})
266-
267-
observer.observe(document.documentElement, {
268-
attributes: true,
269-
attributeFilter: ["data-palette"],
270-
})
271-
272-
return () => observer.disconnect()
273-
}, [])
254+
useCmPaletteObserver(viewRef, themeCompartment)
274255

275256
return (
276257
<div

dashboard/src/components/sandbox/themes/gruvpuccin-cm-theme.ts renamed to dashboard/src/lib/cm-themes/gruvpuccin-cm-theme.ts

File renamed without changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export { gruvpuccinCmTheme } from "./gruvpuccin-cm-theme"
2+
export { tokyoNightCmTheme } from "./tokyo-night-cm-theme"
3+
export {
4+
createThemeCompartment,
5+
getActiveTheme,
6+
useCmPaletteObserver,
7+
} from "./use-cm-theme"

dashboard/src/components/sandbox/themes/tokyo-night-cm-theme.ts renamed to dashboard/src/lib/cm-themes/tokyo-night-cm-theme.ts

File renamed without changes.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { Compartment } from "@codemirror/state"
2+
import type { EditorView } from "@codemirror/view"
3+
import { useEffect } from "react"
4+
import { gruvpuccinCmTheme } from "./gruvpuccin-cm-theme"
5+
import { tokyoNightCmTheme } from "./tokyo-night-cm-theme"
6+
7+
/** Returns the CodeMirror theme matching the current palette. */
8+
export function getActiveTheme() {
9+
if (typeof document === "undefined") return gruvpuccinCmTheme
10+
return document.documentElement.dataset.palette === "tokyo-night"
11+
? tokyoNightCmTheme
12+
: gruvpuccinCmTheme
13+
}
14+
15+
/** Factory — each EditorState needs its own Compartment instance. */
16+
export function createThemeCompartment() {
17+
return new Compartment()
18+
}
19+
20+
/**
21+
* Observes `data-palette` mutations on `<html>` and reconfigures
22+
* the given theme compartment on the referenced EditorView.
23+
*/
24+
export function useCmPaletteObserver(
25+
viewRef: React.RefObject<EditorView | null>,
26+
themeCompartment: Compartment,
27+
) {
28+
useEffect(() => {
29+
const observer = new MutationObserver(() => {
30+
const view = viewRef.current
31+
if (!view) return
32+
view.dispatch({
33+
effects: themeCompartment.reconfigure(getActiveTheme()),
34+
})
35+
})
36+
observer.observe(document.documentElement, {
37+
attributes: true,
38+
attributeFilter: ["data-palette"],
39+
})
40+
return () => observer.disconnect()
41+
}, [viewRef, themeCompartment])
42+
}

0 commit comments

Comments
 (0)