User: rednesto Date: 15 Jul 24 12:07 Revision: 613bb4d5650646c6217083a085632ad7e2018ee0 Summary: Migrate templates init and loading code to coroutines Future IDE versions are switching to coroutines for background tasks, and 2023.3 seems like the earliest version we can do that comfortably This also makes some long IO tasks actually cancellable TeamCity URL: https://ci.mcdev.io/viewModification.html?tab=vcsModificationFiles&modId=9457&personal=false Index: src/main/kotlin/creator/creator-utils.kt =================================================================== --- src/main/kotlin/creator/creator-utils.kt (revision d7cafeba2eab2ae1c6ced5663880c92dc56ee932) +++ src/main/kotlin/creator/creator-utils.kt (revision 613bb4d5650646c6217083a085632ad7e2018ee0) @@ -37,10 +37,12 @@ import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.observable.properties.ObservableMutableProperty import com.intellij.openapi.observable.properties.ObservableProperty +import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.project.Project import com.intellij.openapi.util.Key import com.intellij.openapi.util.RecursionManager import java.time.ZonedDateTime +import javax.swing.JComponent val NewProjectWizardStep.gitEnabled get() = data.getUserData(GitNewProjectWizardData.KEY)!!.git @@ -165,10 +167,13 @@ ).notify(null) } +val WizardContext.contentPanel: JComponent? + get() = this.getUserData(AbstractWizard.KEY)?.contentPanel + val WizardContext.modalityState: ModalityState get() { - val contentPanel = this.getUserData(AbstractWizard.KEY)?.contentPanel - + ProgressManager.checkCanceled() + val contentPanel = contentPanel if (contentPanel == null) { thisLogger().error("Wizard content panel is null, using default modality state") return ModalityState.defaultModalityState() Index: src/main/kotlin/creator/custom/CustomPlatformStep.kt =================================================================== --- src/main/kotlin/creator/custom/CustomPlatformStep.kt (revision d7cafeba2eab2ae1c6ced5663880c92dc56ee932) +++ src/main/kotlin/creator/custom/CustomPlatformStep.kt (revision 613bb4d5650646c6217083a085632ad7e2018ee0) @@ -38,7 +38,9 @@ import com.intellij.ide.wizard.GitNewProjectWizardData import com.intellij.ide.wizard.NewProjectWizardBaseData import com.intellij.ide.wizard.NewProjectWizardStep +import com.intellij.openapi.application.EDT import com.intellij.openapi.application.WriteAction +import com.intellij.openapi.application.asContextElement import com.intellij.openapi.diagnostic.Attachment import com.intellij.openapi.diagnostic.ControlFlowException import com.intellij.openapi.diagnostic.getOrLogException @@ -48,11 +50,9 @@ import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.module.ModuleTypeId import com.intellij.openapi.observable.util.transform -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.progress.Task import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.util.Disposer import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager @@ -77,6 +77,11 @@ import kotlin.collections.set import kotlin.io.path.createDirectories import kotlin.io.path.writeText +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * The step to select a custom template repo. @@ -85,6 +90,7 @@ parent: NewProjectWizardStep, ) : AbstractNewProjectWizardStep(parent) { + val creatorUiScope = TemplateService.instance.scope("MinecraftDev Creator UI") val templateRepos = MinecraftSettings.instance.creatorTemplateRepos val templateRepoProperty = propertyGraph.property( @@ -109,17 +115,23 @@ val templateProvidersText2Property = propertyGraph.property("") lateinit var templateProvidersProcessIcon: Cell - val templateLoadingProperty = propertyGraph.property(true) + val templateLoadingProperty = propertyGraph.property(false) val templateLoadingTextProperty = propertyGraph.property("") val templateLoadingText2Property = propertyGraph.property("") lateinit var templatePropertiesProcessIcon: Cell lateinit var noTemplatesAvailable: Cell - var templateLoadingIndicator: ProgressIndicator? = null + var templateLoadingJob: Job? = null private var hasTemplateErrors: Boolean = true private var properties = mutableMapOf>() + init { + Disposer.register(context.disposable) { + creatorUiScope.cancel("The creator got disposed") + } + } + override fun setupUI(builder: Panel) { lateinit var templatePropertyPlaceholder: Placeholder @@ -131,16 +143,12 @@ builder.row { templateProvidersProcessIcon = cell(AsyncProcessIcon("TemplateProviders init")) - .visibleIf(templateProvidersLoadingProperty) label(MCDevBundle("creator.step.generic.init_template_providers.message")) - .visibleIf(templateProvidersLoadingProperty) label("") .bindText(templateProvidersTextProperty) - .visibleIf(templateProvidersLoadingProperty) label("") .bindText(templateProvidersText2Property) - .visibleIf(templateProvidersLoadingProperty) - } + }.visibleIf(templateProvidersLoadingProperty) templateRepoProperty.afterChange { templateRepo -> templatePropertyPlaceholder.component = null @@ -218,96 +226,64 @@ private fun initTemplates() { selectedTemplate = EmptyLoadedTemplate - val task = object : Task.Backgroundable( - context.project, - MCDevBundle("creator.step.generic.init_template_providers.message"), - true, - ALWAYS_BACKGROUND, - ) { + templateRepoProperty.set(templateRepos.first()) - override fun run(indicator: ProgressIndicator) { - if (project?.isDisposed == true) { - return - } + val indicator = CreatorProgressIndicator( + templateProvidersLoadingProperty, + templateProvidersTextProperty, + templateProvidersText2Property + ) - application.invokeAndWait({ - ProgressManager.checkCanceled() + templateProvidersTextProperty.set(MCDevBundle("creator.step.generic.init_template_providers.message")) - templateProvidersLoadingProperty.set(true) + templateProvidersLoadingProperty.set(true) - VirtualFileManager.getInstance().syncRefresh() - }, context.modalityState) + val dialogCoroutineContext = context.modalityState.asContextElement() + val uiContext = dialogCoroutineContext + Dispatchers.EDT + creatorUiScope.launch(dialogCoroutineContext) { + withContext(uiContext) { + application.runWriteAction { VirtualFileManager.getInstance().syncRefresh() } + } + - for ((providerKey, repos) in templateRepos.groupBy { it.provider }) { + for ((providerKey, repos) in templateRepos.groupBy { it.provider }) { - ProgressManager.checkCanceled() - val provider = TemplateProvider.get(providerKey) - ?: continue - indicator.text = provider.label - runCatching { provider.init(indicator, repos) } - .getOrLogException(logger()) - } + val provider = TemplateProvider.get(providerKey) + ?: continue + indicator.text = provider.label + runCatching { provider.init(indicator, repos) } + .getOrLogException(logger()) + } - ProgressManager.checkCanceled() - application.invokeAndWait({ - ProgressManager.checkCanceled() + withContext(uiContext) { - templateProvidersLoadingProperty.set(false) - // Force refresh to trigger template loading - templateRepoProperty.set(templateRepo) + templateProvidersLoadingProperty.set(false) + // Force refresh to trigger template loading + templateRepoProperty.set(templateRepo) - }, context.modalityState) } } - - val indicator = CreatorProgressIndicator( - templateProvidersLoadingProperty, - templateProvidersTextProperty, - templateProvidersText2Property - ) - ProgressManager.getInstance().runProcessWithProgressAsynchronously(task, indicator) } - private fun loadTemplatesInBackground(provider: () -> Collection) { + private fun loadTemplatesInBackground(provider: suspend () -> Collection) { selectedTemplate = EmptyLoadedTemplate - val task = object : Task.Backgroundable( - context.project, - MCDevBundle("creator.step.generic.load_template.message"), - true, - ALWAYS_BACKGROUND, - ) { + templateLoadingTextProperty.set(MCDevBundle("creator.step.generic.load_template.message")) + templateLoadingProperty.set(true) - override fun run(indicator: ProgressIndicator) { - if (project?.isDisposed == true) { - return + val dialogCoroutineContext = context.modalityState.asContextElement() + val uiContext = dialogCoroutineContext + Dispatchers.EDT + templateLoadingJob?.cancel("Another template has been selected") + templateLoadingJob = creatorUiScope.launch(dialogCoroutineContext) { + withContext(uiContext) { + application.runWriteAction { VirtualFileManager.getInstance().syncRefresh() } - } + } - application.invokeAndWait({ - ProgressManager.checkCanceled() - templateLoadingProperty.set(true) - VirtualFileManager.getInstance().syncRefresh() - }, context.modalityState) - - ProgressManager.checkCanceled() - val newTemplates = runCatching { provider() } - .getOrLogException(logger()) - ?: emptyList() + val newTemplates = runCatching { provider() } + .getOrLogException(logger()) + ?: emptyList() - ProgressManager.checkCanceled() - application.invokeAndWait({ - ProgressManager.checkCanceled() + withContext(uiContext) { - templateLoadingProperty.set(false) - noTemplatesAvailable.visible(newTemplates.isEmpty()) - availableTemplates = newTemplates + templateLoadingProperty.set(false) + noTemplatesAvailable.visible(newTemplates.isEmpty()) + availableTemplates = newTemplates - }, context.modalityState) } } - - templateLoadingIndicator?.cancel() - - val indicator = CreatorProgressIndicator( - templateLoadingProperty, - templateLoadingTextProperty, - templateLoadingText2Property - ) - templateLoadingIndicator = indicator - ProgressManager.getInstance().runProcessWithProgressAsynchronously(task, indicator) } private fun createOptionsPanelInBackground(template: LoadedTemplate, placeholder: Placeholder) { Index: src/main/kotlin/creator/custom/TemplateService.kt =================================================================== --- src/main/kotlin/creator/custom/TemplateService.kt (revision d7cafeba2eab2ae1c6ced5663880c92dc56ee932) +++ src/main/kotlin/creator/custom/TemplateService.kt (revision 613bb4d5650646c6217083a085632ad7e2018ee0) @@ -26,9 +26,11 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.startup.ProjectActivity import com.intellij.util.application +import com.intellij.util.namedChildScope +import kotlinx.coroutines.CoroutineScope @Service -class TemplateService { +class TemplateService(private val scope: CoroutineScope) { private val pendingActions: MutableMap Unit> = mutableMapOf() @@ -45,6 +47,9 @@ pendingActions.remove(project.locationHash)?.invoke() } + @Suppress("UnstableApiUsage") // namedChildScope is Internal right now but has been promoted to Stable in 2024.2 + fun scope(name: String): CoroutineScope = scope.namedChildScope(name) + companion object { val instance: TemplateService Index: src/main/kotlin/creator/custom/providers/BuiltinTemplateProvider.kt =================================================================== --- src/main/kotlin/creator/custom/providers/BuiltinTemplateProvider.kt (revision d7cafeba2eab2ae1c6ced5663880c92dc56ee932) +++ src/main/kotlin/creator/custom/providers/BuiltinTemplateProvider.kt (revision 613bb4d5650646c6217083a085632ad7e2018ee0) @@ -45,7 +45,7 @@ override val hasConfig: Boolean = true - override fun init(indicator: ProgressIndicator, repos: List) { + override suspend fun init(indicator: ProgressIndicator, repos: List) { if (repoUpdated || repos.none { it.data.toBoolean() }) { // Auto update is disabled return @@ -56,7 +56,7 @@ } } - override fun loadTemplates( + override suspend fun loadTemplates( context: WizardContext, repo: MinecraftSettings.TemplateRepo ): Collection { Index: src/main/kotlin/creator/custom/providers/LocalTemplateProvider.kt =================================================================== --- src/main/kotlin/creator/custom/providers/LocalTemplateProvider.kt (revision d7cafeba2eab2ae1c6ced5663880c92dc56ee932) +++ src/main/kotlin/creator/custom/providers/LocalTemplateProvider.kt (revision 613bb4d5650646c6217083a085632ad7e2018ee0) @@ -46,7 +46,7 @@ override val hasConfig: Boolean = true - override fun loadTemplates( + override suspend fun loadTemplates( context: WizardContext, repo: MinecraftSettings.TemplateRepo ): Collection { Index: src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt =================================================================== --- src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt (revision d7cafeba2eab2ae1c6ced5663880c92dc56ee932) +++ src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt (revision 613bb4d5650646c6217083a085632ad7e2018ee0) @@ -29,6 +29,7 @@ import com.demonwav.mcdev.update.PluginUtil import com.demonwav.mcdev.util.refreshSync import com.github.kittinunf.fuel.core.FuelManager +import com.github.kittinunf.fuel.coroutines.awaitByteArrayResult import com.github.kittinunf.result.getOrNull import com.github.kittinunf.result.onError import com.intellij.ide.util.projectWizard.WizardContext @@ -62,7 +63,7 @@ override val hasConfig: Boolean = true - override fun init(indicator: ProgressIndicator, repos: List) { + override suspend fun init(indicator: ProgressIndicator, repos: List) { for (repo in repos) { ProgressManager.checkCanceled() val remote = RemoteTemplateRepo.deserialize(repo.data) @@ -77,7 +78,7 @@ } } - protected fun doUpdateRepo( + protected suspend fun doUpdateRepo( indicator: ProgressIndicator, repoName: String, originalRepoUrl: String @@ -88,11 +89,11 @@ val manager = FuelManager() manager.proxy = selectProxy(repoUrl) - val (_, _, result) = manager.get(repoUrl) + val result = manager.get(repoUrl) .header("User-Agent", "github_org/minecraft-dev/${PluginUtil.pluginVersion}") .header("Accepts", "application/json") .timeout(10000) - .response() + .awaitByteArrayResult() val data = result.onError { thisLogger().warn("Could not fetch remote templates repository update at $repoUrl", it) @@ -114,7 +115,7 @@ return false } - override fun loadTemplates( + override suspend fun loadTemplates( context: WizardContext, repo: MinecraftSettings.TemplateRepo ): Collection { Index: src/main/kotlin/creator/custom/providers/TemplateProvider.kt =================================================================== --- src/main/kotlin/creator/custom/providers/TemplateProvider.kt (revision d7cafeba2eab2ae1c6ced5663880c92dc56ee932) +++ src/main/kotlin/creator/custom/providers/TemplateProvider.kt (revision 613bb4d5650646c6217083a085632ad7e2018ee0) @@ -57,9 +57,9 @@ val hasConfig: Boolean - fun init(indicator: ProgressIndicator, repos: List) = Unit + suspend fun init(indicator: ProgressIndicator, repos: List) = Unit - fun loadTemplates(context: WizardContext, repo: MinecraftSettings.TemplateRepo): Collection + suspend fun loadTemplates(context: WizardContext, repo: MinecraftSettings.TemplateRepo): Collection fun setupConfigUi(data: String, dataSetter: (String) -> Unit): JComponent? Index: src/main/kotlin/creator/custom/providers/ZipTemplateProvider.kt =================================================================== --- src/main/kotlin/creator/custom/providers/ZipTemplateProvider.kt (revision d7cafeba2eab2ae1c6ced5663880c92dc56ee932) +++ src/main/kotlin/creator/custom/providers/ZipTemplateProvider.kt (revision 613bb4d5650646c6217083a085632ad7e2018ee0) @@ -45,7 +45,7 @@ override val hasConfig: Boolean = true - override fun loadTemplates( + override suspend fun loadTemplates( context: WizardContext, repo: MinecraftSettings.TemplateRepo ): Collection {