11package 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
36import com.intellij.util.ui.JBUI
47import com.intellij.util.ui.UIUtil
58import org.commonmark.ext.autolink.AutolinkExtension
@@ -11,22 +14,27 @@ import java.awt.Color
1114import java.awt.Font
1215import java.awt.Point
1316import javax.swing.JComponent
14- import javax.swing.JEditorPane
1517import 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" )
2430abstract 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