import { useEffect, useRef } from 'react' import { EditorView, keymap } from '@codemirror/view' import { EditorState } from '@codemirror/state' import { indentWithTab } from '@codemirror/commands' import { markdown } from '@codemirror/lang-markdown' import { basicSetup } from 'codemirror' interface Props { value: string onChange: (value: string) => void } // Thin React wrapper around a CodeMirror 6 editor. The editor owns the text // while the user types; `value` is only pushed in when it differs from the // editor's contents (i.e. on external changes like a hash edit). export default function CodeMirrorEditor({ value, onChange }: Props) { const containerRef = useRef(null) const viewRef = useRef(null) const onChangeRef = useRef(onChange) onChangeRef.current = onChange useEffect(() => { if (!containerRef.current) return const view = new EditorView({ state: EditorState.create({ doc: value, extensions: [ basicSetup, keymap.of([indentWithTab]), markdown(), EditorView.lineWrapping, EditorView.updateListener.of((update) => { if (update.docChanged) { onChangeRef.current(update.state.doc.toString()) } }), EditorView.theme({ '&': { height: '100%', fontSize: '13px' }, '.cm-scroller': { fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace", }, }), ], }), parent: containerRef.current, }) viewRef.current = view return () => { view.destroy() viewRef.current = null } // The editor is created once; `value` afterwards flows through the effect below. // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { const view = viewRef.current if (!view) return const current = view.state.doc.toString() if (current !== value) { view.dispatch({ changes: { from: 0, to: current.length, insert: value }, }) } }, [value]) return
}