mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-248623 dispose AWB empty-state prompt on splitters shutdown
The inline Agent Workbench prompt can be mounted through the editor empty-state controller without another disposable owner. When the owning splitters scope completes, keep the rich empty-state entries tied to that lifecycle and dispose them on EDT instead of leaving the Swing tree reachable. Make prompt palette teardown exhaustive so one cleanup failure cannot skip EditorTextField disposal, and clear the inline component reference before disposing nested content. Cover both the splitters-scope owner path and the inline prompt editor disposal path with regression tests. GitOrigin-RevId: b135ec8c04f2f210768a1ca475c6286e3df69721
This commit is contained in:
committed by
intellij-monorepo-bot
parent
9d244e6208
commit
2c8a446b64
+19
@@ -3,6 +3,8 @@ package com.intellij.openapi.fileEditor.impl
|
||||
|
||||
import com.intellij.diagnostic.PluginException
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.writeIntentReadAction
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.extensions.PluginDescriptor
|
||||
@@ -14,6 +16,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
@@ -52,6 +55,12 @@ internal class EditorEmptyStateComponentController(
|
||||
private var creationDelay: Duration = EMPTY_STATE_COMPONENT_CREATION_DELAY
|
||||
private var creationGate: (suspend () -> Unit)? = null
|
||||
|
||||
init {
|
||||
coroutineScope.coroutineContext.job.invokeOnCompletion {
|
||||
disposeComponentsOnEdt()
|
||||
}
|
||||
}
|
||||
|
||||
fun isCreationPending(): Boolean = creationJob != null
|
||||
|
||||
fun isVisible(): Boolean = componentHost != null
|
||||
@@ -109,6 +118,16 @@ internal class EditorEmptyStateComponentController(
|
||||
splitters.repaint()
|
||||
}
|
||||
|
||||
private fun disposeComponentsOnEdt() {
|
||||
val application = ApplicationManager.getApplication()
|
||||
if (application.isDispatchThread) {
|
||||
disposeComponents()
|
||||
}
|
||||
else {
|
||||
application.invokeLater({ disposeComponents() }, ModalityState.any())
|
||||
}
|
||||
}
|
||||
|
||||
fun setCreationDelayForTests(delay: Duration) {
|
||||
creationDelay = delay
|
||||
}
|
||||
|
||||
+29
@@ -29,8 +29,11 @@ import com.intellij.testFramework.junit5.fixture.projectFixture
|
||||
import com.intellij.testFramework.junit5.fixture.tempPathFixture
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.jdom.Element
|
||||
@@ -167,6 +170,32 @@ internal class EditorEmptyTextPainterTest {
|
||||
assertThat(findEmptyStateComponent(splitters)).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("RAW_SCOPE_CREATION")
|
||||
fun splittersScopeDisposesVisibleEmptyStateComponent(@TestDisposable disposable: Disposable) {
|
||||
val disposedComponents = AtomicInteger()
|
||||
registerComponentProvider(disposable, disposedComponents)
|
||||
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
val splitters = EditorsSplitters(manager = manager, coroutineScope = scope)
|
||||
try {
|
||||
enableRichEmptyStateComponentsWithoutDelay(splitters)
|
||||
splitters.updateEmptyStateComponent()
|
||||
waitForEmptyStateComponentCreation(splitters)
|
||||
|
||||
assertThat(findEmptyStateComponent(splitters)).isNotNull()
|
||||
|
||||
scope.cancel()
|
||||
PlatformTestUtil.dispatchAllEventsInIdeEventQueue()
|
||||
|
||||
assertThat(findEmptyStateComponent(splitters)).isNull()
|
||||
assertThat(disposedComponents).hasValue(1)
|
||||
}
|
||||
finally {
|
||||
scope.cancel()
|
||||
PlatformTestUtil.dispatchAllEventsInIdeEventQueue()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun componentProviderIsMountedAsEmptyStatePanel(@TestDisposable disposable: Disposable) {
|
||||
val splitters = manager.mainSplitters
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.wm.IdeFocusManager
|
||||
import com.intellij.openapi.wm.IdeFrame
|
||||
import com.intellij.openapi.wm.ex.IdeFocusTraversalPolicy
|
||||
import com.intellij.util.runSuppressing
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -193,25 +194,15 @@ internal class AgentPromptPaletteContent(
|
||||
return
|
||||
}
|
||||
disposed = true
|
||||
imageDropSupportScope.cancel(reason)
|
||||
try {
|
||||
sessionController.onHostClosed()
|
||||
}
|
||||
finally {
|
||||
try {
|
||||
view.headerToolbar.targetComponent = null
|
||||
view.footerPinToolbar.targetComponent = null
|
||||
rootPanel.parent?.remove(rootPanel)
|
||||
}
|
||||
finally {
|
||||
try {
|
||||
Disposer.dispose(swingDisposable)
|
||||
}
|
||||
finally {
|
||||
sessionScope.cancel(reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
runSuppressing(
|
||||
{ imageDropSupportScope.cancel(reason) },
|
||||
{ sessionController.onHostClosed() },
|
||||
{ view.headerToolbar.targetComponent = null },
|
||||
{ view.footerPinToolbar.targetComponent = null },
|
||||
{ rootPanel.parent?.remove(rootPanel) },
|
||||
{ Disposer.dispose(swingDisposable) },
|
||||
{ sessionScope.cancel(reason) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,10 +321,14 @@ internal fun createAgentPromptPaletteContent(
|
||||
}
|
||||
catch (error: Throwable) {
|
||||
try {
|
||||
imageDropSupportScope.cancel("Agent prompt palette content creation failed", error)
|
||||
runSuppressing(
|
||||
{ imageDropSupportScope.cancel("Agent prompt palette content creation failed", error) },
|
||||
{ Disposer.dispose(swingDisposable) },
|
||||
{ sessionScope.cancel("Agent prompt palette content creation failed", error) },
|
||||
)
|
||||
}
|
||||
finally {
|
||||
Disposer.dispose(swingDisposable)
|
||||
catch (cleanupError: Throwable) {
|
||||
error.addSuppressed(cleanupError)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
+6
-1
@@ -183,8 +183,13 @@ class AgentWorkbenchInlinePromptEmptyStateComponent internal constructor(
|
||||
return
|
||||
}
|
||||
disposed = true
|
||||
content?.dispose(configuration.disposeReason)
|
||||
disposeContent(configuration.disposeReason)
|
||||
}
|
||||
|
||||
private fun disposeContent(reason: String) {
|
||||
val promptContent = content ?: return
|
||||
content = null
|
||||
promptContent.dispose(reason)
|
||||
}
|
||||
|
||||
override fun paintComponent(g: Graphics) {
|
||||
|
||||
+50
@@ -31,6 +31,7 @@ import com.intellij.openapi.fileEditor.impl.EditorEmptyStateComponentHost
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.testFramework.ExtensionTestUtil
|
||||
import com.intellij.testFramework.PlatformTestUtil
|
||||
import com.intellij.testFramework.junit5.TestApplication
|
||||
import com.intellij.testFramework.junit5.fixture.fileEditorManagerFixture
|
||||
import com.intellij.testFramework.junit5.fixture.projectFixture
|
||||
@@ -106,6 +107,45 @@ class AgentWorkbenchInlinePromptEmptyStateProviderTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun disposingInitializedPromptDisposesPromptEditorAndClearsContent() {
|
||||
runInEdtAndWait {
|
||||
val component = AgentWorkbenchInlinePromptEmptyStateComponent(ProjectManager.getInstance().defaultProject)
|
||||
component.ensureContentInitialized()
|
||||
val promptArea = collectComponents(component, AgentPromptTextField::class.java).single()
|
||||
val editor = checkNotNull(promptArea.getEditor(true))
|
||||
|
||||
disposeComponent(component)
|
||||
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue()
|
||||
|
||||
assertThat(editor.isDisposed).isTrue()
|
||||
assertThat(collectComponents(component, AgentPromptTextField::class.java)).isEmpty()
|
||||
assertThat(component.preferredFocusedComponent).isSameAs(component)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun disposingInlineNewThreadPromptDisposesPromptEditorAndClearsContent() {
|
||||
runInEdtAndWait {
|
||||
val project = ProjectManager.getInstance().defaultProject
|
||||
val component = createAgentWorkbenchInlineNewThreadPromptComponent(
|
||||
project = project,
|
||||
invocationData = testInvocationData(project),
|
||||
launcherProvider = { TestPromptLauncher },
|
||||
initialLaunchProfileId = null,
|
||||
)
|
||||
val promptArea = collectComponents(component, AgentPromptTextField::class.java).single()
|
||||
val editor = checkNotNull(promptArea.getEditor(true))
|
||||
|
||||
disposeComponent(component)
|
||||
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue()
|
||||
|
||||
assertThat(editor.isDisposed).isTrue()
|
||||
assertThat(collectComponents(component, AgentPromptTextField::class.java)).isEmpty()
|
||||
assertThat(component.preferredFocusedComponent).isSameAs(component)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun providerReturnsNullWhenFeatureDisabled() {
|
||||
val previous = System.getProperty(INLINE_EMPTY_STATE_PROMPT_PROPERTY)
|
||||
@@ -304,6 +344,16 @@ class AgentWorkbenchInlinePromptEmptyStateProviderTest {
|
||||
return component as AgentWorkbenchInlinePromptEmptyStateComponent
|
||||
}
|
||||
|
||||
private fun testInvocationData(project: com.intellij.openapi.project.Project): AgentPromptInvocationData {
|
||||
return AgentPromptInvocationData(
|
||||
project = project,
|
||||
actionId = "AgentWorkbenchPrompt.OpenGlobalPalette",
|
||||
actionText = "Ask Agent",
|
||||
actionPlace = "EditorEmptyState",
|
||||
invokedAtMs = 0L,
|
||||
)
|
||||
}
|
||||
|
||||
private fun disposeComponent(component: AgentWorkbenchInlinePromptEmptyStateComponent) {
|
||||
if (ApplicationManager.getApplication().isDispatchThread) {
|
||||
Disposer.dispose(component)
|
||||
|
||||
Reference in New Issue
Block a user