Files
gpui-native-kit/docs/markdown-view-comparison.md
T

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 (![alt|480](url)); 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.

  1. Streaming append + coalesced background parse (gpuikit). For an LLM client this is the headline feature; our synchronous set_source blocks the UI thread on large documents.
  2. Text selection with copy (both). The pattern exists in the code view; a wrapped-text version is real work but the biggest functional gap.
  3. Per-block a11y roles + stable ids (gpuikit). Medium effort, high correctness value; the recorded-run test technique is reusable as an idea.
  4. Incremental highlight caching (gpuikit). Cheap version for us: reuse per-block tree-sitter trees across parses — highlight_with_tree already takes old_tree — instead of re-parsing every fence per set_source.
  5. GFM alerts + table column alignment (both). Nearly free; pulldown emits BlockQuoteKind and Alignment today and we ignore both.
  6. soft_break_as_hard_break (gpuikit). One option, real payoff for agent output.
  7. Exhaustive match, no wildcard arms (gpuikit). Turns pulldown upgrades into compile errors.
  8. Heading anchors/slugs (bezel). Useful when a consumer wants in-document navigation.
  9. 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.