Skip to content

Math rendering for Markdown preview using RaTeX - #58593

Closed
mengh04 wants to merge 6 commits into
zed-industries:mainfrom
mengh04:math-rendering
Closed

Math rendering for Markdown preview using RaTeX#58593
mengh04 wants to merge 6 commits into
zed-industries:mainfrom
mengh04:math-rendering

Conversation

@mengh04

@mengh04 mengh04 commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Renders inline ($x^2$) and display ($$x = y$$) LaTeX math in
Markdown preview using RaTeX.
Math is painted with GPUI's native text primitives, not rasterized
to an image, so it participates in line wrapping, text selection,
and theming.

Why this changes GPUI

GPUI's text layout today only handles plain characters. Inline math
needs a way to say "this byte range isn't a real character, it has
these specific metrics, paint a glyph there" — that's the new
InlineReplacement API in gpui/text_system/line_layout.rs. The
markdown side fills a U+FFFC placeholder with the math's
pre-computed width / ascent / descent and paints the real
glyphs at the resolved positions.

InlineReplacement is generic, not math-specific. It can back any
inline object (emoji glyphs, syntax-highlighted characters, etc.).

Why not the image-embedding route

Three prior math PRs took the "render to PNG/SVG, embed via
img(...)" path:

Image embedding is correct but loses three things native text gives
us for free: text selection, copy-paste of the LaTeX source, and
zero-cost theme switching. The cross-crate cost here is ~234 lines in
gpui; the prior PRs added a new latex_render crate instead.

What changed

  • gpui (+234 lines) — new InlineReplacement type,
    layout_*_with_replacements cache-aware variants, post-process
    function that shifts subsequent glyphs and bumps line ascent/
    descent. Old APIs delegate to the new ones with an empty slice,
    so behavior is unchanged for unrelated callers.
  • gpui_wgpu (+24 lines) — allow-list the KaTeX symbol faces
    (KaTeX_*) alongside the existing Segoe Fluent Icons entries
    so they pass the charmap sanity check.
  • markdown (+1832 lines) — new math.rs (DisplayList → GPUI
    primitives, background layout, OnceLock cache), parser pre-
    processor for multi-line $$...$$ blocks, integration as
    U+FFFC placeholders with InlineReplacement metrics. Includes
    12 new unit tests for math helpers and cache fallback.
  • markdown_preview (+1 line) — set render_math: true.

Testing

  • cargo test -p markdown: 123 passed

  • cargo test -p gpui: 169 passed

  • cargo fmt --check and cargo clippy clean

  • Manually verified: display math, inline math in prose, multi-line
    display, Chinese adjacent to math, light/dark theme

  • I've reviewed my own diff for quality, security, and reliability

  • Unsafe blocks (if any) have justifying comments ← N/A, 0 unsafe

  • The content is consistent with the UI/UX checklist

  • Tests cover the new/changed behavior

  • Performance impact has been considered and is acceptable
    (one known follow-up documented above)

Release Notes:

  • Added inline and display math rendering to Markdown preview

mengh04 added 6 commits June 5, 2026 06:32
ratex-font, ratex-katex-fonts, ratex-layout, ratex-parser, and
ratex-types are KaTeX-compatible math typesetting crates that
the markdown crate uses to render LaTeX expressions to a
platform-neutral DisplayList.
GPUI's text layout previously only handled plain characters. To
embed math expressions inline, we need a way to say "this byte
range in the text isn't a real character — it has these specific
metrics, place a glyph there on paint."

The new InlineReplacement struct carries:
- range: the UTF-8 byte range (usually one U+FFFC placeholder)
- width, ascent, descent: the replacement's metrics

The shape pipeline (layout_line / layout_wrapped_line /
shape_text) gained new _with_replacements variants that thread
the replacements through to the platform shaping layer. After
shaping, apply_inline_replacements_to_layout walks each
replacement: finds the corresponding shaped glyph, computes the
delta between the shaped advance and the replacement's width, and
shifts every glyph after the replacement by that delta. The
line's ascent/descent are bumped if the replacement is taller
than the surrounding text.

