[JEWEL-1313] Add syntax highlighting to FencedCodeBlock in standalone

closes https://github.com/JetBrains/intellij-community/pull/3503

(cherry picked from commit 43950673391b442b7e1251df7322a9128ebea4a0)


(cherry picked from commit ec8df5acf7c87b5ccef05c972df10254667acf2e)

IJ-MR-220560

GitOrigin-RevId: bbba6c36da8819d6d03d7e4d418a4d40eca634ac
This commit is contained in:
Daniel Bertoldi
2026-09-07 22:29:09 +00:00
committed by intellij-monorepo-bot
parent 0aa073b7ba
commit f8ec281887
26 changed files with 1596 additions and 20 deletions
@@ -0,0 +1,251 @@
# Standalone code highlighting
The standalone Jewel module ships a lightweight, dependency-free syntax highlighter. It's mainly useful for non-IDE
apps that render Markdown or display code.
## How it works
`SimpleCodeHighlighter` implements `CodeHighlighter` from the `foundation` module. `ProvideMarkdownStyling` wires it
up by default, so fenced code blocks in rendered Markdown are highlighted automatically.
The rule engine works in three steps:
1. Each language maps to a `LanguageGrammar`, which is an ordered list of `TokenRule`s.
2. At each position in the input, the tokenizer picks the rule whose match starts earliest. Ties go to whichever
rule appears first.
3. Each rule maps capture group indices to `TokenType` values, which `SyntaxHighlightColors` turns into styles.
Built-in grammars for languages are stored in `intui.standalone.code.highlighting.languages`.
## Sourcing patterns
Before writing regex patterns from scratch, check whether the IntelliJ TextMate bundles already cover the language.
They're at:
```
plugins/textmate/lib/bundles/<language>/syntaxes/<language>.tmLanguage.json
```
The Kotlin and Java grammars in this module came from:
```
plugins/textmate/lib/bundles/kotlin/syntaxes/kotlin.tmLanguage.json
plugins/textmate/lib/bundles/java/syntaxes/java.tmLanguage.json
```
The fields you need: `match` (single-line pattern), `begin`/`end` (block patterns like strings and comments), and
`captures` (group-to-scope mappings). Look for scopes that map to `TokenType` values: `keyword.*`, `string.*`,
`comment.*`, `storage.type`/`support.type`, `constant.*`, `constant.numeric`, and `entity.name.function`.
If the IntelliJ bundle doesn't cover your language, check the
[shikijs/textmate-grammars-themes](https://github.com/shikijs/textmate-grammars-themes) repo or the
[VSCode extension marketplace](https://github.com/microsoft/vscode/tree/main/extensions).
tmLanguage grammars use [Oniguruma](https://macromates.com/manual/en/regular_expressions) RegEx library. See [Regex engine limitations](#regex-engine-limitations) for what breaks
when porting to Java's regex engine.
Add a comment at the top of the file pointing to the source:
```kotlin
// Patterns adapted from plugins/textmate/lib/bundles/rust/syntaxes/rust.tmLanguage.json
```
## Adding a new language grammar
### 1. Create a grammar file
Create a file in:
```
int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/languages/
```
Name it `<Language>LanguageGrammar.kt` and declare a top-level `internal val`:
```kotlin
// Patterns adapted from plugins/textmate/lib/bundles/rust/syntaxes/rust.tmLanguage.json
internal val RUST =
LanguageGrammar(
name = "rust",
aliases = listOf("rs"),
rules = listOf(
// Comments first — prevent keywords from matching inside them
TokenRule.comment("""\/\/[^\n]*"""),
TokenRule.comment("""\/\*[\s\S]*?\*\/"""),
// Strings
TokenRule.string(""""(?:[^"\\]|\\.)*""""),
// Function declarations: fn <name>
TokenRule.functionDeclaration("""\b(fn)\s+([A-Za-z_][A-Za-z0-9_]*)"""),
// Keywords
TokenRule.keyword("""\b(fn|let|mut|pub|use|mod|struct|enum|impl|trait|for|while|loop|if|else|match|return|self|super|crate|as|in|where|async|await|move|dyn|ref|type|const|static|unsafe|extern)\b"""),
// Constants
TokenRule.constant("""\b(true|false|None|Some|Ok|Err)\b"""),
// Types
TokenRule.type("""\b(String|str|i8|i16|i32|i64|i128|isize|u8|u16|u32|u64|u128|usize|f32|f64|bool|char|Vec|Option|Result|Box|Rc|Arc)\b"""),
// Function call sites — after keywords so control flow keywords don't match
TokenRule.functionCall("""\b([A-Za-z_][A-Za-z0-9_]*)\s*(?=\()"""),
// Numbers
TokenRule.number("""\b0x[0-9a-fA-F_]+\b"""),
TokenRule.number("""\b0b[01_]+\b"""),
TokenRule.number("""\b[0-9][0-9_]*(?:\.[0-9_]+)?(?:[eE][+-]?[0-9]+)?(?:_?[a-z0-9]+)?\b"""),
),
)
```
The `name` and `aliases` values are matched case-insensitively against the language tag in fenced code blocks
(e.g., ` ```rust ` or ` ```rs `).
To add the proper extension names a language has, check both the `package.json` file in the TextMate bundle in the
plugins folder (e.g, `plugins/textmate/lib/bundles/<language>/package.json`) and find the field
`contributes.languages[].extensions`. Also check [GitHub's linguist languages file](https://github.com/github-linguist/linguist/blob/main/lib/linguist/languages.yml),
if you really want to be thorough.
### 2. Registering a LanguageGrammar
#### Registering as a built-in Jewel language
For languages that Jewel should use directly, add your `LanguageGrammar` to `BuiltInLanguageGrammars.kt` in the `all` list:
```kotlin
internal object BuiltInLanguageGrammars {
val all: List<LanguageGrammar> by lazy { listOf(KOTLIN, JAVA, RUST) }
}
```
When opening the PR, make sure to test all possible keywords for a language. Feel free to mix and match regexes,
sometimes TextMate won't highlight a keyword that GitHub does, and vice versa. Just make sure the highlighting is being
properly applied. Here's a very simple template for testing:
```
Keywords, types, constants:
<add examples>
Method declarations + calls:
<add examples>
Control flow:
<add examples>
Comments:
<add examples>
Numbers:
<add examples>
Strings:
<add examples>
```
#### Registering as an additional language or overriding the LanguageGrammar for a built-in language
To highlight a language that isn't built in or override an existing one, just pass your `LanguageGrammar`
to `SimpleCodeHighlighter`:
```kotlin
val myGrammar = LanguageGrammar(
name = "toml",
rules = listOf(
TokenRule.comment("""#[^\n]*"""),
TokenRule.string(""""(?:[^"\\]|\\.)*""""),
TokenRule.keyword("""\b(true|false)\b"""),
),
)
val highlighter = SimpleCodeHighlighter(
colors = SyntaxHighlightColors.light(),
additionalGrammars = listOf(myGrammar), // <--- Add your custom language grammars here!
)
```
### 3. Write tests
Create a test file in:
```
int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/code/highlighting/languages/
```
Name it `<Language>GrammarTest.kt`. Use `testColors` from `CodeHighlightingTestUtils` so your tests check the
tokenizer, not the color values:
```kotlin
internal class RustGrammarTest {
private val highlighter = SimpleCodeHighlighter(testColors)
private suspend fun highlight(code: String) = highlighter.highlight(code, "rust").first()
@Test
fun `line comment is colored as comment`() = runTest {
assertEquals(Color.Gray, highlight("// a comment").colorAt(0))
}
@Test
fun `fn declaration colors keyword and function name separately`() = runTest {
val result = highlight("fn my_func() {}")
assertEquals(Color.Red, result.colorAt(0)) // "fn" → keyword
assertEquals(Color.Yellow, result.colorAt(3)) // "my_func" → function call
}
// ...
}
```
## Rule ordering
Order matters more than it looks. Put comments or strings in the wrong position and keywords will match inside them.
The general pattern:
| Order | Rule type | Reason |
|-------|-----------|--------|
| 1st | Comments | Must come first, or keywords and strings will match inside them |
| 2nd | Strings | Must come before keywords, or keywords inside strings get colored |
| 3rd | Function declarations | Matches `fn foo` before the keyword rule can grab just `fn` |
| 4th | Keywords | |
| 5th | Constants | |
| 6th | Types | |
| 7th | Function call sites | After keywords in Kotlin, so `if(` stays KEYWORD rather than FUNCTION_CALL |
| Last | Numbers | |
The table is a **starting point**, not a recipe. Every language has quirks you won't find until you test against real code.
Java's `functionCall`-first ordering is just one of them — run your grammar against actual snippets before assuming it works.
## Factory functions
`TokenRule.Companion` has factory functions for the common cases. Every factory takes a regex pattern string; the
pattern is compiled once when the `TokenRule` is created.
| Factory | Captures | Description |
|---------|----------|-------------|
| `comment(pattern)` | group 0 → COMMENT | Entire match colored as comment |
| `string(pattern)` | group 0 → STRING | Entire match colored as string |
| `keyword(pattern)` | group 0 → KEYWORD | Entire match colored as keyword (bold) |
| `type(pattern)` | group 0 → TYPE | Entire match colored as type |
| `constant(pattern)` | group 0 → CONSTANT | Entire match colored as constant |
| `number(pattern)` | group 0 → NUMBER | Entire match colored as number |
| `builtin(pattern)` | group 0 → BUILTIN | Entire match colored as builtin |
| `functionCall(pattern)` | group 1 → FUNCTION_CALL | Group 1 must isolate the function name |
| `functionDeclaration(pattern)` | group 1 → KEYWORD, group 2 → FUNCTION_CALL | Group 1 = keyword, group 2 = name |
| `typeDeclaration(pattern)` | group 1 → KEYWORD, group 2 → BUILTIN | Group 1 = keyword, group 2 = type name |
If the regex has fewer groups than the factory expects, the missing groups are silently skipped and no span is emitted.
For anything not covered by the factories, construct `TokenRule` directly with a `captures` map:
```kotlin
TokenRule(
pattern = """(\w+)\s*:\s*(\w+)""",
captures = mapOf(1 to TokenType.CONSTANT, 2 to TokenType.TYPE),
)
```
## Regex engine limitations
Patterns run through Java's `java.util.regex`. Most tmLanguage patterns work, but a few `PCRE`/`Oniguruma` features
don't exist in Java:
- **POSIX character classes** (`[[:alpha:]]`, etc.) -> use `[a-zA-Z]` etc. instead.
- **Variable-length lookbehind** -> only fixed-width works (`(?<=fun )` is fine, `(?<=fun\s+)` is not). Rewrite as
a capturing-group rule.
- **Named backreferences**, **conditional patterns**, **subroutine calls**, and **recursive patterns** -> not
supported.
`additionalGrammars` is searched before the built-in list, so you can also use it to override a built-in grammar for
an existing language.
+1 -2
View File
@@ -46,13 +46,12 @@ junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerialization" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
ktor-client-java = { module = "io.ktor:ktor-client-java", version = "3.0.3" }
metalava = { module = "com.android.tools.metalava:metalava", version.ref = "metalava" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test" }
# Plugin libraries for build-logic's convention plugins to use to resolve the types/tasks coming from these plugins
detekt-gradlePlugin = { module = "dev.detekt:detekt-gradle-plugin", version.ref = "detekt" }
@@ -58,6 +58,8 @@ jvm_library(
"//libraries/junit5:junit5_test_lib",
"//platform/jewel/int-ui/int-ui-standalone:jewel-intUi-standalone",
"//platform/jewel/int-ui/int-ui-standalone:jewel-intUi-standalone_test_lib",
"//platform/jewel/foundation",
"//platform/jewel/foundation:foundation_test_lib",
"//libraries/kotlinx/coroutines/core",
"//libraries/kotlinx/coroutines/core:core_test_lib",
"//libraries/kotlinx/coroutines/test",
@@ -2,12 +2,13 @@ plugins { jewel }
dependencies {
api(projects.intUi.intUiStandalone)
testImplementation(projects.foundation)
testImplementation(kotlin("test"))
testImplementation(libs.junit.jupiter)
testImplementation(libs.coroutines.core)
testImplementation(libs.coroutines.test)
testRuntimeOnly(libs.junit.platform.engine)
testRuntimeOnly(libs.junit.platform.launcher)
testImplementation(libs.kotlinx.coroutines.core)
testImplementation(libs.kotlinx.coroutines.test)
}
tasks.test { useJUnitPlatform() }
@@ -35,6 +35,7 @@
<orderEntry type="library" scope="TEST" name="kotlin-test" level="project" />
<orderEntry type="module" module-name="intellij.libraries.junit5" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.jewel.intUi.standalone" />
<orderEntry type="module" module-name="intellij.platform.jewel.foundation" scope="TEST" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.coroutines.core" scope="TEST" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.coroutines.test" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.jewel.ui" scope="TEST" />
@@ -0,0 +1,39 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jewel.intui.code.highlighting
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors
/**
* A test-only [SyntaxHighlightColors] palette where each
* [org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType] maps to a unique, easily identifiable color. Use
* this in grammar tests so assertions are purely about tokenizer correctness, not production palette values.
*/
internal val testColors =
SyntaxHighlightColors(
keyword = Color.Red,
type = Color.Blue,
constant = Color.Green,
functionCall = Color.Yellow,
string = Color.Cyan,
comment = Color.Gray,
number = Color.Magenta,
builtin = Color.White,
)
/** All [SpanStyle]s covering character offset [index], in [spanStyles] order. */
internal fun AnnotatedString.spansAt(index: Int): List<SpanStyle> =
spanStyles.filter { it.start <= index && index < it.end }.map { it.item }
/** First non-[Color.Unspecified] color at character offset [index], or null if the position is unstyled. */
internal fun AnnotatedString.colorAt(index: Int): Color? =
spansAt(index).firstOrNull { it.color != Color.Unspecified }?.color
/**
* Color of the [index]-th [SpanStyle] in [spanStyles] order (i.e. the n-th highlighted token), or null if that span has
* no explicit color. Unlike [colorAt], [index] is a span index, not a character offset.
*/
internal fun AnnotatedString.spanColorAt(index: Int): Color? =
spanStyles[index].item.color.takeIf { it != Color.Unspecified }
@@ -0,0 +1,131 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jewel.intui.code.highlighting
import androidx.compose.ui.graphics.Color
import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar
import org.jetbrains.jewel.intui.standalone.code.highlighting.SimpleCodeHighlighter
import org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
import org.junit.jupiter.api.Test
internal class SimpleCodeHighlighterTest {
private val highlighter = SimpleCodeHighlighter(testColors)
private suspend fun highlight(code: String, language: String) = highlighter.highlight(code, language).first()
@Test
fun `blank language returns plain annotated string`() = runTest {
assertTrue(highlight("val x = 1", "").spanStyles.isEmpty())
}
@Test
fun `whitespace-only language returns plain annotated string`() = runTest {
assertTrue(highlight("val x = 1", " ").spanStyles.isEmpty())
}
@Test
fun `unknown language returns plain annotated string`() = runTest {
assertTrue(highlight("some code here", "brainfuck").spanStyles.isEmpty())
}
@Test
fun `empty code returns empty annotated string`() = runTest {
val result = highlight("", "kotlin")
assertEquals("", result.text)
assertTrue(result.spanStyles.isEmpty())
}
@Test
fun `language lookup is case-insensitive`() = runTest {
val lower = highlight("val x = 1", "kotlin")
val upper = highlight("val x = 1", "KOTLIN")
val mixed = highlight("val x = 1", "Kotlin")
assertEquals(lower.spanStyles.size, upper.spanStyles.size)
assertEquals(lower.spanStyles.size, mixed.spanStyles.size)
}
@Test
fun `language tag with extra attributes resolves the base language`() = runTest {
assertTrue(highlight("val x = 1", "kotlin ide runnable").spanStyles.isNotEmpty())
}
@Test
fun `alias with extra attributes resolves the base language`() = runTest {
assertTrue(highlight("val x = 1", "kts something").spanStyles.isNotEmpty())
}
@Test
fun `partial language tag does not match`() = runTest {
assertTrue(highlight("val x = 1", "kot").spanStyles.isEmpty())
}
@Test
fun `grammar name containing a space matches info string with attributes`() = runTest {
val custom = LanguageGrammar(name = "my lang", rules = listOf(TokenRule.keyword("\\bhello\\b")))
val customHighlighter = SimpleCodeHighlighter(testColors, listOf(custom))
assertTrue(customHighlighter.highlight("hello", "my lang").first().spanStyles.isNotEmpty())
assertTrue(customHighlighter.highlight("hello", "my lang extra-attr").first().spanStyles.isNotEmpty())
assertTrue(customHighlighter.highlight("hello", "my").first().spanStyles.isEmpty())
}
@Test
fun `custom grammar alias is recognized`() = runTest {
val custom =
LanguageGrammar(name = "mylang", aliases = listOf("ml"), rules = listOf(TokenRule.keyword("\\bhello\\b")))
val result = SimpleCodeHighlighter(testColors, listOf(custom)).highlight("hello", "ml").first()
assertTrue(result.spanStyles.isNotEmpty())
}
@Test
fun `additional grammars take precedence over built-in grammars`() = runTest {
val override = LanguageGrammar(name = "kotlin", rules = listOf(TokenRule.comment(".*")))
val result = SimpleCodeHighlighter(testColors, listOf(override)).highlight("val x = 1", "kotlin").first()
assertEquals(Color.Gray, result.colorAt(0)) // Gray = comment in testColors
}
@Test
fun `custom language grammar is recognized`() = runTest {
val custom = LanguageGrammar(name = "mylang", rules = listOf(TokenRule.keyword("\\b(hello|world)\\b")))
val result = SimpleCodeHighlighter(testColors, listOf(custom)).highlight("hello world", "mylang").first()
assertEquals(Color.Red, result.colorAt(0)) // Red = keyword in testColors
assertEquals(Color.Red, result.colorAt(6))
}
@Test
fun `functionCall rule with no capturing group produces no spans`() = runTest {
val rule = TokenRule.functionCall("\\b[A-Za-z_]\\w*\\b")
val grammar = LanguageGrammar(name = "test", rules = listOf(rule))
val result = SimpleCodeHighlighter(testColors, listOf(grammar)).highlight("myFunction()", "test").first()
assertTrue(result.spanStyles.isEmpty(), "Expected no spans when capturing group is missing")
}
@Test
fun `functionDeclaration rule with no capturing groups produces no spans`() = runTest {
val rule = TokenRule.functionDeclaration("fun [A-Za-z_]\\w*")
val grammar = LanguageGrammar(name = "test", rules = listOf(rule))
val result = SimpleCodeHighlighter(testColors, listOf(grammar)).highlight("fun myFunc()", "test").first()
assertTrue(result.spanStyles.isEmpty(), "Expected no spans when capturing groups are missing")
}
@Test
fun `zero-length match pattern does not cause infinite loop`() = runTest {
val rule = TokenRule.keyword("(?=\\w)") // lookahead matches at every word character position
val grammar = LanguageGrammar(name = "test", rules = listOf(rule))
val result = SimpleCodeHighlighter(testColors, listOf(grammar)).highlight("hello", "test").first()
assertEquals("hello", result.text)
}
@Test
fun `functionDeclaration rule with only one capturing group produces only the keyword span`() = runTest {
val rule = TokenRule.functionDeclaration("\\b(fun)\\s+[A-Za-z_]\\w*")
val grammar = LanguageGrammar(name = "test", rules = listOf(rule))
val result = SimpleCodeHighlighter(testColors, listOf(grammar)).highlight("fun myFunc()", "test").first()
assertEquals(1, result.spanStyles.size)
assertEquals(Color.Red, result.colorAt(0)) // Red = keyword in testColors
assertNull(result.colorAt(4)) // "myFunc" — no span for missing group 2
}
}
@@ -0,0 +1,69 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jewel.intui.code.highlighting
import kotlin.test.assertEquals
import org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
import org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
import org.junit.jupiter.api.Test
internal class TokenRuleTest {
@Test
fun `comment factory colors entire match`() {
val rule = TokenRule.comment("//.*")
assertEquals(mapOf(0 to TokenType.COMMENT), rule.captures)
}
@Test
fun `string factory colors entire match`() {
val rule = TokenRule.string("\".*\"")
assertEquals(mapOf(0 to TokenType.STRING), rule.captures)
}
@Test
fun `keyword factory colors entire match`() {
val rule = TokenRule.keyword("\\b(if|else)\\b")
assertEquals(mapOf(0 to TokenType.KEYWORD), rule.captures)
}
@Test
fun `type factory colors entire match`() {
val rule = TokenRule.type("\\bString\\b")
assertEquals(mapOf(0 to TokenType.TYPE), rule.captures)
}
@Test
fun `constant factory colors entire match`() {
val rule = TokenRule.constant("\\b(true|false|null)\\b")
assertEquals(mapOf(0 to TokenType.CONSTANT), rule.captures)
}
@Test
fun `number factory colors entire match`() {
val rule = TokenRule.number("\\b[0-9]+\\b")
assertEquals(mapOf(0 to TokenType.NUMBER), rule.captures)
}
@Test
fun `builtin factory colors entire match`() {
val rule = TokenRule.builtin("\\bprintln\\b")
assertEquals(mapOf(0 to TokenType.BUILTIN), rule.captures)
}
@Test
fun `functionCall factory colors group 1`() {
val rule = TokenRule.functionCall("\\b([A-Za-z_]\\w*)\\s*(?=\\()")
assertEquals(mapOf(1 to TokenType.FUNCTION_CALL), rule.captures)
}
@Test
fun `functionDeclaration factory colors group 1 as keyword and group 2 as function call`() {
val rule = TokenRule.functionDeclaration("\\b(fun)\\s+([A-Za-z_]\\w*)")
assertEquals(mapOf(1 to TokenType.KEYWORD, 2 to TokenType.FUNCTION_CALL), rule.captures)
}
@Test
fun `typeDeclaration factory colors group 1 as keyword and group 2 as builtin`() {
val rule = TokenRule.typeDeclaration("\\b(class)\\s+([A-Za-z_]\\w*)")
assertEquals(mapOf(1 to TokenType.KEYWORD, 2 to TokenType.BUILTIN), rule.captures)
}
}
@@ -0,0 +1,148 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jewel.intui.code.highlighting.languages
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.jetbrains.jewel.intui.code.highlighting.colorAt
import org.jetbrains.jewel.intui.code.highlighting.spansAt
import org.jetbrains.jewel.intui.code.highlighting.testColors
import org.jetbrains.jewel.intui.standalone.code.highlighting.SimpleCodeHighlighter
import org.junit.jupiter.api.Test
internal class JavaGrammarTest {
private val highlighter = SimpleCodeHighlighter(testColors)
private suspend fun highlight(code: String, language: String = "java") =
highlighter.highlight(code, language).first()
@Test
fun `name and all aliases are recognized`() = runTest {
for (alias in listOf("java", "jav", "jsh")) {
assertTrue(highlight("int x = 1;", alias).spanStyles.isNotEmpty(), "Alias '$alias' not recognized")
}
}
@Test
fun `line comment is colored as comment`() = runTest {
assertEquals(testColors.comment, highlight("// a comment").colorAt(0))
}
@Test
fun `line comment is italic`() = runTest {
assertEquals(FontStyle.Italic, highlight("// comment").spansAt(0).first().fontStyle)
}
@Test
fun `block comment is colored as comment`() = runTest {
assertEquals(testColors.comment, highlight("/* block */").colorAt(0))
}
@Test
fun `keyword inside line comment is not colored as keyword`() = runTest {
val result = highlight("// public static void")
assertNotEquals(testColors.keyword, result.colorAt(3))
assertEquals(testColors.comment, result.colorAt(3))
}
@Test
fun `double-quoted string is colored as string`() = runTest {
assertEquals(testColors.string, highlight("\"hello\"").colorAt(0))
}
@Test
fun `text block is colored as string`() = runTest {
assertEquals(testColors.string, highlight("\"\"\"hello\"\"\"").colorAt(0))
}
@Test
fun `keyword inside string is not colored as keyword`() = runTest {
val result = highlight("\"public static\"")
assertEquals(testColors.string, result.colorAt(1))
assertNotEquals(testColors.keyword, result.colorAt(1))
}
@Test
fun `method name before parenthesis is colored as function call`() = runTest {
// "main" starts at position 12 in "public void main()"
assertEquals(testColors.functionCall, highlight("public void main()").colorAt(12))
}
@Test
fun `method call site is colored as function call`() = runTest {
assertEquals(testColors.functionCall, highlight("System.out.println()").colorAt(11)) // "println" at 11
}
@Test
fun `modifiers and declaration keywords are colored as keyword`() = runTest {
for (keyword in listOf("public", "private", "static", "final", "class", "interface", "return", "new")) {
val result = highlight(keyword)
assertEquals(testColors.keyword, result.colorAt(0), "'$keyword' should be colored as keyword")
}
}
@Test
fun `keywords before parenthesis are not colored as function calls`() = runTest {
assertEquals(testColors.keyword, highlight("if (x) {}").colorAt(0), "Keyword was colored with the wrong color.")
}
@Test
fun `keywords are bold`() = runTest {
for (keyword in listOf("public", "static", "final")) {
assertEquals(FontWeight.Bold, highlight(keyword).spansAt(0).first().fontWeight, "'$keyword' should be bold")
}
}
@Test
fun `language constants are colored as constant`() = runTest {
for (constant in listOf("true", "false", "null")) {
assertEquals(
testColors.constant,
highlight(constant).colorAt(0),
"'$constant' should be colored as constant",
)
}
}
@Test
fun `primitive types are colored as type`() = runTest {
for (type in listOf("boolean", "int", "long", "double", "float", "void", "char", "byte")) {
assertEquals(testColors.type, highlight(type).colorAt(0), "'$type' should be colored as type")
}
}
@Test
fun `common stdlib roots are colored as builtin`() = runTest {
for (builtin in listOf("String", "Object", "System", "Math")) {
assertEquals(testColors.builtin, highlight(builtin).colorAt(0), "'$builtin' should be colored as builtin")
}
}
@Test
fun `decimal integers are colored as number`() = runTest {
for (number in listOf("42", "42L", "1_000")) {
assertEquals(testColors.number, highlight(number).colorAt(0), "'$number' should be colored as number")
}
}
@Test
fun `hex numbers are colored as number`() = runTest {
assertEquals(testColors.number, highlight("0xDEADBEEF").colorAt(0))
}
@Test
fun `binary numbers are colored as number`() = runTest {
assertEquals(testColors.number, highlight("0b1010").colorAt(0))
}
@Test
fun `floating point numbers are colored as number`() = runTest {
for (number in listOf("3.14", "2.5f", "1.0d", "1e10")) {
assertEquals(testColors.number, highlight(number).colorAt(0), "'$number' should be colored as number")
}
}
}
@@ -0,0 +1,204 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jewel.intui.code.highlighting.languages
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.jetbrains.jewel.intui.code.highlighting.colorAt
import org.jetbrains.jewel.intui.code.highlighting.spanColorAt
import org.jetbrains.jewel.intui.code.highlighting.spansAt
import org.jetbrains.jewel.intui.code.highlighting.testColors
import org.jetbrains.jewel.intui.standalone.code.highlighting.SimpleCodeHighlighter
import org.junit.jupiter.api.Test
internal class KotlinGrammarTest {
private val highlighter = SimpleCodeHighlighter(testColors)
private suspend fun highlight(code: String, language: String = "kotlin") =
highlighter.highlight(code, language).first()
@Test
fun `name and all aliases are recognized`() = runTest {
for (alias in listOf("kotlin", "kt", "kts", "ktm")) {
assertTrue(highlight("val x = 1", alias).spanStyles.isNotEmpty(), "Alias '$alias' not recognized")
}
}
@Test
fun `line comment is colored as comment`() = runTest {
val result = highlight("// this is a comment")
assertEquals(testColors.comment, result.colorAt(0))
}
@Test
fun `line comment is italic`() = runTest {
val result = highlight("// comment")
assertEquals(FontStyle.Italic, result.spansAt(0).first().fontStyle)
}
@Test
fun `block comment is colored as comment`() = runTest {
assertEquals(testColors.comment, highlight("/* block */").colorAt(0))
}
@Test
fun `keyword inside line comment is not colored as keyword`() = runTest {
val result = highlight("// val fun class")
assertNotEquals(testColors.keyword, result.colorAt(3)) // 'v' of val
assertEquals(testColors.comment, result.spanColorAt(0))
}
@Test
fun `keyword inside block comment is not colored as keyword`() = runTest {
val result = highlight("/* val fun */")
assertNotEquals(testColors.keyword, result.colorAt(3))
assertEquals(testColors.comment, result.colorAt(3))
}
@Test
fun `double-quoted string is colored as string`() = runTest {
assertEquals(testColors.string, highlight("\"hello world\"").colorAt(0))
}
@Test
fun `triple-quoted string is colored as string`() = runTest {
assertEquals(testColors.string, highlight("\"\"\"multi\nline\"\"\"").colorAt(0))
}
@Test
fun `keyword inside string is not colored as keyword`() = runTest {
val result = highlight("\"val fun class\"")
assertEquals(testColors.string, result.colorAt(1)) // 'v' of val inside string
assertNotEquals(testColors.keyword, result.colorAt(1))
}
@Test
fun `fun declaration colors keyword and function name separately`() = runTest {
val result = highlight("fun myFunc() {}")
assertEquals(testColors.keyword, result.spanColorAt(0)) // "fun" → keyword
assertEquals(testColors.functionCall, result.spanColorAt(1)) // "myFunc" → function call
}
@Test
fun `fun keyword in declaration is bold`() = runTest {
val result = highlight("fun myFunc() {}")
assertEquals(FontWeight.Bold, result.spansAt(0).first().fontWeight)
}
@Test
fun `class declaration colors keyword and class name separately`() = runTest {
val result = highlight("class MyClass")
assertEquals(testColors.keyword, result.spanColorAt(0)) // "class" → keyword
assertEquals(testColors.builtin, result.spanColorAt(1)) // "MyClass" → builtin
}
@Test
fun `storage keywords are colored as keyword`() = runTest {
for (keyword in listOf("val", "var", "fun", "class", "suspend", "override", "data", "sealed", "inline")) {
val result = highlight(keyword)
assertEquals(testColors.keyword, result.colorAt(0), "'$keyword' should be colored as keyword")
}
}
@Test
fun `keywords are bold`() = runTest {
for (keyword in listOf("val", "var", "fun")) {
val result = highlight(keyword)
assertEquals(FontWeight.Bold, result.spansAt(0).first().fontWeight, "'$keyword' should be bold")
}
}
@Test
fun `control keywords are colored as keyword`() = runTest {
for (keyword in listOf("if", "else", "when", "for", "while", "return", "throw", "try", "catch")) {
val result = highlight(keyword)
assertEquals(testColors.keyword, result.colorAt(0), "'$keyword' should be colored as keyword")
}
}
@Test
fun `control keywords before parenthesis are not colored as function calls`() = runTest {
// functionCall rule comes after keyword rules — keyword wins on ties
for (keyword in listOf("if", "while", "for", "when", "catch")) {
val result = highlight("$keyword (")
assertEquals(
testColors.keyword,
result.colorAt(0),
"'$keyword' should stay keyword, not become function call",
)
assertNotEquals(testColors.functionCall, result.colorAt(0))
}
}
@Test
fun `language constants are colored as constant`() = runTest {
for (constant in listOf("true", "false", "null")) {
assertEquals(
testColors.constant,
highlight(constant).colorAt(0),
"'$constant' should be colored as constant",
)
}
}
@Test
fun `built-in types are colored as type`() = runTest {
for (type in listOf("String", "Int", "Long", "Boolean", "List", "Map", "Unit", "Any")) {
assertEquals(testColors.type, highlight(type).colorAt(0), "'$type' should be colored as type")
}
}
@Test
fun `decimal integers are colored as number`() = runTest {
for (number in listOf("42", "1_000_000", "42L")) {
assertEquals(testColors.number, highlight(number).colorAt(0), "'$number' should be colored as number")
}
}
@Test
fun `decimal floats and scientific notation are colored as number`() = runTest {
for (number in listOf("3.14", "2.5f", "1e10", "6.02e23")) {
assertEquals(testColors.number, highlight(number).colorAt(0), "'$number' should be colored as number")
}
}
@Test
fun `hex numbers are colored as number`() = runTest {
assertEquals(testColors.number, highlight("0xDEADBEEF").colorAt(0))
}
@Test
fun `binary numbers are colored as number`() = runTest {
assertEquals(testColors.number, highlight("0b1010_1010").colorAt(0))
}
@Test
fun `unsigned integer literals are colored as number`() = runTest {
for (number in listOf("42u", "42U", "42uL", "42UL")) {
assertEquals(testColors.number, highlight(number).colorAt(0), "'$number' should be colored as number")
}
}
@Test
fun `function call site is colored as function call`() = runTest {
assertEquals(testColors.functionCall, highlight("tomato()").colorAt(0))
}
@Test
fun `stdlib call site is colored as function call`() = runTest {
assertEquals(testColors.functionCall, highlight("println(\"hi\")").colorAt(0))
}
@Test
fun `enum class should be colored as keyword`() = runTest {
val spanStyles = highlight("enum class Foo")
assertEquals(testColors.keyword, spanStyles.spanColorAt(0))
assertEquals(testColors.keyword, spanStyles.spanColorAt(1))
assertEquals(testColors.builtin, spanStyles.spanColorAt(2))
}
}
@@ -9,6 +9,73 @@
- f:getLazyMessage(java.lang.String,java.lang.Object[]):java.util.function.Supplier
- f:getMessage(java.lang.String,java.lang.Object[]):java.lang.String
*f:org.jetbrains.jewel.intui.standalone.bundle.DynamicBundle$Companion
*f:org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar
- sf:$stable:I
- *sf:Companion:org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar$Companion
- <init>(java.lang.String,java.util.List,java.util.List):V
- b:<init>(java.lang.String,java.util.List,java.util.List,I,kotlin.jvm.internal.DefaultConstructorMarker):V
- f:getAliases():java.util.List
- f:getName():java.lang.String
- f:getRules():java.util.List
- f:matches(java.lang.String):Z
*f:org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar$Companion
*f:org.jetbrains.jewel.intui.standalone.code.highlighting.SimpleCodeHighlighter
- org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter
- sf:$stable:I
- <init>(org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors):V
- <init>(org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors,java.util.List,kotlinx.coroutines.CoroutineDispatcher):V
- b:<init>(org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors,java.util.List,kotlinx.coroutines.CoroutineDispatcher,I,kotlin.jvm.internal.DefaultConstructorMarker):V
- f:getAdditionalGrammars():java.util.List
- f:getColors():org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors
- f:getHighlightDispatcher():kotlinx.coroutines.CoroutineDispatcher
- highlight(java.lang.String,java.lang.String):kotlinx.coroutines.flow.Flow
- b:highlight-zTGadEY(java.lang.String,java.lang.String):kotlinx.coroutines.flow.Flow
*f:org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors
- sf:$stable:I
- *sf:Companion:org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors$Companion
- equals(java.lang.Object):Z
- f:getBuiltin-0d7_KjU():J
- f:getComment-0d7_KjU():J
- f:getConstant-0d7_KjU():J
- f:getFunctionCall-0d7_KjU():J
- f:getKeyword-0d7_KjU():J
- f:getNumber-0d7_KjU():J
- f:getString-0d7_KjU():J
- f:getType-0d7_KjU():J
- hashCode():I
*f:org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors$Companion
- f:dark():org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors
- f:light():org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors
*f:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
- sf:$stable:I
- *sf:Companion:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule$Companion
- <init>(java.lang.String,java.util.Map):V
- f:getCaptures():java.util.Map
- f:getPattern():java.lang.String
*f:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule$Companion
- f:builtin(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
- f:comment(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
- f:constant(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
- f:functionCall(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
- f:functionDeclaration(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
- f:keyword(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
- f:number(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
- f:string(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
- f:type(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
- f:typeDeclaration(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
*e:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
- java.lang.Enum
- sf:BUILTIN:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
- sf:COMMENT:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
- sf:CONSTANT:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
- sf:FUNCTION_CALL:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
- sf:KEYWORD:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
- sf:NUMBER:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
- sf:STRING:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
- sf:TYPE:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
- s:getEntries():kotlin.enums.EnumEntries
- s:valueOf(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
- s:values():org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType[]
f:org.jetbrains.jewel.intui.standalone.styling.IntUiIconButtonStylingKt
- *sf:darkTransparentBackground(org.jetbrains.jewel.ui.component.styling.IconButtonStyle$Companion,org.jetbrains.jewel.ui.component.styling.IconButtonColors,org.jetbrains.jewel.ui.component.styling.IconButtonMetrics):org.jetbrains.jewel.ui.component.styling.IconButtonStyle
- *bs:darkTransparentBackground$default(org.jetbrains.jewel.ui.component.styling.IconButtonStyle$Companion,org.jetbrains.jewel.ui.component.styling.IconButtonColors,org.jetbrains.jewel.ui.component.styling.IconButtonMetrics,I,java.lang.Object):org.jetbrains.jewel.ui.component.styling.IconButtonStyle
@@ -1 +1,2 @@
androidx/compose/**
kotlin/jvm/internal/DefaultConstructorMarker
@@ -106,6 +106,89 @@ package org.jetbrains.jewel.intui.standalone.bundle {
}
package org.jetbrains.jewel.intui.standalone.code.highlighting {
@SuppressCompatibility @androidx.compose.runtime.Immutable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public final class LanguageGrammar {
ctor public LanguageGrammar(String name, optional java.util.List<java.lang.String> aliases, java.util.List<org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule> rules);
method public java.util.List<java.lang.String> getAliases();
method public String getName();
method public java.util.List<org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule> getRules();
method public boolean matches(String languageTag);
property public java.util.List<java.lang.String> aliases;
property public String name;
property public java.util.List<org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule> rules;
field public static final org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar.Companion Companion;
}
public static final class LanguageGrammar.Companion {
}
@SuppressCompatibility @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public final class SimpleCodeHighlighter implements org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter {
ctor public SimpleCodeHighlighter(org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors colors);
ctor public SimpleCodeHighlighter(org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors colors, java.util.List<org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar> additionalGrammars, optional kotlinx.coroutines.CoroutineDispatcher highlightDispatcher);
method public java.util.List<org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar> getAdditionalGrammars();
method public org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors getColors();
method public kotlinx.coroutines.CoroutineDispatcher getHighlightDispatcher();
method public kotlinx.coroutines.flow.Flow<androidx.compose.ui.text.AnnotatedString> highlight(String code, String language);
method @Deprecated public kotlinx.coroutines.flow.Flow<androidx.compose.ui.text.AnnotatedString> highlight(String code, String? mimeType);
property public java.util.List<org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar> additionalGrammars;
property public org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors colors;
property public kotlinx.coroutines.CoroutineDispatcher highlightDispatcher;
}
@SuppressCompatibility @androidx.compose.runtime.Immutable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public final class SyntaxHighlightColors {
ctor @KotlinOnly public SyntaxHighlightColors(androidx.compose.ui.graphics.Color keyword, androidx.compose.ui.graphics.Color type, androidx.compose.ui.graphics.Color constant, androidx.compose.ui.graphics.Color functionCall, androidx.compose.ui.graphics.Color string, androidx.compose.ui.graphics.Color comment, androidx.compose.ui.graphics.Color number, androidx.compose.ui.graphics.Color builtin);
property public androidx.compose.ui.graphics.Color builtin;
property public androidx.compose.ui.graphics.Color comment;
property public androidx.compose.ui.graphics.Color constant;
property public androidx.compose.ui.graphics.Color functionCall;
property public androidx.compose.ui.graphics.Color keyword;
property public androidx.compose.ui.graphics.Color number;
property public androidx.compose.ui.graphics.Color string;
property public androidx.compose.ui.graphics.Color type;
field public static final org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors.Companion Companion;
}
public static final class SyntaxHighlightColors.Companion {
method public org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors dark();
method public org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors light();
}
@SuppressCompatibility @androidx.compose.runtime.Immutable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public final class TokenRule {
ctor public TokenRule(@org.intellij.lang.annotations.Language("RegExp") String pattern, java.util.Map<java.lang.Integer,? extends org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType> captures);
method public java.util.Map<java.lang.Integer,org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType> getCaptures();
method public String getPattern();
property public java.util.Map<java.lang.Integer,org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType> captures;
property @org.intellij.lang.annotations.Language("RegExp") public String pattern;
field public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule.Companion Companion;
}
public static final class TokenRule.Companion {
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule builtin(@org.intellij.lang.annotations.Language("RegExp") String pattern);
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule comment(@org.intellij.lang.annotations.Language("RegExp") String pattern);
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule constant(@org.intellij.lang.annotations.Language("RegExp") String pattern);
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule functionCall(@org.intellij.lang.annotations.Language("RegExp") String pattern);
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule functionDeclaration(@org.intellij.lang.annotations.Language("RegExp") String pattern);
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule keyword(@org.intellij.lang.annotations.Language("RegExp") String pattern);
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule number(@org.intellij.lang.annotations.Language("RegExp") String pattern);
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule string(@org.intellij.lang.annotations.Language("RegExp") String pattern);
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule type(@org.intellij.lang.annotations.Language("RegExp") String pattern);
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule typeDeclaration(@org.intellij.lang.annotations.Language("RegExp") String pattern);
}
@SuppressCompatibility @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public enum TokenType {
enum_constant public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType BUILTIN;
enum_constant public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType COMMENT;
enum_constant public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType CONSTANT;
enum_constant public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType FUNCTION_CALL;
enum_constant public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType KEYWORD;
enum_constant public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType NUMBER;
enum_constant public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType STRING;
enum_constant public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType TYPE;
}
}
package org.jetbrains.jewel.intui.standalone.styling {
public final class IntUIBannerStylingKt {
@@ -0,0 +1,11 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jewel.intui.standalone.code.highlighting
import org.jetbrains.jewel.intui.standalone.code.highlighting.languages.JAVA
import org.jetbrains.jewel.intui.standalone.code.highlighting.languages.KOTLIN
// Patterns are adapted from the tmLanguage grammars in plugins/textmate/lib/bundles/.
// Java's regex engine is used (see TokenRule for known PCRE/Oniguruma incompatibilities).
internal object BuiltInLanguageGrammars {
val all: List<LanguageGrammar> by lazy { listOf(KOTLIN, JAVA) }
}
@@ -0,0 +1,48 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jewel.intui.standalone.code.highlighting
import androidx.compose.runtime.Immutable
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
/**
* Defines the syntax highlighting grammar for a single programming language.
*
* A grammar consists of a canonical [name] and optional [aliases] (all lowercase) that identify the language in a
* Markdown fenced code block info string, and an ordered list of [TokenRule]s that are applied to tokenize the source
* code.
*
* Rules are tried at each position in the input; the rule whose match starts earliest wins. In case of a tie, the rule
* that appears first in [rules] wins, so higher-priority rules (e.g., comments and strings) should come first.
*
* @param name Canonical name of the language (e.g, `"kotlin"`). Matched against the info string of a Markdown fenced
* code block, along with [aliases].
* @param aliases Alternative identifiers for the language, typically file extensions or shorthand (e.g, `["kt",
* "kts"]`).
* @param rules Priority-ordered list of [TokenRule]s. Each rule's [TokenRule.captures] maps capture group indices to
* [TokenType]s. A single rule can color multiple parts of a match independently.
*/
@ApiStatus.Experimental
@ExperimentalJewelApi
@Immutable
public class LanguageGrammar(public val name: String, aliases: List<String> = emptyList(), rules: List<TokenRule>) {
public val aliases: List<String> = aliases.toList()
public val rules: List<TokenRule> = rules.toList()
private val allIdentifiers: Set<String> = buildSet {
add(name)
addAll(this@LanguageGrammar.aliases)
}
/**
* Returns whether this grammar should be used for the given [languageTag], i.e. whether the tag equals this
* grammar's [name] or any of its [aliases].
*
* @param languageTag The language identifier to check, e.g. the info string of a Markdown fenced code block. The
* comparison is case-sensitive, so the tag is expected to be lowercase.
*/
public fun matches(languageTag: String): Boolean = languageTag in allIdentifiers
/** Companion object for [LanguageGrammar]. */
public companion object
}
@@ -0,0 +1,121 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jewel.intui.standalone.code.highlighting
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import kotlin.collections.iterator
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.foundation.code.MimeType
import org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter
/**
* A lightweight [CodeHighlighter] for use in standalone Jewel applications.
*
* Highlighting is driven by a regex rule engine (see [TokenRule]) that mirrors the structure of tmLanguage grammars.
* Each [LanguageGrammar] contains an ordered list of [TokenRule]s; the tokenizer picks the rule whose match starts
* earliest in the remaining input (ties broken by rule order), applies its capture group spans, and advances past the
* match.
*
* Built-in grammars are provided by [BuiltInLanguageGrammars]. Custom or additional grammars can be supplied via
* [additionalGrammars]; these are searched first, so they can override built-in behavior for a given language name.
*
* Token colors are controlled by [SyntaxHighlightColors]. Use [SyntaxHighlightColors.Companion.light] and
* [SyntaxHighlightColors.Companion.dark] for palettes matching IntelliJ's Default and Darcula editor schemes, or supply
* your own. Theme changes are handled at the call site via `remember(isDark)` in `ProvideMarkdownStyling` — this class
* itself is stateless and always emits a single [AnnotatedString].
*
* @param colors The token color palette to use for styling.
* @param additionalGrammars Extra grammars to search before the built-in ones. Use this to support additional languages
* or to override built-in grammar rules for an existing language.
* @param highlightDispatcher The [CoroutineDispatcher] on which tokenization is performed. Defaults to
* [Dispatchers.Default] so the regex scan runs off the collecting thread (e.g. the composition path).
*/
@ApiStatus.Experimental
@ExperimentalJewelApi
public class SimpleCodeHighlighter(
public val colors: SyntaxHighlightColors,
public val additionalGrammars: List<LanguageGrammar>,
public val highlightDispatcher: CoroutineDispatcher = Dispatchers.Default,
) : CodeHighlighter {
public constructor(colors: SyntaxHighlightColors) : this(colors, emptyList())
@Deprecated(message = "Use highlight(code, language) instead.", level = DeprecationLevel.HIDDEN)
override fun highlight(code: String, mimeType: MimeType?): Flow<AnnotatedString> = flowOf(AnnotatedString(code))
override fun highlight(code: String, language: String): Flow<AnnotatedString> {
if (language.isBlank()) return flowOf(AnnotatedString(code))
val grammar = findGrammar(language) ?: return flowOf(AnnotatedString(code))
return flow { emit(withContext(highlightDispatcher) { tokenize(code, grammar) }) }
}
private fun findGrammar(language: String): LanguageGrammar? {
val grammars = additionalGrammars + BuiltInLanguageGrammars.all
val normalizedLanguage = language.trim().lowercase()
val exactMatch = findGrammar(normalizedLanguage, grammars)
if (exactMatch != null) {
return exactMatch
}
var index = normalizedLanguage.lastIndexOf(' ')
while (index != -1) {
val nameWithoutCustomizations = normalizedLanguage.take(index)
val grammar = findGrammar(nameWithoutCustomizations, grammars)
if (grammar != null) {
return grammar
}
index = normalizedLanguage.lastIndexOf(' ', startIndex = (index - 1).coerceAtLeast(0))
}
return null
}
private fun findGrammar(tag: String, grammars: List<LanguageGrammar>): LanguageGrammar? =
grammars.firstOrNull { it.matches(tag) }
private fun tokenize(code: String, grammar: LanguageGrammar): AnnotatedString {
data class Span(val start: Int, val end: Int, val style: SpanStyle)
val spans = mutableListOf<Span>()
var i = 0
while (i < code.length) {
// Find the rule whose match starts earliest; ties broken by rule order
var bestMatch: MatchResult? = null
var bestRule: TokenRule? = null
for (rule in grammar.rules) {
val match = rule.find(code, i) ?: continue
if (bestMatch == null || match.range.first < bestMatch.range.first) {
bestMatch = match
bestRule = rule
}
}
if (bestMatch == null || bestRule == null) break
for ((groupIndex, tokenType) in bestRule.captures) {
// if the regex doesn't have as many groups as bestMatch, skip silently
if (groupIndex >= bestMatch.groupValues.size) continue
val group = bestMatch.groups[groupIndex] ?: continue
if (group.range.isEmpty()) continue
spans += Span(group.range.first, group.range.last + 1, colors.styleFor(tokenType))
}
i = maxOf(i + 1, bestMatch.range.last + 1)
}
return buildAnnotatedString {
append(code)
for (span in spans) addStyle(span.style, span.start, span.end)
}
}
}
@@ -0,0 +1,136 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jewel.intui.standalone.code.highlighting
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
/**
* Defines the colors used to render each [TokenType] in a syntax-highlighted code block.
*
* The default light and dark palettes are based on IntelliJ IDEA's "Default" and "Darcula" editor color schemes
* respectively. Colors are intentionally not sourced from the Jewel UI theme palette — those are UI chrome colors, not
* editor token colors.
*
* Use [SyntaxHighlightColors.light] and [SyntaxHighlightColors.dark] to get the built-in palettes, or construct your
* own instance to fully customize token colors.
*
* @param keyword Color for language keywords (e.g., `val`, `fun`, `class`, `if`). Rendered bold.
* @param type Color for built-in or primitive types (e.g., `String`, `Int`, `void`, `bool`).
* @param constant Color for language constants (e.g., `true`, `false`, `null`, `nil`).
* @param functionCall Color for function and method names.
* @param string Color for string literals.
* @param comment Color for line and block comments. Rendered italic.
* @param number Color for numeric literals.
* @param builtin Color for well-known built-in functions and standard library identifiers.
*/
@ApiStatus.Experimental
@ExperimentalJewelApi
@Immutable
public class SyntaxHighlightColors(
public val keyword: Color,
public val type: Color,
public val constant: Color,
public val functionCall: Color,
public val string: Color,
public val comment: Color,
public val number: Color,
public val builtin: Color,
) {
internal fun styleFor(tokenType: TokenType): SpanStyle =
when (tokenType) {
TokenType.KEYWORD -> SpanStyle(color = keyword, fontWeight = FontWeight.Bold)
TokenType.TYPE -> SpanStyle(color = type)
TokenType.CONSTANT -> SpanStyle(color = constant)
TokenType.FUNCTION_CALL -> SpanStyle(color = functionCall)
TokenType.STRING -> SpanStyle(color = string)
TokenType.COMMENT -> SpanStyle(color = comment, fontStyle = FontStyle.Italic)
TokenType.NUMBER -> SpanStyle(color = number)
TokenType.BUILTIN -> SpanStyle(color = builtin)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SyntaxHighlightColors
if (keyword != other.keyword) return false
if (type != other.type) return false
if (constant != other.constant) return false
if (functionCall != other.functionCall) return false
if (string != other.string) return false
if (comment != other.comment) return false
if (number != other.number) return false
if (builtin != other.builtin) return false
return true
}
override fun hashCode(): Int {
var result = keyword.hashCode()
result = 31 * result + type.hashCode()
result = 31 * result + constant.hashCode()
result = 31 * result + functionCall.hashCode()
result = 31 * result + string.hashCode()
result = 31 * result + comment.hashCode()
result = 31 * result + number.hashCode()
result = 31 * result + builtin.hashCode()
return result
}
override fun toString(): String =
"SyntaxHighlightColors(" +
"keyword=$keyword, " +
"type=$type, " +
"constant=$constant, " +
"functionCall=$functionCall, " +
"string=$string, " +
"comment=$comment, " +
"number=$number, " +
"builtin=$builtin" +
")"
/** Companion object for [SyntaxHighlightColors], holding the built-in palettes. */
public companion object {
/**
* Returns a [SyntaxHighlightColors] palette matching IntelliJ IDEA's "Default" (light) editor color scheme.
*
* Colors sourced from `platform/platform-resources/src/DefaultColorSchemesManager.xml`, scheme `Default`. Token
* types not explicitly defined in that scheme (e.g., function names, type names) use [Color.Unspecified], which
* means they inherit the ambient text color and appear unstyled.
*/
public fun light(): SyntaxHighlightColors =
SyntaxHighlightColors(
keyword = Color(0xFF000080), // DEFAULT_KEYWORD: value="80" → 000080
type = Color.Unspecified, // not defined in Default scheme — inherits
constant = Color(0xFF660E7A), // DEFAULT_CONSTANT: value="660e7a"
functionCall = Color.Unspecified, // DEFAULT_FUNCTION_DECLARATION: not defined in Default — inherits
string = Color(0xFF008000), // DEFAULT_STRING: value="008000"
comment = Color(0xFF808080), // DEFAULT_LINE_COMMENT / DEFAULT_BLOCK_COMMENT: value="808080"
number = Color(0xFF0000FF), // DEFAULT_NUMBER: value="ff" → 0000ff
builtin = Color.Unspecified, // DEFAULT_PREDEFINED_SYMBOL: bold only, no color — inherits
)
/**
* Returns a [SyntaxHighlightColors] palette matching IntelliJ IDEA's "Darcula" (dark) editor color scheme.
*
* Colors sourced from `platform/platform-resources/src/DefaultColorSchemesManager.xml`, scheme `Darcula`.
*/
public fun dark(): SyntaxHighlightColors =
SyntaxHighlightColors(
keyword = Color(0xFFCC7832), // DEFAULT_KEYWORD: value="cc7832"
type = Color(0xFF769AA5), // DEFAULT_CLASS_REFERENCE: value="769aa5"
constant = Color(0xFF9876AA), // DEFAULT_CONSTANT: value="9876aa"
functionCall = Color(0xFFFFC66D), // DEFAULT_FUNCTION_DECLARATION: value="ffc66d"
string = Color(0xFF6A8759), // DEFAULT_STRING: value="6a8759"
comment = Color(0xFF808080), // DEFAULT_LINE_COMMENT / DEFAULT_BLOCK_COMMENT: value="808080"
number = Color(0xFF6897BB), // DEFAULT_NUMBER: value="6897bb"
builtin = Color(0xFF9876AA), // DEFAULT_CONSTANT (inherited by predefined symbols in Darcula)
)
}
}
@@ -0,0 +1,102 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jewel.intui.standalone.code.highlighting
import androidx.compose.runtime.Immutable
import org.intellij.lang.annotations.Language
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
/**
* A single syntax highlighting rule, consisting of a regex [pattern] and a map from capture group indices to
* [TokenType]s.
*
* Rules are applied in order. The tokenizer picks the rule whose match starts earliest in the remaining input; in case
* of a tie, the rule that appears first in the list wins.
*
* The [captures] map controls which parts of the match are colored:
* - Key `0` refers to the entire match.
* - Keys `1`, `2`, etc. refer to specific capturing groups.
*
* A single rule can produce multiple spans — for example, a rule that matches `fun myFunc` can color `fun` as
* [TokenType.KEYWORD] (group 1) and `myFunc` as [TokenType.FUNCTION_CALL] (group 2) in one pass.
*
* Patterns use Java's `java.util.regex` engine, which supports lookahead (`(?=...)`) and fixed-width lookbehind
* (`(?<=...)`, `(?<!...)`). This covers the vast majority of tmLanguage-style patterns.
*
* **Known limitations compared to PCRE/Oniguruma** (used by TextMate grammars):
* - **POSIX character classes** (`[[:alpha:]]`, `[[:digit:]]`, etc.) are not supported — replace with their Unicode
* equivalents (`[a-zA-Z]`, `[0-9]`, etc.).
* - **Variable-length lookbehind** is not supported — only fixed-width lookbehind (e.g., `(?<=fun )` but not
* `(?<=fun\s+)`). Rewrite as a capturing-group rule instead.
* - **Named backreferences** and **conditional patterns** (`(?(condition)yes|no)`) are not supported.
* - **Subroutine calls** (`\g<name>`) and **recursive patterns** are not supported.
*
* @param pattern The regex pattern string used to match against the source code.
* @param captures A map from capture group index to [TokenType]. Groups not listed here produce no colored span.
*/
@ApiStatus.Experimental
@ExperimentalJewelApi
@Immutable
public class TokenRule(@Language("RegExp") public val pattern: String, captures: Map<Int, TokenType>) {
public val captures: Map<Int, TokenType> = captures.toMap()
private val regex: Regex = Regex(pattern)
internal fun find(input: String, startIndex: Int) = regex.find(input, startIndex)
/** Companion object for [TokenRule], holding factories for the most common single-token rules. */
public companion object {
/** Colors the entire match as [TokenType.COMMENT]. */
public fun comment(@Language("RegExp") pattern: String): TokenRule =
TokenRule(pattern, mapOf(0 to TokenType.COMMENT))
/** Colors the entire match as [TokenType.STRING]. */
public fun string(@Language("RegExp") pattern: String): TokenRule =
TokenRule(pattern, mapOf(0 to TokenType.STRING))
/** Colors the entire match as [TokenType.KEYWORD]. */
public fun keyword(@Language("RegExp") pattern: String): TokenRule =
TokenRule(pattern, mapOf(0 to TokenType.KEYWORD))
/** Colors the entire match as [TokenType.TYPE]. */
public fun type(@Language("RegExp") pattern: String): TokenRule = TokenRule(pattern, mapOf(0 to TokenType.TYPE))
/** Colors the entire match as [TokenType.CONSTANT]. */
public fun constant(@Language("RegExp") pattern: String): TokenRule =
TokenRule(pattern, mapOf(0 to TokenType.CONSTANT))
/** Colors the entire match as [TokenType.NUMBER]. */
public fun number(@Language("RegExp") pattern: String): TokenRule =
TokenRule(pattern, mapOf(0 to TokenType.NUMBER))
/** Colors the entire match as [TokenType.BUILTIN]. */
public fun builtin(@Language("RegExp") pattern: String): TokenRule =
TokenRule(pattern, mapOf(0 to TokenType.BUILTIN))
/**
* Colors **group 1** of the match as [TokenType.FUNCTION_CALL].
*
* Use a capturing group to isolate the function name, excluding any surrounding context such as trailing
* whitespace before `(`. Example: `\b([A-Za-z_]\w*)\s*(?=\()` — group 1 captures only the name.
*/
public fun functionCall(@Language("RegExp") pattern: String): TokenRule =
TokenRule(pattern, mapOf(1 to TokenType.FUNCTION_CALL))
/**
* Colors **group 1** as [TokenType.KEYWORD] and **group 2** as [TokenType.FUNCTION_CALL] in a single match.
*
* Use this for languages that declare functions with an explicit keyword, e.g. `fun myFunc` in Kotlin or `func
* myFunc` in Go. Group 1 should capture the keyword, group 2 the function name.
*/
public fun functionDeclaration(@Language("RegExp") pattern: String): TokenRule =
TokenRule(pattern, mapOf(1 to TokenType.KEYWORD, 2 to TokenType.FUNCTION_CALL))
/**
* Colors **group 1** as [TokenType.KEYWORD] and **group 2** as [TokenType.BUILTIN] in a single match.
*
* Use this for type declaration keywords followed by the type name, e.g. `class MyClass` or `interface Foo`.
* Group 1 should capture the keyword, group 2 the type name.
*/
public fun typeDeclaration(@Language("RegExp") pattern: String): TokenRule =
TokenRule(pattern, mapOf(1 to TokenType.KEYWORD, 2 to TokenType.BUILTIN))
}
}
@@ -0,0 +1,38 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jewel.intui.standalone.code.highlighting
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
/**
* Represents the semantic category of a syntax token used during code highlighting.
*
* Each value maps to a specific visual style in [SyntaxHighlightColors].
*/
@ApiStatus.Experimental
@ExperimentalJewelApi
public enum class TokenType {
/** Language keywords: `val`, `var`, `fun`, `class`, `if`, `return`, `abstract`, `override`, etc. */
KEYWORD,
/** Built-in or primitive types: `String`, `Int`, `Boolean`, `void`, `bool`, etc. */
TYPE,
/** Language constants: `true`, `false`, `null`, `nil`, `None`, `undefined`, etc. */
CONSTANT,
/** Function or method names, e.g. the name in `fun myFunc(` or `myMethod(`. */
FUNCTION_CALL,
/** String literals, including multi-line and raw strings. */
STRING,
/** Line and block comments. */
COMMENT,
/** Numeric literals: integers, floats, hex, binary, etc. */
NUMBER,
/** Built-in functions or well-known standard library identifiers, e.g. `println`, `len`, `fmt.Println`. */
BUILTIN,
}
@@ -0,0 +1,44 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jewel.intui.standalone.code.highlighting.languages
import org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar
import org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
// Patterns adapted from plugins/textmate/lib/bundles/java/syntaxes/java.tmLanguage.json
internal val JAVA =
LanguageGrammar(
name = "java",
aliases = listOf("jav", "jsh"),
rules =
listOf(
// Comments must come first
TokenRule.comment("/\\*[\\s\\S]*?\\*/"),
TokenRule.comment("//[^\n]*"),
// Strings (text blocks first — Java 13+)
TokenRule.string("\"\"\"[\\s\\S]*?\"\"\""),
TokenRule.string("\"(?:[^\"\\\\]|\\\\.)*\""),
TokenRule.string("'(?:[^'\\\\]|\\\\.)*'"),
// keyword.control + storage.modifier.java + storage.type.java
TokenRule.keyword(
"\\b(abstract|assert|break|case|catch|class|const|continue|default|do|else|enum|" +
"extends|final|finally|for|goto|if|implements|import|instanceof|interface|native|new|" +
"package|private|protected|public|record|return|sealed|static|strictfp|super|switch|" +
"synchronized|this|throw|throws|transient|try|var|volatile|while|permits)\\b"
),
// constant.language.java
TokenRule.constant("\\b(true|false|null)\\b"),
// storage.type.primitive.java
TokenRule.type("\\b(boolean|byte|char|double|float|int|long|short|void)\\b"),
// entity.name.function.java — identifier immediately before (
TokenRule.functionCall("\\b([A-Za-z_][A-Za-z0-9_]*)\\s*(?=\\()"),
// support.class.java — common boxed types and stdlib roots
TokenRule.builtin(
"\\b(String|Object|Integer|Long|Double|Float|Boolean|Character|Byte|Short|Number|" +
"Math|System)\\b"
),
// constant.numeric.java
TokenRule.number("\\b0[xX][0-9a-fA-F_]+[Ll]?\\b"),
TokenRule.number("\\b0[bB][01_]+[Ll]?\\b"),
TokenRule.number("\\b[0-9][0-9_]*(?:\\.[0-9_]+)?(?:[eE][+-]?[0-9]+)?[LlFfDd]?\\b"),
),
)
@@ -0,0 +1,52 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.jewel.intui.standalone.code.highlighting.languages
import org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar
import org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
// Patterns adapted from plugins/textmate/lib/bundles/kotlin/syntaxes/kotlin.tmLanguage.json
internal val KOTLIN =
LanguageGrammar(
name = "kotlin",
aliases = listOf("kt", "kts", "ktm"),
rules =
listOf(
// Comments must come first to avoid matching inside them
TokenRule.comment("/\\*[\\s\\S]*?\\*/"),
TokenRule.comment("//[^\n]*"),
// Strings
TokenRule.string("\"\"\"[\\s\\S]*?\"\"\""),
TokenRule.string("\"(?:[^\"\\\\]|\\\\.)*\""),
TokenRule.string("'(?:[^'\\\\]|\\\\.)*'"),
// fun <name> — entity.name.function.kotlin
TokenRule.functionDeclaration("\\b(fun)\\s+([A-Za-z_][A-Za-z0-9_]*)"),
// class/interface/object <name> — entity.name.type.class.kotlin
TokenRule.typeDeclaration("\\b(class|interface|object)\\s+([A-Za-z_][A-Za-z0-9_]*)"),
// storage.type + storage.modifier — val/var grouped with modifiers per kotlin.tmLanguage.json
TokenRule.keyword(
"\\b(val|var|fun|class|interface|annotation|companion|object|package|import|" +
"typealias|this|super|constructor|init|value|override|abstract|final|open|enum|sealed|" +
"data|inline|noinline|tailrec|external|const|suspend|expect|actual|private|public|" +
"internal|protected|lateinit|vararg|crossinline|operator|infix|reified)\\b"
),
// keyword.control.kotlin
TokenRule.keyword(
"\\b(if|else|when|for|do|while|return|break|continue|throw|try|catch|finally|in|" +
"is|as|by|get|set|where)\\b"
),
// entity.name.function.kotlin — call sites (after keywords, so if/when/for/while/catch won't match)
TokenRule.functionCall("\\b([A-Za-z_][A-Za-z0-9_]*)\\s*(?=\\()"),
// constant.language.kotlin
TokenRule.constant("\\b(true|false|null)\\b"),
// support.type.kotlin
TokenRule.type(
"\\b(String|Int|Long|Double|Float|Boolean|Char|Byte|Short|Unit|Any|Nothing|Array|" +
"List|MutableList|Map|MutableMap|Set|MutableSet|Pair)\\b"
),
// constant.numeric.kotlin
// Suffix pattern: uL/UL/ul/Ul for ULong, u/U for UInt, L/l for Long, f/F for Float
TokenRule.number("\\b0[bB][01_]+([uU][lL]?|[lL])?\\b"),
TokenRule.number("\\b0[xX][0-9a-fA-F_]+([uU][lL]?|[lL])?\\b"),
TokenRule.number("\\b[0-9][0-9_]*(?:\\.[0-9_]+)?(?:[eE][+-]?[0-9]+)?(?:[uU][lL]?|[lL]|[fF])?\\b"),
),
)
@@ -553,7 +553,9 @@ public open class DefaultMarkdownBlockRenderer(
) {
val content = block.content
val highlighter = LocalCodeHighlighter.current
val highlightedCode by highlighter.highlight(content, mimeType).collectAsState(AnnotatedString(content))
val highlightedCode by
remember(content, mimeType, highlighter) { highlighter.highlight(content, mimeType) }
.collectAsState(AnnotatedString(content))
Text(
text = highlightedCode,
style = styling.editorTextStyle,
@@ -575,9 +577,11 @@ public open class DefaultMarkdownBlockRenderer(
enabled: Boolean,
) {
val content = block.content
val language = block.language.orEmpty()
val highlighter = LocalCodeHighlighter.current
val highlightedCode by
highlighter.highlight(content, block.language.orEmpty()).collectAsState(AnnotatedString(content))
remember(content, language, highlighter) { highlighter.highlight(content, language) }
.collectAsState(AnnotatedString(content))
Text(
text = highlightedCode,
style = styling.editorTextStyle,
@@ -133,8 +133,10 @@ public open class ScrollSyncMarkdownBlockRenderer(
}
val content = block.content
val highlighter = LocalCodeHighlighter.current
val highlightedCode by
LocalCodeHighlighter.current.highlight(content, mimeType).collectAsState(AnnotatedString(content))
remember(content, mimeType, highlighter) { highlighter.highlight(content, mimeType) }
.collectAsState(AnnotatedString(content))
val uniqueBlock = LocalLocatableBlock.current?.takeIf { it.originalBlock == block } ?: block
val actualBlock by rememberUpdatedState(uniqueBlock)
AutoScrollableBlock(actualBlock, synchronizer) {
@@ -163,9 +165,10 @@ public open class ScrollSyncMarkdownBlockRenderer(
}
val content = block.content
val language = block.language.orEmpty()
val highlighter = LocalCodeHighlighter.current
val highlightedCode by
LocalCodeHighlighter.current
.highlight(content, block.language.orEmpty())
remember(content, language, highlighter) { highlighter.highlight(content, language) }
.collectAsState(AnnotatedString(content))
val uniqueBlock = LocalLocatableBlock.current?.takeIf { it.originalBlock == block } ?: block
val actualBlock by rememberUpdatedState(uniqueBlock)
@@ -9,7 +9,7 @@ package org.jetbrains.jewel.intui.markdown.standalone {
public final class IntUiProvideMarkdownStylingKt {
method @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void ProvideMarkdownStyling(optional boolean isDark, optional org.jetbrains.jewel.markdown.rendering.MarkdownStyling markdownStyling, optional org.jetbrains.jewel.markdown.MarkdownMode markdownMode, optional org.jetbrains.jewel.markdown.processing.MarkdownProcessor markdownProcessor, optional org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer markdownBlockRenderer, optional org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter codeHighlighter, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void ProvideMarkdownStyling(org.jetbrains.jewel.markdown.rendering.ImageSourceResolver imageSourceResolver, optional boolean isDark, optional org.jetbrains.jewel.markdown.rendering.MarkdownStyling markdownStyling, optional org.jetbrains.jewel.markdown.MarkdownMode markdownMode, optional org.jetbrains.jewel.markdown.processing.MarkdownProcessor markdownProcessor, optional org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer markdownBlockRenderer, optional org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter codeHighlighter, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void ProvideMarkdownStyling(org.jetbrains.jewel.markdown.rendering.ImageSourceResolver imageSourceResolver, org.jetbrains.jewel.markdown.rendering.MarkdownStyling markdownStyling, org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer markdownBlockRenderer, org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter codeHighlighter, optional org.jetbrains.jewel.markdown.MarkdownMode markdownMode, optional org.jetbrains.jewel.markdown.processing.MarkdownProcessor markdownProcessor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void ProvideMarkdownStyling(org.jetbrains.jewel.markdown.rendering.ImageSourceResolver imageSourceResolver, org.jetbrains.jewel.markdown.rendering.MarkdownStyling markdownStyling, org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer markdownBlockRenderer, optional org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter codeHighlighter, optional org.jetbrains.jewel.markdown.MarkdownMode markdownMode, optional org.jetbrains.jewel.markdown.processing.MarkdownProcessor markdownProcessor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void ProvideMarkdownStyling(org.jetbrains.jewel.markdown.rendering.MarkdownStyling markdownStyling, org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer markdownBlockRenderer, org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter codeHighlighter, optional org.jetbrains.jewel.markdown.MarkdownMode markdownMode, optional org.jetbrains.jewel.markdown.processing.MarkdownProcessor markdownProcessor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
@@ -9,10 +9,11 @@ import org.jetbrains.annotations.ApiStatus
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter
import org.jetbrains.jewel.foundation.code.highlighting.LocalCodeHighlighter
import org.jetbrains.jewel.foundation.code.highlighting.NoOpCodeHighlighter
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.intui.markdown.standalone.styling.dark
import org.jetbrains.jewel.intui.markdown.standalone.styling.light
import org.jetbrains.jewel.intui.standalone.code.highlighting.SimpleCodeHighlighter
import org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors
import org.jetbrains.jewel.markdown.MarkdownMode
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownBlockRenderer
import org.jetbrains.jewel.markdown.extensions.LocalMarkdownProcessor
@@ -26,8 +27,10 @@ import org.jetbrains.jewel.markdown.rendering.MarkdownStyling
/**
* Provide Markdown styling based on the current theme.
*
* By default, no code syntax highlighting will be available. If you do have a [codeHighlighter] instance to use
* instead, you should provide it.
* By default, code syntax highlighting is handled by a [SimpleCodeHighlighter] with a theme-aware
* [SyntaxHighlightColors] palette (dark or light, matching [isDark]). Pass your own [codeHighlighter] to override it,
* or pass [org.jetbrains.jewel.foundation.code.highlighting.NoOpCodeHighlighter] to disable syntax highlighting
* entirely.
*/
@ApiStatus.Experimental
@ExperimentalJewelApi
@@ -53,7 +56,10 @@ public fun ProvideMarkdownStyling(
MarkdownBlockRenderer.light(markdownStyling)
}
},
codeHighlighter: CodeHighlighter = remember { NoOpCodeHighlighter },
codeHighlighter: CodeHighlighter =
remember(isDark) {
SimpleCodeHighlighter(if (isDark) SyntaxHighlightColors.dark() else SyntaxHighlightColors.light())
},
content: @Composable () -> Unit,
) {
CompositionLocalProvider(LocalMarkdownImageSourceResolver provides imageSourceResolver) {
@@ -72,8 +78,10 @@ public fun ProvideMarkdownStyling(
/**
* Provide Markdown styling based on the current theme.
*
* By default, no code syntax highlighting will be available. If you do have a [codeHighlighter] instance to use
* instead, you should provide it.
* By default, code syntax highlighting is handled by a [SimpleCodeHighlighter] with a theme-aware
* [SyntaxHighlightColors] palette (dark or light, matching [isDark]). Pass your own [codeHighlighter] to override it,
* or pass [org.jetbrains.jewel.foundation.code.highlighting.NoOpCodeHighlighter] to disable syntax highlighting
* entirely.
*/
@ApiStatus.Experimental
@ExperimentalJewelApi
@@ -98,7 +106,10 @@ public fun ProvideMarkdownStyling(
MarkdownBlockRenderer.light(markdownStyling)
}
},
codeHighlighter: CodeHighlighter = remember { NoOpCodeHighlighter },
codeHighlighter: CodeHighlighter =
remember(isDark) {
SimpleCodeHighlighter(if (isDark) SyntaxHighlightColors.dark() else SyntaxHighlightColors.light())
},
content: @Composable () -> Unit,
) {
ProvideMarkdownStyling(
@@ -119,7 +130,12 @@ public fun ProvideMarkdownStyling(
imageSourceResolver: ImageSourceResolver,
markdownStyling: MarkdownStyling,
markdownBlockRenderer: MarkdownBlockRenderer,
codeHighlighter: CodeHighlighter,
codeHighlighter: CodeHighlighter =
JewelTheme.isDark.let { isDark ->
remember(isDark) {
SimpleCodeHighlighter(if (isDark) SyntaxHighlightColors.dark() else SyntaxHighlightColors.light())
}
},
markdownMode: MarkdownMode = MarkdownMode.Standalone,
markdownProcessor: MarkdownProcessor = remember(markdownMode) { MarkdownProcessor(markdownMode = markdownMode) },
content: @Composable () -> Unit,
@@ -18,7 +18,6 @@ import java.awt.Desktop.getDesktop
import java.net.URI.create
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.jewel.foundation.code.highlighting.NoOpCodeHighlighter
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.intui.markdown.standalone.ProvideMarkdownStyling
import org.jetbrains.jewel.intui.markdown.standalone.dark
@@ -29,6 +28,8 @@ import org.jetbrains.jewel.intui.markdown.standalone.styling.extensions.github.a
import org.jetbrains.jewel.intui.markdown.standalone.styling.extensions.github.tables.dark
import org.jetbrains.jewel.intui.markdown.standalone.styling.extensions.github.tables.light
import org.jetbrains.jewel.intui.markdown.standalone.styling.light
import org.jetbrains.jewel.intui.standalone.code.highlighting.SimpleCodeHighlighter
import org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors
import org.jetbrains.jewel.markdown.LazyMarkdown
import org.jetbrains.jewel.markdown.MarkdownBlock
import org.jetbrains.jewel.markdown.extensions.autolink.AutolinkProcessorExtension
@@ -111,8 +112,12 @@ internal fun MarkdownPreview(rawMarkdown: CharSequence, modifier: Modifier = Mod
// Using the values from the GitHub rendering to ensure contrast
val background = remember(instanceUuid) { if (isDark) Color(0xff0d1117) else Color.White }
val codeHighlighter =
remember(isDark) {
SimpleCodeHighlighter(if (isDark) SyntaxHighlightColors.dark() else SyntaxHighlightColors.light())
}
ProvideMarkdownStyling(markdownStyling, blockRenderer, NoOpCodeHighlighter) {
ProvideMarkdownStyling(markdownStyling, blockRenderer, codeHighlighter) {
val lazyListState = rememberLazyListState()
VerticallyScrollableContainer(lazyListState as ScrollableState, modifier.background(background)) {
LazyMarkdown(