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

137 lines
7.7 KiB
Markdown

# Markdown view — comparison with bezel and gpuikit
Peer read for API ideas only; nothing ported, nothing depended on
([AGENTS.md](../AGENTS.md)). Assessed 2026-11 against `main` of
[bezel](https://github.com/crabtalk/bezel) (`crates/markdown`) and
[gpuikit](https://github.com/iamnbutler/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](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.