1import { useEffect, useRef } from 'react'
2import { EditorView, basicSetup } from 'codemirror'
3import { EditorState, Compartment } from '@codemirror/state'
4import { keymap } from '@codemirror/view'
5import { indentUnit, StreamLanguage } from '@codemirror/language'
6import { octave } from '@codemirror/legacy-modes/mode/octave'
7import { oneDark } from '@codemirror/theme-one-dark'
9export interface ScriptEditorProps {
10 value: string
11 onChange: (value: string) => void
12 /** Ctrl/Cmd+Enter */
13 onRun: () => void
14}
16const readOnlyCompartment = new Compartment()
18export default function ScriptEditor({ value, onChange, onRun }: ScriptEditorProps) {
19 const hostRef = useRef<HTMLDivElement>(null)
20 const viewRef = useRef<EditorView | null>(null)
21 // Keep the latest callbacks without recreating the editor
22 const onChangeRef = useRef(onChange)
23 onChangeRef.current = onChange
24 const onRunRef = useRef(onRun)
25 onRunRef.current = onRun
27 useEffect(() => {
28 if (!hostRef.current) return
29 const view = new EditorView({
30 parent: hostRef.current,
31 state: EditorState.create({
32 doc: value,
33 extensions: [
34 basicSetup,
35 keymap.of([
36 {
37 key: 'Mod-Enter',
38 run: () => {
39 onRunRef.current()
40 return true
41 },
42 },
43 ]),
44 StreamLanguage.define(octave),
45 oneDark,
46 indentUnit.of(' '),
47 EditorView.updateListener.of((update) => {
48 if (update.docChanged) onChangeRef.current(update.state.doc.toString())
49 }),
50 readOnlyCompartment.of([]),
51 ],
52 }),
53 })
54 viewRef.current = view
55 return () => {
56 view.destroy()
57 viewRef.current = null
58 }
59 // The editor is created once; external value changes are synced below.
60 // eslint-disable-next-line react-hooks/exhaustive-deps
61 }, [])
63 // Sync external value changes (example loaded, etc.) into the editor
64 useEffect(() => {
65 const view = viewRef.current
66 if (!view) return
67 const current = view.state.doc.toString()
68 if (current !== value) {
69 view.dispatch({ changes: { from: 0, to: current.length, insert: value } })
70 }
71 }, [value])
73 return <div className="editor-host" ref={hostRef} />
74}