ui: syntax-highlight fenced code blocks, every surface - #85
Conversation
Language-specific colours for ```lang fences in the reading view and the
editor, from one engine and one palette.
The dependency is CodeMirror's own registry, @codemirror/language-data —
the pragmatic pick because the editor is already CodeMirror. Handing its
resolver to markdown({ codeLanguages }) parses a fence's body with that
language's grammar; running the same grammars over the reading view's
<pre><code> keeps read and edit identical, which a standalone
highlighter (highlight.js, Shiki) could not. Grammars are lazily
imported one chunk per language, so a vault of plain prose loads none of
them and the entry bundle is unchanged.
- ui/src/highlight.ts: the shared resolver (fuzzy alias matching, minus a
guard so ```text doesn't fuzzy-match TeX), a tagHighlighter mapping
Lezer tags to a `tok-*` vocabulary — ours rather than the stock
classHighlighter, which covers neither tagName nor attributeName — and
the reading view's post-render DOM pass.
- style.css: the `tok-*` palette in light and dark, scoped to code on
both sides so live preview keeps owning the Markdown's own tokens.
- escape.ts: escapeHtml moved out of render.ts (re-exported there) so
highlight.ts, and its node test, don't pull in the view layer.
Nothing here changes a byte of a note: the reading view's pass re-escapes
every text run on the way back in, and the editor's colours are
decorations over an untouched buffer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuFQeYyBA4buEeBnRQ9rMx
📝 WalkthroughWalkthroughThe UI adds CodeMirror-based fenced-code highlighting with shared language resolution and token styling across reading and editing surfaces. It introduces HTML escaping, asynchronous post-render painting, theme variables, grammar dependencies, and focused highlighting tests. ChangesFenced-code highlighting
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant NoteRender
participant HighlightPass
participant CodeMirror
participant CodeBlock
NoteRender->>HighlightPass: schedule fence painting
HighlightPass->>CodeMirror: resolve language and load grammar
CodeMirror-->>HighlightPass: return highlighted escaped HTML
HighlightPass->>CodeBlock: replace connected block contents
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@ui/src/main.ts`:
- Around line 1604-1614: Update livePreviewConf in ui/src/main.ts at lines
1604-1614 to register b2Highlighter when state.sourceOpen, retaining
defaultHighlightStyle only as the fallback if needed. Extend the token styling
in ui/style.css at lines 1019-1072 to apply the resulting tok-* classes in
source-editor mode as well as reading view and .lp-fence live-preview blocks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: fddb86f9-06fb-4181-bf04-c39e5c4cc208
⛔ Files ignored due to path filters (1)
ui/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (8)
CLAUDE.mdui/package.jsonui/src/escape.tsui/src/highlight.test.tsui/src/highlight.tsui/src/main.tsui/src/render.tsui/style.css
Conflict: ui/package.json's `test` script. main replaced the explicit per-file list with node's own runner globbing src/*.test.ts — took main's, which is strictly better here (highlight.test.ts is picked up without registration, and can never be silently skipped).
Source mode (`</>`) was still on CodeMirror's stock defaultHighlightStyle,
so it missed the tok-* palette the reading view and live preview share —
and, now that codeLanguages nests real grammars, its fence bodies came out
in a light-only palette that reads as ink-on-ink against the dark theme.
Register b2Highlighter there instead, and drop defaultHighlightStyle: with
another highlighter registered its {fallback: true} would never apply, so
keeping it would have been dead weight.
What differs between the surfaces is now purely CSS reach, and the scope
names say so: source mode takes the palette document-wide via a new
`src-body` content class (the symmetric twin of live preview's `lp-body`),
while live preview stays bounded to `.lp-fence` — the Markdown's own markup
there is already spoken for by the `.lp-*` decorations.
That makes Markdown's marks a source-mode surface rather than a code-only
one, so `processingInstruction` moves from tok-keyword to tok-punct: it is
what lezer-markdown tags `#`, `**` and the fences with, and keyword-purple
markup drowns the prose. highlight.test.ts now pins the class pairs the
CSS rule order depends on, and parses through the editor's own configured
parser so the codeLanguages wiring is covered end to end.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuFQeYyBA4buEeBnRQ9rMx
An empty 81-byte lockfile ("name": "ui", no packages) — the artifact of
an `npm install` run from the wrong directory, committed by accident in
d8a82dc. The real one is ui/package-lock.json; nothing reads this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuFQeYyBA4buEeBnRQ9rMx
Language-specific colours for ```lang fences in the reading view and the
editor, from one engine and one palette.
The dependency is CodeMirror's own registry, @codemirror/language-data —
the pragmatic pick because the editor is already CodeMirror. Handing its
resolver to markdown({ codeLanguages }) parses a fence's body with that
language's grammar; running the same grammars over the reading view's