Skip to content

Split tokenizing into ktsu.SyntaxHighlighting and highlight embedded languages - #357

Merged
matt-edmondson merged 4 commits into
mainfrom
claude/embedded-language-highlighting-so2k74
Sep 8, 2026
Merged

Split tokenizing into ktsu.SyntaxHighlighting and highlight embedded languages#357
matt-edmondson merged 4 commits into
mainfrom
claude/embedded-language-highlighting-so2k74

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

What

Two changes, in this order:

1. The renderer-agnostic half is its own library. ktsu.SyntaxHighlighting now holds the languages, tokenizers, token kinds and themes — no ImGui, no graphics API, no third-party parser — so it can move to its own repository unchanged. ktsu.ImGui.SyntaxHighlighting keeps the drawing half (CodeRenderer, SyntaxHighlightConfig, SyntaxColors, ScopedCodeFont) and references it.

Public API is unchanged apart from namespaces and one member: ImGuiSyntaxHighlighting.Render/.Highlight still work as before (Highlight forwards to SyntaxHighlighter.Highlight), and HighlightedCode.Render(config) still compiles — it is now an extension method in the ImGui package, because the tokenized type no longer knows what ImGui is. Callers of LanguageDefinition, TokenKind, SyntaxTheme etc. add using ktsu.SyntaxHighlighting;.

2. Comments and strings are searched for the language written inside them. XML doc comments, JSON payloads and SQL queries are all written inside a host language's literals, and a lexer that stops at the quote leaves them one flat color:

/// <summary>Posts an order.</summary>          // tags are XML, the prose stays a comment
string body = @"{""id"": 7, ""paid"": true}";   // keys, numbers and constants are JSON
string query = "SELECT id FROM receipts";       // keywords are SQL

// lang=sql
string tail = "ORDER BY total DESC";            // named outright when nothing can recognize it

Each LanguageDefinition carries EmbeddedLanguages, a list of EmbeddedLanguageRule tried in order. The built-in programming languages get JSON, markup and SQL; JSON, YAML and SQL get JSON and markup; C# also gets "doc comments are XML". An empty list turns it off, so BuiltInLanguages.CSharp with { EmbeddedLanguages = [] } is the opt-out.

