mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
fix(jetbrains): dispose embedded markdown editors
This commit is contained in:
+12
-1
@@ -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() }
|
||||
|
||||
+23
-1
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+19
-2
@@ -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)
|
||||
|
||||
+2
@@ -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())
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
+2
@@ -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())
|
||||
|
||||
+12
-1
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -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)
|
||||
|
||||
+30
@@ -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
|
||||
|
||||
+52
-3
@@ -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)
|
||||
}
|
||||
|
||||
+49
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user