Repository: JetBrains/compose-multiplatform
File: idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewStateService.kt (Kotlin)

Code before:
import com.intellij.openapi.externalSystem.model.task.*
import com.intellij.openapi.externalSystem.service.notification.ExternalSystemProgressNotificationManager
import com.intellij.openapi.module.Module
import com.intellij.openapi.util.Disposer
import com.intellij.ui.components.JBLoadingPanel
import com.intellij.util.concurrency.annotations.RequiresReadLock
import org.jetbrains.compose.desktop.ide.preview.ui.PreviewPanel
import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.*
import org.jetbrains.plugins.gradle.util.GradleConstants
import javax.swing.JComponent
import javax.swing.event.AncestorEvent
import javax.swing.event.AncestorListener

@Service(Service.Level.APP)
class PreviewStateService() : Disposable {
    private val previewListener = CompositePreviewListener()
    private val previewManager: PreviewManager = PreviewManagerImpl(previewListener)
    val gradleCallbackPort: Int
        get() = previewManager.gradleCallbackPort
    private val configurePreviewTaskNameCache =
        ConfigurePreviewTaskNameCache(ConfigurePreviewTaskNameProviderImpl())

    init {
        val projectRefreshListener = ConfigurePreviewTaskNameCacheInvalidator(configurePreviewTaskNameCache)
        ExternalSystemProgressNotificationManager.getInstance()
            .addNotificationListener(projectRefreshListener, this)
    }

    @RequiresReadLock
    internal fun configurePreviewTaskNameOrNull(module: Module): String? =
        configurePreviewTaskNameCache.configurePreviewTaskNameOrNull(module)

    override fun dispose() {
        previewManager.close()
        configurePreviewTaskNameCache.invalidate()
    }

    internal fun registerPreviewPanels(
        previewPanel: PreviewPanel,
        loadingPanel: JBLoadingPanel
    ) {
        val previewResizeListener = PreviewResizeListener(previewManager)
        previewPanel.addAncestorListener(previewResizeListener)
        Disposer.register(this) { previewPanel.removeAncestorListener(previewResizeListener) }

        previewListener.addListener(PreviewPanelUpdater(previewPanel))
        previewListener.addListener(LoadingPanelUpdater(loadingPanel))
    }

    internal fun buildStarted() {
        previewListener.onNewBuildRequest()

Review comment (suggestion):
I'm not sure what is the expected behaviour here — the service was marked as an app-level service and the project used as disposable, but using Project as disposable is bad. It feels like the service should be project-level, so this subscription would maintain the same scope, but I don't know if it's the right way to go about it.

Code after:
import com.intellij.openapi.externalSystem.model.task.*
import com.intellij.openapi.externalSystem.service.notification.ExternalSystemProgressNotificationManager
import com.intellij.openapi.module.Module
import com.intellij.openapi.util.Disposer
import com.intellij.ui.components.JBLoadingPanel
import com.intellij.util.concurrency.annotations.RequiresReadLock
import org.jetbrains.compose.desktop.ide.preview.ui.PreviewPanel
import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.*
import org.jetbrains.plugins.gradle.util.GradleConstants
import javax.swing.JComponent
import javax.swing.event.AncestorEvent
import javax.swing.event.AncestorListener

@Service(Service.Level.PROJECT)
class PreviewStateService : Disposable {
    private val previewListener = CompositePreviewListener()
    private val previewManager: PreviewManager = PreviewManagerImpl(previewListener)
    val gradleCallbackPort: Int
        get() = previewManager.gradleCallbackPort
    private val configurePreviewTaskNameCache =
        ConfigurePreviewTaskNameCache(ConfigurePreviewTaskNameProviderImpl())

    init {
        val projectRefreshListener = ConfigurePreviewTaskNameCacheInvalidator(configurePreviewTaskNameCache)
        ExternalSystemProgressNotificationManager.getInstance()
            .addNotificationListener(projectRefreshListener, this)
    }

    @RequiresReadLock
    internal fun configurePreviewTaskNameOrNull(module: Module): String? =
        configurePreviewTaskNameCache.configurePreviewTaskNameOrNull(module)

    override fun dispose() {
        previewManager.close()
        configurePreviewTaskNameCache.invalidate()
    }

    internal fun registerPreviewPanels(
        previewPanel: PreviewPanel,
        loadingPanel: JBLoadingPanel
    ) {
        val previewResizeListener = PreviewResizeListener(previewManager)
        previewPanel.addAncestorListener(previewResizeListener)
        Disposer.register(this) { previewPanel.removeAncestorListener(previewResizeListener) }

        previewListener.addListener(PreviewPanelUpdater(previewPanel))
        previewListener.addListener(LoadingPanelUpdater(loadingPanel))
    }

    internal fun buildStarted() {
        previewListener.onNewBuildRequest()