diff --git a/platform/jewel/docs/standalone-code-highlighting.md b/platform/jewel/docs/standalone-code-highlighting.md new file mode 100644 index 000000000000..d92173d16456 --- /dev/null +++ b/platform/jewel/docs/standalone-code-highlighting.md @@ -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//syntaxes/.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 `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 + 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//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 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: + + +Method declarations + calls: + + +Control flow: + + +Comments: + + +Numbers: + + +Strings: + +``` + +#### 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 `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. diff --git a/platform/jewel/gradle/libs.versions.toml b/platform/jewel/gradle/libs.versions.toml index 18a3ca917c67..725b5fe6fd5a 100644 --- a/platform/jewel/gradle/libs.versions.toml +++ b/platform/jewel/gradle/libs.versions.toml @@ -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" } diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/BUILD.bazel b/platform/jewel/int-ui/int-ui-standalone-tests/BUILD.bazel index bb9de9114893..a8ff0445dd99 100644 --- a/platform/jewel/int-ui/int-ui-standalone-tests/BUILD.bazel +++ b/platform/jewel/int-ui/int-ui-standalone-tests/BUILD.bazel @@ -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", diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/build.gradle.kts b/platform/jewel/int-ui/int-ui-standalone-tests/build.gradle.kts index 5abb5d7694fd..d570d0f35c45 100644 --- a/platform/jewel/int-ui/int-ui-standalone-tests/build.gradle.kts +++ b/platform/jewel/int-ui/int-ui-standalone-tests/build.gradle.kts @@ -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() } diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/intellij.platform.jewel.intUi.standalone.tests.iml b/platform/jewel/int-ui/int-ui-standalone-tests/intellij.platform.jewel.intUi.standalone.tests.iml index b7ff15f88d0b..36f48c07db6b 100644 --- a/platform/jewel/int-ui/int-ui-standalone-tests/intellij.platform.jewel.intUi.standalone.tests.iml +++ b/platform/jewel/int-ui/int-ui-standalone-tests/intellij.platform.jewel.intUi.standalone.tests.iml @@ -35,6 +35,7 @@ + diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/code/highlighting/CodeHighlightingTestUtils.kt b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/code/highlighting/CodeHighlightingTestUtils.kt new file mode 100644 index 000000000000..5ec554dccf23 --- /dev/null +++ b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/code/highlighting/CodeHighlightingTestUtils.kt @@ -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 = + 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 } diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/code/highlighting/SimpleCodeHighlighterTest.kt b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/code/highlighting/SimpleCodeHighlighterTest.kt new file mode 100644 index 000000000000..53ed314ebcbd --- /dev/null +++ b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/code/highlighting/SimpleCodeHighlighterTest.kt @@ -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 + } +} diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/code/highlighting/TokenRuleTest.kt b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/code/highlighting/TokenRuleTest.kt new file mode 100644 index 000000000000..2f61d1443281 --- /dev/null +++ b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/code/highlighting/TokenRuleTest.kt @@ -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) + } +} diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/code/highlighting/languages/JavaGrammarTest.kt b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/code/highlighting/languages/JavaGrammarTest.kt new file mode 100644 index 000000000000..f40bd2815425 --- /dev/null +++ b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/code/highlighting/languages/JavaGrammarTest.kt @@ -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") + } + } +} diff --git a/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/code/highlighting/languages/KotlinGrammarTest.kt b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/code/highlighting/languages/KotlinGrammarTest.kt new file mode 100644 index 000000000000..55d10d06cf2a --- /dev/null +++ b/platform/jewel/int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/code/highlighting/languages/KotlinGrammarTest.kt @@ -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)) + } +} diff --git a/platform/jewel/int-ui/int-ui-standalone/api-dump-experimental.txt b/platform/jewel/int-ui/int-ui-standalone/api-dump-experimental.txt index 171efe41906a..a1be640c9c11 100644 --- a/platform/jewel/int-ui/int-ui-standalone/api-dump-experimental.txt +++ b/platform/jewel/int-ui/int-ui-standalone/api-dump-experimental.txt @@ -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 +- (java.lang.String,java.util.List,java.util.List):V +- b:(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 +- (org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors):V +- (org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors,java.util.List,kotlinx.coroutines.CoroutineDispatcher):V +- b:(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 +- (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 diff --git a/platform/jewel/int-ui/int-ui-standalone/exposed-third-party-api.txt b/platform/jewel/int-ui/int-ui-standalone/exposed-third-party-api.txt index 103cb043598f..168de8a3b0bb 100644 --- a/platform/jewel/int-ui/int-ui-standalone/exposed-third-party-api.txt +++ b/platform/jewel/int-ui/int-ui-standalone/exposed-third-party-api.txt @@ -1 +1,2 @@ androidx/compose/** +kotlin/jvm/internal/DefaultConstructorMarker diff --git a/platform/jewel/int-ui/int-ui-standalone/metalava/int-ui-standalone-api-0.41.0.txt b/platform/jewel/int-ui/int-ui-standalone/metalava/int-ui-standalone-api-0.41.0.txt index 6c4dc0754548..39b286a35355 100644 --- a/platform/jewel/int-ui/int-ui-standalone/metalava/int-ui-standalone-api-0.41.0.txt +++ b/platform/jewel/int-ui/int-ui-standalone/metalava/int-ui-standalone-api-0.41.0.txt @@ -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 aliases, java.util.List rules); + method public java.util.List getAliases(); + method public String getName(); + method public java.util.List getRules(); + method public boolean matches(String languageTag); + property public java.util.List aliases; + property public String name; + property public java.util.List 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 additionalGrammars, optional kotlinx.coroutines.CoroutineDispatcher highlightDispatcher); + method public java.util.List getAdditionalGrammars(); + method public org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors getColors(); + method public kotlinx.coroutines.CoroutineDispatcher getHighlightDispatcher(); + method public kotlinx.coroutines.flow.Flow highlight(String code, String language); + method @Deprecated public kotlinx.coroutines.flow.Flow highlight(String code, String? mimeType); + property public java.util.List 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 captures); + method public java.util.Map getCaptures(); + method public String getPattern(); + property public java.util.Map 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 { diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/BuiltInLanguageGrammars.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/BuiltInLanguageGrammars.kt new file mode 100644 index 000000000000..87fe43b13b4a --- /dev/null +++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/BuiltInLanguageGrammars.kt @@ -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 by lazy { listOf(KOTLIN, JAVA) } +} diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/LanguageGrammar.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/LanguageGrammar.kt new file mode 100644 index 000000000000..ee1026b0fefd --- /dev/null +++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/LanguageGrammar.kt @@ -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 = emptyList(), rules: List) { + public val aliases: List = aliases.toList() + public val rules: List = rules.toList() + + private val allIdentifiers: Set = 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 +} diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/SimpleCodeHighlighter.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/SimpleCodeHighlighter.kt new file mode 100644 index 000000000000..5786fbbae242 --- /dev/null +++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/SimpleCodeHighlighter.kt @@ -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, + 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 = flowOf(AnnotatedString(code)) + + override fun highlight(code: String, language: String): Flow { + 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? = + 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() + 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) + } + } +} diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/SyntaxHighlightColors.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/SyntaxHighlightColors.kt new file mode 100644 index 000000000000..083af8b98469 --- /dev/null +++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/SyntaxHighlightColors.kt @@ -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) + ) + } +} diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/TokenRule.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/TokenRule.kt new file mode 100644 index 000000000000..1326cf2af409 --- /dev/null +++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/TokenRule.kt @@ -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 + * (`(?<=...)`, `(?`) 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) { + public val captures: Map = 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)) + } +} diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/TokenType.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/TokenType.kt new file mode 100644 index 000000000000..13f744f4f048 --- /dev/null +++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/TokenType.kt @@ -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, +} diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/languages/JavaLanguageGrammar.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/languages/JavaLanguageGrammar.kt new file mode 100644 index 000000000000..c0fe131db21e --- /dev/null +++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/languages/JavaLanguageGrammar.kt @@ -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"), + ), + ) diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/languages/KotlinLanguageGrammar.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/languages/KotlinLanguageGrammar.kt new file mode 100644 index 000000000000..08c47f6a9726 --- /dev/null +++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/languages/KotlinLanguageGrammar.kt @@ -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 — entity.name.function.kotlin + TokenRule.functionDeclaration("\\b(fun)\\s+([A-Za-z_][A-Za-z0-9_]*)"), + // class/interface/object — 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"), + ), + ) diff --git a/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer.kt b/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer.kt index f93c44290925..d8415bd025e3 100644 --- a/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer.kt +++ b/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/rendering/DefaultMarkdownBlockRenderer.kt @@ -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, diff --git a/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/scrolling/ScrollSyncMarkdownBlockRenderer.kt b/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/scrolling/ScrollSyncMarkdownBlockRenderer.kt index 6ea1ed2ccadb..5501921a1d08 100644 --- a/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/scrolling/ScrollSyncMarkdownBlockRenderer.kt +++ b/platform/jewel/markdown/core/src/main/kotlin/org/jetbrains/jewel/markdown/scrolling/ScrollSyncMarkdownBlockRenderer.kt @@ -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) diff --git a/platform/jewel/markdown/int-ui-standalone-styling/metalava/int-ui-standalone-styling-api-0.41.0.txt b/platform/jewel/markdown/int-ui-standalone-styling/metalava/int-ui-standalone-styling-api-0.41.0.txt index 83f555aff230..04dd5b113efb 100644 --- a/platform/jewel/markdown/int-ui-standalone-styling/metalava/int-ui-standalone-styling-api-0.41.0.txt +++ b/platform/jewel/markdown/int-ui-standalone-styling/metalava/int-ui-standalone-styling-api-0.41.0.txt @@ -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 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 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 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 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 content); } diff --git a/platform/jewel/markdown/int-ui-standalone-styling/src/main/kotlin/org/jetbrains/jewel/intui/markdown/standalone/IntUiProvideMarkdownStyling.kt b/platform/jewel/markdown/int-ui-standalone-styling/src/main/kotlin/org/jetbrains/jewel/intui/markdown/standalone/IntUiProvideMarkdownStyling.kt index 40ff49164ae5..cd107336c2af 100644 --- a/platform/jewel/markdown/int-ui-standalone-styling/src/main/kotlin/org/jetbrains/jewel/intui/markdown/standalone/IntUiProvideMarkdownStyling.kt +++ b/platform/jewel/markdown/int-ui-standalone-styling/src/main/kotlin/org/jetbrains/jewel/intui/markdown/standalone/IntUiProvideMarkdownStyling.kt @@ -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, diff --git a/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/markdown/MarkdownPreview.kt b/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/markdown/MarkdownPreview.kt index af4c2a331db4..a6e6c1dd715f 100644 --- a/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/markdown/MarkdownPreview.kt +++ b/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/markdown/MarkdownPreview.kt @@ -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(