C.W.K.
Stream
Lesson 02 of 05 · published

Job One — Live Preview

~11 min · live-preview, mark-decoration, job-one, cm6

Level 0Cold Draft
0 XP0/33 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Live Preview is the decoration engine's first job: render the look over the source, and reveal the raw markup where the cursor is."

Job One — Render the Look Over the Source

Live Preview is the most visible of the three jobs and, mechanically, the simplest: walk the Markdown syntax, and for each construct attach mark decorations that render its look. A **fire** gets a mark that renders bold; a # heading gets marks that enlarge the text and dim the hash. Nothing is inserted or deleted — the source is exactly the Markdown you typed, wearing a rendered coat.

The Active-Line Reveal

The behavior that makes it feel alive is the active-line reveal. On lines your cursor isn't on, the markup is rendered (bold looks bold, the # is dimmed). On the line your cursor is on, the raw markup snaps back so you can edit it precisely. The decoration set is simply recomputed with the cursor's line excluded from the "render" treatment. Read a clean document; edit exact source; the boundary follows your attention.

function buildLivePreview(state) {
  const cursorLine = state.doc.lineAt(state.selection.main.head).number;
  const marks = [];
  for (const node of parseMarkdown(state.doc)) {
    if (node.line === cursorLine) continue;   // reveal raw markup on the active line
    marks.push(renderMark(node));             // render the look everywhere else
  }
  return Decoration.set(marks, true);
}

All Marks, No Mutation

Every bit of Live Preview is mark decorations over unchanged source. That's why it can coexist with everything else: turn it off and the document is byte-identical Markdown; turn it on and you've added a layer, not a model. It's also why this is the same engine as the next two jobs — voice underline and CMD+K are the same 'compute ranges, attach decorations' shape, just with different ranges and, for CMD+K, a widget instead of a mark. Job one is the gentle introduction to a mechanism the whole soul layer rides.

Code

Live Preview: marks everywhere except the cursor's line·typescript
import { Decoration } from "@codemirror/view";

function buildLivePreview(state) {
  const cursorLine = state.doc.lineAt(state.selection.main.head).number;
  const marks = [];

  for (const node of parseMarkdown(state.doc)) {
    // On the active line, reveal raw markup so you can edit it precisely.
    if (node.line === cursorLine) continue;
    // Everywhere else, render the look with a mark decoration.
    marks.push(renderMark(node)); // e.g. bold, dimmed #, larger heading
  }

  return Decoration.set(marks, /* sort */ true);
}

// Source is untouched. Toggle this off and you have byte-identical Markdown.

External links

Exercise

Describe, in ranges, what Live Preview does to the line ## The **fire** catches when your cursor is NOT on it, and when it IS. List which character ranges get a mark and which get revealed. Notice that both states are the same source string — only the decoration set differs.
Hint
Cursor off: mark the ## dimmed, enlarge the heading text, render **fire** bold (asterisks hidden). Cursor on: no marks — the raw ## The **fire** catches is shown for editing. Same string, two decoration sets.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.