Draw replies as pieces of one parse, and lists an item at a time

A settled reply used to be cut into block *strings*, each parsed on its own
and each a unit of the lazy list; the live reply split the same way with a
whole-message parse per delta on top. Now a message is parsed once, and a
Piece addresses a top-level block of that tree -- or one item of a
top-level list, which was the one block still unbounded: a list of forty
sources was one item composed whole in the frame it scrolled into. Units,
the live reply's column and peer messages all draw from the same parse,
so warm parses each message once instead of once per block, a delta costs
one background parse instead of two, and a reference definition at the
foot of a message resolves again because nothing is parsed apart from it.

The renderer keeps parsing and providing its environment; MarkdownRoot
wraps that around a piece, and a whole block still goes through its
dispatch with our component table. List items are drawn here, with the
renderer's own paddings so a split list looks like an unsplit one, and
lists inside quotes come to the same code through the table -- the marker
is drawn in one place, which is what a styled bullet would need later.

Found on the way: a heading's words are a child of the heading node, and
the inline builder draws nothing for a node type it does not know, so the
span-link path had been drawing headings empty. LinkedHeading hands it the
content child.

Lint: profileable's shell attribute scoped to API 29 where it exists, and
recordFrames renamed to the composable convention. What remains is the
AGP 9.4.0 notice.

Verified on the emulator against a fixture of every block kind (headings,
nested and ordered lists with a start number, task items, a quote holding
a list, a fence, a rule, a table with a linked cell, a setext heading), a
forty-item list which the render report now shows as per-item units, a
reply streamed live (34 deltas: 34 background reparses, one warm at
settle, no crash), and the older link fixture. ktfmt, build and lint run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-02 19:34:41 -04:00
1 parent a9ab9b3e5a
commit 48bb7de304
12 files changed
+457 -221

No files matched your search

+22 -7
View File
@@ -334,17 +334,32 @@ first if a remote spawn ever mangles an argument.
success. The phone's half is `uniqueItems`, which every list keyed on a
server-chosen id goes through: a repeat there must never be able to
close the app, whatever produced it.
- **A reply is drawn as pieces of one parse, never as re-parsed
substrings.** `MarkdownPieces.kt`: a `Piece` addresses a top-level block
of the message's tree, or one item of a top-level list, and every piece
is drawn from the same `State.Success` that `ParsedReplies` cached and
`warm` made. That is what bounds a lazy-list item (one paragraph, one
bullet) without parsing a message more than once, and it is why a
forty-item list of sources is forty units rather than one. The renderer
is still the parser and the environment: `MarkdownRoot` provides its
locals and `MarkdownElement` dispatches a whole block through our
component table, so paragraphs, headings and table cells are span-linked
`LinkedText` (links as spans with one tap detector per text, not a layout
node per link -- the cost that made a list of sources bumpy) and lists
are ours wherever the dispatch meets one. A heading's words are its
`ATX_CONTENT`/`SETEXT_CONTENT` child; the inline builder draws nothing
for a node type it does not know, so hand it the child.
- **A markdown table wraps its cells and never cuts one off.** The
renderer's own defaults draw every cell at one line with an ellipsis,
which on a phone loses most of a table -- and an elided cell looks
exactly like a short one, so nothing on screen says anything was cut.
`Markdown.kt` supplies its own header and row blocks with `maxLines =
Int.MAX_VALUE` and `TextOverflow.Clip`, cells aligned to the top of the
row so a two-line cell does not re-centre its neighbours. Width is the
other half: a column narrows to 136dp and no further, and past that the
whole table scrolls sideways rather than squeezing -- 136 because it is
the widest floor that still fits three columns across a phone, which is
the commonest table there is. Exercise it with the echo driver's
`Markdown.kt` supplies its own rows (`LinkedTableRow`): as many lines as
a cell needs, cells aligned to the top of the row so a two-line cell
does not re-centre its neighbours, and each cell a `LinkedText`. Width is
the other half: a column narrows to 136dp and no further, and past that
the whole table scrolls sideways rather than squeezing -- 136 because it
is the widest floor that still fits three columns across a phone, which
is the commonest table there is. Exercise it with the echo driver's
`/table N` (default six columns), which writes long cells on purpose:
a fixture of tidy one-word values renders fine whether or not the
truncation is fixed.