Skip to content

Commit da7b600

Browse files
committed
refactor(jetbrains): use JBHtmlPane with editor-aware styling in MdView
- Replace plain JEditorPane + manual CSS with JBHtmlPane, the IntelliJ platform's flagship HTML component - Font and colours now default to the global editor colour scheme via JBHtmlPane's built-in EditorCssFontResolver and colorSchemeProvider - Code font defaults to _EditorFontNoLigatures_ placeholder resolved by EditorCssFontResolver at render time - All style properties become optional overrides applied via a customStyleSheetProvider; the override sheet is empty until a property is set, so editor defaults always win unless explicitly overridden - Add resetStyles() to revert all overrides back to editor defaults - Transparency (opaque=false) handled by Swing isOpaque + transparent CSS body rule; no manual background injection when transparent - Update tests to use overrideSheet() and component state assertions
1 parent a385e6a commit da7b600

2 files changed

Lines changed: 290 additions & 188 deletions

File tree

  • packages/kilo-jetbrains/frontend/src
Lines changed: 149 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package ai.kilocode.client.ui.md
22

3+
import com.intellij.ui.components.JBHtmlPane
4+
import com.intellij.ui.components.JBHtmlPaneConfiguration
5+
import com.intellij.ui.components.JBHtmlPaneStyleConfiguration
36
import com.intellij.util.ui.JBUI
47
import com.intellij.util.ui.UIUtil
58
import org.commonmark.ext.autolink.AutolinkExtension
@@ -11,22 +14,27 @@ import java.awt.Color
1114
import java.awt.Font
1215
import java.awt.Point
1316
import javax.swing.JComponent
14-
import javax.swing.JEditorPane
1517
import javax.swing.event.HyperlinkEvent
16-
import javax.swing.text.html.HTMLEditorKit
18+
import javax.swing.text.html.StyleSheet
1719

