Skip to content
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/44 lessons0/13 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 most direct: walk the Markdown syntax, and for each construct attach mark decorations that render its look, plus replace decorations where a markup token should disappear from view. A **fire** gets a mark that renders bold and replace decorations that hide its asterisks; 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 text styling stays put; the hidden markup tokens return. The decoration set is 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) { marks.push(revealTokens(node)); continue; }  // active line: styling stays, hidden markup tokens return
    marks.push(renderMark(node));             // render the look everywhere else
  }
  return Decoration.set(marks, true);
}

Marks and Replacements, No Mutation

Every bit of Live Preview is decorations over unchanged source — marks that style, replace decorations that hide a token from view while it stays in the text. 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: styled everywhere; the cursor's line reveals its tokens·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) { marks.push(revealTokens(node)); continue; }  // styling stays; hidden markup tokens return
    // 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: the styling stays and the hidden markup tokens return — the raw ## The **fire** catches is editable. 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 in — Please sign in to comment.

No comments yet — be the first.