feat: add markdown view component
This commit is contained in:
+9
-9
@@ -9,15 +9,13 @@ is the first downstream consumer; its screens drive the order.
|
||||
|
||||
## Next
|
||||
|
||||
- [ ] **Markdown view** (M16, `editor`) — only if a consumer confirms.
|
||||
- [ ] API sketch
|
||||
- [ ] Implementation
|
||||
- [ ] Tests
|
||||
- [ ] Gallery panel
|
||||
- [ ] Review
|
||||
|
||||
## Queue — to follow
|
||||
|
||||
- [ ] **Markdown view follow-ups** — streaming `append` with a coalesced
|
||||
background parse, text selection with copy, per-block a11y roles,
|
||||
GFM alerts + table column alignment, per-fence highlight reuse,
|
||||
`soft_break_as_hard_break`; ranked notes in
|
||||
[markdown-view-comparison.md](markdown-view-comparison.md).
|
||||
- [ ] **Editor follow-ups** — language detection by file path, meta line
|
||||
overlay, per-view config overrides (line height, gutter visibility).
|
||||
- [ ] **Pickers** — Calendar / DatePicker, TimeField, ColorPicker (decide at
|
||||
@@ -76,8 +74,10 @@ is the first downstream consumer; its screens drive the order.
|
||||
incremental line re-highlight
|
||||
- [x] Diff view — unified rows via `similar`, highlighted with the code
|
||||
view's palette; change washes from the ramps, `scroll_to_first_change`
|
||||
- [x] Diff view — unified rows via `similar`, highlighted with the code
|
||||
view's palette; change washes from the ramps, `scroll_to_first_change`
|
||||
- [x] Markdown view — `pulldown-cmark` blocks rendered as formatted text:
|
||||
headings, emphasis runs, ordered/task lists, quotes, fenced code
|
||||
highlighted with the code view's palette, tables, clickable links;
|
||||
virtualized variable-height rows over gpui's `list`
|
||||
|
||||
### layout (`crates/layout`)
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# 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 (``); 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.
|
||||
Reference in New Issue
Block a user