Math rendering for Markdown preview using RaTeX - #58593
Conversation
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
recording_20260605_055740.mp4 |
|
Closing since this is a duplicate of #57339 which we are already reviewing. |
|
I built this PR locally and fixed a bug related to KaTeX fonts; it works exactly as expected!
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 |
|
There are three competing RaTeX implementation pull requests:
But keep in mind that LaTeX support was already closed in 2024 as "not planned": 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: 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. |

Summary
Renders inline (
$x^2$) and display ($$x = y$$) LaTeX math inMarkdown 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
InlineReplacementAPI ingpui/text_system/line_layout.rs. Themarkdown side fills a
U+FFFCplaceholder with the math'spre-computed
width/ascent/descentand paints the realglyphs at the resolved positions.
InlineReplacementis generic, not math-specific. It can back anyinline 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:GpuiMathBackend(closed: Windows breakage)proportion for right now", ConradIrwin)
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 newlatex_rendercrate instead.What changed
gpui(+234 lines) — newInlineReplacementtype,layout_*_with_replacementscache-aware variants, post-processfunction 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 existingSegoe Fluent Iconsentriesso they pass the charmap sanity check.
markdown(+1832 lines) — newmath.rs(DisplayList → GPUIprimitives, background layout,
OnceLockcache), parser pre-processor for multi-line
$$...$$blocks, integration asU+FFFCplaceholders withInlineReplacementmetrics. Includes12 new unit tests for math helpers and cache fallback.
markdown_preview(+1 line) — setrender_math: true.Testing
cargo test -p markdown: 123 passedcargo test -p gpui: 169 passedcargo fmt --checkandcargo clippycleanManually 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: