mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[JEWEL-1313] Add syntax highlighting to FencedCodeBlock in standalone
closes https://github.com/JetBrains/intellij-community/pull/3503 (cherry picked from commit 43950673391b442b7e1251df7322a9128ebea4a0) (cherry picked from commit ec8df5acf7c87b5ccef05c972df10254667acf2e) IJ-MR-220560 GitOrigin-RevId: bbba6c36da8819d6d03d7e4d418a4d40eca634ac
This commit is contained in:
committed by
intellij-monorepo-bot
parent
0aa073b7ba
commit
f8ec281887
@@ -0,0 +1,251 @@
|
||||
# Standalone code highlighting
|
||||
|
||||
The standalone Jewel module ships a lightweight, dependency-free syntax highlighter. It's mainly useful for non-IDE
|
||||
apps that render Markdown or display code.
|
||||
|
||||
## How it works
|
||||
|
||||
`SimpleCodeHighlighter` implements `CodeHighlighter` from the `foundation` module. `ProvideMarkdownStyling` wires it
|
||||
up by default, so fenced code blocks in rendered Markdown are highlighted automatically.
|
||||
|
||||
The rule engine works in three steps:
|
||||
|
||||
1. Each language maps to a `LanguageGrammar`, which is an ordered list of `TokenRule`s.
|
||||
2. At each position in the input, the tokenizer picks the rule whose match starts earliest. Ties go to whichever
|
||||
rule appears first.
|
||||
3. Each rule maps capture group indices to `TokenType` values, which `SyntaxHighlightColors` turns into styles.
|
||||
|
||||
Built-in grammars for languages are stored in `intui.standalone.code.highlighting.languages`.
|
||||
|
||||
## Sourcing patterns
|
||||
|
||||
Before writing regex patterns from scratch, check whether the IntelliJ TextMate bundles already cover the language.
|
||||
They're at:
|
||||
|
||||
```
|
||||
plugins/textmate/lib/bundles/<language>/syntaxes/<language>.tmLanguage.json
|
||||
```
|
||||
|
||||
The Kotlin and Java grammars in this module came from:
|
||||
|
||||
```
|
||||
plugins/textmate/lib/bundles/kotlin/syntaxes/kotlin.tmLanguage.json
|
||||
plugins/textmate/lib/bundles/java/syntaxes/java.tmLanguage.json
|
||||
```
|
||||
|
||||
The fields you need: `match` (single-line pattern), `begin`/`end` (block patterns like strings and comments), and
|
||||
`captures` (group-to-scope mappings). Look for scopes that map to `TokenType` values: `keyword.*`, `string.*`,
|
||||
`comment.*`, `storage.type`/`support.type`, `constant.*`, `constant.numeric`, and `entity.name.function`.
|
||||
|
||||
If the IntelliJ bundle doesn't cover your language, check the
|
||||
[shikijs/textmate-grammars-themes](https://github.com/shikijs/textmate-grammars-themes) repo or the
|
||||
[VSCode extension marketplace](https://github.com/microsoft/vscode/tree/main/extensions).
|
||||
|
||||
tmLanguage grammars use [Oniguruma](https://macromates.com/manual/en/regular_expressions) RegEx library. See [Regex engine limitations](#regex-engine-limitations) for what breaks
|
||||
when porting to Java's regex engine.
|
||||
|
||||
Add a comment at the top of the file pointing to the source:
|
||||
|
||||
```kotlin
|
||||
// Patterns adapted from plugins/textmate/lib/bundles/rust/syntaxes/rust.tmLanguage.json
|
||||
```
|
||||
|
||||
## Adding a new language grammar
|
||||
|
||||
### 1. Create a grammar file
|
||||
|
||||
Create a file in:
|
||||
|
||||
```
|
||||
int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/code/highlighting/languages/
|
||||
```
|
||||
|
||||
Name it `<Language>LanguageGrammar.kt` and declare a top-level `internal val`:
|
||||
|
||||
```kotlin
|
||||
// Patterns adapted from plugins/textmate/lib/bundles/rust/syntaxes/rust.tmLanguage.json
|
||||
internal val RUST =
|
||||
LanguageGrammar(
|
||||
name = "rust",
|
||||
aliases = listOf("rs"),
|
||||
rules = listOf(
|
||||
// Comments first — prevent keywords from matching inside them
|
||||
TokenRule.comment("""\/\/[^\n]*"""),
|
||||
TokenRule.comment("""\/\*[\s\S]*?\*\/"""),
|
||||
// Strings
|
||||
TokenRule.string(""""(?:[^"\\]|\\.)*""""),
|
||||
// Function declarations: fn <name>
|
||||
TokenRule.functionDeclaration("""\b(fn)\s+([A-Za-z_][A-Za-z0-9_]*)"""),
|
||||
// Keywords
|
||||
TokenRule.keyword("""\b(fn|let|mut|pub|use|mod|struct|enum|impl|trait|for|while|loop|if|else|match|return|self|super|crate|as|in|where|async|await|move|dyn|ref|type|const|static|unsafe|extern)\b"""),
|
||||
// Constants
|
||||
TokenRule.constant("""\b(true|false|None|Some|Ok|Err)\b"""),
|
||||
// Types
|
||||
TokenRule.type("""\b(String|str|i8|i16|i32|i64|i128|isize|u8|u16|u32|u64|u128|usize|f32|f64|bool|char|Vec|Option|Result|Box|Rc|Arc)\b"""),
|
||||
// Function call sites — after keywords so control flow keywords don't match
|
||||
TokenRule.functionCall("""\b([A-Za-z_][A-Za-z0-9_]*)\s*(?=\()"""),
|
||||
// Numbers
|
||||
TokenRule.number("""\b0x[0-9a-fA-F_]+\b"""),
|
||||
TokenRule.number("""\b0b[01_]+\b"""),
|
||||
TokenRule.number("""\b[0-9][0-9_]*(?:\.[0-9_]+)?(?:[eE][+-]?[0-9]+)?(?:_?[a-z0-9]+)?\b"""),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
The `name` and `aliases` values are matched case-insensitively against the language tag in fenced code blocks
|
||||
(e.g., ` ```rust ` or ` ```rs `).
|
||||
To add the proper extension names a language has, check both the `package.json` file in the TextMate bundle in the
|
||||
plugins folder (e.g, `plugins/textmate/lib/bundles/<language>/package.json`) and find the field
|
||||
`contributes.languages[].extensions`. Also check [GitHub's linguist languages file](https://github.com/github-linguist/linguist/blob/main/lib/linguist/languages.yml),
|
||||
if you really want to be thorough.
|
||||
|
||||
### 2. Registering a LanguageGrammar
|
||||
|
||||
#### Registering as a built-in Jewel language
|
||||
For languages that Jewel should use directly, add your `LanguageGrammar` to `BuiltInLanguageGrammars.kt` in the `all` list:
|
||||
|
||||
```kotlin
|
||||
internal object BuiltInLanguageGrammars {
|
||||
val all: List<LanguageGrammar> by lazy { listOf(KOTLIN, JAVA, RUST) }
|
||||
}
|
||||
```
|
||||
|
||||
When opening the PR, make sure to test all possible keywords for a language. Feel free to mix and match regexes,
|
||||
sometimes TextMate won't highlight a keyword that GitHub does, and vice versa. Just make sure the highlighting is being
|
||||
properly applied. Here's a very simple template for testing:
|
||||
|
||||
```
|
||||
Keywords, types, constants:
|
||||
<add examples>
|
||||
|
||||
Method declarations + calls:
|
||||
<add examples>
|
||||
|
||||
Control flow:
|
||||
<add examples>
|
||||
|
||||
Comments:
|
||||
<add examples>
|
||||
|
||||
Numbers:
|
||||
<add examples>
|
||||
|
||||
Strings:
|
||||
<add examples>
|
||||
```
|
||||
|
||||
#### Registering as an additional language or overriding the LanguageGrammar for a built-in language
|
||||
|
||||
To highlight a language that isn't built in or override an existing one, just pass your `LanguageGrammar`
|
||||
to `SimpleCodeHighlighter`:
|
||||
|
||||
```kotlin
|
||||
val myGrammar = LanguageGrammar(
|
||||
name = "toml",
|
||||
rules = listOf(
|
||||
TokenRule.comment("""#[^\n]*"""),
|
||||
TokenRule.string(""""(?:[^"\\]|\\.)*""""),
|
||||
TokenRule.keyword("""\b(true|false)\b"""),
|
||||
),
|
||||
)
|
||||
|
||||
val highlighter = SimpleCodeHighlighter(
|
||||
colors = SyntaxHighlightColors.light(),
|
||||
additionalGrammars = listOf(myGrammar), // <--- Add your custom language grammars here!
|
||||
)
|
||||
```
|
||||
|
||||
### 3. Write tests
|
||||
|
||||
Create a test file in:
|
||||
|
||||
```
|
||||
int-ui/int-ui-standalone-tests/src/test/kotlin/org/jetbrains/jewel/intui/code/highlighting/languages/
|
||||
```
|
||||
|
||||
Name it `<Language>GrammarTest.kt`. Use `testColors` from `CodeHighlightingTestUtils` so your tests check the
|
||||
tokenizer, not the color values:
|
||||
|
||||
```kotlin
|
||||
internal class RustGrammarTest {
|
||||
private val highlighter = SimpleCodeHighlighter(testColors)
|
||||
|
||||
private suspend fun highlight(code: String) = highlighter.highlight(code, "rust").first()
|
||||
|
||||
@Test
|
||||
fun `line comment is colored as comment`() = runTest {
|
||||
assertEquals(Color.Gray, highlight("// a comment").colorAt(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fn declaration colors keyword and function name separately`() = runTest {
|
||||
val result = highlight("fn my_func() {}")
|
||||
assertEquals(Color.Red, result.colorAt(0)) // "fn" → keyword
|
||||
assertEquals(Color.Yellow, result.colorAt(3)) // "my_func" → function call
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Rule ordering
|
||||
|
||||
Order matters more than it looks. Put comments or strings in the wrong position and keywords will match inside them.
|
||||
The general pattern:
|
||||
|
||||
| Order | Rule type | Reason |
|
||||
|-------|-----------|--------|
|
||||
| 1st | Comments | Must come first, or keywords and strings will match inside them |
|
||||
| 2nd | Strings | Must come before keywords, or keywords inside strings get colored |
|
||||
| 3rd | Function declarations | Matches `fn foo` before the keyword rule can grab just `fn` |
|
||||
| 4th | Keywords | |
|
||||
| 5th | Constants | |
|
||||
| 6th | Types | |
|
||||
| 7th | Function call sites | After keywords in Kotlin, so `if(` stays KEYWORD rather than FUNCTION_CALL |
|
||||
| Last | Numbers | |
|
||||
|
||||
The table is a **starting point**, not a recipe. Every language has quirks you won't find until you test against real code.
|
||||
Java's `functionCall`-first ordering is just one of them — run your grammar against actual snippets before assuming it works.
|
||||
|
||||
## Factory functions
|
||||
|
||||
`TokenRule.Companion` has factory functions for the common cases. Every factory takes a regex pattern string; the
|
||||
pattern is compiled once when the `TokenRule` is created.
|
||||
|
||||
| Factory | Captures | Description |
|
||||
|---------|----------|-------------|
|
||||
| `comment(pattern)` | group 0 → COMMENT | Entire match colored as comment |
|
||||
| `string(pattern)` | group 0 → STRING | Entire match colored as string |
|
||||
| `keyword(pattern)` | group 0 → KEYWORD | Entire match colored as keyword (bold) |
|
||||
| `type(pattern)` | group 0 → TYPE | Entire match colored as type |
|
||||
| `constant(pattern)` | group 0 → CONSTANT | Entire match colored as constant |
|
||||
| `number(pattern)` | group 0 → NUMBER | Entire match colored as number |
|
||||
| `builtin(pattern)` | group 0 → BUILTIN | Entire match colored as builtin |
|
||||
| `functionCall(pattern)` | group 1 → FUNCTION_CALL | Group 1 must isolate the function name |
|
||||
| `functionDeclaration(pattern)` | group 1 → KEYWORD, group 2 → FUNCTION_CALL | Group 1 = keyword, group 2 = name |
|
||||
| `typeDeclaration(pattern)` | group 1 → KEYWORD, group 2 → BUILTIN | Group 1 = keyword, group 2 = type name |
|
||||
|
||||
If the regex has fewer groups than the factory expects, the missing groups are silently skipped and no span is emitted.
|
||||
|
||||
For anything not covered by the factories, construct `TokenRule` directly with a `captures` map:
|
||||
|
||||
```kotlin
|
||||
TokenRule(
|
||||
pattern = """(\w+)\s*:\s*(\w+)""",
|
||||
captures = mapOf(1 to TokenType.CONSTANT, 2 to TokenType.TYPE),
|
||||
)
|
||||
```
|
||||
|
||||
## Regex engine limitations
|
||||
|
||||
Patterns run through Java's `java.util.regex`. Most tmLanguage patterns work, but a few `PCRE`/`Oniguruma` features
|
||||
don't exist in Java:
|
||||
|
||||
- **POSIX character classes** (`[[:alpha:]]`, etc.) -> use `[a-zA-Z]` etc. instead.
|
||||
- **Variable-length lookbehind** -> only fixed-width works (`(?<=fun )` is fine, `(?<=fun\s+)` is not). Rewrite as
|
||||
a capturing-group rule.
|
||||
- **Named backreferences**, **conditional patterns**, **subroutine calls**, and **recursive patterns** -> not
|
||||
supported.
|
||||
|
||||
`additionalGrammars` is searched before the built-in list, so you can also use it to override a built-in grammar for
|
||||
an existing language.
|
||||
@@ -46,13 +46,12 @@ junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher
|
||||
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
|
||||
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerialization" }
|
||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
|
||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
|
||||
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
|
||||
ktor-client-java = { module = "io.ktor:ktor-client-java", version = "3.0.3" }
|
||||
metalava = { module = "com.android.tools.metalava:metalava", version.ref = "metalava" }
|
||||
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
|
||||
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" }
|
||||
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test" }
|
||||
|
||||
# Plugin libraries for build-logic's convention plugins to use to resolve the types/tasks coming from these plugins
|
||||
detekt-gradlePlugin = { module = "dev.detekt:detekt-gradle-plugin", version.ref = "detekt" }
|
||||
|
||||
@@ -58,6 +58,8 @@ jvm_library(
|
||||
"//libraries/junit5:junit5_test_lib",
|
||||
"//platform/jewel/int-ui/int-ui-standalone:jewel-intUi-standalone",
|
||||
"//platform/jewel/int-ui/int-ui-standalone:jewel-intUi-standalone_test_lib",
|
||||
"//platform/jewel/foundation",
|
||||
"//platform/jewel/foundation:foundation_test_lib",
|
||||
"//libraries/kotlinx/coroutines/core",
|
||||
"//libraries/kotlinx/coroutines/core:core_test_lib",
|
||||
"//libraries/kotlinx/coroutines/test",
|
||||
|
||||
@@ -2,12 +2,13 @@ plugins { jewel }
|
||||
|
||||
dependencies {
|
||||
api(projects.intUi.intUiStandalone)
|
||||
testImplementation(projects.foundation)
|
||||
testImplementation(kotlin("test"))
|
||||
testImplementation(libs.junit.jupiter)
|
||||
testImplementation(libs.coroutines.core)
|
||||
testImplementation(libs.coroutines.test)
|
||||
testRuntimeOnly(libs.junit.platform.engine)
|
||||
testRuntimeOnly(libs.junit.platform.launcher)
|
||||
testImplementation(libs.kotlinx.coroutines.core)
|
||||
testImplementation(libs.kotlinx.coroutines.test)
|
||||
}
|
||||
|
||||
tasks.test { useJUnitPlatform() }
|
||||
|
||||
+1
@@ -35,6 +35,7 @@
|
||||
<orderEntry type="library" scope="TEST" name="kotlin-test" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.libraries.junit5" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.jewel.intUi.standalone" />
|
||||
<orderEntry type="module" module-name="intellij.platform.jewel.foundation" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.libraries.kotlinx.coroutines.core" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.libraries.kotlinx.coroutines.test" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.platform.jewel.ui" scope="TEST" />
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.jewel.intui.code.highlighting
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors
|
||||
|
||||
/**
|
||||
* A test-only [SyntaxHighlightColors] palette where each
|
||||
* [org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType] maps to a unique, easily identifiable color. Use
|
||||
* this in grammar tests so assertions are purely about tokenizer correctness, not production palette values.
|
||||
*/
|
||||
internal val testColors =
|
||||
SyntaxHighlightColors(
|
||||
keyword = Color.Red,
|
||||
type = Color.Blue,
|
||||
constant = Color.Green,
|
||||
functionCall = Color.Yellow,
|
||||
string = Color.Cyan,
|
||||
comment = Color.Gray,
|
||||
number = Color.Magenta,
|
||||
builtin = Color.White,
|
||||
)
|
||||
|
||||
/** All [SpanStyle]s covering character offset [index], in [spanStyles] order. */
|
||||
internal fun AnnotatedString.spansAt(index: Int): List<SpanStyle> =
|
||||
spanStyles.filter { it.start <= index && index < it.end }.map { it.item }
|
||||
|
||||
/** First non-[Color.Unspecified] color at character offset [index], or null if the position is unstyled. */
|
||||
internal fun AnnotatedString.colorAt(index: Int): Color? =
|
||||
spansAt(index).firstOrNull { it.color != Color.Unspecified }?.color
|
||||
|
||||
/**
|
||||
* Color of the [index]-th [SpanStyle] in [spanStyles] order (i.e. the n-th highlighted token), or null if that span has
|
||||
* no explicit color. Unlike [colorAt], [index] is a span index, not a character offset.
|
||||
*/
|
||||
internal fun AnnotatedString.spanColorAt(index: Int): Color? =
|
||||
spanStyles[index].item.color.takeIf { it != Color.Unspecified }
|
||||
+131
@@ -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
|
||||
}
|
||||
}
|
||||
+69
@@ -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)
|
||||
}
|
||||
}
|
||||
+148
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.jewel.intui.code.highlighting.languages
|
||||
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertTrue
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.jetbrains.jewel.intui.code.highlighting.colorAt
|
||||
import org.jetbrains.jewel.intui.code.highlighting.spanColorAt
|
||||
import org.jetbrains.jewel.intui.code.highlighting.spansAt
|
||||
import org.jetbrains.jewel.intui.code.highlighting.testColors
|
||||
import org.jetbrains.jewel.intui.standalone.code.highlighting.SimpleCodeHighlighter
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
internal class KotlinGrammarTest {
|
||||
private val highlighter = SimpleCodeHighlighter(testColors)
|
||||
|
||||
private suspend fun highlight(code: String, language: String = "kotlin") =
|
||||
highlighter.highlight(code, language).first()
|
||||
|
||||
@Test
|
||||
fun `name and all aliases are recognized`() = runTest {
|
||||
for (alias in listOf("kotlin", "kt", "kts", "ktm")) {
|
||||
assertTrue(highlight("val x = 1", alias).spanStyles.isNotEmpty(), "Alias '$alias' not recognized")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `line comment is colored as comment`() = runTest {
|
||||
val result = highlight("// this is a comment")
|
||||
assertEquals(testColors.comment, result.colorAt(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `line comment is italic`() = runTest {
|
||||
val result = highlight("// comment")
|
||||
assertEquals(FontStyle.Italic, result.spansAt(0).first().fontStyle)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `block comment is colored as comment`() = runTest {
|
||||
assertEquals(testColors.comment, highlight("/* block */").colorAt(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `keyword inside line comment is not colored as keyword`() = runTest {
|
||||
val result = highlight("// val fun class")
|
||||
assertNotEquals(testColors.keyword, result.colorAt(3)) // 'v' of val
|
||||
assertEquals(testColors.comment, result.spanColorAt(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `keyword inside block comment is not colored as keyword`() = runTest {
|
||||
val result = highlight("/* val fun */")
|
||||
assertNotEquals(testColors.keyword, result.colorAt(3))
|
||||
assertEquals(testColors.comment, result.colorAt(3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `double-quoted string is colored as string`() = runTest {
|
||||
assertEquals(testColors.string, highlight("\"hello world\"").colorAt(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `triple-quoted string is colored as string`() = runTest {
|
||||
assertEquals(testColors.string, highlight("\"\"\"multi\nline\"\"\"").colorAt(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `keyword inside string is not colored as keyword`() = runTest {
|
||||
val result = highlight("\"val fun class\"")
|
||||
assertEquals(testColors.string, result.colorAt(1)) // 'v' of val inside string
|
||||
assertNotEquals(testColors.keyword, result.colorAt(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fun declaration colors keyword and function name separately`() = runTest {
|
||||
val result = highlight("fun myFunc() {}")
|
||||
assertEquals(testColors.keyword, result.spanColorAt(0)) // "fun" → keyword
|
||||
assertEquals(testColors.functionCall, result.spanColorAt(1)) // "myFunc" → function call
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fun keyword in declaration is bold`() = runTest {
|
||||
val result = highlight("fun myFunc() {}")
|
||||
assertEquals(FontWeight.Bold, result.spansAt(0).first().fontWeight)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `class declaration colors keyword and class name separately`() = runTest {
|
||||
val result = highlight("class MyClass")
|
||||
assertEquals(testColors.keyword, result.spanColorAt(0)) // "class" → keyword
|
||||
assertEquals(testColors.builtin, result.spanColorAt(1)) // "MyClass" → builtin
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `storage keywords are colored as keyword`() = runTest {
|
||||
for (keyword in listOf("val", "var", "fun", "class", "suspend", "override", "data", "sealed", "inline")) {
|
||||
val result = highlight(keyword)
|
||||
assertEquals(testColors.keyword, result.colorAt(0), "'$keyword' should be colored as keyword")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `keywords are bold`() = runTest {
|
||||
for (keyword in listOf("val", "var", "fun")) {
|
||||
val result = highlight(keyword)
|
||||
assertEquals(FontWeight.Bold, result.spansAt(0).first().fontWeight, "'$keyword' should be bold")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `control keywords are colored as keyword`() = runTest {
|
||||
for (keyword in listOf("if", "else", "when", "for", "while", "return", "throw", "try", "catch")) {
|
||||
val result = highlight(keyword)
|
||||
assertEquals(testColors.keyword, result.colorAt(0), "'$keyword' should be colored as keyword")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `control keywords before parenthesis are not colored as function calls`() = runTest {
|
||||
// functionCall rule comes after keyword rules — keyword wins on ties
|
||||
for (keyword in listOf("if", "while", "for", "when", "catch")) {
|
||||
val result = highlight("$keyword (")
|
||||
assertEquals(
|
||||
testColors.keyword,
|
||||
result.colorAt(0),
|
||||
"'$keyword' should stay keyword, not become function call",
|
||||
)
|
||||
assertNotEquals(testColors.functionCall, result.colorAt(0))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `language constants are colored as constant`() = runTest {
|
||||
for (constant in listOf("true", "false", "null")) {
|
||||
assertEquals(
|
||||
testColors.constant,
|
||||
highlight(constant).colorAt(0),
|
||||
"'$constant' should be colored as constant",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `built-in types are colored as type`() = runTest {
|
||||
for (type in listOf("String", "Int", "Long", "Boolean", "List", "Map", "Unit", "Any")) {
|
||||
assertEquals(testColors.type, highlight(type).colorAt(0), "'$type' should be colored as type")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `decimal integers are colored as number`() = runTest {
|
||||
for (number in listOf("42", "1_000_000", "42L")) {
|
||||
assertEquals(testColors.number, highlight(number).colorAt(0), "'$number' should be colored as number")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `decimal floats and scientific notation are colored as number`() = runTest {
|
||||
for (number in listOf("3.14", "2.5f", "1e10", "6.02e23")) {
|
||||
assertEquals(testColors.number, highlight(number).colorAt(0), "'$number' should be colored as number")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hex numbers are colored as number`() = runTest {
|
||||
assertEquals(testColors.number, highlight("0xDEADBEEF").colorAt(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `binary numbers are colored as number`() = runTest {
|
||||
assertEquals(testColors.number, highlight("0b1010_1010").colorAt(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unsigned integer literals are colored as number`() = runTest {
|
||||
for (number in listOf("42u", "42U", "42uL", "42UL")) {
|
||||
assertEquals(testColors.number, highlight(number).colorAt(0), "'$number' should be colored as number")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `function call site is colored as function call`() = runTest {
|
||||
assertEquals(testColors.functionCall, highlight("tomato()").colorAt(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stdlib call site is colored as function call`() = runTest {
|
||||
assertEquals(testColors.functionCall, highlight("println(\"hi\")").colorAt(0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `enum class should be colored as keyword`() = runTest {
|
||||
val spanStyles = highlight("enum class Foo")
|
||||
|
||||
assertEquals(testColors.keyword, spanStyles.spanColorAt(0))
|
||||
assertEquals(testColors.keyword, spanStyles.spanColorAt(1))
|
||||
assertEquals(testColors.builtin, spanStyles.spanColorAt(2))
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,73 @@
|
||||
- f:getLazyMessage(java.lang.String,java.lang.Object[]):java.util.function.Supplier
|
||||
- f:getMessage(java.lang.String,java.lang.Object[]):java.lang.String
|
||||
*f:org.jetbrains.jewel.intui.standalone.bundle.DynamicBundle$Companion
|
||||
*f:org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar
|
||||
- sf:$stable:I
|
||||
- *sf:Companion:org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar$Companion
|
||||
- <init>(java.lang.String,java.util.List,java.util.List):V
|
||||
- b:<init>(java.lang.String,java.util.List,java.util.List,I,kotlin.jvm.internal.DefaultConstructorMarker):V
|
||||
- f:getAliases():java.util.List
|
||||
- f:getName():java.lang.String
|
||||
- f:getRules():java.util.List
|
||||
- f:matches(java.lang.String):Z
|
||||
*f:org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar$Companion
|
||||
*f:org.jetbrains.jewel.intui.standalone.code.highlighting.SimpleCodeHighlighter
|
||||
- org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter
|
||||
- sf:$stable:I
|
||||
- <init>(org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors):V
|
||||
- <init>(org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors,java.util.List,kotlinx.coroutines.CoroutineDispatcher):V
|
||||
- b:<init>(org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors,java.util.List,kotlinx.coroutines.CoroutineDispatcher,I,kotlin.jvm.internal.DefaultConstructorMarker):V
|
||||
- f:getAdditionalGrammars():java.util.List
|
||||
- f:getColors():org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors
|
||||
- f:getHighlightDispatcher():kotlinx.coroutines.CoroutineDispatcher
|
||||
- highlight(java.lang.String,java.lang.String):kotlinx.coroutines.flow.Flow
|
||||
- b:highlight-zTGadEY(java.lang.String,java.lang.String):kotlinx.coroutines.flow.Flow
|
||||
*f:org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors
|
||||
- sf:$stable:I
|
||||
- *sf:Companion:org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors$Companion
|
||||
- equals(java.lang.Object):Z
|
||||
- f:getBuiltin-0d7_KjU():J
|
||||
- f:getComment-0d7_KjU():J
|
||||
- f:getConstant-0d7_KjU():J
|
||||
- f:getFunctionCall-0d7_KjU():J
|
||||
- f:getKeyword-0d7_KjU():J
|
||||
- f:getNumber-0d7_KjU():J
|
||||
- f:getString-0d7_KjU():J
|
||||
- f:getType-0d7_KjU():J
|
||||
- hashCode():I
|
||||
*f:org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors$Companion
|
||||
- f:dark():org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors
|
||||
- f:light():org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors
|
||||
*f:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
|
||||
- sf:$stable:I
|
||||
- *sf:Companion:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule$Companion
|
||||
- <init>(java.lang.String,java.util.Map):V
|
||||
- f:getCaptures():java.util.Map
|
||||
- f:getPattern():java.lang.String
|
||||
*f:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule$Companion
|
||||
- f:builtin(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
|
||||
- f:comment(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
|
||||
- f:constant(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
|
||||
- f:functionCall(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
|
||||
- f:functionDeclaration(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
|
||||
- f:keyword(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
|
||||
- f:number(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
|
||||
- f:string(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
|
||||
- f:type(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
|
||||
- f:typeDeclaration(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
|
||||
*e:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
|
||||
- java.lang.Enum
|
||||
- sf:BUILTIN:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
|
||||
- sf:COMMENT:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
|
||||
- sf:CONSTANT:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
|
||||
- sf:FUNCTION_CALL:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
|
||||
- sf:KEYWORD:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
|
||||
- sf:NUMBER:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
|
||||
- sf:STRING:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
|
||||
- sf:TYPE:org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
|
||||
- s:getEntries():kotlin.enums.EnumEntries
|
||||
- s:valueOf(java.lang.String):org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType
|
||||
- s:values():org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType[]
|
||||
f:org.jetbrains.jewel.intui.standalone.styling.IntUiIconButtonStylingKt
|
||||
- *sf:darkTransparentBackground(org.jetbrains.jewel.ui.component.styling.IconButtonStyle$Companion,org.jetbrains.jewel.ui.component.styling.IconButtonColors,org.jetbrains.jewel.ui.component.styling.IconButtonMetrics):org.jetbrains.jewel.ui.component.styling.IconButtonStyle
|
||||
- *bs:darkTransparentBackground$default(org.jetbrains.jewel.ui.component.styling.IconButtonStyle$Companion,org.jetbrains.jewel.ui.component.styling.IconButtonColors,org.jetbrains.jewel.ui.component.styling.IconButtonMetrics,I,java.lang.Object):org.jetbrains.jewel.ui.component.styling.IconButtonStyle
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
androidx/compose/**
|
||||
kotlin/jvm/internal/DefaultConstructorMarker
|
||||
|
||||
@@ -106,6 +106,89 @@ package org.jetbrains.jewel.intui.standalone.bundle {
|
||||
|
||||
}
|
||||
|
||||
package org.jetbrains.jewel.intui.standalone.code.highlighting {
|
||||
|
||||
@SuppressCompatibility @androidx.compose.runtime.Immutable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public final class LanguageGrammar {
|
||||
ctor public LanguageGrammar(String name, optional java.util.List<java.lang.String> aliases, java.util.List<org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule> rules);
|
||||
method public java.util.List<java.lang.String> getAliases();
|
||||
method public String getName();
|
||||
method public java.util.List<org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule> getRules();
|
||||
method public boolean matches(String languageTag);
|
||||
property public java.util.List<java.lang.String> aliases;
|
||||
property public String name;
|
||||
property public java.util.List<org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule> rules;
|
||||
field public static final org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar.Companion Companion;
|
||||
}
|
||||
|
||||
public static final class LanguageGrammar.Companion {
|
||||
}
|
||||
|
||||
@SuppressCompatibility @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public final class SimpleCodeHighlighter implements org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter {
|
||||
ctor public SimpleCodeHighlighter(org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors colors);
|
||||
ctor public SimpleCodeHighlighter(org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors colors, java.util.List<org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar> additionalGrammars, optional kotlinx.coroutines.CoroutineDispatcher highlightDispatcher);
|
||||
method public java.util.List<org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar> getAdditionalGrammars();
|
||||
method public org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors getColors();
|
||||
method public kotlinx.coroutines.CoroutineDispatcher getHighlightDispatcher();
|
||||
method public kotlinx.coroutines.flow.Flow<androidx.compose.ui.text.AnnotatedString> highlight(String code, String language);
|
||||
method @Deprecated public kotlinx.coroutines.flow.Flow<androidx.compose.ui.text.AnnotatedString> highlight(String code, String? mimeType);
|
||||
property public java.util.List<org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar> additionalGrammars;
|
||||
property public org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors colors;
|
||||
property public kotlinx.coroutines.CoroutineDispatcher highlightDispatcher;
|
||||
}
|
||||
|
||||
@SuppressCompatibility @androidx.compose.runtime.Immutable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public final class SyntaxHighlightColors {
|
||||
ctor @KotlinOnly public SyntaxHighlightColors(androidx.compose.ui.graphics.Color keyword, androidx.compose.ui.graphics.Color type, androidx.compose.ui.graphics.Color constant, androidx.compose.ui.graphics.Color functionCall, androidx.compose.ui.graphics.Color string, androidx.compose.ui.graphics.Color comment, androidx.compose.ui.graphics.Color number, androidx.compose.ui.graphics.Color builtin);
|
||||
property public androidx.compose.ui.graphics.Color builtin;
|
||||
property public androidx.compose.ui.graphics.Color comment;
|
||||
property public androidx.compose.ui.graphics.Color constant;
|
||||
property public androidx.compose.ui.graphics.Color functionCall;
|
||||
property public androidx.compose.ui.graphics.Color keyword;
|
||||
property public androidx.compose.ui.graphics.Color number;
|
||||
property public androidx.compose.ui.graphics.Color string;
|
||||
property public androidx.compose.ui.graphics.Color type;
|
||||
field public static final org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors.Companion Companion;
|
||||
}
|
||||
|
||||
public static final class SyntaxHighlightColors.Companion {
|
||||
method public org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors dark();
|
||||
method public org.jetbrains.jewel.intui.standalone.code.highlighting.SyntaxHighlightColors light();
|
||||
}
|
||||
|
||||
@SuppressCompatibility @androidx.compose.runtime.Immutable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public final class TokenRule {
|
||||
ctor public TokenRule(@org.intellij.lang.annotations.Language("RegExp") String pattern, java.util.Map<java.lang.Integer,? extends org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType> captures);
|
||||
method public java.util.Map<java.lang.Integer,org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType> getCaptures();
|
||||
method public String getPattern();
|
||||
property public java.util.Map<java.lang.Integer,org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType> captures;
|
||||
property @org.intellij.lang.annotations.Language("RegExp") public String pattern;
|
||||
field public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule.Companion Companion;
|
||||
}
|
||||
|
||||
public static final class TokenRule.Companion {
|
||||
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule builtin(@org.intellij.lang.annotations.Language("RegExp") String pattern);
|
||||
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule comment(@org.intellij.lang.annotations.Language("RegExp") String pattern);
|
||||
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule constant(@org.intellij.lang.annotations.Language("RegExp") String pattern);
|
||||
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule functionCall(@org.intellij.lang.annotations.Language("RegExp") String pattern);
|
||||
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule functionDeclaration(@org.intellij.lang.annotations.Language("RegExp") String pattern);
|
||||
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule keyword(@org.intellij.lang.annotations.Language("RegExp") String pattern);
|
||||
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule number(@org.intellij.lang.annotations.Language("RegExp") String pattern);
|
||||
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule string(@org.intellij.lang.annotations.Language("RegExp") String pattern);
|
||||
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule type(@org.intellij.lang.annotations.Language("RegExp") String pattern);
|
||||
method public org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule typeDeclaration(@org.intellij.lang.annotations.Language("RegExp") String pattern);
|
||||
}
|
||||
|
||||
@SuppressCompatibility @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public enum TokenType {
|
||||
enum_constant public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType BUILTIN;
|
||||
enum_constant public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType COMMENT;
|
||||
enum_constant public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType CONSTANT;
|
||||
enum_constant public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType FUNCTION_CALL;
|
||||
enum_constant public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType KEYWORD;
|
||||
enum_constant public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType NUMBER;
|
||||
enum_constant public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType STRING;
|
||||
enum_constant public static final org.jetbrains.jewel.intui.standalone.code.highlighting.TokenType TYPE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
package org.jetbrains.jewel.intui.standalone.styling {
|
||||
|
||||
public final class IntUIBannerStylingKt {
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.jewel.intui.standalone.code.highlighting
|
||||
|
||||
import org.jetbrains.jewel.intui.standalone.code.highlighting.languages.JAVA
|
||||
import org.jetbrains.jewel.intui.standalone.code.highlighting.languages.KOTLIN
|
||||
|
||||
// Patterns are adapted from the tmLanguage grammars in plugins/textmate/lib/bundles/.
|
||||
// Java's regex engine is used (see TokenRule for known PCRE/Oniguruma incompatibilities).
|
||||
internal object BuiltInLanguageGrammars {
|
||||
val all: List<LanguageGrammar> by lazy { listOf(KOTLIN, JAVA) }
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.jewel.intui.standalone.code.highlighting
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
|
||||
|
||||
/**
|
||||
* Defines the syntax highlighting grammar for a single programming language.
|
||||
*
|
||||
* A grammar consists of a canonical [name] and optional [aliases] (all lowercase) that identify the language in a
|
||||
* Markdown fenced code block info string, and an ordered list of [TokenRule]s that are applied to tokenize the source
|
||||
* code.
|
||||
*
|
||||
* Rules are tried at each position in the input; the rule whose match starts earliest wins. In case of a tie, the rule
|
||||
* that appears first in [rules] wins, so higher-priority rules (e.g., comments and strings) should come first.
|
||||
*
|
||||
* @param name Canonical name of the language (e.g, `"kotlin"`). Matched against the info string of a Markdown fenced
|
||||
* code block, along with [aliases].
|
||||
* @param aliases Alternative identifiers for the language, typically file extensions or shorthand (e.g, `["kt",
|
||||
* "kts"]`).
|
||||
* @param rules Priority-ordered list of [TokenRule]s. Each rule's [TokenRule.captures] maps capture group indices to
|
||||
* [TokenType]s. A single rule can color multiple parts of a match independently.
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
@ExperimentalJewelApi
|
||||
@Immutable
|
||||
public class LanguageGrammar(public val name: String, aliases: List<String> = emptyList(), rules: List<TokenRule>) {
|
||||
public val aliases: List<String> = aliases.toList()
|
||||
public val rules: List<TokenRule> = rules.toList()
|
||||
|
||||
private val allIdentifiers: Set<String> = buildSet {
|
||||
add(name)
|
||||
addAll(this@LanguageGrammar.aliases)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this grammar should be used for the given [languageTag], i.e. whether the tag equals this
|
||||
* grammar's [name] or any of its [aliases].
|
||||
*
|
||||
* @param languageTag The language identifier to check, e.g. the info string of a Markdown fenced code block. The
|
||||
* comparison is case-sensitive, so the tag is expected to be lowercase.
|
||||
*/
|
||||
public fun matches(languageTag: String): Boolean = languageTag in allIdentifiers
|
||||
|
||||
/** Companion object for [LanguageGrammar]. */
|
||||
public companion object
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.jewel.intui.standalone.code.highlighting
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import kotlin.collections.iterator
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
|
||||
import org.jetbrains.jewel.foundation.code.MimeType
|
||||
import org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter
|
||||
|
||||
/**
|
||||
* A lightweight [CodeHighlighter] for use in standalone Jewel applications.
|
||||
*
|
||||
* Highlighting is driven by a regex rule engine (see [TokenRule]) that mirrors the structure of tmLanguage grammars.
|
||||
* Each [LanguageGrammar] contains an ordered list of [TokenRule]s; the tokenizer picks the rule whose match starts
|
||||
* earliest in the remaining input (ties broken by rule order), applies its capture group spans, and advances past the
|
||||
* match.
|
||||
*
|
||||
* Built-in grammars are provided by [BuiltInLanguageGrammars]. Custom or additional grammars can be supplied via
|
||||
* [additionalGrammars]; these are searched first, so they can override built-in behavior for a given language name.
|
||||
*
|
||||
* Token colors are controlled by [SyntaxHighlightColors]. Use [SyntaxHighlightColors.Companion.light] and
|
||||
* [SyntaxHighlightColors.Companion.dark] for palettes matching IntelliJ's Default and Darcula editor schemes, or supply
|
||||
* your own. Theme changes are handled at the call site via `remember(isDark)` in `ProvideMarkdownStyling` — this class
|
||||
* itself is stateless and always emits a single [AnnotatedString].
|
||||
*
|
||||
* @param colors The token color palette to use for styling.
|
||||
* @param additionalGrammars Extra grammars to search before the built-in ones. Use this to support additional languages
|
||||
* or to override built-in grammar rules for an existing language.
|
||||
* @param highlightDispatcher The [CoroutineDispatcher] on which tokenization is performed. Defaults to
|
||||
* [Dispatchers.Default] so the regex scan runs off the collecting thread (e.g. the composition path).
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
@ExperimentalJewelApi
|
||||
public class SimpleCodeHighlighter(
|
||||
public val colors: SyntaxHighlightColors,
|
||||
public val additionalGrammars: List<LanguageGrammar>,
|
||||
public val highlightDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
) : CodeHighlighter {
|
||||
public constructor(colors: SyntaxHighlightColors) : this(colors, emptyList())
|
||||
|
||||
@Deprecated(message = "Use highlight(code, language) instead.", level = DeprecationLevel.HIDDEN)
|
||||
override fun highlight(code: String, mimeType: MimeType?): Flow<AnnotatedString> = flowOf(AnnotatedString(code))
|
||||
|
||||
override fun highlight(code: String, language: String): Flow<AnnotatedString> {
|
||||
if (language.isBlank()) return flowOf(AnnotatedString(code))
|
||||
val grammar = findGrammar(language) ?: return flowOf(AnnotatedString(code))
|
||||
return flow { emit(withContext(highlightDispatcher) { tokenize(code, grammar) }) }
|
||||
}
|
||||
|
||||
private fun findGrammar(language: String): LanguageGrammar? {
|
||||
val grammars = additionalGrammars + BuiltInLanguageGrammars.all
|
||||
val normalizedLanguage = language.trim().lowercase()
|
||||
|
||||
val exactMatch = findGrammar(normalizedLanguage, grammars)
|
||||
if (exactMatch != null) {
|
||||
return exactMatch
|
||||
}
|
||||
|
||||
var index = normalizedLanguage.lastIndexOf(' ')
|
||||
while (index != -1) {
|
||||
val nameWithoutCustomizations = normalizedLanguage.take(index)
|
||||
val grammar = findGrammar(nameWithoutCustomizations, grammars)
|
||||
if (grammar != null) {
|
||||
return grammar
|
||||
}
|
||||
index = normalizedLanguage.lastIndexOf(' ', startIndex = (index - 1).coerceAtLeast(0))
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun findGrammar(tag: String, grammars: List<LanguageGrammar>): LanguageGrammar? =
|
||||
grammars.firstOrNull { it.matches(tag) }
|
||||
|
||||
private fun tokenize(code: String, grammar: LanguageGrammar): AnnotatedString {
|
||||
data class Span(val start: Int, val end: Int, val style: SpanStyle)
|
||||
|
||||
val spans = mutableListOf<Span>()
|
||||
var i = 0
|
||||
|
||||
while (i < code.length) {
|
||||
// Find the rule whose match starts earliest; ties broken by rule order
|
||||
var bestMatch: MatchResult? = null
|
||||
var bestRule: TokenRule? = null
|
||||
|
||||
for (rule in grammar.rules) {
|
||||
val match = rule.find(code, i) ?: continue
|
||||
if (bestMatch == null || match.range.first < bestMatch.range.first) {
|
||||
bestMatch = match
|
||||
bestRule = rule
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMatch == null || bestRule == null) break
|
||||
|
||||
for ((groupIndex, tokenType) in bestRule.captures) {
|
||||
// if the regex doesn't have as many groups as bestMatch, skip silently
|
||||
if (groupIndex >= bestMatch.groupValues.size) continue
|
||||
val group = bestMatch.groups[groupIndex] ?: continue
|
||||
if (group.range.isEmpty()) continue
|
||||
spans += Span(group.range.first, group.range.last + 1, colors.styleFor(tokenType))
|
||||
}
|
||||
|
||||
i = maxOf(i + 1, bestMatch.range.last + 1)
|
||||
}
|
||||
|
||||
return buildAnnotatedString {
|
||||
append(code)
|
||||
for (span in spans) addStyle(span.style, span.start, span.end)
|
||||
}
|
||||
}
|
||||
}
|
||||
+136
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.jewel.intui.standalone.code.highlighting
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import org.intellij.lang.annotations.Language
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
|
||||
|
||||
/**
|
||||
* A single syntax highlighting rule, consisting of a regex [pattern] and a map from capture group indices to
|
||||
* [TokenType]s.
|
||||
*
|
||||
* Rules are applied in order. The tokenizer picks the rule whose match starts earliest in the remaining input; in case
|
||||
* of a tie, the rule that appears first in the list wins.
|
||||
*
|
||||
* The [captures] map controls which parts of the match are colored:
|
||||
* - Key `0` refers to the entire match.
|
||||
* - Keys `1`, `2`, etc. refer to specific capturing groups.
|
||||
*
|
||||
* A single rule can produce multiple spans — for example, a rule that matches `fun myFunc` can color `fun` as
|
||||
* [TokenType.KEYWORD] (group 1) and `myFunc` as [TokenType.FUNCTION_CALL] (group 2) in one pass.
|
||||
*
|
||||
* Patterns use Java's `java.util.regex` engine, which supports lookahead (`(?=...)`) and fixed-width lookbehind
|
||||
* (`(?<=...)`, `(?<!...)`). This covers the vast majority of tmLanguage-style patterns.
|
||||
*
|
||||
* **Known limitations compared to PCRE/Oniguruma** (used by TextMate grammars):
|
||||
* - **POSIX character classes** (`[[:alpha:]]`, `[[:digit:]]`, etc.) are not supported — replace with their Unicode
|
||||
* equivalents (`[a-zA-Z]`, `[0-9]`, etc.).
|
||||
* - **Variable-length lookbehind** is not supported — only fixed-width lookbehind (e.g., `(?<=fun )` but not
|
||||
* `(?<=fun\s+)`). Rewrite as a capturing-group rule instead.
|
||||
* - **Named backreferences** and **conditional patterns** (`(?(condition)yes|no)`) are not supported.
|
||||
* - **Subroutine calls** (`\g<name>`) and **recursive patterns** are not supported.
|
||||
*
|
||||
* @param pattern The regex pattern string used to match against the source code.
|
||||
* @param captures A map from capture group index to [TokenType]. Groups not listed here produce no colored span.
|
||||
*/
|
||||
@ApiStatus.Experimental
|
||||
@ExperimentalJewelApi
|
||||
@Immutable
|
||||
public class TokenRule(@Language("RegExp") public val pattern: String, captures: Map<Int, TokenType>) {
|
||||
public val captures: Map<Int, TokenType> = captures.toMap()
|
||||
private val regex: Regex = Regex(pattern)
|
||||
|
||||
internal fun find(input: String, startIndex: Int) = regex.find(input, startIndex)
|
||||
|
||||
/** Companion object for [TokenRule], holding factories for the most common single-token rules. */
|
||||
public companion object {
|
||||
/** Colors the entire match as [TokenType.COMMENT]. */
|
||||
public fun comment(@Language("RegExp") pattern: String): TokenRule =
|
||||
TokenRule(pattern, mapOf(0 to TokenType.COMMENT))
|
||||
|
||||
/** Colors the entire match as [TokenType.STRING]. */
|
||||
public fun string(@Language("RegExp") pattern: String): TokenRule =
|
||||
TokenRule(pattern, mapOf(0 to TokenType.STRING))
|
||||
|
||||
/** Colors the entire match as [TokenType.KEYWORD]. */
|
||||
public fun keyword(@Language("RegExp") pattern: String): TokenRule =
|
||||
TokenRule(pattern, mapOf(0 to TokenType.KEYWORD))
|
||||
|
||||
/** Colors the entire match as [TokenType.TYPE]. */
|
||||
public fun type(@Language("RegExp") pattern: String): TokenRule = TokenRule(pattern, mapOf(0 to TokenType.TYPE))
|
||||
|
||||
/** Colors the entire match as [TokenType.CONSTANT]. */
|
||||
public fun constant(@Language("RegExp") pattern: String): TokenRule =
|
||||
TokenRule(pattern, mapOf(0 to TokenType.CONSTANT))
|
||||
|
||||
/** Colors the entire match as [TokenType.NUMBER]. */
|
||||
public fun number(@Language("RegExp") pattern: String): TokenRule =
|
||||
TokenRule(pattern, mapOf(0 to TokenType.NUMBER))
|
||||
|
||||
/** Colors the entire match as [TokenType.BUILTIN]. */
|
||||
public fun builtin(@Language("RegExp") pattern: String): TokenRule =
|
||||
TokenRule(pattern, mapOf(0 to TokenType.BUILTIN))
|
||||
|
||||
/**
|
||||
* Colors **group 1** of the match as [TokenType.FUNCTION_CALL].
|
||||
*
|
||||
* Use a capturing group to isolate the function name, excluding any surrounding context such as trailing
|
||||
* whitespace before `(`. Example: `\b([A-Za-z_]\w*)\s*(?=\()` — group 1 captures only the name.
|
||||
*/
|
||||
public fun functionCall(@Language("RegExp") pattern: String): TokenRule =
|
||||
TokenRule(pattern, mapOf(1 to TokenType.FUNCTION_CALL))
|
||||
|
||||
/**
|
||||
* Colors **group 1** as [TokenType.KEYWORD] and **group 2** as [TokenType.FUNCTION_CALL] in a single match.
|
||||
*
|
||||
* Use this for languages that declare functions with an explicit keyword, e.g. `fun myFunc` in Kotlin or `func
|
||||
* myFunc` in Go. Group 1 should capture the keyword, group 2 the function name.
|
||||
*/
|
||||
public fun functionDeclaration(@Language("RegExp") pattern: String): TokenRule =
|
||||
TokenRule(pattern, mapOf(1 to TokenType.KEYWORD, 2 to TokenType.FUNCTION_CALL))
|
||||
|
||||
/**
|
||||
* Colors **group 1** as [TokenType.KEYWORD] and **group 2** as [TokenType.BUILTIN] in a single match.
|
||||
*
|
||||
* Use this for type declaration keywords followed by the type name, e.g. `class MyClass` or `interface Foo`.
|
||||
* Group 1 should capture the keyword, group 2 the type name.
|
||||
*/
|
||||
public fun typeDeclaration(@Language("RegExp") pattern: String): TokenRule =
|
||||
TokenRule(pattern, mapOf(1 to TokenType.KEYWORD, 2 to TokenType.BUILTIN))
|
||||
}
|
||||
}
|
||||
+38
@@ -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,
|
||||
}
|
||||
+44
@@ -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"),
|
||||
),
|
||||
)
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.jewel.intui.standalone.code.highlighting.languages
|
||||
|
||||
import org.jetbrains.jewel.intui.standalone.code.highlighting.LanguageGrammar
|
||||
import org.jetbrains.jewel.intui.standalone.code.highlighting.TokenRule
|
||||
|
||||
// Patterns adapted from plugins/textmate/lib/bundles/kotlin/syntaxes/kotlin.tmLanguage.json
|
||||
internal val KOTLIN =
|
||||
LanguageGrammar(
|
||||
name = "kotlin",
|
||||
aliases = listOf("kt", "kts", "ktm"),
|
||||
rules =
|
||||
listOf(
|
||||
// Comments must come first to avoid matching inside them
|
||||
TokenRule.comment("/\\*[\\s\\S]*?\\*/"),
|
||||
TokenRule.comment("//[^\n]*"),
|
||||
// Strings
|
||||
TokenRule.string("\"\"\"[\\s\\S]*?\"\"\""),
|
||||
TokenRule.string("\"(?:[^\"\\\\]|\\\\.)*\""),
|
||||
TokenRule.string("'(?:[^'\\\\]|\\\\.)*'"),
|
||||
// fun <name> — entity.name.function.kotlin
|
||||
TokenRule.functionDeclaration("\\b(fun)\\s+([A-Za-z_][A-Za-z0-9_]*)"),
|
||||
// class/interface/object <name> — entity.name.type.class.kotlin
|
||||
TokenRule.typeDeclaration("\\b(class|interface|object)\\s+([A-Za-z_][A-Za-z0-9_]*)"),
|
||||
// storage.type + storage.modifier — val/var grouped with modifiers per kotlin.tmLanguage.json
|
||||
TokenRule.keyword(
|
||||
"\\b(val|var|fun|class|interface|annotation|companion|object|package|import|" +
|
||||
"typealias|this|super|constructor|init|value|override|abstract|final|open|enum|sealed|" +
|
||||
"data|inline|noinline|tailrec|external|const|suspend|expect|actual|private|public|" +
|
||||
"internal|protected|lateinit|vararg|crossinline|operator|infix|reified)\\b"
|
||||
),
|
||||
// keyword.control.kotlin
|
||||
TokenRule.keyword(
|
||||
"\\b(if|else|when|for|do|while|return|break|continue|throw|try|catch|finally|in|" +
|
||||
"is|as|by|get|set|where)\\b"
|
||||
),
|
||||
// entity.name.function.kotlin — call sites (after keywords, so if/when/for/while/catch won't match)
|
||||
TokenRule.functionCall("\\b([A-Za-z_][A-Za-z0-9_]*)\\s*(?=\\()"),
|
||||
// constant.language.kotlin
|
||||
TokenRule.constant("\\b(true|false|null)\\b"),
|
||||
// support.type.kotlin
|
||||
TokenRule.type(
|
||||
"\\b(String|Int|Long|Double|Float|Boolean|Char|Byte|Short|Unit|Any|Nothing|Array|" +
|
||||
"List|MutableList|Map|MutableMap|Set|MutableSet|Pair)\\b"
|
||||
),
|
||||
// constant.numeric.kotlin
|
||||
// Suffix pattern: uL/UL/ul/Ul for ULong, u/U for UInt, L/l for Long, f/F for Float
|
||||
TokenRule.number("\\b0[bB][01_]+([uU][lL]?|[lL])?\\b"),
|
||||
TokenRule.number("\\b0[xX][0-9a-fA-F_]+([uU][lL]?|[lL])?\\b"),
|
||||
TokenRule.number("\\b[0-9][0-9_]*(?:\\.[0-9_]+)?(?:[eE][+-]?[0-9]+)?(?:[uU][lL]?|[lL]|[fF])?\\b"),
|
||||
),
|
||||
)
|
||||
+6
-2
@@ -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,
|
||||
|
||||
+6
-3
@@ -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)
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ package org.jetbrains.jewel.intui.markdown.standalone {
|
||||
public final class IntUiProvideMarkdownStylingKt {
|
||||
method @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void ProvideMarkdownStyling(optional boolean isDark, optional org.jetbrains.jewel.markdown.rendering.MarkdownStyling markdownStyling, optional org.jetbrains.jewel.markdown.MarkdownMode markdownMode, optional org.jetbrains.jewel.markdown.processing.MarkdownProcessor markdownProcessor, optional org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer markdownBlockRenderer, optional org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter codeHighlighter, kotlin.jvm.functions.Function0<kotlin.Unit> content);
|
||||
method @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void ProvideMarkdownStyling(org.jetbrains.jewel.markdown.rendering.ImageSourceResolver imageSourceResolver, optional boolean isDark, optional org.jetbrains.jewel.markdown.rendering.MarkdownStyling markdownStyling, optional org.jetbrains.jewel.markdown.MarkdownMode markdownMode, optional org.jetbrains.jewel.markdown.processing.MarkdownProcessor markdownProcessor, optional org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer markdownBlockRenderer, optional org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter codeHighlighter, kotlin.jvm.functions.Function0<kotlin.Unit> content);
|
||||
method @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void ProvideMarkdownStyling(org.jetbrains.jewel.markdown.rendering.ImageSourceResolver imageSourceResolver, org.jetbrains.jewel.markdown.rendering.MarkdownStyling markdownStyling, org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer markdownBlockRenderer, org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter codeHighlighter, optional org.jetbrains.jewel.markdown.MarkdownMode markdownMode, optional org.jetbrains.jewel.markdown.processing.MarkdownProcessor markdownProcessor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
|
||||
method @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void ProvideMarkdownStyling(org.jetbrains.jewel.markdown.rendering.ImageSourceResolver imageSourceResolver, org.jetbrains.jewel.markdown.rendering.MarkdownStyling markdownStyling, org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer markdownBlockRenderer, optional org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter codeHighlighter, optional org.jetbrains.jewel.markdown.MarkdownMode markdownMode, optional org.jetbrains.jewel.markdown.processing.MarkdownProcessor markdownProcessor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
|
||||
method @SuppressCompatibility @androidx.compose.runtime.Composable @org.jetbrains.annotations.ApiStatus.Experimental @org.jetbrains.jewel.foundation.ExperimentalJewelApi public static void ProvideMarkdownStyling(org.jetbrains.jewel.markdown.rendering.MarkdownStyling markdownStyling, org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer markdownBlockRenderer, org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter codeHighlighter, optional org.jetbrains.jewel.markdown.MarkdownMode markdownMode, optional org.jetbrains.jewel.markdown.processing.MarkdownProcessor markdownProcessor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
|
||||
}
|
||||
|
||||
|
||||
+24
-8
@@ -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,
|
||||
|
||||
+7
-2
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user