text: render inline code spans in the theme's mono font - #2949
Conversation
huacnlee
left a comment
There was a problem hiding this comment.
The requirement is right — inline code should render in the mono family, and consolidating the two duplicate run builders into one text_runs is a genuine cleanup. But I don't think the shape of the change is the one this needs, and there are two measurement paths that were left behind. Requesting changes on the modeling question first, since fixing it makes most of the rest go away.
Inline code should ride in highlights, not in a second parallel list
code_ranges is introduced as an independent partition of the same text, keyed to the same byte offsets, and then threaded through every layer that already carries highlights: InlineFlowItem, MeasureItem, LineFragmentKind, PositionedFragment, Inline, and both Paragraph builders. Two lists over one coordinate space now have to be sliced, cloned and cleared in lockstep at every one of those sites. That is the source of most of the +166.
It also forces text_runs to reconcile the two partitions at render time — sorting, clamping, and splitting a highlight run at a code boundary. That reconciliation is dead code for every input this pipeline can actually produce. In node.rs the same inner_range goes into both lists:
if style.code {
highlight = highlight.highlight(node_cx.style.inline_code_highlight());
code_ranges.push(inner_range.clone());
}
// ...
node_highlights.push((inner_range, highlight));Every code range therefore has a highlights entry with identical bounds, and gpui::combine_highlights is an endpoint sweep that cuts at the start and end of every input range. So each code range's endpoints are always cut points, and a highlight segment arriving at text_runs is either entirely inside a code range or entirely outside it. It can never straddle one.
That makes text_runs_split_a_highlight_at_the_code_boundary a test for an input the production path cannot construct: bold 0..10 with code 6..10 reaches text_runs already split into 0..6 bold and 6..10 bold+code.
The suggestion: change the payload of the existing list from HighlightStyle to a small struct carrying the highlight plus the mono flag (HighlightStyle has no family field, which is the real constraint here — that part of the diagnosis is correct). Same files touched, one list instead of two, no splitting, no sort, no per-fragment Vec<(Range<usize>, ())> allocated in inline_flow.rs:453 just to reuse slice_ranges, and the run builder stays the original loop plus one assignment to run.font.family.
Two measurement paths still assume a single font
These are the reason I'd rather not land the current shape: the information lives on a channel that the measurement code does not subscribe to.
inline_flow.rs:573 — line breaking is measured in the body font only. line_ranges() builds one line_wrapper(text_style.font(), font_size) and wraps the whole flow with it, while layout_flow now shapes each fragment with mono runs and sums those shaped widths into line_width/max_width. Mono glyphs are generally wider, so the break points chosen for the body font yield a line whose shaped width exceeds wrap_width, and InlineFlowLayout.size reports max_width > wrap_width — horizontal overflow. Repro shape, in a narrow text view:
Text  with a fairly long `inline_code_span` at the end.The same class of error existed for bold and italic, but the proportional-to-mono delta is much larger.
node.rs:2081 — table column widths are measured with a body-font run per cell. The max-content pass does text_style.to_run(line.len()) over the cell's plain text with no knowledge of the code ranges, but cells render through Paragraph::render → Inline and are now shaped in mono. A column of `method_name()` values gets a col_w narrower than the content, so cells wrap to extra lines or clip inside CELL_PAD_PX.
Smaller points
- The family bypasses
TextViewStyle, with no opt-out. It is read straight fromcx.theme().tokens.typography.monoinside the base element (inline.rs:364,inline_flow.rs:216), butTextViewStyle::inline_codeis the documented seam for inline code presentation. An app that styles inline code as a badge in the body face can no longer do so, and inline code cannot be given a family distinct from fenced blocks. CLAUDE.md asks the base layer to stay visually unopinionated with presentation above the seam, so this wants an optional family onTextViewStyledefaulting to the mono token. - Per-frame work.
text_runsre-clones, re-filters and re-sorts on every layout pass, and thepushclosure rescans the whole vec for each run, givingO(highlights × code_ranges)plus an allocation per call — inInline::request_layout(per frame, per visible inline element) and once per line fragment inlayout_flow. Folding the flag into the highlights payload removes this entirely.
What I verified
cargo check -p gpui-base passes, clippy is clean with --all-targets, and both new tests pass. I traced the text_runs splitting logic for length and underflow correctness and it is sound in isolation: run lengths always sum to text_len, and empty, reversed, unsorted, overlapping and out-of-range inputs are all handled. All three Inline::new call sites and both Paragraph builders were updated consistently, and the per-line slice_ranges keeps the code ranges in the same coordinate space as the sliced text and highlights. The problems are the modeling and the two measurement paths, not the run construction.
🤖 Review assisted by Claude Code
An inline code span (
`like this`) is styled throughTextViewStyle::inline_code_highlight(), which returns a gpuiHighlightStyle.A
HighlightStylecarries color, weight, style, background, underline,strikethrough and fade — but no font family. So inline code gets the code
background and nothing else: it is shaped in the body font, while fenced code
blocks (which go through a
div().font_family(theme.tokens.typography.mono))render in the mono font. The two kinds of code in one document disagree on the
typeface.
Reproduction
Set a distinct sans and mono family on the theme (any pair that differ visibly,
e.g.
Noto Sans/JetBrainsMono Nerd Font) and render: