7.7 KiB
Markdown view — comparison with bezel and gpuikit
Peer read for API ideas only; nothing ported, nothing depended on
(AGENTS.md). Assessed 2026-11 against main of
bezel (crates/markdown) and
gpuikit (src/markdown). Both move
fast and bezel's APIs are explicitly still evolving, so details may have
shifted when this is revisited. Queue entry:
CHECKLIST.md — "Markdown view follow-ups".
Positioning — three different products
| gpui-native-kit (ours) | bezel | gpuikit | |
|---|---|---|---|
| Ambition | Read-only document viewer | Editable Notion-style document engine, markdown as wire form | Streaming reader built for LLM/chat output |
| Core model | Vec<MarkdownBlock> IR |
Doc = flat block list + indent; Text = string + marks over byte ranges |
Pulldown events kept with source ranges, rendered straight to elements |
| Editing | None (Input covers fields) | Full: splice/shortcut edits, caret, selection, parse↔serialize fixed point | None |
| gpui coupling | One crate, feature-gated | Pure core + gpui feature; builds for wasm |
All gpui |
Dimension by dimension
Parsing. All three use pulldown-cmark. gpuikit's option set is the most
disciplined: GFM + footnotes on, every option left off documented with what
turning it on would silently break (~x~ becoming subscript instead of
strikethrough, wikilinks, math), and exhaustive Start/End matches with no
wildcard arm, so a pulldown upgrade is a compile error instead of silently
dropped styling. We match on tables/strikethrough/tasklists but skip footnotes
and GFM alerts, and use _ => {} wildcard arms in two places.
Block model. bezel is the outlier: flat blocks with a one-level-deeper
indent invariant; marks as ordered spans (**_x_** differs from _**x**_,
which run flags cannot express); block-level Bookmark/Mention rich links;
image width in the title slot (); GFM alert quotes
([!NOTE]); GitHub-style heading slugs; a parse↔serialize fixed-point
guarantee so an edit/save cycle never drifts. Neither viewer attempts
round-tripping — fine for us, and the reason bezel's parse layer is its own
crate.
Streaming (gpuikit's headline). append() for LLM deltas: parse on the
background executor, previous events keep rendering (never blanks), deltas
arriving mid-parse coalesce into one follow-up parse, first parse synchronous
so the document is never empty on frame one. Two extra streaming details: a
stitch preprocessing pass closes unterminated inline syntax (**bold)
before parsing so text does not flicker between plain and styled mid-stream,
and an unclosed fence is drawn plain until its closer arrives — a streaming
fence is a new cache key per delta, so highlighting it costs a full pass per
frame. Their open-fence byte scan errs only toward "closed", the harmless
direction. We parse synchronously in set_source and re-highlight every fence
on each parse.
Code highlighting. tree-sitter for bezel, syntect for gpuikit (feature-gated, with a highlight cache and theme selection), tree-sitter with per-language cargo features for us. Ours shares one grammar stack with the code view — one dependency set across both surfaces — but re-highlights every block on every parse where gpuikit caches settled blocks and suspends the open one.
Layout and performance. The one place we are clearly ahead: neither
library virtualizes. gpuikit builds one flex column of every block in the
document each frame (confirmed in its render_events); bezel renders blocks
directly (not assessed in depth). We window over blocks like the code and diff
views do. The trade: an all-in-one-column layout makes text selection
tractable, which our windowing makes harder.
Text selection. Both ship it, we do not. gpuikit's is the deeper
implementation: a document-wide selection of (run, byte offset) positions, a
per-frame run registry, append preserves the selection while set_source
drops it, selected_text() for the copy binding. bezel's rides on its
editable model. This is our biggest functional gap against both.
Accessibility. gpuikit is far ahead: Role::Document on the root, per-run
roles (heading with level, paragraph, list item, blockquote, code), and
element ids stable across frames because assistive technology reads a changed
node id as a replaced element. Tested through a recorded-run harness. We ship
one optional aria_label on the root. bezel's a11y not assessed.
Tables. gpuikit honors GFM column alignment, stripes body rows, borders
per column. We parse cell order but dropped Alignment — cheap to add since
pulldown hands it over.
Images. gpuikit renders real image elements with alt text; bezel supports captions and widths. We render alt text only (documented M16 scope).
Styling philosophy. gpuikit: a per-instance MarkdownStyle struct with
per-key theme fallbacks, plus knobs like soft_break_as_hard_break (LLM
output treats single newlines as breaks). bezel: nothing passed at all — style
flows through the environment, Brand/Tint rotate one palette. Ours is
closest to bezel's philosophy: everything resolves from the native system
theme, zero per-instance color knobs, which is the kit's stated goal. We have
no soft-break knob yet.
Lists. All three solved the same loose/tight and multi-block-item
problems. gpuikit's mechanism is elegant: the item's first block takes the
marker (Shown), later blocks reserve the marker's width but draw it
Hidden, so ordinals are not burned and continuation text aligns exactly with
the first block's text column. Ours reaches the same semantics (marker on the
first row, continuations as deeper paragraphs) with approximate alignment
(indent step 1.5rem vs marker column 1.75rem).
Ideas worth stealing
Ranked by value to downstream consumers, signed first. API ideas only — implementation stays ours.
- Streaming
append+ coalesced background parse (gpuikit). For an LLM client this is the headline feature; our synchronousset_sourceblocks the UI thread on large documents. - Text selection with copy (both). The pattern exists in the code view; a wrapped-text version is real work but the biggest functional gap.
- Per-block a11y roles + stable ids (gpuikit). Medium effort, high correctness value; the recorded-run test technique is reusable as an idea.
- Incremental highlight caching (gpuikit). Cheap version for us: reuse
per-block tree-sitter trees across parses —
highlight_with_treealready takesold_tree— instead of re-parsing every fence perset_source. - GFM alerts + table column alignment (both). Nearly free; pulldown emits
BlockQuoteKindandAlignmenttoday and we ignore both. soft_break_as_hard_break(gpuikit). One option, real payoff for agent output.- Exhaustive match, no wildcard arms (gpuikit). Turns pulldown upgrades into compile errors.
- Heading anchors/slugs (bezel). Useful when a consumer wants in-document navigation.
- Per-view typography overrides (gpuikit's
MarkdownStyle). Conflicts with the theme-global discipline; the checklist's editor follow-ups already gesture at per-view config — decide deliberately, not by accident.
Deliberate differences
- Virtualization is ours alone and is the right call for a kit whose gallery ships 5,000-line perf tests.
- Native theme resolution (platform sources → tokens) instead of per-instance style structs is the kit's identity, not a gap.
- No editing or serialization. bezel's editor-grade model is a different product; if a consumer ever wants an editable markdown surface, bezel's flat-block model is the architecture study to make first, and its pure-core split is the cleanest layering of the three.