Why it is safe to leave on by default

  • Recognition is strict. LooksLikeJson parses — a fragment, trailing text, or a brace-heavy sentence is rejected. Markup must open with a tag and end with >. SQL must open with a statement keyword and use a second one, and its rule applies to string literals only, so "Update the cache and carry on" in a comment is never a query.
  • Unclassified embedded text keeps its host's kind. A Plain token from the inner tokenizer is re-emitted as Comment/DocComment/StringLiteral, so prose between doc comment tags still reads as a comment and a false positive costs a few punctuation glyphs, not a paragraph.
  • The rendered text is never rewritten. Escapes are resolved so the inner tokenizer sees {"a": 1} where the source holds {\"a\": 1}, but every token is re-sliced from the original through an index map, and the expander verifies the run reconstructs the host token before keeping it, falling back to the unexpanded token otherwise.
  • Embedding is one level deep, so definitions cannot cycle however they refer to each other.

Also

Fixes the highlight cache keying by language name: re-registering a definition under an existing name served lines tokenized by the old rules. It now keys on the definition instance.

Testing

  • tests/SyntaxHighlighting.Tests/ (moved from tests/ImGui.SyntaxHighlighting.Tests/, now referencing the core package only): 70 tests pass, including a new EmbeddedLanguageTests covering each recognizer, the escaped-JSON index mapping, hint comments, the one-level-deep bound, and the negative cases.
  • tests/ImGuiSyntaxHighlightingDemo.UITests/: 12 pass, including the demo's new Embedded tab.
  • Full solution: 1376 pass. The 43 failures are in ImGuiAppDemo.UITests, ImGuiWidgetsDemo.UITests and ImGuiMarkdownDemo.UITests, all UnknownImageFormatException from Git LFS pointer files that were not resolved in this sandbox — untouched by this change and expected to pass in CI.

Docs

Root README.md, CLAUDE.md (new Embedded languages architecture section), DESCRIPTION.md, TAGS.md, ImGui.SyntaxHighlighting/README.md, and a new SyntaxHighlighting/README.md for the package that is destined for its own repo.

🤖 Generated with Claude Code

https://claude.ai/code/session_019uHKuTn4tHvuCwCFZ9UYVW


Generated by Claude Code

…mbedded languages

Syntax highlighting was one package that both classified code and drew it
with Dear ImGui. Tokenizing has nothing to do with a graphics API, so it now
lives in ktsu.SyntaxHighlighting: languages, tokenizers, themes and the
token kinds, with no ImGui reference anywhere, ready to move to its own
repository. ktsu.ImGui.SyntaxHighlighting keeps the drawing half and
forwards to it, so ImGuiSyntaxHighlighting.Render and .Highlight are
unchanged; HighlightedCode.Render moves to an extension method, since the
tokenized type itself no longer knows about ImGui.

On top of that split, comments and strings are now searched for the
language written inside them. XML doc comments, JSON payloads and SQL
queries are all written inside a host language's literals, and a lexer that
stops at the quote leaves them one flat color. Each LanguageDefinition
carries EmbeddedLanguages rules; the built-ins get JSON, markup and SQL,
plus XML doc comments for C#.

Four things keep this safe to leave on by default:

- Recognition is strict. LooksLikeJson parses rather than pattern matches,
  markup must open with a tag and end with '>', and SQL needs an opening
  keyword *and* a second one, in a string only, so prose is not recolored.
- Embedded text the inner tokenizer does not classify keeps its host's
  kind, so prose between doc comment tags still reads as a comment.
- Escapes are resolved for the tokenizer but every token is re-sliced from
  the original text through an index map, and the expander verifies the run
  reconstructs the host token before keeping it, so the rendered text is
  never rewritten.
- Embedding is one level deep, so definitions cannot cycle.

A '// lang=json' hint comment names the language of the next string
literal outright, for snippets no recognizer can catch.

Also fixes the highlight cache keying by language name, which served stale
lines after a definition was re-registered under the same name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uHKuTn4tHvuCwCFZ9UYVW
Comment thread SyntaxHighlighting/Tokenizing/EmbeddedExpander.cs Fixed
Comment thread SyntaxHighlighting/Languages/EmbeddedContent.cs
Comment thread SyntaxHighlighting/Languages/EmbeddedContent.cs
Comment thread SyntaxHighlighting/Tokenizing/EmbeddedExpander.cs Fixed
Two of the four CodeQL comments on #357 are actionable:

- TryResolve's foreach filtered its sequence implicitly; the AppliesTo test
  moves into a Where, and the comment now says why a matching rule naming an
  unregistered language falls through to the next rule rather than failing.
- UnescapedBody.Of leaned on a null-forgiving rule! after proving non-null
  through a separate bool. It now returns early on null (NeedsUnescaping
  already answers false when neither escape mechanism applies) and caches the
  escape, close and doubled-close members in locals, so no nullable member is
  dereferenced in the loop.

The other two flag foreach loops over string[] whose bodies compare against a
ReadOnlySpan<char> parameter. A Where predicate cannot capture one — CS9108,
"cannot use parameter that has ref-like type inside a lambda" — so the
suggestion does not compile there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uHKuTn4tHvuCwCFZ9UYVW
Comment thread SyntaxHighlighting/Tokenizing/EmbeddedExpander.cs Fixed
Moving TryResolve's AppliesTo test into a Where left the loop body a single
if, so the same "missed opportunity to use Where" finding fired again on the
rewritten loop. Reshaping it a third time would keep the shape that draws the
finding, so the loop is gone: the rule that claims the body and the language
it names are now selected together, and the first that resolves is the match.

Behavior is unchanged, including the part worth stating — a rule that claims
the body but names a language the registry does not know falls through to the
next rule rather than ending the search. That was implicit in the loop and had
no test; it has one now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uHKuTn4tHvuCwCFZ9UYVW
The hint is read from a comment's body, so /* lang=json */ and an inline
/* language=sql */ beside the string both work, but only the // form had a
test and only the // form was in the README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019uHKuTn4tHvuCwCFZ9UYVW
@sonarqubecloud

sonarqubecloud Bot commented Sep 8, 2026

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 083a5a3 into main Sep 8, 2026
15 checks passed
@matt-edmondson
matt-edmondson deleted the claude/embedded-language-highlighting-so2k74 branch September 8, 2026 05:01
matt-edmondson pushed a commit that referenced this pull request Sep 8, 2026
…-354-7ftjju

#356 and #357 landed: ImGuiNodeEditor was renamed to ImGui.NodeEditor, and
tokenizing was split out of ImGui.SyntaxHighlighting into its own
SyntaxHighlighting library. Neither touches the image decoder.

The only conflict was CLAUDE.md's Libraries list, where main rewrote both
the ImGui.App and ImGui.Widgets entries. Took main's text for both and
re-applied just this branch's addition: the sentence on ImGui.App pointing
at the self-contained image decoding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCFaH3HSDMKHTmrQB7LcSP
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.

2 participants