// diffGutterExtension.js — Phase 4 of the Contribute rewrite. Restores // §8.10's paragraph-margin gutter accent against the CM6 raw pane in // the Contribute split, the surface where editing happens. The matching // inline tracked-change overlay (Phase 3) lives in the preview pane on // the right; the two visual layers answer different questions — // "did anything change in this region?" (gutter) vs. "what changed // here?" (inline) — and live on the two halves of the split for that // reason. Discuss mode has no editor, so no gutter. // // Surface model // ------------- // CM6's natural unit is the line, not the paragraph, so the marker is // per-line: every line whose text differs from the same-indexed line of // the open-session baseline gets a thin amber bar in its own gutter // (sat left of the line-numbers gutter). The spec talks // "paragraph-margin marker"; collapsing line-marks back to // paragraph-block ranges is more spec-faithful but adds code and risks // hiding small inline changes inside large paragraphs. Per-line is the // pre-fancy stance — a future pass can collapse if it reads noisy. // // Baseline reset // -------------- // Baseline = the last server-confirmed branch body. Initialized to // `initialDoc` at editor construct; re-set via the `setBaseline` effect // on accept / decline / manual flush / branch switch — i.e. every time // the parent re-pushes `initialDoc`. The marker thus reads as // "what's in the buffer but not on the server." // // The reset must dispatch atomically with the doc replacement — // otherwise there's a one-frame window where the new doc differs from // the old baseline and every line lights up. MarkdownSourceEditor's // initialDoc effect bundles both into a single dispatch. // // Caveats kept deliberately // ------------------------- // Insert/delete shifts. Adding a line in the middle shifts every // subsequent line's index and the naive index-compare lights up // everything below the insertion. We tolerate it: the gutter is a // scannable cue, not a precision diff, and "you've touched stuff below // this point" remains honest. A future pass against a proper LCS diff // could anchor the marks to stable lines, but Phase 4 doesn't need it. import { StateEffect, StateField } from '@codemirror/state' import { GutterMarker, gutter } from '@codemirror/view' export const setBaseline = StateEffect.define() class DiffMarker extends GutterMarker { toDOM() { const el = document.createElement('div') el.className = 'cm-diff-gutter-mark' return el } } const diffMarker = new DiffMarker() // Baseline doc as a frozen line array. We split once on reset rather // than on every gutter query — the doc itself can change far more // often than the baseline. const baselineField = StateField.define({ create: () => [], update(value, tr) { for (const e of tr.effects) { if (e.is(setBaseline)) return splitLines(e.value || '') } return value }, }) function splitLines(text) { return (text ?? '').split('\n') } export function diffGutterExtension() { return [ baselineField, gutter({ class: 'cm-diff-gutter', lineMarker(view, line) { const baseline = view.state.field(baselineField, false) if (!baseline || baseline.length === 0) return null const lineNumber = view.state.doc.lineAt(line.from).number const baselineLine = baseline[lineNumber - 1] const currentLine = view.state.doc.lineAt(line.from).text if (baselineLine === undefined) return diffMarker return currentLine === baselineLine ? null : diffMarker }, lineMarkerChange(update) { if (update.docChanged) return true for (const tr of update.transactions) { for (const e of tr.effects) if (e.is(setBaseline)) return true } return false }, }), ] }