1820
/**
19-
* Markdown rendering component that hides the concrete rendering strategy
20-
* behind a uniform API. Create instances via [MdView.html].
21+
* Markdown rendering component backed by [JBHtmlPane] with editor-aware styling.
2122
*
22-
* All public methods must be called on the EDT.
23+
* By default, font and colors are derived from the global editor colour scheme.
24+
* All style properties are optional overrides on top of those defaults.
25+
* Call [resetStyles] to revert to editor defaults after overriding.
26+
*
27+
* Create instances via [MdView.html]. All public methods must be called on the EDT.
2328
*/
29+
@Suppress("UnstableApiUsage")
2430
abstract class MdView private constructor() {
2531

2632
abstract val component: JComponent
2733
abstract fun set(text: String)
2834
abstract fun append(delta: String)
2935
abstract fun clear()
36+
/** Revert all style overrides to editor-derived defaults. */
37+
abstract fun resetStyles()
3038
abstract fun addLinkListener(listener: LinkListener)
3139
abstract fun removeLinkListener(listener: LinkListener)
3240

@@ -43,8 +51,8 @@ abstract class MdView private constructor() {
4351
abstract var tableBorder: Color
4452

4553
/**
46-
* When `false`, body background CSS is omitted and the Swing component
47-
* is set to non-opaque so the parent's background shows through.
54+
* When `false`, the component is transparent — the parent's background shows through
55+
* and no background is forced in the CSS body rule.
4856
*/
4957
abstract var opaque: Boolean
5058

@@ -59,103 +67,155 @@ abstract class MdView private constructor() {
5967

6068
internal abstract fun markdown(): String
6169
internal abstract fun html(): String
62-
internal abstract fun styledHtml(): String
70+
/** Returns the current CSS override rules applied on top of JBHtmlPane's default stylesheet. */
71+
internal abstract fun overrideSheet(): String
6372
internal abstract fun simulateLink(href: String)
6473

6574
companion object {
6675
fun html(): MdView = HtmlImpl()
6776
}
6877

78+
@Suppress("UnstableApiUsage")
6979
private class HtmlImpl : MdView() {
7080

7181
private val listeners = mutableListOf<LinkListener>()
7282
private val source = StringBuilder()
7383
private var rendered = ""
74-
private var wrapped = ""
75-
private var dirty = false
7684

7785
private val extensions = listOf(
7886
AutolinkExtension.create(),
7987
TablesExtension.create(),
8088
StrikethroughExtension.create(),
8189
)
8290

83-
private val parser: Parser = Parser.builder()
84-
.extensions(extensions)
85-
.build()
91+
private val parser: Parser = Parser.builder().extensions(extensions).build()
8692

8793
private val renderer: HtmlRenderer = HtmlRenderer.builder()
8894
.extensions(extensions)
8995
.escapeHtml(true)
9096
.sanitizeUrls(true)
9197
.build()
9298

93-
private val pane: JEditorPane = JEditorPane().apply {
99+
// nullable overrides — null means "use JBHtmlPane / editor default"
100+
private var fontOverride: Font? = null
101+
private var foregroundOverride: Color? = null
102+
private var backgroundOverride: Color? = null
103+
private var linkColorOverride: Color? = null
104+
private var codeBgOverride: Color? = null
105+
private var preBgOverride: Color? = null
106+
private var preFgOverride: Color? = null
107+
private var codeFontOverride: String? = null
108+
private var quoteBorderOverride: Color? = null
109+
private var quoteFgOverride: Color? = null
110+
private var tableBorderOverride: Color? = null
111+
private var opaqueState = true
112+
113+
private val pane: JBHtmlPane = JBHtmlPane(
114+
JBHtmlPaneStyleConfiguration {
115+
// colorSchemeProvider defaults to EditorColorsManager.getInstance().globalScheme
116+
enableInlineCodeBackground = true
117+
enableCodeBlocksBackground = true
118+
},
119+
JBHtmlPaneConfiguration {
120+
// fontResolver defaults to EditorCssFontResolver.getGlobalInstance() via JBHtmlPane's ImplService
121+
customStyleSheetProvider { buildOverrideStyleSheet() }
122+
}
123+
).apply {
94124
isEditable = false
95-
contentType = "text/html"
96-
editorKit = HTMLEditorKit()
97-
putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, true)
98-
border = JBUI.Borders.empty()
125+
isOpaque = true
126+
background = UIUtil.getPanelBackground()
99127

100128
addHyperlinkListener { e ->
101129
if (e.eventType == HyperlinkEvent.EventType.ACTIVATED) {
102130
val href = e.description ?: return@addHyperlinkListener
103131
val pt = (e.inputEvent as? java.awt.event.MouseEvent)?.point
104132
val event = LinkEvent(href, pt)
105-
for (l in listeners) {
106-
l.onLink(event)
107-
}
133+
for (l in listeners) l.onLink(event)
108134
}
109135
}
110136
}
111137

112-
override var font: Font = JBUI.Fonts.label()
113-
set(value) { field = value; markDirty() }
138+
override val component: JComponent get() = pane
139+
140+
// -- style properties (non-null API backed by nullable overrides) ----
141+
142+
override var font: Font
143+
get() = fontOverride ?: JBUI.Fonts.label()
144+
set(value) { fontOverride = value; markDirty() }
114145

115-
override var foreground: Color = UIUtil.getLabelForeground()
116-
set(value) { field = value; pane.foreground = value; markDirty() }
146+
override var foreground: Color
147+
get() = foregroundOverride ?: UIUtil.getLabelForeground()
148+
set(value) { foregroundOverride = value; markDirty() }
117149

118-
override var background: Color = UIUtil.getPanelBackground()
119-
set(value) { field = value; pane.background = value; markDirty() }
150+
override var background: Color
151+
get() = backgroundOverride ?: pane.background
152+
set(value) {
153+
backgroundOverride = value
154+
if (opaqueState) pane.background = value
155+
markDirty()
156+
}
120157

121-
override var linkColor: Color = Color(0x58, 0x9D, 0xF6)
122-
set(value) { field = value; markDirty() }
158+
override var linkColor: Color
159+
get() = linkColorOverride ?: Color(0x58, 0x9D, 0xF6)
160+
set(value) { linkColorOverride = value; markDirty() }
123161

124-
override var codeBg: Color = Color(0x3C, 0x3F, 0x41)
125-
set(value) { field = value; markDirty() }
162+
override var codeBg: Color
163+
get() = codeBgOverride ?: Color(0x3C, 0x3F, 0x41)
164+
set(value) { codeBgOverride = value; markDirty() }
126165

127-
override var preBg: Color = Color(0x2B, 0x2B, 0x2B)
128-
set(value) { field = value; markDirty() }
166+
override var preBg: Color
167+
get() = preBgOverride ?: Color(0x2B, 0x2B, 0x2B)
168+
set(value) { preBgOverride = value; markDirty() }
129169

130-
override var preFg: Color = Color(0xA9, 0xB7, 0xC6)
131-
set(value) { field = value; markDirty() }
170+
override var preFg: Color
171+
get() = preFgOverride ?: Color(0xA9, 0xB7, 0xC6)
172+
set(value) { preFgOverride = value; markDirty() }
132173

133-
override var codeFont: String = "JetBrains Mono"
134-
set(value) { field = value; markDirty() }
174+
override var codeFont: String
175+
// _EditorFontNoLigatures_ is resolved by EditorCssFontResolver to the global editor font
176+
get() = codeFontOverride ?: "_EditorFontNoLigatures_"
177+
set(value) { codeFontOverride = value; markDirty() }
135178

136-
override var quoteBorder: Color = Color(0x55, 0x55, 0x55)
137-
set(value) { field = value; markDirty() }
179+
override var quoteBorder: Color
180+
get() = quoteBorderOverride ?: Color(0x55, 0x55, 0x55)
181+
set(value) { quoteBorderOverride = value; markDirty() }
138182

139-
override var quoteFg: Color = Color(0x99, 0x99, 0x99)
140-
set(value) { field = value; markDirty() }
183+
override var quoteFg: Color
184+
get() = quoteFgOverride ?: Color(0x99, 0x99, 0x99)
185+
set(value) { quoteFgOverride = value; markDirty() }
141186

142-
override var tableBorder: Color = Color(0x55, 0x55, 0x55)
143-
set(value) { field = value; markDirty() }
187+
override var tableBorder: Color
188+
get() = tableBorderOverride ?: Color(0x55, 0x55, 0x55)
189+
set(value) { tableBorderOverride = value; markDirty() }
144190

145-
override var opaque: Boolean = true
191+
override var opaque: Boolean
192+
get() = opaqueState
146193
set(value) {
147-
field = value
194+
opaqueState = value
148195
pane.isOpaque = value
196+
if (value) pane.background = backgroundOverride ?: UIUtil.getPanelBackground()
149197
markDirty()
150198
}
151199

152-
init {
153-
pane.font = font
154-
pane.foreground = foreground
155-
pane.background = background
200+
override fun resetStyles() {
201+
fontOverride = null
202+
foregroundOverride = null
203+
backgroundOverride = null
204+
linkColorOverride = null
205+
codeBgOverride = null
206+
preBgOverride = null
207+
preFgOverride = null
208+
codeFontOverride = null
209+
quoteBorderOverride = null
210+
quoteFgOverride = null
211+
tableBorderOverride = null
212+
opaqueState = true
213+
pane.isOpaque = true
214+
pane.background = UIUtil.getPanelBackground()
215+
markDirty()
156216
}
157217

158-
override val component: JComponent get() = pane
218+
// -- content API ---------------------------------------------------
159219

160220
override fun set(text: String) {
161221
source.clear()
@@ -171,73 +231,68 @@ abstract class MdView private constructor() {
171231
override fun clear() {
172232
source.clear()
173233
rendered = ""
174-
wrapped = ""
175234
pane.text = ""
176235
}
177236

178-
override fun addLinkListener(listener: LinkListener) {
179-
listeners.add(listener)
180-
}
181-
182-
override fun removeLinkListener(listener: LinkListener) {
183-
listeners.remove(listener)
184-
}
237+
override fun addLinkListener(listener: LinkListener) { listeners.add(listener) }
238+
override fun removeLinkListener(listener: LinkListener) { listeners.remove(listener) }
185239

186240
override fun markdown(): String = source.toString()
187241
override fun html(): String = rendered
188-
override fun styledHtml(): String = wrapped
242+
override fun overrideSheet(): String = buildOverrideRulesString()
189243

190244
override fun simulateLink(href: String) {
191245
val event = LinkEvent(href)
192-
for (l in listeners) {
193-
l.onLink(event)
194-
}
246+
for (l in listeners) l.onLink(event)
195247
}
196248

197249
private fun markDirty() {
198-
dirty = true
250+
pane.reloadCssStylesheets()
199251
if (source.isNotEmpty()) render()
200252
}
201253

202254
private fun render() {
203-
dirty = false
204-
val md = source.toString()
205-
val body = renderer.render(parser.parse(md))
255+
val body = renderer.render(parser.parse(source.toString()))
206256
rendered = body
207-
wrapped = wrap(body)
208-
pane.text = wrapped
257+
pane.text = "<html><body>$body</body></html>"
209258
pane.caretPosition = 0
210259
}
211260

212-
private fun wrap(body: String): String {
213-
val fg = hex(foreground)
214-
val size = font.size
215-
val family = font.family
216-
val codeSize = (size - 1).coerceAtLeast(1)
217-
val bgCss = if (opaque) "background: ${hex(background)}; " else ""
218-
return """
219-
<html>
220-
<head><style>
221-
body { font-family: '$family', sans-serif; font-size: ${size}pt;
222-
color: $fg; ${bgCss}margin: 0; padding: 0; }
223-
a { color: ${hex(linkColor)}; }
224-
pre { background: ${hex(preBg)}; color: ${hex(preFg)}; padding: 8px;
225-
border-radius: 4px; overflow-x: auto; }
226-
code { font-family: '$codeFont', monospace; font-size: ${codeSize}pt; }
227-
p code { background: ${hex(codeBg)}; padding: 1px 4px; border-radius: 3px; }
228-
table { border-collapse: collapse; }
229-
th, td { border: 1px solid ${hex(tableBorder)}; padding: 4px 8px; }
230-
blockquote { border-left: 3px solid ${hex(quoteBorder)}; margin-left: 0;
231-
padding-left: 8px; color: ${hex(quoteFg)}; }
232-
</style></head>
233-
<body>$body</body>
234-
</html>
235-
""".trimIndent()
261+
private fun buildOverrideStyleSheet(): StyleSheet {
262+
val sheet = StyleSheet()
263+
val rules = buildOverrideRulesString()
264+
if (rules.isNotEmpty()) {
265+
try { sheet.addRule(rules) } catch (_: Exception) {}
266+
}
267+
return sheet
268+
}
269+
270+
private fun buildOverrideRulesString(): String {
271+
val rules = StringBuilder()
272+
273+
val body = mutableListOf<String>()
274+
foregroundOverride?.let { body.add("color: ${hex(it)}") }
275+
if (!opaqueState) body.add("background: transparent")
276+
fontOverride?.let {
277+
body.add("font-family: '${it.family}', sans-serif")
278+
body.add("font-size: ${it.size}pt")
279+
}
280+
if (body.isNotEmpty()) rules.append("body { ${body.joinToString("; ")} } ")
281+
282+
linkColorOverride?.let { rules.append("a { color: ${hex(it)} } ") }
283+
codeFontOverride?.let { rules.append("tt, code, samp, pre { font-family: '${it}', monospace } ") }
284+
preBgOverride?.let { rules.append("pre { background: ${hex(it)} } ") }
285+
preFgOverride?.let { rules.append("pre { color: ${hex(it)} } ") }
286+
codeBgOverride?.let { rules.append("code { background: ${hex(it)} } ") }
287+
quoteBorderOverride?.let { rules.append("blockquote { border-left-color: ${hex(it)} } ") }
288+
quoteFgOverride?.let { rules.append("blockquote { color: ${hex(it)} } ") }
289+
tableBorderOverride?.let { rules.append("th, td { border-color: ${hex(it)} } ") }
290+
291+
return rules.toString().trim()
236292
}
237293

238294
companion object {
239-
private fun hex(c: Color): String =
240-
String.format("#%02x%02x%02x", c.red, c.green, c.blue)
295+
private fun hex(c: Color): String = String.format("#%02x%02x%02x", c.red, c.green, c.blue)
241296
}
242297
}
243298
}

0 commit comments

Comments
 (0)