fix(jetbrains): dispose embedded markdown editors

This commit is contained in:
kirillk
2026-06-01 18:29:22 -04:00
parent 746b33e0ed
commit bee54755ff
12 changed files with 209 additions and 10 deletions
@@ -145,6 +145,7 @@ class SessionUi(
private var style = SessionEditorStyle.current()
private var editorTheme = style.editorScheme
private var colorTheme = UIManager.getLookAndFeel()
private var disposed = false
init {
buildUi()
@@ -158,12 +159,14 @@ class SessionUi(
}
override fun addNotify() {
if (disposed) return
super.addNotify()
resumeOpen()
}
override fun doLayout() {
super.doLayout()
if (disposed) return
resumeOpen()
}
@@ -499,6 +502,7 @@ class SessionUi(
}
private fun onStateChanged(state: SessionState) {
if (disposed) return
prompt.setBusy(state.isBusy())
load.setState(state)
scroll.setQuestionPending(questionPending(state))
@@ -512,6 +516,7 @@ class SessionUi(
}
private fun refresh() {
if (disposed) return
scroll.refresh()
root.revalidate()
root.repaint()
@@ -549,7 +554,13 @@ class SessionUi(
)
}
override fun dispose() {}
override fun dispose() {
disposed = true
modalFocus = null
empty = null
if (this::root.isInitialized) root.setModalContent(null)
removeAll()
}
}
private fun variantTitle(value: String): String = value.replaceFirstChar { it.titlecase() }
@@ -13,6 +13,7 @@ import ai.kilocode.client.session.views.permission.PermissionView
import ai.kilocode.client.session.views.question.QuestionView
import ai.kilocode.client.session.views.TurnView
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import com.intellij.util.ui.JBUI
/**
@@ -56,7 +57,7 @@ class SessionMessageListPanel(
SessionUiStyle.SessionLayout.TRANSCRIPT_PADDING,
SessionUiStyle.SessionLayout.TRANSCRIPT_PADDING,
),
), SessionEditorStyleTarget {
), Disposable, SessionEditorStyleTarget {
private val turnViews = LinkedHashMap<String, TurnView>()
private val msgToTurn = HashMap<String, TurnView>()
@@ -69,6 +70,7 @@ class SessionMessageListPanel(
init {
isOpaque = false
Disposer.register(parent, this)
model.addListener(parent) { event ->
when (event) {
@@ -215,11 +217,16 @@ class SessionMessageListPanel(
val tv = turnViews.remove(id) ?: return
for (msgId in tv.messageIds()) unregister(msgId)
remove(tv)
Disposer.dispose(tv)
anchorFooter()
refresh()
}
private fun rebuild() {
turnViews.values.forEach {
remove(it)
Disposer.dispose(it)
}
turnViews.clear()
msgToTurn.clear()
msgToView.clear()
@@ -242,6 +249,10 @@ class SessionMessageListPanel(
}
private fun clear() {
turnViews.values.forEach {
remove(it)
Disposer.dispose(it)
}
turnViews.clear()
msgToTurn.clear()
msgToView.clear()
@@ -336,4 +347,15 @@ class SessionMessageListPanel(
progress.applyStyle(style)
refresh()
}
override fun dispose() {
turnViews.values.forEach {
remove(it)
Disposer.dispose(it)
}
turnViews.clear()
msgToTurn.clear()
msgToView.clear()
removeAll()
}
}
@@ -11,6 +11,8 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
import ai.kilocode.client.session.views.base.PartView
import ai.kilocode.client.session.ui.style.SessionUiStyle
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import com.intellij.util.ui.JBUI
import java.awt.Graphics
import java.awt.Graphics2D
@@ -34,7 +36,7 @@ class MessageView(
private val openUrl: (String) -> Unit = {},
) : ai.kilocode.client.session.ui.SessionLayoutPanel(
JBUI.scale(SessionUiStyle.SessionLayout.GAP),
), SessionEditorStyleTarget, SessionView {
), Disposable, SessionEditorStyleTarget, SessionView {
constructor(msg: Message, openFile: (String) -> Unit) : this(msg, openFile, SessionEditorStyle.current())
@@ -84,6 +86,7 @@ class MessageView(
val stale = parts.remove(content.id)
if (stale != null) {
remove(stale)
Disposer.dispose(stale)
syncBorder()
refresh()
}
@@ -111,6 +114,7 @@ class MessageView(
val at = components.indexOfFirst { it === existing }.takeIf { it >= 0 } ?: componentCount
parts.remove(content.id)
remove(existing)
Disposer.dispose(existing)
val view = view(content)
view.applyStyle(style)
parts[content.id] = view
@@ -123,6 +127,7 @@ class MessageView(
fun removePart(contentId: String) {
val view = parts.remove(contentId) ?: return
remove(view)
Disposer.dispose(view)
syncBorder()
refresh()
}
@@ -146,7 +151,10 @@ class MessageView(
* Called only when the hidden ref changes to avoid unnecessary rebuilds.
*/
private fun rebuildParts() {
parts.values.forEach { remove(it) }
parts.values.forEach {
remove(it)
Disposer.dispose(it)
}
parts.clear()
for ((_, content) in msg.parts) {
if (content is StepFinish) continue
@@ -194,6 +202,15 @@ class MessageView(
refresh()
}
override fun dispose() {
parts.values.forEach {
remove(it)
Disposer.dispose(it)
}
parts.clear()
hidden = null
}
override fun paintComponent(g: Graphics) {
if (msg.info.role != SessionUiStyle.View.Message.USER_ROLE) {
super.paintComponent(g)
@@ -7,6 +7,7 @@ import ai.kilocode.client.session.model.ToolExecState
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.views.base.PartView
import ai.kilocode.client.ui.md.MdViewFactory
import com.intellij.openapi.util.Disposer
import java.awt.BorderLayout
class PlanExitView(tool: Tool, openFile: (String) -> Unit) : PartView() {
@@ -22,6 +23,7 @@ class PlanExitView(tool: Tool, openFile: (String) -> Unit) : PartView() {
init {
layout = BorderLayout()
isOpaque = false
Disposer.register(this, md)
md.addLinkListener { openFile(it.href) }
add(md.component, BorderLayout.CENTER)
applyStyle(SessionEditorStyle.current())
@@ -12,6 +12,7 @@ import ai.kilocode.client.ui.UiStyle
import ai.kilocode.client.ui.md.MdView
import ai.kilocode.client.ui.md.MdViewFactory
import com.intellij.icons.AllIcons
import com.intellij.openapi.util.Disposer
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBScrollPane
import com.intellij.util.ui.JBUI
@@ -39,6 +40,7 @@ class ReasoningView(
private var source = reasoning.content.toString()
init {
Disposer.register(this, md)
bindHeader(parts.title, parts.icon)
applyStyle(style)
md.opaque = false
@@ -6,6 +6,7 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.views.base.PartView
import ai.kilocode.client.ui.md.MdView
import ai.kilocode.client.ui.md.MdViewFactory
import com.intellij.openapi.util.Disposer
import java.awt.BorderLayout
/**
@@ -26,6 +27,7 @@ class TextView(
init {
layout = BorderLayout()
isOpaque = false
Disposer.register(this, md)
md.opaque = !transparent
md.addLinkListener { openUrl(it.href) }
applyStyle(SessionEditorStyle.current())
@@ -5,6 +5,8 @@ import ai.kilocode.client.session.ui.SessionLayoutPanel
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
import ai.kilocode.client.session.ui.style.SessionUiStyle
import com.intellij.openapi.Disposable
import com.intellij.openapi.util.Disposer
import com.intellij.util.ui.JBUI
/**
@@ -21,7 +23,7 @@ class TurnView(
private val openFile: (String) -> Unit,
private var style: SessionEditorStyle = SessionEditorStyle.current(),
private val openUrl: (String) -> Unit = {},
) : SessionLayoutPanel(JBUI.scale(SessionUiStyle.SessionLayout.GAP)), SessionEditorStyleTarget {
) : SessionLayoutPanel(JBUI.scale(SessionUiStyle.SessionLayout.GAP)), Disposable, SessionEditorStyleTarget {
constructor(id: String, openFile: (String) -> Unit) : this(id, openFile, SessionEditorStyle.current())
@@ -44,6 +46,7 @@ class TurnView(
fun removeMessage(msgId: String) {
val view = messages.remove(msgId) ?: return
remove(view)
Disposer.dispose(view)
revalidate()
}
@@ -62,4 +65,12 @@ class TurnView(
revalidate()
repaint()
}
override fun dispose() {
messages.values.forEach {
remove(it)
Disposer.dispose(it)
}
messages.clear()
}
}
@@ -3,6 +3,7 @@ package ai.kilocode.client.session.views.base
import ai.kilocode.client.session.model.Content
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
import com.intellij.openapi.Disposable
import javax.swing.JPanel
/**
@@ -14,7 +15,7 @@ import javax.swing.JPanel
*
* All methods must be called on the EDT.
*/
abstract class PartView : JPanel(), SessionEditorStyleTarget {
abstract class PartView : JPanel(), Disposable, SessionEditorStyleTarget {
/** Stable [Content.id] this renderer was created for. */
abstract val contentId: String
@@ -34,6 +35,8 @@ abstract class PartView : JPanel(), SessionEditorStyleTarget {
override fun applyStyle(style: SessionEditorStyle) {}
override fun dispose() {}
/** Readable name for test dumps. */
open fun dumpLabel(): String = javaClass.simpleName
}
@@ -1,13 +1,14 @@
package ai.kilocode.client.ui.md
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import com.intellij.openapi.Disposable
import java.awt.Color
import java.awt.Font
import java.awt.Point
import javax.swing.JComponent
/** Markdown rendering component. All public methods must be called on the EDT. */
interface MdView {
interface MdView : Disposable {
val component: JComponent
fun set(text: String)
@@ -28,6 +28,7 @@ internal class MdViewHtmlPane(
private val source = StringBuilder()
private var rendered = ""
private var style = style
private var disposed = false
private val extensions = listOf(
AutolinkExtension.create(),
@@ -82,6 +83,7 @@ internal class MdViewHtmlPane(
override var font: Font
get() = fontOverride ?: opts().font
set(value) {
if (disposed) return
if (fontOverride == value) return
fontOverride = value
markDirty()
@@ -90,6 +92,7 @@ internal class MdViewHtmlPane(
override var foreground: Color
get() = foregroundOverride ?: opts().foreground
set(value) {
if (disposed) return
if (foregroundOverride == value) return
foregroundOverride = value
markDirty()
@@ -98,6 +101,7 @@ internal class MdViewHtmlPane(
override var background: Color
get() = backgroundOverride ?: opts().background
set(value) {
if (disposed) return
if (backgroundOverride == value) return
backgroundOverride = value
if (opaqueState) pane.background = value
@@ -107,6 +111,7 @@ internal class MdViewHtmlPane(
override var linkColor: Color
get() = linkColorOverride ?: opts().linkColor
set(value) {
if (disposed) return
if (linkColorOverride == value) return
linkColorOverride = value
markDirty()
@@ -115,6 +120,7 @@ internal class MdViewHtmlPane(
override var codeBg: Color
get() = codeBgOverride ?: opts().codeBg
set(value) {
if (disposed) return
if (codeBgOverride == value) return
codeBgOverride = value
markDirty()
@@ -123,6 +129,7 @@ internal class MdViewHtmlPane(
override var preBg: Color
get() = preBgOverride ?: opts().preBg
set(value) {
if (disposed) return
if (preBgOverride == value) return
preBgOverride = value
markDirty()
@@ -131,6 +138,7 @@ internal class MdViewHtmlPane(
override var preFg: Color
get() = preFgOverride ?: opts().preFg
set(value) {
if (disposed) return
if (preFgOverride == value) return
preFgOverride = value
markDirty()
@@ -139,6 +147,7 @@ internal class MdViewHtmlPane(
override var codeFont: String
get() = codeFontOverride ?: opts().codeFont
set(value) {
if (disposed) return
if (codeFontOverride == value) return
codeFontOverride = value
markDirty()
@@ -147,6 +156,7 @@ internal class MdViewHtmlPane(
override var quoteBorder: Color
get() = quoteBorderOverride ?: opts().quoteBorder
set(value) {
if (disposed) return
if (quoteBorderOverride == value) return
quoteBorderOverride = value
markDirty()
@@ -155,6 +165,7 @@ internal class MdViewHtmlPane(
override var quoteFg: Color
get() = quoteFgOverride ?: opts().quoteFg
set(value) {
if (disposed) return
if (quoteFgOverride == value) return
quoteFgOverride = value
markDirty()
@@ -163,6 +174,7 @@ internal class MdViewHtmlPane(
override var tableBorder: Color
get() = tableBorderOverride ?: opts().tableBorder
set(value) {
if (disposed) return
if (tableBorderOverride == value) return
tableBorderOverride = value
markDirty()
@@ -171,6 +183,7 @@ internal class MdViewHtmlPane(
override var opaque: Boolean
get() = opaqueState
set(value) {
if (disposed) return
if (opaqueState == value) return
opaqueState = value
pane.isOpaque = value
@@ -179,6 +192,7 @@ internal class MdViewHtmlPane(
}
override fun applyStyle(style: SessionEditorStyle) {
if (disposed) return
if (this.style == style) return
this.style = style
if (opaqueState) pane.background = background
@@ -186,6 +200,7 @@ internal class MdViewHtmlPane(
}
override fun resetStyles() {
if (disposed) return
fontOverride = null
foregroundOverride = null
backgroundOverride = null
@@ -204,6 +219,7 @@ internal class MdViewHtmlPane(
}
override fun set(text: String) {
if (disposed) return
if (source.toString() == text) return
source.clear()
source.append(text)
@@ -211,12 +227,14 @@ internal class MdViewHtmlPane(
}
override fun append(delta: String) {
if (disposed) return
if (delta.isEmpty()) return
source.append(delta)
syncHtml()
}
override fun clear() {
if (disposed) return
if (source.isEmpty() && rendered.isEmpty() && pane.text.isEmpty()) return
source.clear()
rendered = ""
@@ -224,6 +242,7 @@ internal class MdViewHtmlPane(
}
override fun addLinkListener(listener: MdView.LinkListener) {
if (disposed) return
listeners.add(listener)
}
@@ -238,19 +257,30 @@ internal class MdViewHtmlPane(
override fun overrideSheet(): String = MdCommon.rules(opts())
override fun simulateLink(href: String) {
if (disposed) return
dispatch(MdView.LinkEvent(href))
}
override fun dispose() {
disposed = true
listeners.clear()
source.clear()
rendered = ""
pane.text = ""
}
private fun dispatch(event: MdView.LinkEvent) {
for (l in listeners) l.onLink(event)
}
private fun markDirty() {
if (disposed) return
pane.reloadCssStylesheets()
if (source.isNotEmpty()) syncHtml()
}
private fun syncHtml() {
if (disposed) return
val body = renderer.render(parser.parse(source.toString()))
if (rendered == body && pane.text == "<html><body>$body</body></html>") return
rendered = body
@@ -3,12 +3,14 @@ package ai.kilocode.client.ui.md
import ai.kilocode.client.session.ui.style.SessionEditorStyle
import ai.kilocode.client.session.ui.style.SessionUiStyle
import ai.kilocode.log.KiloLog
import com.intellij.openapi.Disposable
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.fileTypes.FileTypeRegistry
import com.intellij.openapi.fileTypes.PlainTextFileType
import com.intellij.openapi.fileTypes.UnknownFileType
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.util.Disposer
import com.intellij.ui.components.JBTextArea
import com.intellij.ui.components.JBHtmlPane
import com.intellij.ui.components.JBHtmlPaneConfiguration
@@ -122,6 +124,8 @@ internal class MdViewHybrid(
private val source = StringBuilder()
private var style = style
private var rendered = ""
private var block: Disposable? = null
private var disposed = false
private val extensions = listOf(
AutolinkExtension.create(),
@@ -161,6 +165,7 @@ internal class MdViewHybrid(
override var font: Font
get() = fontOverride ?: opts().font
set(value) {
if (disposed) return
if (fontOverride == value) return
fontOverride = value
syncStyle()
@@ -169,6 +174,7 @@ internal class MdViewHybrid(
override var foreground: Color
get() = foregroundOverride ?: opts().foreground
set(value) {
if (disposed) return
if (foregroundOverride == value) return
foregroundOverride = value
syncStyle()
@@ -177,6 +183,7 @@ internal class MdViewHybrid(
override var background: Color
get() = backgroundOverride ?: opts().background
set(value) {
if (disposed) return
if (backgroundOverride == value) return
backgroundOverride = value
syncStyle()
@@ -185,6 +192,7 @@ internal class MdViewHybrid(
override var linkColor: Color
get() = linkColorOverride ?: opts().linkColor
set(value) {
if (disposed) return
if (linkColorOverride == value) return
linkColorOverride = value
syncStyle()
@@ -193,6 +201,7 @@ internal class MdViewHybrid(
override var codeBg: Color
get() = codeBgOverride ?: opts().codeBg
set(value) {
if (disposed) return
if (codeBgOverride == value) return
codeBgOverride = value
syncStyle()
@@ -201,6 +210,7 @@ internal class MdViewHybrid(
override var preBg: Color
get() = preBgOverride ?: opts().preBg
set(value) {
if (disposed) return
if (preBgOverride == value) return
preBgOverride = value
syncStyle()
@@ -209,6 +219,7 @@ internal class MdViewHybrid(
override var preFg: Color
get() = preFgOverride ?: opts().preFg
set(value) {
if (disposed) return
if (preFgOverride == value) return
preFgOverride = value
syncStyle()
@@ -217,6 +228,7 @@ internal class MdViewHybrid(
override var codeFont: String
get() = codeFontOverride ?: opts().codeFont
set(value) {
if (disposed) return
if (codeFontOverride == value) return
codeFontOverride = value
syncStyle()
@@ -225,6 +237,7 @@ internal class MdViewHybrid(
override var quoteBorder: Color
get() = quoteBorderOverride ?: opts().quoteBorder
set(value) {
if (disposed) return
if (quoteBorderOverride == value) return
quoteBorderOverride = value
syncStyle()
@@ -233,6 +246,7 @@ internal class MdViewHybrid(
override var quoteFg: Color
get() = quoteFgOverride ?: opts().quoteFg
set(value) {
if (disposed) return
if (quoteFgOverride == value) return
quoteFgOverride = value
syncStyle()
@@ -241,6 +255,7 @@ internal class MdViewHybrid(
override var tableBorder: Color
get() = tableBorderOverride ?: opts().tableBorder
set(value) {
if (disposed) return
if (tableBorderOverride == value) return
tableBorderOverride = value
syncStyle()
@@ -249,18 +264,21 @@ internal class MdViewHybrid(
override var opaque: Boolean
get() = opaqueState
set(value) {
if (disposed) return
if (opaqueState == value) return
opaqueState = value
syncStyle()
}
override fun applyStyle(style: SessionEditorStyle) {
if (disposed) return
if (this.style == style) return
this.style = style
syncStyle()
}
override fun resetStyles() {
if (disposed) return
fontOverride = null
foregroundOverride = null
backgroundOverride = null
@@ -277,6 +295,7 @@ internal class MdViewHybrid(
}
override fun set(text: String) {
if (disposed) return
if (source.toString() == text) return
source.clear()
source.append(text)
@@ -284,21 +303,24 @@ internal class MdViewHybrid(
}
override fun append(delta: String) {
if (disposed) return
if (delta.isEmpty()) return
source.append(delta)
syncBlocks()
}
override fun clear() {
if (disposed) return
if (source.isEmpty() && rendered.isEmpty() && root.componentCount == 0) return
source.clear()
rendered = ""
root.removeAll()
clearBlocks()
root.revalidate()
root.repaint()
}
override fun addLinkListener(listener: MdView.LinkListener) {
if (disposed) return
listeners.add(listener)
}
@@ -313,10 +335,20 @@ internal class MdViewHybrid(
override fun overrideSheet(): String = MdCommon.rules(opts())
override fun simulateLink(href: String) {
if (disposed) return
dispatch(MdView.LinkEvent(href))
}
override fun dispose() {
disposed = true
listeners.clear()
source.clear()
rendered = ""
clearBlocks()
}
private fun syncStyle() {
if (disposed) return
val opts = opts()
root.isOpaque = opts.opaque
if (opts.opaque) root.background = opts.background
@@ -324,11 +356,12 @@ internal class MdViewHybrid(
}
private fun syncBlocks() {
if (disposed) return
val text = source.toString()
val doc = parser.parse(text)
val body = renderer.render(doc)
rendered = body
root.removeAll()
resetBlocks()
if (text.isEmpty()) {
root.revalidate()
root.repaint()
@@ -340,6 +373,18 @@ internal class MdViewHybrid(
root.repaint()
}
private fun resetBlocks() {
block?.let { Disposer.dispose(it) }
block = Disposer.newDisposable("Markdown blocks")
root.removeAll()
}
private fun clearBlocks() {
block?.let { Disposer.dispose(it) }
block = null
root.removeAll()
}
private fun addGap() {
if (root.componentCount == 0) return
root.add(Box.createVerticalStrut(JBUI.scale(SessionUiStyle.View.Code.BLOCK_GAP)))
@@ -379,7 +424,11 @@ internal class MdViewHybrid(
private fun codeBlock(text: String, lang: String?): JComponent {
val opts = opts()
val value = text.trimEnd('\n')
val field = runCatching { CodeField(file(lang), opts, text) }.getOrElse { err ->
val field = runCatching {
CodeField(file(lang), opts, text).also { ed ->
block?.let { ed.setDisposedWith(it) }
}
}.getOrElse { err ->
LOG.warn("kind=markdown codeEditor=true failed message=${err.message}", err)
textArea(text, opts)
}
@@ -6,9 +6,11 @@ import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.fileTypes.FileTypeRegistry
import com.intellij.openapi.fileTypes.PlainTextFileType
import com.intellij.openapi.fileTypes.UnknownFileType
import com.intellij.openapi.util.Disposer
import com.intellij.testFramework.fixtures.BasePlatformTestCase
import com.intellij.ui.EditorTextField
import com.intellij.ui.components.JBScrollPane
import com.intellij.util.ui.UIUtil
import java.awt.BorderLayout
import javax.swing.JPanel
import javax.swing.ScrollPaneConstants
@@ -16,10 +18,20 @@ import javax.swing.ScrollPaneConstants
@Suppress("UnstableApiUsage")
class MdViewHybridTest : BasePlatformTestCase() {
private lateinit var view: MdView
private var disposed = false
override fun setUp() {
super.setUp()
view = MdViewFactory.hybrid()
disposed = false
}
override fun tearDown() {
try {
if (this::view.isInitialized && !disposed) Disposer.dispose(view)
} finally {
super.tearDown()
}
}
fun `test set stores source`() {
@@ -199,6 +211,39 @@ class MdViewHybridTest : BasePlatformTestCase() {
assertTrue(scrolls().isEmpty())
}
fun `test rerender disposes previous code block editor`() {
view.set("```kotlin\nval value = 1\n```")
val editor = editors().single().getEditor(true)!!
view.set("plain text")
drainEdt()
assertTrue(editor.isDisposed)
assertTrue(scrolls().isEmpty())
}
fun `test clear disposes code block editor`() {
view.set("```kotlin\nval value = 1\n```")
val editor = editors().single().getEditor(true)!!
view.clear()
drainEdt()
assertTrue(editor.isDisposed)
}
fun `test dispose disposes code block editor`() {
view.set("```kotlin\nval value = 1\n```")
val editor = editors().single().getEditor(true)!!
Disposer.dispose(view)
disposed = true
drainEdt()
assertTrue(editor.isDisposed)
assertTrue(scrolls().isEmpty())
}
fun `test applyStyle updates current and future blocks`() {
val style = SessionEditorStyle.create(family = "Courier New", size = 21)
@@ -248,4 +293,8 @@ class MdViewHybridTest : BasePlatformTestCase() {
view.component.doLayout()
scrolls().forEach { it.doLayout() }
}
private fun drainEdt() {
UIUtil.dispatchAllInvocationEvents()
}
}