Old APIs (layout_line, layout_wrapped_line, shape_text) delegate
to the new ones with an empty replacement slice, so behavior for
existing callers is unchanged. The cache key includes
inline_replacements so the same text with a different replacement
vector doesn't share a cache entry.
The cosmic-text font loader has a sanity check that filters out
fonts without the 'm' glyph (used as a basic 'is this a usable
text font' test). KaTeX's symbol faces (AMS, Caligraphic,
Fraktur, SansSerif, Script, Size1–4) don't include Latin
characters, so they trip the check. Allow-list the KaTeX PostScript
names alongside the existing Segoe Fluent Icons entries.
math.rs is the main implementation: DisplayList → GPUI canvas
painter, KaTeX font registration, per-expression OnceLock cache,
the U+FFFC placeholder integration, and 12 unit tests for font
mapping, color resolution, fallback decisions, and cache
behavior.

DisplayList maps 1:1 to GPUI primitives:
- Rect     → paint_quad
- Line     → thin paint_quad (solid) or paint_path (dashed)
- Path     → paint_path (Quad/Cubic/QuadTo/Close)
- GlyphPath → shape_line (KaTeX font) + paint_glyph

The math expression cache is per-content; the first paint
kicks off a background task to run ratex parser + layout, and
subsequent paints read from the cache. If the layout task
fails, the cache falls back to the previous frame's display
list so a parse error in one expression doesn't blank the
whole preview.
pulldown-cmark treats block-level constructs (lists, block
quotes) before math, so a '+' at the start of a line inside
a $$\n...\n$$ block would tear the block in two and the
inner lines would be parsed as list items. Pre-collapse the
newlines inside $$...$$ blocks into U+2060 (WORD JOINER),
which is invisible to markdown parsing, and translate the
resulting event ranges back to the original source positions
via an offset map.

Without this, both parsing of the math event and the URL
autolinking path that peeks at upcoming events would slice the
source at byte offsets pointing into the preprocessed text —
which can land inside a multi-byte character (Chinese
text adjacent to math was the panic case) or past the end of
the source.
Wire math rendering into the markdown rendering pipeline:
- MarkdownOptions gains render_math: bool
- Markdown::parse extracts math expressions from the parsed
  event stream and stores them in ParsedMarkdown
- MarkdownElementBuilder turns InlineMath into a U+FFFC
  placeholder with an InlineReplacement carrying the math's
  pre-computed metrics (width / ascent / descent)
- DisplayMath becomes a centered div containing a canvas()
  element that paints the math at its natural size
- Line height is grown if the math expression is taller than
  the surrounding text
- markdown_preview_view sets render_math: true so the preview
  pane shows math by default
@cla-bot cla-bot Bot added the cla-signed The user has signed the Contributor License Agreement label Jun 4, 2026
@zed-community-bot zed-community-bot Bot added the first contribution the author's first pull request to Zed. NOTE: the label application is automated via github actions label Jun 4, 2026
@mengh04

mengh04 commented Jun 4, 2026

Copy link
Copy Markdown
Contributor Author
recording_20260605_055740.mp4

@yara-blue

Copy link
Copy Markdown
Contributor

Closing since this is a duplicate of #57339 which we are already reviewing.

@yara-blue yara-blue closed this Jun 5, 2026
@mengh04
mengh04 deleted the math-rendering branch July 10, 2026 05:43
@wesleyel

Copy link
Copy Markdown

I built this PR locally and fixed a bug related to KaTeX fonts; it works exactly as expected!

image

For those who want to give it a try but don't want to spend 30 minutes building it—only to run into multiple LTO out-of-memory errors—you might want to try this release.

https://github.com/wesleyel/zed/releases/tag/v1.17.0-math-rendering

@Arcitec

Arcitec commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

There are three competing RaTeX implementation pull requests:

But keep in mind that LaTeX support was already closed in 2024 as "not planned":

#11126

And confirmed again in 2026 as "out of scope and not on our roadmap" because Zed is a code editor. It's not a LaTeX math formula renderer:

#54668 (comment)

But it was reopened (without comment), so maybe there's a chance. If so, someone will have to take the time to figure out which of all the pull requests is the cleanest solution. And more importantly: Explain why Zed needs to render math formulas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed The user has signed the Contributor License Agreement first contribution the author's first pull request to Zed. NOTE: the label application is automated via github actions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants