IJPL-787 JBHtmlPane - improve configuration API and move implementation details to impl module through dedicated service

GitOrigin-RevId: 3fe0f6be3a43f128bc95d5931bec2e1af24fa6af
This commit is contained in:
Piotr Tomiak
2024-03-14 18:24:29 +01:00
committed by intellij-monorepo-bot
parent 2235b2f0c9
commit d7bc671a54
15 changed files with 489 additions and 312 deletions

View File

@@ -5,7 +5,6 @@ import com.intellij.lang.documentation.DocumentationImageResolver;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.colors.*;
import com.intellij.openapi.editor.impl.EditorCssFontResolver;
import com.intellij.openapi.options.FontSize;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.ui.JBColor;
@@ -34,7 +33,8 @@ import java.util.Map;
import java.util.function.Function;
import static com.intellij.codeInsight.documentation.DocumentationHtmlUtil.*;
import static com.intellij.lang.documentation.DocumentationMarkup.*;
import static com.intellij.lang.documentation.DocumentationMarkup.CLASS_BOTTOM;
import static com.intellij.lang.documentation.DocumentationMarkup.CLASS_DEFINITION;
import static com.intellij.lang.documentation.QuickDocHighlightingHelper.getDefaultDocStyleOptions;
@Internal
@@ -63,14 +63,13 @@ public abstract class DocumentationEditorPane extends JBHtmlPane implements Disp
) {
super(
getDefaultDocStyleOptions(EditorColorsManager.getInstance().getGlobalScheme(), false),
new JBHtmlPaneConfiguration(
keyboardActions,
component -> new DocumentationImageProvider(component, imageResolver),
getModuleIconResolver(iconResolver),
bg -> getDocumentationPaneAdditionalCssRules(),
EditorCssFontResolver.getGlobalInstance(),
Collections.singletonList(ExtendableHTMLViewFactory.Extensions.FIT_TO_WIDTH_IMAGES)
)
JBHtmlPaneConfiguration.builder()
.keyboardActions(keyboardActions)
.imageResolverFactory(component -> new DocumentationImageProvider(component, imageResolver))
.iconResolver(name -> iconResolver.apply(name))
.customStyleSheetProvider(bg -> getDocumentationPaneAdditionalCssRules())
.extensions(Collections.singletonList(ExtendableHTMLViewFactory.Extensions.FIT_TO_WIDTH_IMAGES))
.build()
);
setBackground(BACKGROUND_COLOR);
}

View File

@@ -404,9 +404,11 @@ public final class DocRenderer implements CustomFoldRegionRenderer {
EditorInlineHtmlPane(boolean trackMemory, Editor editor) {
super(
QuickDocHighlightingHelper.getDefaultDocStyleOptions(editor.getColorsScheme(), true),
new JBHtmlPaneConfiguration(Collections.emptyMap(), pane -> IMAGE_MANAGER.getImageProvider(),
url -> null, bg -> getStyleSheet(editor),
EditorCssFontResolver.getInstance(editor), Collections.emptyList())
JBHtmlPaneConfiguration.builder()
.imageResolverFactory(pane -> IMAGE_MANAGER.getImageProvider())
.customStyleSheetProvider(bg -> getStyleSheet(editor))
.fontResolver(EditorCssFontResolver.getInstance(editor))
.build()
);
if (trackMemory) {
MEMORY_MANAGER.register(DocRenderer.this, 50 /* rough size estimation */);

View File

@@ -13,7 +13,8 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.psi.PsiElement
import com.intellij.ui.components.JBHtmlPaneStyleConfiguration
import com.intellij.ui.components.JBHtmlPaneStyleConfiguration.*
import com.intellij.ui.components.JBHtmlPaneStyleConfiguration.ElementKind
import com.intellij.ui.components.JBHtmlPaneStyleConfiguration.ElementProperty
import com.intellij.util.applyIf
import com.intellij.util.concurrency.annotations.RequiresReadLock
import com.intellij.xml.util.XmlStringUtil
@@ -272,26 +273,22 @@ object QuickDocHighlightingHelper {
@Internal
@JvmStatic
fun getDefaultDocStyleOptions(colorScheme: EditorColorsScheme, editorInlineContext: Boolean): JBHtmlPaneStyleConfiguration =
JBHtmlPaneStyleConfiguration(
colorScheme = colorScheme,
editorInlineContext = editorInlineContext,
JBHtmlPaneStyleConfiguration {
this.colorScheme = colorScheme
this.editorInlineContext = editorInlineContext
inlineCodeParentSelectors = listOf(".$CLASS_CONTENT", ".$CLASS_CONTENT div:not(.$CLASS_BOTTOM)",
".$CLASS_CONTENT div:not(.$CLASS_TOP)", ".$CLASS_SECTIONS"),
largeCodeFontSizeSelectors = listOf(".$CLASS_DEFINITION code", ".$CLASS_DEFINITION pre", ".$CLASS_BOTTOM code", ".$CLASS_TOP code"),
enableInlineCodeBackground = DocumentationSettings.isCodeBackgroundEnabled()
&& DocumentationSettings.getInlineCodeHighlightingMode() !== InlineCodeHighlightingMode.NO_HIGHLIGHTING,
".$CLASS_CONTENT div:not(.$CLASS_TOP)", ".$CLASS_SECTIONS")
largeCodeFontSizeSelectors = listOf(".$CLASS_DEFINITION code", ".$CLASS_DEFINITION pre", ".$CLASS_BOTTOM code", ".$CLASS_TOP code")
enableInlineCodeBackground = (DocumentationSettings.isCodeBackgroundEnabled()
&& DocumentationSettings.getInlineCodeHighlightingMode() !== InlineCodeHighlightingMode.NO_HIGHLIGHTING)
enableCodeBlocksBackground = DocumentationSettings.isCodeBackgroundEnabled()
&& DocumentationSettings.isHighlightingOfCodeBlocksEnabled(),
useFontLigaturesInCode = false,
controlStyleOverrides = if (editorInlineContext)
ControlStyleOverrides(
controlKindSuffix = "EditorPane",
overrides = mapOf(
ControlKind.CodeBlock to listOf(ControlProperty.BackgroundColor, ControlProperty.BackgroundOpacity, ControlProperty.BorderColor)
)
)
else null
)
&& DocumentationSettings.isHighlightingOfCodeBlocksEnabled()
if (editorInlineContext)
overrideElementStyle {
elementKindThemePropertySuffix = "EditorPane"
overrideThemeProperties(ElementKind.CodeBlock, ElementProperty.BackgroundColor, ElementProperty.BackgroundOpacity, ElementProperty.BorderColor)
}
}
private fun StringBuilder.appendHighlightedCode(project: Project, language: Language?, doHighlighting: Boolean,
code: CharSequence, isForRenderedDoc: Boolean, trim: Boolean): StringBuilder {

View File

@@ -15,7 +15,6 @@ import org.jetbrains.annotations.Nls
import org.jsoup.Jsoup
import java.awt.Color
import java.io.IOException
import java.util.*
import javax.swing.JEditorPane
/**
@@ -25,12 +24,9 @@ import javax.swing.JEditorPane
*/
open class DescriptionEditorPane : JBHtmlPane(
JBHtmlPaneStyleConfiguration(),
JBHtmlPaneConfiguration(
emptyMap(), { null }, { null },
{ StyleSheetUtil.loadStyleSheet("pre {white-space: pre-wrap;} code, pre, a {overflow-wrap: anywhere;}") },
null, Collections.emptyList()
)
) {
JBHtmlPaneConfiguration {
customStyleSheetProvider { StyleSheetUtil.loadStyleSheet("pre {white-space: pre-wrap;} code, pre, a {overflow-wrap: anywhere;}") }
}) {
init {
isEditable = false

View File

@@ -36,6 +36,5 @@
<orderEntry type="library" name="caffeine" level="project" />
<orderEntry type="module" module-name="intellij.platform.ide.progress" exported="" />
<orderEntry type="module" module-name="intellij.platform.util.diff" />
<orderEntry type="library" name="jsoup" level="project" />
</component>
</module>

View File

@@ -1,16 +1,10 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ui.components
import com.intellij.ide.ui.text.ShortcutsRenderingUtil
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.editor.impl.EditorCssFontResolver
import com.intellij.openapi.util.text.StringUtil
import com.intellij.ui.components.JBHtmlPaneStyleSheetRulesProvider.buildCodeBlock
import com.intellij.ui.components.JBHtmlPaneStyleSheetRulesProvider.getStyleSheet
import com.intellij.util.SmartList
import com.intellij.util.asSafely
import com.intellij.util.containers.CollectionFactory
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.colors.EditorColorsScheme
import com.intellij.util.containers.addAllIfNotNull
import com.intellij.util.ui.*
import com.intellij.util.ui.ExtendableHTMLViewFactory.Extensions.icons
@@ -20,13 +14,9 @@ import com.intellij.util.ui.html.cssPadding
import com.intellij.util.ui.html.width
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.ApiStatus.Experimental
import org.jetbrains.annotations.Nls
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.jsoup.nodes.Node
import org.jsoup.nodes.TextNode
import org.jsoup.select.NodeVisitor
import java.awt.AWTEvent
import java.awt.Color
import java.awt.Graphics
@@ -123,7 +113,7 @@ open class JBHtmlPane(
private val myPaneConfiguration: JBHtmlPaneConfiguration
) : JEditorPane(), Disposable {
private val service: ImplService = ApplicationManager.getApplication().service()
private var myText: @Nls String = "" // getText() surprisingly crashes…, let's cache the text
private var myCurrentDefaultStyleSheet: StyleSheet? = null
private val mutableBackgroundFlow: MutableStateFlow<Color>
@@ -158,7 +148,7 @@ open class JBHtmlPane(
val editorKit = HTMLEditorKitBuilder()
.replaceViewFactoryExtensions(*extensions.toTypedArray())
.withFontResolver(myPaneConfiguration.fontResolver ?: EditorCssFontResolver.getGlobalInstance())
.withFontResolver(myPaneConfiguration.fontResolver ?: service.defaultEditorCssFontResolver())
.build()
updateDocumentationPaneDefaultCssRules(editorKit)
@@ -183,7 +173,7 @@ open class JBHtmlPane(
}
override fun setText(t: @Nls String?) {
myText = t?.let { transpileHtmlPaneInput(it) } ?: ""
myText = t?.let { service.transpileHtmlPaneInput(it) } ?: ""
super.setText(myText)
}
@@ -201,8 +191,8 @@ open class JBHtmlPane(
val newStyleSheet = StyleSheet()
.also { myCurrentDefaultStyleSheet = it }
val background = background
newStyleSheet.addStyleSheet(getStyleSheet(background, myStyleConfiguration))
newStyleSheet.addStyleSheet(EditorColorsSchemeStyleProvider(myStyleConfiguration.colorScheme))
newStyleSheet.addStyleSheet(service.getDefaultStyleSheet(background, myStyleConfiguration))
newStyleSheet.addStyleSheet(service.getEditorColorsSchemeStyleSheet(myStyleConfiguration.colorScheme))
myPaneConfiguration.customStyleSheetProvider(background)?.let {
newStyleSheet.addStyleSheet(it)
}
@@ -260,196 +250,17 @@ open class JBHtmlPane(
return null
}
/**
* Transpiler pane input to fit to limited AWT HTML toolkit support.
*/
@Suppress("HardCodedStringLiteral")
private fun transpileHtmlPaneInput(text: @Nls String): @Nls String {
val document = Jsoup.parse(text)
document.traverse(NodeVisitor { node, _ ->
when (node) {
is TextNode -> {
transpileTextNode(node)
}
is Element -> {
when {
node.nameIs("p") -> transpileParagraph(node)
node.nameIs("shortcut") -> transpileShortcut(node)
node.nameIs("blockquote") -> transpileBlockquote(node)
node.nameIs("pre") -> transpilePre(node)
node.nameIs("icon") -> transpileIcon(node)
}
}
}
})
document.outputSettings().prettyPrint(false)
return document.html()
@ApiStatus.Internal
interface ImplService {
fun transpileHtmlPaneInput(text: @Nls String): @Nls String
fun defaultEditorCssFontResolver(): CSSFontResolver
fun getDefaultStyleSheet(paneBackgroundColor: Color, configuration: JBHtmlPaneStyleConfiguration): StyleSheet
fun getEditorColorsSchemeStyleSheet(editorColorsScheme: EditorColorsScheme): StyleSheet
}
/**
* Remove empty `<p>` before some tags - workaround for Swing html renderer not removing empty paragraphs before non-inline tags
*/
private fun transpileParagraph(node: Element) {
if (node.childNodeSize() == 0
|| (node.childNodeSize() == 1
&& node.childNode(0).asSafely<TextNode>()?.wholeText?.isBlank() == true)
&& node.nextElementSibling()?.let { dropPrecedingEmptyParagraphTags.contains(it.tagName()) } == true
) {
node.remove()
}
}
/**
* Expand `<shortcut raw|actionId="*"/>` tag into a sequence of `<kbd>` tags
*/
private fun transpileShortcut(node: Element) {
val actionId = node.attributes().getIgnoreCase("actionid")
.takeIf { it.isNotEmpty() }
val raw = node.attributes().getIgnoreCase("raw")
.takeIf { it.isNotEmpty() }
if (actionId != null || raw != null) {
val shortcutData =
if (actionId != null)
ShortcutsRenderingUtil.getShortcutByActionId(actionId)
?.let { ShortcutsRenderingUtil.getKeyboardShortcutData(it) }?.first
?: ShortcutsRenderingUtil.getGotoActionData(actionId, false)
.takeIf { ActionManager.getInstance().getAction(actionId) != null }
?.first
else
KeyStroke.getKeyStroke(raw)
?.let { ShortcutsRenderingUtil.getKeyStrokeData(it) }
?.first
if (shortcutData != null) {
val replacement = shortcutData
.splitToSequence(ShortcutsRenderingUtil.SHORTCUT_PART_SEPARATOR)
.fold(mutableListOf<Node>()) { acc, s ->
if (acc.isNotEmpty()) {
acc.add(TextNode(StringUtil.NON_BREAK_SPACE))
}
acc.add(Element("kbd").text(s))
acc
}
node.replaceWith(replacement)
}
else {
node.replaceWith(Element("kbd").text(actionId ?: raw!!))
}
}
}
/**
* Replace `<pre><code>(...)</code></pre>` with [JBHtmlPaneStyleSheetRulesProvider.buildCodeBlock]
*/
private fun transpilePre(node: Element) {
if (node.childNodeSize() != 1) return
val childNodes =
node.childNode(0)
.asSafely<Element>()
?.takeIf { it.nameIs("code") }
?.childNodes()
?: return
node.replaceWith(buildCodeBlock(childNodes))
}
/**
* Replace `<blockquote>\\s*<pre>(...)</pre>\\s*</blockquote>` with [JBHtmlPaneStyleSheetRulesProvider.buildCodeBlock]
*/
private fun transpileBlockquote(node: Element) {
if (node.childNodeSize() > 3 || node.childNodeSize() < 1) return
val nodes = node.childNodes()
val wsNode1 = nodes.getOrNull(0).asSafely<TextNode>()
val preNode = nodes.getOrNull(if (wsNode1 == null) 0 else 1).asSafely<Element>()
?: return
val wsNode2 = nodes.getOrNull(if (wsNode1 == null) 1 else 2)
if (wsNode1?.wholeText.isNullOrBlank()
&& preNode.nameIs("pre")
&& wsNode2.let { it == null || it is TextNode && it.wholeText.isBlank() }) {
val preNodes = preNode.childNodes()
preNodes.getOrNull(0)?.asSafely<TextNode>()?.let {
it.text(it.wholeText.trim('\n', '\r'))
}
preNodes.lastOrNull()?.asSafely<TextNode>()?.let {
it.text(it.wholeText.trimEnd())
}
node.replaceWith(buildCodeBlock(preNodes))
}
}
/**
* Move icon children to parent node
*/
private fun transpileIcon(node: Element) {
node.parent()
?.insertChildren(node.siblingIndex() + 1, node.childNodes())
}
/**
* - Add `<wbr>` after `.` if surrounded by letters
* - Add `<wbr>` after `]`, `)` or `/` followed by a char or digit
*/
private fun transpileTextNode(node: TextNode) {
val builder = StringBuilder()
val text = node.wholeText
val codePoints = text.codePoints().iterator()
if (!codePoints.hasNext()) return
var codePoint = codePoints.nextInt()
fun next() {
builder.appendCodePoint(codePoint)
codePoint = if (codePoints.hasNext())
codePoints.nextInt()
else
-1
}
val replacement = SmartList<Node>()
while (codePoint >= 0) {
when {
// break after dot if surrounded by letters
Character.isLetter(codePoint) -> {
next()
if (codePoint == '.'.code) {
next()
if (Character.isLetter(codePoint)) {
replacement.add(TextNode(builder.toString()))
replacement.add(Element("wbr"))
builder.clear()
}
}
}
// break after ], ) or / followed by a char or digit
codePoint == ')'.code || codePoint == ']'.code || codePoint == '/'.code -> {
next()
if (Character.isLetterOrDigit(codePoint)) {
replacement.add(TextNode(builder.toString()))
replacement.add(Element("wbr"))
builder.clear()
}
}
else -> next()
}
}
if (!replacement.isEmpty()) {
replacement.add(TextNode(builder.toString()))
node.replaceWith(replacement)
}
}
private fun Node.replaceWith(nodes: List<Node>) {
val parent = parent() as? Element ?: return
parent.insertChildren(siblingIndex(), nodes)
remove()
}
companion object {
private val dropPrecedingEmptyParagraphTags = CollectionFactory.createCharSequenceSet(false).also {
it.addAll(listOf("ul", "ol", "dl", "h1", "h2", "h3", "h4", "h5", "h6", "p", "tr", "td",
"table", "pre", "blockquote", "div"))
}
}
}

View File

@@ -14,11 +14,54 @@ import javax.swing.KeyStroke
import javax.swing.text.html.StyleSheet
@Experimental
data class JBHtmlPaneConfiguration(
val keyboardActions: Map<KeyStroke, ActionListener> = emptyMap(),
val imageResolverFactory: (JBHtmlPane) -> Dictionary<URL, Image>? = { null },
val iconResolver: (String) -> Icon? = { null },
val customStyleSheetProvider: (backgroundColor: Color) -> StyleSheet? = { null },
val fontResolver: CSSFontResolver? = null,
val extensions: List<ExtendableHTMLViewFactory.Extension> = emptyList()
)
class JBHtmlPaneConfiguration private constructor(builder: Builder) {
val keyboardActions: Map<KeyStroke, ActionListener> = builder.keyboardActions
val imageResolverFactory: (JBHtmlPane) -> Dictionary<URL, Image>? = builder.imageResolverFactory
val iconResolver: (String) -> Icon? = builder.iconResolver
val customStyleSheetProvider: (backgroundColor: Color) -> StyleSheet? = builder.customStyleSheetProvider
val fontResolver: CSSFontResolver? = builder.fontResolver
val extensions: List<ExtendableHTMLViewFactory.Extension> = builder.extensions
constructor() : this(builder())
constructor(configure: Builder.() -> Unit) : this(builder().also { configure(it) })
class Builder {
var keyboardActions: Map<KeyStroke, ActionListener> = emptyMap()
var imageResolverFactory: (JBHtmlPane) -> Dictionary<URL, Image>? = { null }
var iconResolver: (String) -> Icon? = { null }
var customStyleSheetProvider: (backgroundColor: Color) -> StyleSheet? = { null }
var fontResolver: CSSFontResolver? = null
var extensions: List<ExtendableHTMLViewFactory.Extension> = emptyList()
fun build(): JBHtmlPaneConfiguration = JBHtmlPaneConfiguration(this)
fun keyboardActions(keyboardActions: Map<KeyStroke, ActionListener>): Builder =
apply { this.keyboardActions = keyboardActions }
fun imageResolverFactory(imageResolverFactory: (JBHtmlPane) -> Dictionary<URL, Image>?): Builder =
apply { this.imageResolverFactory = imageResolverFactory }
fun iconResolver(iconResolver: (String) -> Icon?): Builder =
apply { this.iconResolver = iconResolver }
fun customStyleSheetProvider(customStyleSheetProvider: (backgroundColor: Color) -> StyleSheet?): Builder =
apply { this.customStyleSheetProvider = customStyleSheetProvider }
fun fontResolver(fontResolver: CSSFontResolver?): Builder =
apply { this.fontResolver = fontResolver }
fun extensions(extensions: List<ExtendableHTMLViewFactory.Extension>): Builder =
apply { this.extensions = extensions }
}
companion object {
@JvmStatic
fun builder(): Builder =
Builder()
}
}

View File

@@ -9,20 +9,26 @@ import org.jetbrains.annotations.ApiStatus.Experimental
import java.util.*
@Experimental
data class JBHtmlPaneStyleConfiguration(
val colorScheme: EditorColorsScheme = EditorColorsManager.getInstance().globalScheme,
val editorInlineContext: Boolean = false,
val inlineCodeParentSelectors: List<String> = listOf(""),
val largeCodeFontSizeSelectors: List<String> = emptyList(),
val enableInlineCodeBackground: Boolean = true,
val enableCodeBlocksBackground: Boolean = true,
val useFontLigaturesInCode: Boolean = false,
/** Unscaled */
val spaceBeforeParagraph: Int = defaultSpaceBeforeParagraph,
/** Unscaled */
val spaceAfterParagraph: Int = defaultSpaceAfterParagraph,
val controlStyleOverrides: ControlStyleOverrides? = null,
) {
class JBHtmlPaneStyleConfiguration private constructor(builder: Builder) {
val colorScheme: EditorColorsScheme = builder.colorScheme
val editorInlineContext: Boolean = builder.editorInlineContext
val inlineCodeParentSelectors: List<String> = builder.inlineCodeParentSelectors
val largeCodeFontSizeSelectors: List<String> = builder.largeCodeFontSizeSelectors
val enableInlineCodeBackground: Boolean = builder.enableInlineCodeBackground
val enableCodeBlocksBackground: Boolean = builder.enableCodeBlocksBackground
val useFontLigaturesInCode: Boolean = builder.useFontLigaturesInCode
/** unscaled */
val spaceBeforeParagraph: Int = builder.spaceBeforeParagraph
/** unscaled */
val spaceAfterParagraph: Int = builder.spaceAfterParagraph
val elementStyleOverrides: ElementStyleOverrides? = builder.elementStyleOverrides
constructor() : this(builder())
constructor(configure: Builder.() -> Unit) : this(builder().also { configure(it) })
override fun equals(other: Any?): Boolean =
other is JBHtmlPaneStyleConfiguration
&& colorSchemesEqual(colorScheme, other.colorScheme)
@@ -38,12 +44,11 @@ data class JBHtmlPaneStyleConfiguration(
// Update here when more colors are used from the colorScheme
colorScheme.defaultBackground.rgb == colorScheme2.defaultBackground.rgb
&& colorScheme.defaultForeground.rgb == colorScheme2.defaultForeground.rgb
&& ControlKind.entries.all {
&& ElementKind.entries.all {
colorScheme.getAttributes(it.colorSchemeKey, false) ==
colorScheme2.getAttributes(it.colorSchemeKey, false)
}
override fun hashCode(): Int =
Objects.hash(colorScheme.defaultBackground.rgb and 0xffffff,
colorScheme.defaultForeground.rgb and 0xffffff,
@@ -51,18 +56,51 @@ data class JBHtmlPaneStyleConfiguration(
enableInlineCodeBackground, enableCodeBlocksBackground,
useFontLigaturesInCode, spaceBeforeParagraph, spaceAfterParagraph)
data class ControlStyleOverrides(
val controlKindSuffix: String,
val overrides: Map<ControlKind, Collection<ControlProperty>>
)
class ElementStyleOverrides(builder: Builder) {
val elementKindThemePropertySuffix: String = builder.elementKindThemePropertySuffix?.takeUnless { it.isBlank() }
?: throw IllegalStateException("elementKindThemePropertySuffix must not be null or blank")
val overrides: Map<ElementKind, Collection<ElementProperty>> = builder.overrides.mapValues { it.value.toList() }
enum class ControlKind(val id: String, val colorSchemeKey: TextAttributesKey) {
override fun equals(other: Any?): Boolean =
other is ElementStyleOverrides
&& other.elementKindThemePropertySuffix == elementKindThemePropertySuffix
&& other.overrides == overrides
override fun hashCode(): Int =
Objects.hash(elementKindThemePropertySuffix, overrides)
class Builder {
var elementKindThemePropertySuffix: String? = null
internal val overrides: MutableMap<ElementKind, MutableCollection<ElementProperty>> = mutableMapOf()
fun overrideThemeProperties(elementKind: ElementKind, vararg properties: ElementProperty): Builder =
apply { overrides.getOrPut(elementKind) { mutableListOf() } += properties }
fun elementKindThemePropertySuffix(elementKindThemePropertySuffix: String): Builder =
apply { this.elementKindThemePropertySuffix = elementKindThemePropertySuffix }
fun build(): ElementStyleOverrides =
ElementStyleOverrides(this)
}
companion object {
@JvmStatic
fun builder(): Builder =
Builder()
}
}
enum class ElementKind(val id: String, val colorSchemeKey: TextAttributesKey) {
CodeInline("Code.Inline", DefaultLanguageHighlighterColors.DOC_CODE_INLINE),
CodeBlock("Code.Block", DefaultLanguageHighlighterColors.DOC_CODE_BLOCK),
Shortcut("Shortcut", DefaultLanguageHighlighterColors.DOC_TIPS_SHORTCUT),
}
enum class ControlProperty(val id: String) {
enum class ElementProperty(val id: String) {
BackgroundColor("backgroundColor"),
ForegroundColor("foregroundColor"),
BorderColor("borderColor"),
@@ -71,6 +109,59 @@ data class JBHtmlPaneStyleConfiguration(
BorderRadius("borderRadius"),
}
class Builder {
var colorScheme: EditorColorsScheme = EditorColorsManager.getInstance().globalScheme
var editorInlineContext: Boolean = false
var inlineCodeParentSelectors: List<String> = listOf("")
var largeCodeFontSizeSelectors: List<String> = emptyList()
var enableInlineCodeBackground: Boolean = true
var enableCodeBlocksBackground: Boolean = true
var useFontLigaturesInCode: Boolean = false
/** unscaled */
var spaceBeforeParagraph: Int = defaultSpaceBeforeParagraph
/** Unscaled */
var spaceAfterParagraph: Int = defaultSpaceAfterParagraph
var elementStyleOverrides: ElementStyleOverrides? = null
fun build(): JBHtmlPaneStyleConfiguration = JBHtmlPaneStyleConfiguration(this)
fun colorScheme(colorScheme: EditorColorsScheme): Builder =
apply { this.colorScheme = colorScheme }
fun editorInlineContext(editorInlineContext: Boolean): Builder =
apply { this.editorInlineContext = editorInlineContext }
fun inlineCodeParentSelectors(inlineCodeParentSelectors: List<String>): Builder =
apply { this.inlineCodeParentSelectors = inlineCodeParentSelectors }
fun largeCodeFontSizeSelectors(largeCodeFontSizeSelectors: List<String>): Builder =
apply { this.largeCodeFontSizeSelectors = largeCodeFontSizeSelectors }
fun enableInlineCodeBackground(enableInlineCodeBackground: Boolean): Builder =
apply { this.enableInlineCodeBackground = enableInlineCodeBackground }
fun enableCodeBlocksBackground(enableCodeBlocksBackground: Boolean): Builder =
apply { this.enableCodeBlocksBackground = enableCodeBlocksBackground }
fun useFontLigaturesInCode(useFontLigaturesInCode: Boolean): Builder =
apply { this.useFontLigaturesInCode = useFontLigaturesInCode }
fun spaceBeforeParagraph(spaceBeforeParagraph: Int): Builder =
apply { this.spaceBeforeParagraph = spaceBeforeParagraph }
fun spaceAfterParagraph(spaceAfterParagraph: Int): Builder =
apply { this.spaceAfterParagraph = spaceAfterParagraph }
fun overrideElementStyle(elementStyleOverrides: ElementStyleOverrides): Builder =
apply { this.elementStyleOverrides = elementStyleOverrides }
fun overrideElementStyle(configuration: ElementStyleOverrides.Builder.() -> Unit): Builder =
apply { this.elementStyleOverrides = ElementStyleOverrides.builder().also(configuration).build() }
}
companion object {
@JvmStatic
val defaultSpaceBeforeParagraph: Int get() = 4
@@ -80,6 +171,11 @@ data class JBHtmlPaneStyleConfiguration(
@JvmStatic
val editorColorClassPrefix: String = "editor-color-"
@JvmStatic
fun builder(): Builder =
Builder()
}
}

View File

@@ -41,7 +41,6 @@ import javax.swing.border.Border;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.util.Collections;
// Android team doesn't want to use new mockito for now, so, class cannot be final
public class IdeTooltipManager implements Disposable {
@@ -713,11 +712,9 @@ public class IdeTooltipManager implements Disposable {
boolean limitWidthToScreen) {
JBHtmlPaneStyleConfiguration styleConfiguration = new JBHtmlPaneStyleConfiguration();
JBHtmlPaneConfiguration paneConfiguration = new JBHtmlPaneConfiguration(
Collections.emptyMap(), url -> null, icon -> null,
color -> StyleSheetUtil.loadStyleSheet("pre {white-space: pre-wrap;} code, pre {overflow-wrap: anywhere;}"),
null, Collections.emptyList()
);
JBHtmlPaneConfiguration paneConfiguration = JBHtmlPaneConfiguration.builder()
.customStyleSheetProvider(color -> StyleSheetUtil.loadStyleSheet("pre {white-space: pre-wrap;} code, pre {overflow-wrap: anywhere;}"))
.build();
Ref<Boolean> prefSizeWasComputed = new Ref<>(false);

View File

@@ -1,5 +1,5 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ui.components
package com.intellij.ui.components.impl
import com.intellij.openapi.editor.colors.EditorColorsScheme
import com.intellij.openapi.editor.colors.TextAttributesKey
@@ -16,7 +16,7 @@ import javax.swing.text.html.CSS
import javax.swing.text.html.HTML
import javax.swing.text.html.StyleSheet
internal class EditorColorsSchemeStyleProvider(private val editorColorsScheme: EditorColorsScheme) : StyleSheet() {
internal class EditorColorsSchemeStyleSheet(private val editorColorsScheme: EditorColorsScheme) : StyleSheet() {
private val computedStyles = mutableMapOf<String, Style>()
@@ -113,10 +113,6 @@ internal class EditorColorsSchemeStyleProvider(private val editorColorsScheme: E
ColorValueConstructor.newInstance()
.also { ColorValueColorField.set(it, color) }
private fun stringValue(value: String): Any =
StringValueConstructor.newInstance()
.also { CssCssValueSvalueField.set(it, value) }
private fun lengthValue(value: Float): Any =
LengthValueConstructor.newInstance()
.also { LengthValueSpanField.set(it, value) }

View File

@@ -0,0 +1,27 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ui.components.impl
import com.intellij.openapi.editor.colors.EditorColorsScheme
import com.intellij.openapi.editor.impl.EditorCssFontResolver
import com.intellij.ui.components.JBHtmlPane
import com.intellij.ui.components.JBHtmlPaneStyleConfiguration
import com.intellij.util.ui.CSSFontResolver
import org.jetbrains.annotations.Nls
import java.awt.Color
import javax.swing.text.html.StyleSheet
internal class JBHtmlPaneImplService: JBHtmlPane.ImplService {
override fun transpileHtmlPaneInput(@Nls text: String): @Nls String =
JBHtmlPaneInputTranspiler.transpileHtmlPaneInput(text)
override fun defaultEditorCssFontResolver(): CSSFontResolver =
EditorCssFontResolver.getGlobalInstance()
override fun getDefaultStyleSheet(paneBackgroundColor: Color, configuration: JBHtmlPaneStyleConfiguration): StyleSheet =
JBHtmlPaneStyleSheetRulesProvider.getStyleSheet(paneBackgroundColor, configuration)
override fun getEditorColorsSchemeStyleSheet(editorColorsScheme: EditorColorsScheme): StyleSheet =
EditorColorsSchemeStyleSheet(editorColorsScheme)
}

View File

@@ -0,0 +1,211 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ui.components.impl
import com.intellij.ide.ui.text.ShortcutsRenderingUtil
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.util.text.StringUtil
import com.intellij.ui.components.impl.JBHtmlPaneStyleSheetRulesProvider.buildCodeBlock
import com.intellij.util.SmartList
import com.intellij.util.asSafely
import com.intellij.util.containers.CollectionFactory
import org.jetbrains.annotations.Nls
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.jsoup.nodes.Node
import org.jsoup.nodes.TextNode
import org.jsoup.select.NodeVisitor
import javax.swing.KeyStroke
internal object JBHtmlPaneInputTranspiler {
private val dropPrecedingEmptyParagraphTags = CollectionFactory.createCharSequenceSet(false).also {
it.addAll(listOf("ul", "ol", "dl", "h1", "h2", "h3", "h4", "h5", "h6", "p", "tr", "td",
"table", "pre", "blockquote", "div"))
}
/**
* Transpiler pane input to fit to limited AWT HTML toolkit support.
*/
@Suppress("HardCodedStringLiteral")
fun transpileHtmlPaneInput(text: @Nls String): @Nls String {
val document = Jsoup.parse(text)
document.traverse(NodeVisitor { node, _ ->
when (node) {
is TextNode -> {
transpileTextNode(node)
}
is Element -> {
when {
node.nameIs("p") -> transpileParagraph(node)
node.nameIs("shortcut") -> transpileShortcut(node)
node.nameIs("blockquote") -> transpileBlockquote(node)
node.nameIs("pre") -> transpilePre(node)
node.nameIs("icon") -> transpileIcon(node)
}
}
}
})
document.outputSettings().prettyPrint(false)
return document.html()
}
/**
* Remove empty `<p>` before some tags - workaround for Swing html renderer not removing empty paragraphs before non-inline tags
*/
private fun transpileParagraph(node: Element) {
if (node.childNodeSize() == 0
|| (node.childNodeSize() == 1
&& node.childNode(0).asSafely<TextNode>()?.wholeText?.isBlank() == true)
&& node.nextElementSibling()?.let { dropPrecedingEmptyParagraphTags.contains(it.tagName()) } == true
) {
node.remove()
}
}
/**
* Expand `<shortcut raw|actionId="*"/>` tag into a sequence of `<kbd>` tags
*/
private fun transpileShortcut(node: Element) {
val actionId = node.attributes().getIgnoreCase("actionid")
.takeIf { it.isNotEmpty() }
val raw = node.attributes().getIgnoreCase("raw")
.takeIf { it.isNotEmpty() }
if (actionId != null || raw != null) {
val shortcutData =
if (actionId != null)
ShortcutsRenderingUtil.getShortcutByActionId(actionId)
?.let { ShortcutsRenderingUtil.getKeyboardShortcutData(it) }?.first
?: ShortcutsRenderingUtil.getGotoActionData(actionId, false)
.takeIf { ActionManager.getInstance().getAction(actionId) != null }
?.first
else
KeyStroke.getKeyStroke(raw)
?.let { ShortcutsRenderingUtil.getKeyStrokeData(it) }
?.first
if (shortcutData != null) {
val replacement = shortcutData
.splitToSequence(ShortcutsRenderingUtil.SHORTCUT_PART_SEPARATOR)
.fold(mutableListOf<Node>()) { acc, s ->
if (acc.isNotEmpty()) {
acc.add(TextNode(StringUtil.NON_BREAK_SPACE))
}
acc.add(Element("kbd").text(s))
acc
}
node.replaceWith(replacement)
}
else {
node.replaceWith(Element("kbd").text(actionId ?: raw!!))
}
}
}
/**
* Replace `<pre><code>(...)</code></pre>` with [JBHtmlPaneStyleSheetRulesProvider.buildCodeBlock]
*/
private fun transpilePre(node: Element) {
if (node.childNodeSize() != 1) return
val childNodes =
node.childNode(0)
.asSafely<Element>()
?.takeIf { it.nameIs("code") }
?.childNodes()
?: return
node.replaceWith(buildCodeBlock(childNodes))
}
/**
* Replace `<blockquote>\\s*<pre>(...)</pre>\\s*</blockquote>` with [JBHtmlPaneStyleSheetRulesProvider.buildCodeBlock]
*/
private fun transpileBlockquote(node: Element) {
if (node.childNodeSize() > 3 || node.childNodeSize() < 1) return
val nodes = node.childNodes()
val wsNode1 = nodes.getOrNull(0).asSafely<TextNode>()
val preNode = nodes.getOrNull(if (wsNode1 == null) 0 else 1).asSafely<Element>()
?: return
val wsNode2 = nodes.getOrNull(if (wsNode1 == null) 1 else 2)
if (wsNode1?.wholeText.isNullOrBlank()
&& preNode.nameIs("pre")
&& wsNode2.let { it == null || it is TextNode && it.wholeText.isBlank() }) {
val preNodes = preNode.childNodes()
preNodes.getOrNull(0)?.asSafely<TextNode>()?.let {
it.text(it.wholeText.trim('\n', '\r'))
}
preNodes.lastOrNull()?.asSafely<TextNode>()?.let {
it.text(it.wholeText.trimEnd())
}
node.replaceWith(buildCodeBlock(preNodes))
}
}
/**
* Move icon children to parent node
*/
private fun transpileIcon(node: Element) {
node.parent()
?.insertChildren(node.siblingIndex() + 1, node.childNodes())
}
/**
* - Add `<wbr>` after `.` if surrounded by letters
* - Add `<wbr>` after `]`, `)` or `/` followed by a char or digit
*/
private fun transpileTextNode(node: TextNode) {
val builder = StringBuilder()
val text = node.wholeText
val codePoints = text.codePoints().iterator()
if (!codePoints.hasNext()) return
var codePoint = codePoints.nextInt()
fun next() {
builder.appendCodePoint(codePoint)
codePoint = if (codePoints.hasNext())
codePoints.nextInt()
else
-1
}
val replacement = SmartList<Node>()
while (codePoint >= 0) {
when {
// break after dot if surrounded by letters
Character.isLetter(codePoint) -> {
next()
if (codePoint == '.'.code) {
next()
if (Character.isLetter(codePoint)) {
replacement.add(TextNode(builder.toString()))
replacement.add(Element("wbr"))
builder.clear()
}
}
}
// break after ], ) or / followed by a char or digit
codePoint == ')'.code || codePoint == ']'.code || codePoint == '/'.code -> {
next()
if (Character.isLetterOrDigit(codePoint)) {
replacement.add(TextNode(builder.toString()))
replacement.add(Element("wbr"))
builder.clear()
}
}
else -> next()
}
}
if (!replacement.isEmpty()) {
replacement.add(TextNode(builder.toString()))
node.replaceWith(replacement)
}
}
private fun Node.replaceWith(nodes: List<Node>) {
val parent = parent() as? Element ?: return
parent.insertChildren(siblingIndex(), nodes)
remove()
}
}

View File

@@ -1,5 +1,5 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ui.components
package com.intellij.ui.components.impl
import com.github.benmanes.caffeine.cache.Caffeine
import com.github.benmanes.caffeine.cache.LoadingCache
@@ -11,8 +11,9 @@ import com.intellij.openapi.editor.impl.EditorCssFontResolver.EDITOR_FONT_NAME_P
import com.intellij.openapi.editor.markup.EffectType
import com.intellij.ui.ColorUtil
import com.intellij.ui.Gray
import com.intellij.ui.components.JBHtmlPaneStyleConfiguration.ControlKind
import com.intellij.ui.components.JBHtmlPaneStyleConfiguration.ControlProperty
import com.intellij.ui.components.JBHtmlPaneStyleConfiguration
import com.intellij.ui.components.JBHtmlPaneStyleConfiguration.ElementKind
import com.intellij.ui.components.JBHtmlPaneStyleConfiguration.ElementProperty
import com.intellij.ui.scale.JBUIScale.scale
import com.intellij.util.containers.addAllIfNotNull
import com.intellij.util.ui.StartupUiUtil
@@ -46,7 +47,7 @@ internal object JBHtmlPaneStyleSheetRulesProvider {
)
private val inlineCodeStyling = ControlColorStyleBuilder(
ControlKind.CodeInline,
ElementKind.CodeInline,
defaultBackgroundColor = Color(0x5A5D6B),
defaultBackgroundOpacity = 10,
defaultBorderRadius = 10,
@@ -54,7 +55,7 @@ internal object JBHtmlPaneStyleSheetRulesProvider {
)
private val blockCodeStyling = ControlColorStyleBuilder(
ControlKind.CodeBlock,
ElementKind.CodeBlock,
defaultBorderColor = Color(0xEBECF0),
defaultBorderRadius = 10,
defaultBorderWidth = 1,
@@ -63,7 +64,7 @@ internal object JBHtmlPaneStyleSheetRulesProvider {
)
private val shortcutStyling = ControlColorStyleBuilder(
ControlKind.Shortcut,
ElementKind.Shortcut,
defaultBorderColor = Color(0xA8ADBD),
defaultBorderRadius = 7,
defaultBorderWidth = 1,
@@ -199,7 +200,7 @@ internal object JBHtmlPaneStyleSheetRulesProvider {
toHexString(color.rgb and 0xFFFFFF)
private data class ControlColorStyleBuilder(
val controlKind: ControlKind,
val elementKind: ElementKind,
val defaultBackgroundColor: Color? = null,
val defaultBackgroundOpacity: Int = 100,
val defaultForegroundColor: Color? = null,
@@ -211,23 +212,23 @@ internal object JBHtmlPaneStyleSheetRulesProvider {
val fallbackToEditorBorder: Boolean = false
) {
private fun getBackgroundColor(configuration: JBHtmlPaneStyleConfiguration): Color? = getColor(configuration, ControlProperty.BackgroundColor)
private fun getBackgroundColor(configuration: JBHtmlPaneStyleConfiguration): Color? = getColor(configuration, ElementProperty.BackgroundColor)
private fun getForegroundColor(configuration: JBHtmlPaneStyleConfiguration): Color? = getColor(configuration, ControlProperty.ForegroundColor)
private fun getForegroundColor(configuration: JBHtmlPaneStyleConfiguration): Color? = getColor(configuration, ElementProperty.ForegroundColor)
private fun getBorderColor(configuration: JBHtmlPaneStyleConfiguration): Color? = getColor(configuration, ControlProperty.BorderColor)
private fun getBorderColor(configuration: JBHtmlPaneStyleConfiguration): Color? = getColor(configuration, ElementProperty.BorderColor)
private fun getBackgroundOpacity(configuration: JBHtmlPaneStyleConfiguration): Int? = getInt(configuration, ControlProperty.BackgroundOpacity)
private fun getBackgroundOpacity(configuration: JBHtmlPaneStyleConfiguration): Int? = getInt(configuration, ElementProperty.BackgroundOpacity)
private fun getBorderWidth(configuration: JBHtmlPaneStyleConfiguration): Int? = getInt(configuration, ControlProperty.BorderWidth)
private fun getBorderWidth(configuration: JBHtmlPaneStyleConfiguration): Int? = getInt(configuration, ElementProperty.BorderWidth)
private fun getBorderRadius(configuration: JBHtmlPaneStyleConfiguration): Int? = getInt(configuration, ControlProperty.BorderRadius)
private fun getBorderRadius(configuration: JBHtmlPaneStyleConfiguration): Int? = getInt(configuration, ElementProperty.BorderRadius)
fun getCssStyle(editorPaneBackgroundColor: Color, configuration: JBHtmlPaneStyleConfiguration): String {
val result = StringBuilder()
if (configuration.editorInlineContext) {
val attributes = configuration.colorScheme.getAttributes(controlKind.colorSchemeKey, false)
val attributes = configuration.colorScheme.getAttributes(elementKind.colorSchemeKey, false)
if (attributes != null) {
attributes.backgroundColor?.let { result.append("background-color: #${toHtmlColor(it)};") }
attributes.foregroundColor?.let { result.append("color: #${toHtmlColor(it)};") }
@@ -300,19 +301,19 @@ internal object JBHtmlPaneStyleSheetRulesProvider {
)
}
private fun getColor(configuration: JBHtmlPaneStyleConfiguration, property: ControlProperty): Color? =
private fun getColor(configuration: JBHtmlPaneStyleConfiguration, property: ElementProperty): Color? =
UIManager.getColor(getKey(configuration, property))
private fun getInt(configuration: JBHtmlPaneStyleConfiguration, property: ControlProperty): Int? =
private fun getInt(configuration: JBHtmlPaneStyleConfiguration, property: ElementProperty): Int? =
UIManager.get(getKey(configuration, property)) as Int?
private fun getKey(configuration: JBHtmlPaneStyleConfiguration, property: ControlProperty): String {
val themeOverrides = configuration.controlStyleOverrides
val suffix = if (themeOverrides != null && themeOverrides.overrides[controlKind]?.contains(property) == true) {
"." + themeOverrides.controlKindSuffix
private fun getKey(configuration: JBHtmlPaneStyleConfiguration, property: ElementProperty): String {
val themeOverrides = configuration.elementStyleOverrides
val suffix = if (themeOverrides != null && themeOverrides.overrides[elementKind]?.contains(property) == true) {
"." + themeOverrides.elementKindThemePropertySuffix
}
else ""
return "${controlKind.id}$suffix.${property.id}"
return "${elementKind.id}$suffix.${property.id}"
}
}

View File

@@ -286,6 +286,8 @@
serviceImplementation="com.intellij.ui.TreeUIHelperImpl"/>
<applicationService serviceInterface="com.intellij.ui.ExpandableItemsHandlerFactory"
serviceImplementation="com.intellij.ui.ExpandableItemsHandlerFactoryImpl"/>
<applicationService serviceInterface="com.intellij.ui.components.JBHtmlPane$ImplService"
serviceImplementation="com.intellij.ui.components.impl.JBHtmlPaneImplService"/>
<applicationService
serviceInterface="com.intellij.openapi.ui.messages.MessagesService"
serviceImplementation="com.intellij.ui.messages.MessagesServiceImpl"/>