// MarkdownSourceEditor.jsx — the Contribute-mode raw markdown source editor. // // CodeMirror 6 per §18 (Phase 1 of the Contribute rewrite). The editing // surface is plain markdown; the rendered preview lives in a sibling pane // (added in Phase 2). No HTML→markdown round-trip — `getDoc()` returns // the source verbatim, which is what we POST in §8.11's manual flush. // // The parent receives an imperative handle on `editorRef`: // { view, getDoc(), setDoc(text) } // modeled lightly after the Tiptap surface so RFCView can branch on // whichever editor is mounted without an adapter layer. import { useEffect, useRef } from 'react' import { EditorState } from '@codemirror/state' import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter } from '@codemirror/view' import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands' import { markdown } from '@codemirror/lang-markdown' import { syntaxHighlighting, defaultHighlightStyle, indentOnInput, bracketMatching } from '@codemirror/language' import { diffGutterExtension, setBaseline } from './diffGutterExtension.js' export default function MarkdownSourceEditor({ initialDoc = '', editorRef, onUpdate, }) { const hostRef = useRef(null) const viewRef = useRef(null) const onUpdateRef = useRef(onUpdate) useEffect(() => { onUpdateRef.current = onUpdate }, [onUpdate]) useEffect(() => { if (!hostRef.current) return const state = EditorState.create({ doc: initialDoc, extensions: [ history(), diffGutterExtension(), lineNumbers(), highlightActiveLine(), highlightActiveLineGutter(), indentOnInput(), bracketMatching(), keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]), markdown(), syntaxHighlighting(defaultHighlightStyle), EditorView.lineWrapping, EditorView.updateListener.of(u => { if (u.docChanged) { onUpdateRef.current?.(u.state.doc.toString()) } }), ], }) const view = new EditorView({ state, parent: hostRef.current }) // Seed the §8.10 gutter baseline to the initial doc — nothing // diverges at construct time. Subsequent baseline resets ride the // initialDoc-watching effect below so they dispatch atomically // with the doc replacement. view.dispatch({ effects: setBaseline.of(initialDoc ?? '') }) viewRef.current = view if (editorRef) { editorRef.current = { view, getDoc: () => view.state.doc.toString(), setDoc: (text) => { const cur = view.state.doc.toString() if (cur === text) return view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: text }, }) }, } } return () => { view.destroy() viewRef.current = null if (editorRef && editorRef.current?.view === view) editorRef.current = null } // Construct once. Doc changes flow through the setDoc effect below. // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // Reflect external doc changes (branch reload, server-side flush, etc). // The §8.10 gutter baseline is reset in the same dispatch so the // newly-pushed doc doesn't briefly read as "divergent" against the // old baseline. useEffect(() => { const v = viewRef.current if (!v) return if (v.state.doc.toString() === initialDoc) return v.dispatch({ changes: { from: 0, to: v.state.doc.length, insert: initialDoc ?? '' }, effects: setBaseline.of(initialDoc ?? ''), }) }, [initialDoc]) return
}