diff --git a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/EditorEmptyStateComponentController.kt b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/EditorEmptyStateComponentController.kt index c96c671ae364..17bd13227c70 100644 --- a/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/EditorEmptyStateComponentController.kt +++ b/platform/platform-impl/src/com/intellij/openapi/fileEditor/impl/EditorEmptyStateComponentController.kt @@ -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 } diff --git a/platform/platform-impl/testSrc/com/intellij/openapi/fileEditor/impl/EditorEmptyTextPainterTest.kt b/platform/platform-impl/testSrc/com/intellij/openapi/fileEditor/impl/EditorEmptyTextPainterTest.kt index 4d06cdfb135f..ae95cf280610 100644 --- a/platform/platform-impl/testSrc/com/intellij/openapi/fileEditor/impl/EditorEmptyTextPainterTest.kt +++ b/platform/platform-impl/testSrc/com/intellij/openapi/fileEditor/impl/EditorEmptyTextPainterTest.kt @@ -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 diff --git a/plugins/agent-workbench/prompt/ui/src/AgentPromptPalettePopup.kt b/plugins/agent-workbench/prompt/ui/src/AgentPromptPalettePopup.kt index 1adfea210bdf..be93c40f07a1 100644 --- a/plugins/agent-workbench/prompt/ui/src/AgentPromptPalettePopup.kt +++ b/plugins/agent-workbench/prompt/ui/src/AgentPromptPalettePopup.kt @@ -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 } diff --git a/plugins/agent-workbench/prompt/ui/src/emptyState/AgentWorkbenchInlinePromptEmptyStateProvider.kt b/plugins/agent-workbench/prompt/ui/src/emptyState/AgentWorkbenchInlinePromptEmptyStateProvider.kt index 62164d3500c2..4e08390e3c6b 100644 --- a/plugins/agent-workbench/prompt/ui/src/emptyState/AgentWorkbenchInlinePromptEmptyStateProvider.kt +++ b/plugins/agent-workbench/prompt/ui/src/emptyState/AgentWorkbenchInlinePromptEmptyStateProvider.kt @@ -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) { diff --git a/plugins/agent-workbench/prompt/ui/testSrc/emptyState/AgentWorkbenchInlinePromptEmptyStateProviderTest.kt b/plugins/agent-workbench/prompt/ui/testSrc/emptyState/AgentWorkbenchInlinePromptEmptyStateProviderTest.kt index 74eff8b1c109..1f3126f37583 100644 --- a/plugins/agent-workbench/prompt/ui/testSrc/emptyState/AgentWorkbenchInlinePromptEmptyStateProviderTest.kt +++ b/plugins/agent-workbench/prompt/ui/testSrc/emptyState/AgentWorkbenchInlinePromptEmptyStateProviderTest.kt @@ -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)