-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add syntax highlighting to markdown preview (#592)
* Introduce code highlighting API (#386) * Provide default no-op highlighter * Use highlighter to render code in `DefaultMarkdownBlockRenderer` * Extract `MimeType` from markdown to core * Provide highlighter implementations * Use no-op in standalone * Use IJP lexer-based highlighter in IDE bridge; no-op as default * otherwise we need to pass the project around * Provide option to define own highlighter via `ProvideMarkdownStyling` * Re-highlight code when the editor scheme color changed * API dump * Inject highlighting dispatcher into the constructor * Add `ProvideMarkdownStyling` override with the project Using this override will automatically set up a highlighter, no need to do that manually. * Fail fast if file type is unknown * Background and text decoration in TextAttributes -> SpanStyle conversion * Revert MimeType looks * Simplify NoOpCodeHighlighter * Remove unnecessary color specifier
- Loading branch information
1 parent
27b0e40
commit b09e3f1
Showing
20 changed files
with
366 additions
and
104 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 10 additions & 10 deletions
20
.../org/jetbrains/jewel/markdown/MimeType.kt → ...tbrains/jewel/foundation/code/MimeType.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
...ation/src/main/kotlin/org/jetbrains/jewel/foundation/code/highlighting/CodeHighlighter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package org.jetbrains.jewel.foundation.code.highlighting | ||
|
||
import androidx.compose.runtime.ProvidableCompositionLocal | ||
import androidx.compose.runtime.staticCompositionLocalOf | ||
import androidx.compose.ui.text.AnnotatedString | ||
import kotlinx.coroutines.flow.Flow | ||
import org.jetbrains.jewel.foundation.ExperimentalJewelApi | ||
import org.jetbrains.jewel.foundation.code.MimeType | ||
|
||
@ExperimentalJewelApi | ||
public interface CodeHighlighter { | ||
/** | ||
* Highlights [code] according to rules for the language specified by [mimeType], and returns flow of styled | ||
* strings. For basic highlighters with rigid color schemes it is enough to return a flow of one element: | ||
* ``` | ||
* return flowOf(highlightedString(code, mimeType)) | ||
* ``` | ||
* | ||
* However, some implementations might want gradual highlighting (for example, apply something simple while waiting | ||
* for the extensive info from server), or they might rely upon a color scheme that can change at any time. | ||
* | ||
* In such cases, they need to produce more than one styled string for the same piece of code, and that's when flows | ||
* come in handy. | ||
* | ||
* @see [NoOpCodeHighlighter] | ||
*/ | ||
public fun highlight(code: String, mimeType: MimeType): Flow<AnnotatedString> | ||
} | ||
|
||
public val LocalCodeHighlighter: ProvidableCompositionLocal<CodeHighlighter> = staticCompositionLocalOf { | ||
NoOpCodeHighlighter | ||
} |
10 changes: 10 additions & 0 deletions
10
...n/src/main/kotlin/org/jetbrains/jewel/foundation/code/highlighting/NoOpCodeHighlighter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package org.jetbrains.jewel.foundation.code.highlighting | ||
|
||
import androidx.compose.ui.text.AnnotatedString | ||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.flowOf | ||
import org.jetbrains.jewel.foundation.code.MimeType | ||
|
||
public object NoOpCodeHighlighter : CodeHighlighter { | ||
override fun highlight(code: String, mimeType: MimeType): Flow<AnnotatedString> = flowOf(AnnotatedString(code)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
...ge/src/main/kotlin/org/jetbrains/jewel/bridge/code/highlighting/CodeHighlighterFactory.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package org.jetbrains.jewel.bridge.code.highlighting | ||
|
||
import com.intellij.openapi.components.Service | ||
import com.intellij.openapi.components.service | ||
import com.intellij.openapi.editor.colors.EditorColorsListener | ||
import com.intellij.openapi.editor.colors.EditorColorsManager | ||
import com.intellij.openapi.project.Project | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.flow.MutableSharedFlow | ||
import kotlinx.coroutines.launch | ||
import org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter | ||
|
||
@Service(Service.Level.PROJECT) | ||
public class CodeHighlighterFactory(private val project: Project, private val coroutineScope: CoroutineScope) { | ||
private val reHighlightingRequests = MutableSharedFlow<Unit>(replay = 0) | ||
|
||
init { | ||
project.messageBus | ||
.connect(coroutineScope) | ||
.subscribe( | ||
EditorColorsManager.TOPIC, | ||
EditorColorsListener { coroutineScope.launch { reHighlightingRequests.emit(Unit) } }, | ||
) | ||
} | ||
|
||
public fun createHighlighter(): CodeHighlighter = LexerBasedCodeHighlighter(project, reHighlightingRequests) | ||
|
||
public companion object { | ||
public fun getInstance(project: Project): CodeHighlighterFactory = project.service() | ||
} | ||
} |
104 changes: 104 additions & 0 deletions
104
...src/main/kotlin/org/jetbrains/jewel/bridge/code/highlighting/LexerBasedCodeHighlighter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
package org.jetbrains.jewel.bridge.code.highlighting | ||
|
||
import androidx.compose.ui.text.AnnotatedString | ||
import androidx.compose.ui.text.SpanStyle | ||
import androidx.compose.ui.text.buildAnnotatedString | ||
import androidx.compose.ui.text.font.FontStyle | ||
import androidx.compose.ui.text.font.FontWeight | ||
import androidx.compose.ui.text.style.TextDecoration | ||
import androidx.compose.ui.text.withStyle | ||
import com.intellij.lang.Language | ||
import com.intellij.lang.LanguageUtil | ||
import com.intellij.openapi.editor.colors.EditorColorsManager | ||
import com.intellij.openapi.editor.colors.EditorColorsScheme | ||
import com.intellij.openapi.editor.markup.EffectType | ||
import com.intellij.openapi.editor.markup.TextAttributes | ||
import com.intellij.openapi.fileTypes.SyntaxHighlighter | ||
import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory | ||
import com.intellij.openapi.project.Project | ||
import com.intellij.testFramework.LightVirtualFile | ||
import java.awt.Font | ||
import kotlinx.coroutines.CoroutineDispatcher | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.flow.Flow | ||
import kotlinx.coroutines.flow.FlowCollector | ||
import kotlinx.coroutines.flow.flow | ||
import kotlinx.coroutines.flow.flowOf | ||
import kotlinx.coroutines.withContext | ||
import org.jetbrains.jewel.bridge.toComposeColorOrUnspecified | ||
import org.jetbrains.jewel.foundation.code.MimeType | ||
import org.jetbrains.jewel.foundation.code.highlighting.CodeHighlighter | ||
|
||
internal class LexerBasedCodeHighlighter( | ||
private val project: Project, | ||
private val reHighlightingRequests: Flow<Unit>, | ||
private val highlightDispatcher: CoroutineDispatcher = Dispatchers.Default, | ||
) : CodeHighlighter { | ||
override fun highlight(code: String, mimeType: MimeType): Flow<AnnotatedString> { | ||
val language = mimeType.toLanguageOrNull() ?: return flowOf(AnnotatedString(code)) | ||
val fileExtension = language.associatedFileType?.defaultExtension ?: return flowOf(AnnotatedString(code)) | ||
val virtualFile = LightVirtualFile("markdown_code_block_${code.hashCode()}.$fileExtension", language, code) | ||
val colorScheme = EditorColorsManager.getInstance().globalScheme | ||
val highlighter = | ||
SyntaxHighlighterFactory.getSyntaxHighlighter(language, project, virtualFile) | ||
?: return flowOf(AnnotatedString(code)) | ||
|
||
return flow { | ||
highlightAndEmit(highlighter, code, colorScheme) | ||
reHighlightingRequests.collect { highlightAndEmit(highlighter, code, colorScheme) } | ||
} | ||
} | ||
|
||
private suspend fun FlowCollector<AnnotatedString>.highlightAndEmit( | ||
highlighter: SyntaxHighlighter, | ||
code: String, | ||
colorScheme: EditorColorsScheme, | ||
) { | ||
emit(withContext(highlightDispatcher) { doHighlight(highlighter, code, colorScheme) }) | ||
} | ||
|
||
private fun doHighlight( | ||
highlighter: SyntaxHighlighter, | ||
code: String, | ||
colorScheme: EditorColorsScheme, | ||
): AnnotatedString = buildAnnotatedString { | ||
with(highlighter.highlightingLexer) { | ||
start(code) | ||
|
||
while (tokenType != null) { | ||
val attributes: TextAttributes? = run { | ||
val attrKey = highlighter.getTokenHighlights(tokenType).lastOrNull() ?: return@run null | ||
colorScheme.getAttributes(attrKey) ?: attrKey.defaultAttributes | ||
} | ||
withTextAttributes(attributes) { append(tokenText) } | ||
advance() | ||
} | ||
} | ||
} | ||
|
||
private fun MimeType.toLanguageOrNull(): Language? = LanguageUtil.findRegisteredLanguage(displayName().lowercase()) | ||
|
||
private fun AnnotatedString.Builder.withTextAttributes( | ||
textAttributes: TextAttributes?, | ||
block: AnnotatedString.Builder.() -> Unit, | ||
) { | ||
if (textAttributes == null) { | ||
return block() | ||
} | ||
withStyle(textAttributes.toSpanStyle(), block) | ||
} | ||
|
||
private fun TextAttributes.toSpanStyle() = | ||
SpanStyle( | ||
color = foregroundColor.toComposeColorOrUnspecified(), | ||
fontWeight = if (fontType and Font.BOLD != 0) FontWeight.Bold else null, | ||
fontStyle = if (fontType and Font.ITALIC != 0) FontStyle.Italic else null, | ||
background = backgroundColor.toComposeColorOrUnspecified(), | ||
textDecoration = | ||
when (effectType) { | ||
EffectType.LINE_UNDERSCORE -> TextDecoration.Underline | ||
EffectType.STRIKEOUT -> TextDecoration.LineThrough | ||
else -> null | ||
}, | ||
) | ||
} |
Oops, something went wrong.