Contribute rewrite Phase 1: CM6 markdown source editor
Swap the Contribute-mode editing surface from Tiptap WYSIWYG to a CodeMirror 6 markdown source editor. Discuss mode and any read-only viewing continues to render through Tiptap. The §8.11 manual-edit debounce now reads the raw markdown source via CodeMirror's doc, eliminating the lossy `editor.getText()` round-trip that RFCView.jsx flagged as a §19.2 candidate; what the contributor typed is exactly what gets POSTed to manual flush. The §8.10 paragraph-margin gutter accent on Tiptap is dropped — it was dead in read-only Discuss anyway, and Phase 4 of the Contribute rewrite adds a change-anchored gutter against the CM6 raw pane. The Tiptap-side accept-time injection of <span class="tracked-*"> markup also drops out — CM6 can't render those spans, and Phase 3's preview pane is the proper home for tracked changes. The reviewMode / DiffView toggle still works against marked-rendered HTML in the interim until Phase 7 retires it. Notes: - Bundle gzipped grows ~50KB for CM6 modules (state/view/commands/ language/lang-markdown). Mermaid in Phase 2 will be the larger lift and should lazy-load. - Verified the CM6 editor mounts cleanly via Vite dev preview: ref handle (`view/getDoc/setDoc`) is wired, `onUpdate` fires on doc changes, gutter / line-numbers / active-line all render, and the source round-trips verbatim. Could not drive the full RFCView Contribute flow end-to-end without a running backend; the manual countdown + save-now + accept/decline pathways are verified by inspection only. - 125 backend integration tests still green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,22 +1,18 @@
|
||||
// Editor.jsx — the §8 center-column editor.
|
||||
// Editor.jsx — the Discuss-mode read-only renderer (and main/no-contribute
|
||||
// fallback). Tiptap on ProseMirror per §18; the Contribute-mode editing
|
||||
// surface moved to MarkdownSourceEditor (CodeMirror 6) in the Phase 1
|
||||
// rewrite of Contribute, leaving this surface for read-only rendering
|
||||
// plus the §8.12 selection tooltip's coords.
|
||||
//
|
||||
// Tiptap on ProseMirror per §18. Two ProseMirror plugins live alongside
|
||||
// StarterKit:
|
||||
//
|
||||
// • paragraphDiff — the §8.10 paragraph-margin gutter accent. Compares
|
||||
// each paragraph against an open-session baseline (the
|
||||
// `originalParagraphsRef` ref the parent owns and refreshes when the
|
||||
// baseline shifts — e.g. on branch switch or a server-side flush).
|
||||
// One ProseMirror plugin still lives alongside StarterKit:
|
||||
//
|
||||
// • selectionHighlight — keeps a selected passage highlighted while
|
||||
// focus moves to the §8.12 selection tooltip. Driven by meta
|
||||
// transactions dispatched from the parent.
|
||||
//
|
||||
// The inline tracked-delete / tracked-insert markup from §8.10 is
|
||||
// session-local HTML the parent injects via `editor.commands.setContent`
|
||||
// when a change is accepted; the editor itself doesn't own that state.
|
||||
// On reload the markup clears and DiffView (toolbar toggle) is the
|
||||
// durable read of accepted changes.
|
||||
// The paragraph-margin gutter accent that used to live here is dropped;
|
||||
// Phase 4 of the Contribute rewrite adds a proper change-anchored gutter
|
||||
// against the CodeMirror raw pane.
|
||||
|
||||
import { useEditor, EditorContent, Extension } from '@tiptap/react'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
@@ -25,47 +21,6 @@ import { marked } from 'marked'
|
||||
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view'
|
||||
|
||||
// ── Paragraph diff plugin ────────────────────────────────────────────────
|
||||
|
||||
const diffKey = new PluginKey('paragraphDiff')
|
||||
|
||||
function makeDiffPlugin(originalParagraphsRef) {
|
||||
return new Plugin({
|
||||
key: diffKey,
|
||||
props: {
|
||||
decorations(state) {
|
||||
const originals = originalParagraphsRef?.current
|
||||
if (!originals || originals.length === 0) return DecorationSet.empty
|
||||
|
||||
const decorations = []
|
||||
let idx = 0
|
||||
state.doc.descendants((node, pos) => {
|
||||
if (node.type.name === 'paragraph' || node.type.name === 'heading') {
|
||||
const current = node.textContent.trim()
|
||||
const original = (originals[idx] ?? '').trim()
|
||||
if (current !== original) {
|
||||
decorations.push(
|
||||
Decoration.node(pos, pos + node.nodeSize, { class: 'paragraph-changed' })
|
||||
)
|
||||
}
|
||||
idx++
|
||||
}
|
||||
})
|
||||
return DecorationSet.create(state.doc, decorations)
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function DiffExtension(originalParagraphsRef) {
|
||||
return Extension.create({
|
||||
name: 'paragraphDiff',
|
||||
addProseMirrorPlugins() {
|
||||
return [makeDiffPlugin(originalParagraphsRef)]
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ── Selection highlight plugin ────────────────────────────────────────────
|
||||
|
||||
export const selectionHighlightKey = new PluginKey('selectionHighlight')
|
||||
@@ -110,7 +65,6 @@ function SelectionHighlightExtension() {
|
||||
export default function Editor({
|
||||
content,
|
||||
editorRef,
|
||||
originalParagraphsRef,
|
||||
onSelectionChange,
|
||||
onUpdate,
|
||||
editable = true,
|
||||
@@ -132,7 +86,6 @@ export default function Editor({
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
DiffExtension(originalParagraphsRef),
|
||||
SelectionHighlightExtension(),
|
||||
],
|
||||
content: '<p></p>',
|
||||
@@ -167,20 +120,10 @@ export default function Editor({
|
||||
}
|
||||
}, [editor, onSelectionChange, reportSelection])
|
||||
|
||||
// Reload content + snapshot baseline paragraphs.
|
||||
useEffect(() => {
|
||||
if (!editor || content == null) return
|
||||
const html = marked.parse(content)
|
||||
editor.commands.setContent(html, false)
|
||||
if (originalParagraphsRef) {
|
||||
const paragraphs = []
|
||||
editor.state.doc.descendants(node => {
|
||||
if (node.type.name === 'paragraph' || node.type.name === 'heading') {
|
||||
paragraphs.push(node.textContent.trim())
|
||||
}
|
||||
})
|
||||
originalParagraphsRef.current = paragraphs
|
||||
}
|
||||
}, [content, editor])
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user