Skip to content

Preserve caller attributes on code block content - #83

Open
cjs226 wants to merge 1 commit into
gonzalezreal:mainfrom
cjs226:preserve-code-block-attributes
Open

Preserve caller attributes on code block content#83
cjs226 wants to merge 1 commit into
gonzalezreal:mainfrom
cjs226:preserve-code-block-attributes

Conversation

@cjs226

@cjs226 cjs226 commented Aug 2, 2026

Copy link
Copy Markdown

HighlightedTextFragment rebuilds every syntax token from its plain String:

let code = String(content.characters[...])
tokens = [CodeToken(content: code, type: .plain)]
...
var content = AttributedString(token.content)

That discards every attribute the caller's MarkupParser applied to the code block. Any decoration a client adds, such as search highlighting and custom inline styling, is applied correctly at parse time, survives into the AttributedSubstring the code block receives, and is then silently dropped before it reaches the screen.

There is no way to work around it from outside the library. CodeBlockStyleConfiguration exposes only a type-erased label, an indentationLevel, a languageHint, an opaque CodeBlockProxy, and the theme. So a custom CodeBlockStyle cannot recover the text to re-render it, and cannot even detect that a block contains anything of interest.

The change

Walk the original attributed content in parallel with the tokens and slice it, rather than rebuilding each token from its plain string. Theme attributes are still merged on top, so syntax colouring continues to win for the keys it sets (foregroundColor, presentationIntent) while caller attributes survive for the rest.

If the tokenizer's total length ever disagrees with the source, that token falls back to the previous plain-string behaviour, so text can never be lost — only the attributes on that token.

No API change, no behaviour change for callers that don't attach attributes.

How it came up

Adding find-in-text search to an app that renders notes with Textual. Matches inside fenced code blocks were located and highlighted correctly by a custom MarkupParser, then vanished on screen — the highlight renders for one frame before .onChange triggers highlight(tokens:...) and replaces the attributed text wholesale.

Tests

Adds Tests/TextualTests/Internal/Highlighter/HighlightedTextFragmentTests.swift covering:

  • a caller attribute survives on code block content
  • it survives being split across token boundaries
  • text and attributes are unchanged when the caller applies none
  • theme token properties still style tokens alongside a caller attribute
  • the length-mismatch path still emits the plain token, losing no text

All five fail against the previous plain-string rebuild and pass with the fix.

Also verified in a real macOS 26 app: search highlighting now renders inside fenced code blocks, and syntax colouring in those blocks is unchanged.

Local verification

make test-macos, make test-maccatalyst and make build-demo all pass.

make test-ios has one pre-existing snapshot failure on my machine (TextLayoutCollectionTests.twoParagraphsBidiStructuredTextLayout) — 164 of 165 pass. Running the same suite against a different simulator turns that into four layout snapshot failures, so these look device/OS sensitive rather than related to this change, which touches only code block rendering. Happy to look if CI disagrees.

I could not run the Xcode 16.4 backward-compatibility job (no 16.4 locally), nor tvOS/visionOS (runtimes not installed).

`HighlightedTextFragment` rebuilt every syntax token from its plain
`String`, which discarded all attributes the caller's `MarkupParser` had
applied to the code block. Any decoration a client added — search
highlighting, custom inline styling — was silently dropped between
parsing and rendering, with no seam to intervene: `CodeBlockStyle` only
receives a type-erased label, so a custom style cannot recover the text
either.

Walk the original attributed content in parallel with the tokens and
slice it instead. Theme attributes are still merged on top, so syntax
colouring wins for the keys it sets while caller attributes survive for
the rest. If the tokenizer's total length ever disagrees with the source,
fall back to the previous plain-token behaviour so text is never lost.

Found while adding find-in-text search highlighting to an app that
renders notes with Textual: matches inside fenced code blocks were
located and highlighted correctly by the parser, then vanished before
reaching the screen.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant