dev.snipme:highlights 1.1.0 found comments before it knew the language and
paired /* with */ by ordinal, so `//` in any URL commented out the rest of
its line, every Rust `#[derive(...)]` greyed out as a comment, a `#` inside
a Kotlin string swallowed the line, and `x '*/a/*'` in shell produced a span
whose end preceded its start -- the one that crashed a card holding
`-path '*/.git/*'`. None of that could be post-processed away, because
comments won over strings before the language was known.
Highlighter.kt is one left-to-right scanner: at each position it is in a
line comment, a block comment, a string, or ordinary code, and every span is
emitted by advancing an index, so spans cannot overlap, arrive out of order
or run backwards. Languages.kt is a `Rules` row per language -- comment
tokens, block comment and whether it nests, the string forms, what opens an
attribute, and the keyword set -- so a new language is a table entry. The
keyword lists came from the library's SyntaxTokens.kt (Apache-2.0, noted at
the table) so nothing that is coloured today turns plain, and RON, TOML,
fish and JSON are coloured for the first time.
HighlighterTest.kt is a new JVM unit test source set -- 24 cases, the
library's mistakes kept as regressions, plus a sweep asserting no span
escapes the code for any language on unterminated and empty input.
AGENTS.md's app line now runs :androidApp:testDebugUnitTest.
Measured on the ai-app emulator, debug build, a ~200-line Kotlin fence sent
into a sandbox session:
before code highlighted: 1, 101.9ms total, 101.9ms mean, 101.9ms worst
after code highlighted: 1, 15.0ms total, 15.0ms mean, 15.0ms worst
and a second fence in the same run took 13.9ms, so that is the steady cost
rather than class loading. stream-bench.sh after the change:
code highlighted: 1, 12.1ms total, 12.1ms mean, 12.1ms worst
markdown reparsed while streaming: 1329, 2130.7ms total, 1.6ms mean, 8.7ms worst
record: one block: 131, 11.4ms total, 0.1ms mean, 0.4ms worst
draw phase 1.21ms per frame, the transcript 0.23ms of it
transcript-bench.sh after: draw phase 1.10ms per frame, the transcript
0.49ms (place 0.48), worst place 4.3ms -- unchanged within run-to-run noise,
as expected, since the scan happens in `warm` and not while drawing.
Looked at on the emulator: a URL inside a Kotlin string, a Rust attribute
with a lifetime and a raw string, a shell line with globs and `$#`, a RON
fence and a TOML fence all colour correctly; a Bash tool card still colours
its command; a plain Python fence -- which this change had no reason to
touch -- looks as it did; an unknown language stays plain; and a fence is
plain while it streams and colours when it freezes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Warming covers a settled message, so it did not reach the block a reply is
still writing: that one was re-lexed at every delta, on the composing
thread. Measured streaming a two-hundred-line Kotlin fence -- 211 lexes,
13.7 seconds across the turn, the worst 177ms -- for colours on text being
replaced as fast as they were computed.
A live reply's last segment now draws its code plain and takes its colours
when the block freezes, which for a finished fence is as soon as the next
block starts. The settle-time lex then happens in warm, off the drawing
thread. Same fixture after: one lex of 374ms in warm, draw phase 1.51ms to
1.01ms per frame while streaming, and the fence coloured on screen once the
reply settles.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things the measurements for the previous commit turned up.
Highlighting a fence cost 174ms for a two-hundred-line Kotlin block, and
the lazy list charged it again every time that block scrolled back into
composition -- six times in one bench run, with the scroll's draw phase at
1.29ms per frame. So it is warmed and cached where parses already are:
`highlight` is a plain function taking no colour from the theme, `warm`
fills `ParsedReplies.highlighted` from `fences(parse)` off the drawing
thread, and `fenceContent` extracts the code here rather than through the
library's composable, so the string warmed is the string drawn.
And the anchor restore asked for a span counted in events, which goes
negative when the anchor's row is the oldest half-row and was coerced to
one -- a request per delta, six hundred round trips walking one reply back
a word at a time with the spinner up. It asks for a page of rows now.
Clean pairs, fresh sessions each side, same gestures. Fence scroll
(transcript-bench.sh): draw phase 0.74ms per frame before highlighting
existed, 0.77ms after, no lexing in the window either side. Streaming
forty linked items (stream-bench.sh): 2412ms of reparsing before, 674ms
after; mean 5.0ms to 1.4ms, worst 8.9ms to 7.9ms. Bullet glyphs, fence
colours, the image links and the reference link checked on the emulator;
lint clean on AGP 9.4.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Items 1-5 of TRANSCRIPT_RENDERING.md's list, plus the AGP 9.4.0 bump from 7.
MarkdownRoot provides the renderer's locals itself instead of calling its
Markdown() composable; fences and indented blocks go through CodeFence.kt,
which shares the tool-input highlighter and a fence-language alias table;
an image in a paragraph is a link carrying its alt text, so every paragraph
is now platform text; LiveParse freezes the finished items of the tail list
so a forty-item list streams as forty paragraphs would.
Measured before, on the emulator (report from transcript-bench.sh over the
200-line fence fixture): draw phase 0.72ms per frame, transcript 0.36ms.
stream-bench.sh (new) streaming forty linked bullets on the old build:
markdown reparsed while streaming 483, 3.9ms mean, 11.6ms worst; record:
one block worst 1.6ms. The after runs, the on-screen check of the glyphs
and lint are recorded as owed in the doc's "What is next".
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>