import { useEffect, useRef } from "react"; import { EditorState, Compartment } from "@codemirror/state"; import { EditorView, keymap } from "@codemirror/view"; import { indentWithTab } from "@codemirror/commands"; import { basicSetup } from "codemirror"; import { oneDark } from "@codemirror/theme-one-dark"; import { languages } from "@codemirror/language-data"; import { LanguageDescription } from "@codemirror/language"; const LINE_HEIGHT_EM = 1.5; const theme = EditorView.theme( { "&": { backgroundColor: "rgb(3 7 18)", fontSize: "13px", minHeight: "var(--editor-min-height)" }, ".cm-scroller": { fontFamily: "var(--font-mono)", lineHeight: "1.5", minHeight: "var(--editor-min-height)" }, ".cm-content": { fontFamily: "var(--font-mono)", padding: "12px 0" }, ".cm-gutters": { backgroundColor: "rgb(3 7 18)", borderRight: "1px solid rgb(31 41 55)", color: "rgb(75 85 99)" }, ".cm-activeLineGutter, .cm-activeLine": { backgroundColor: "rgb(255 255 255 / 0.03)" }, "&.cm-focused": { outline: "none" }, }, { dark: true }, ); type Props = { value: string; onChange: (value: string) => void; filename: string; minLines?: number; id?: string; }; /** CodeMirror 6 editor with line numbers and a language loaded from the file name; the view owns the text. */ export function CodeEditor({ value, onChange, filename, minLines = 28, id }: Props) { const host = useRef(null); const view = useRef(null); const language = useRef(new Compartment()); const latest = useRef(onChange); latest.current = onChange; const initial = useRef(value); useEffect(() => { if (!host.current) return; const editor = new EditorView({ parent: host.current, state: EditorState.create({ doc: initial.current, extensions: [ basicSetup, keymap.of([indentWithTab]), theme, oneDark, language.current.of([]), EditorView.updateListener.of((update) => { if (update.docChanged) latest.current(update.state.doc.toString()); }), ], }), }); view.current = editor; return () => { editor.destroy(); view.current = null; }; }, []); useEffect(() => { const editor = view.current; if (!editor) return; const current = editor.state.doc.toString(); if (current !== value) editor.dispatch({ changes: { from: 0, to: current.length, insert: value } }); }, [value]); useEffect(() => { const editor = view.current; if (!editor) return; const description = LanguageDescription.matchFilename(languages, filename); if (!description) { editor.dispatch({ effects: language.current.reconfigure([]) }); return; } let cancelled = false; description.load().then((support) => { if (!cancelled && view.current) view.current.dispatch({ effects: language.current.reconfigure(support) }); }); return () => { cancelled = true; }; }, [filename]); useEffect(() => { host.current?.style.setProperty("--editor-min-height", `${minLines * LINE_HEIGHT_EM}em`); }, [minLines]); return
; }