⁠
kyle wood: Fix UI freezes, SlowOperations on EDT, and write-unsafe context issues
These issues were happening in the project creator, both from loading
the initial wizard, as well as during actual project creation.
These issues were happening in the project creator, both from loading
the initial wizard, as well as during actual project creation.
- /*
- * Minecraft Development for IntelliJ
- *
- * https://mcdev.io/
- *
- * Copyright (C) 2025 minecraft-dev
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, version 3.0 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
- package com.demonwav.mcdev.creator.custom.providers
-
- import com.demonwav.mcdev.MinecraftSettings
- import com.demonwav.mcdev.creator.custom.TemplateDescriptor
- import com.demonwav.mcdev.creator.custom.TemplateResourceBundle
- import com.demonwav.mcdev.util.fromJson
- import com.demonwav.mcdev.util.refreshSync
- import com.google.gson.Gson
- import com.intellij.DynamicBundle
- import com.intellij.ide.util.projectWizard.WizardContext
- import com.intellij.openapi.application.ModalityState
- import com.intellij.openapi.diagnostic.Attachment
- import com.intellij.openapi.diagnostic.ControlFlowException
- import com.intellij.openapi.diagnostic.thisLogger
- import com.intellij.openapi.extensions.ExtensionPointName
- import com.intellij.openapi.extensions.RequiredElement
- import com.intellij.openapi.progress.ProgressIndicator
- import com.intellij.openapi.util.KeyedExtensionCollector
- import com.intellij.openapi.vfs.VfsUtilCore
- import com.intellij.openapi.vfs.VirtualFile
- import com.intellij.openapi.vfs.VirtualFileVisitor
- import com.intellij.openapi.vfs.isFile
- import com.intellij.openapi.vfs.readText
- import com.intellij.serviceContainer.BaseKeyedLazyInstance
- import com.intellij.util.KeyedLazyInstance
- import com.intellij.util.xmlb.annotations.Attribute
- import java.util.ResourceBundle
- import javax.swing.JComponent
-
- /**
- * Extensions responsible for creating a [TemplateDescriptor] based on whatever data it is provided in its configuration
- * [UI][setupConfigUi].
- */
- interface TemplateProvider {
-
- val label: String
-
- val hasConfig: Boolean
-
- suspend fun init(indicator: ProgressIndicator, repos: List<MinecraftSettings.TemplateRepo>) = Unit
-
- suspend fun loadTemplates(context: WizardContext, repo: MinecraftSettings.TemplateRepo): Collection<LoadedTemplate>
-
- fun setupConfigUi(data: String, dataSetter: (String) -> Unit): JComponent?
-
- companion object {
-
- private val EP_NAME =
- ExtensionPointName<TemplateProviderBean>("com.demonwav.minecraft-dev.creatorTemplateProvider")
- private val COLLECTOR = KeyedExtensionCollector<TemplateProvider, String>(EP_NAME)
-
- fun get(key: String): TemplateProvider? = COLLECTOR.findSingle(key)
-
- fun getAllKeys() = EP_NAME.extensionList.mapNotNull { it.key }
-
- fun findTemplates(
- modalityState: ModalityState,
- repoRoot: VirtualFile,
- templates: MutableList<VfsLoadedTemplate> = mutableListOf(),
- bundle: ResourceBundle? = loadMessagesBundle(modalityState, repoRoot)
- ): List<VfsLoadedTemplate> {
- val templatesToLoad = mutableListOf<VirtualFile>()
- val visitor = object : VirtualFileVisitor<Unit>() {
- override fun visitFile(file: VirtualFile): Boolean {
- if (!file.isFile || !file.name.endsWith(".mcdev.template.json")) {
- return true
- }
-
- templatesToLoad += file
-
- return true
- }
- }
- VfsUtilCore.visitChildrenRecursively(repoRoot, visitor)
-
- for (file in templatesToLoad) {
- try {
- createVfsLoadedTemplate(modalityState, file.parent, file, bundle = bundle)
- ?.let(templates::add)
- } catch (t: Throwable) {
- if (t is ControlFlowException) {
- throw t
- }
-
- val attachment = runCatching { Attachment(file.name, file.readText()) }.getOrNull()
- if (attachment != null) {
- thisLogger().error("Failed to load template ${file.path}", t, attachment)
- } else {
- thisLogger().error("Failed to load template ${file.path}", t)
- }
- }
- }
-
- return templates
- }
-
- fun loadMessagesBundle(modalityState: ModalityState, repoRoot: VirtualFile): ResourceBundle? = try {
- val locale = DynamicBundle.getLocale()
- // Simplified bundle resolution, but covers all the most common cases
- val baseBundle = doLoadMessageBundle(
- repoRoot.findChild("messages.properties"),
- modalityState,
- null
- )
- val languageBundle = doLoadMessageBundle(
- repoRoot.findChild("messages_${locale.language}.properties"),
- modalityState,
- baseBundle
- )
- doLoadMessageBundle(
- repoRoot.findChild("messages_${locale.language}_${locale.country}.properties"),
- modalityState,
- languageBundle
- )
- } catch (t: Throwable) {
- if (t is ControlFlowException) {
- throw t
- }
-
- thisLogger().error("Failed to load resource bundle of template repository ${repoRoot.path}", t)
- null
- }
-
- private fun doLoadMessageBundle(
- file: VirtualFile?,
- modalityState: ModalityState,
- parent: ResourceBundle?
- ): ResourceBundle? {
- if (file == null) {
- return parent
- }
-
- try {
- return file.refreshSync(modalityState)
- ?.inputStream?.reader()?.use { TemplateResourceBundle(it, parent) }
- } catch (t: Throwable) {
- if (t is ControlFlowException) {
- return parent
- }
-
- thisLogger().error("Failed to load resource bundle ${file.path}", t)
- }
-
- return parent
- }
-
- fun createVfsLoadedTemplate(
- modalityState: ModalityState,
- templateRoot: VirtualFile,
- descriptorFile: VirtualFile,
- tooltip: String? = null,
- bundle: ResourceBundle? = null
- ): VfsLoadedTemplate? {
- descriptorFile.refreshSync(modalityState)
- var descriptor = Gson().fromJson<TemplateDescriptor>(descriptorFile.readText())
- if (descriptor.version != TemplateDescriptor.FORMAT_VERSION) {
- thisLogger().warn("Cannot handle template ${descriptorFile.path} of version ${descriptor.version}")
- return null
- }
-
- if (descriptor.hidden == true) {
- return null
- }
-
- descriptor.bundle = bundle
-
- val labelKey = descriptor.label
- ?: descriptorFile.name.removeSuffix(".mcdev.template.json").takeIf(String::isNotBlank)
- ?: templateRoot.presentableName
- val label =
- descriptor.translateOrNull("platform.${labelKey.lowercase()}.label") ?: descriptor.translate(labelKey)
-
- if (descriptor.inherit != null) {
- val parent = templateRoot.findFileByRelativePath(descriptor.inherit)
- if (parent != null) {
- parent.refresh(false, false)
- val parentDescriptor = Gson().fromJson<TemplateDescriptor>(parent.readText())
- val mergedProperties = parentDescriptor.properties.orEmpty() + descriptor.properties.orEmpty()
- val mergedFiles = parentDescriptor.files.orEmpty() + descriptor.files.orEmpty()
- descriptor = descriptor.copy(properties = mergedProperties, files = mergedFiles)
- } else {
- thisLogger().error(
- "Could not find inherited template descriptor ${descriptor.inherit} from ${descriptorFile.path}"
- )
- }
- }
-
- if (bundle != null) {
- descriptor.properties?.forEach { property ->
- property.bundle = bundle
- }
- }
-
- return VfsLoadedTemplate(templateRoot, label, tooltip, descriptor, true)
- }
- }
- }
-
- class TemplateProviderBean : BaseKeyedLazyInstance<TemplateProvider>(), KeyedLazyInstance<TemplateProvider> {
-
- @Attribute("key")
- @RequiredElement
- lateinit var name: String
-
- @Attribute("implementation")
- @RequiredElement
- lateinit var implementation: String
-
- override fun getKey(): String = name
-
- override fun getImplementationClassName(): String = implementation
- }
- /*
- * Minecraft Development for IntelliJ
- *
- * https://mcdev.io/
- *
- * Copyright (C) 2025 minecraft-dev
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License as published
- * by the Free Software Foundation, version 3.0 only.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
- package com.demonwav.mcdev.creator.custom.providers
-
- import com.demonwav.mcdev.MinecraftSettings
- import com.demonwav.mcdev.creator.custom.TemplateDescriptor
- import com.demonwav.mcdev.creator.custom.TemplateResourceBundle
- import com.demonwav.mcdev.util.fromJson
- import com.demonwav.mcdev.util.refreshSync
- import com.google.gson.Gson
- import com.intellij.DynamicBundle
- import com.intellij.ide.util.projectWizard.WizardContext
- import com.intellij.openapi.application.ModalityState
- import com.intellij.openapi.diagnostic.Attachment
- import com.intellij.openapi.diagnostic.ControlFlowException
- import com.intellij.openapi.diagnostic.thisLogger
- import com.intellij.openapi.extensions.ExtensionPointName
- import com.intellij.openapi.extensions.RequiredElement
- import com.intellij.openapi.progress.ProgressIndicator
- import com.intellij.openapi.util.KeyedExtensionCollector
- import com.intellij.openapi.vfs.VfsUtilCore
- import com.intellij.openapi.vfs.VirtualFile
- import com.intellij.openapi.vfs.VirtualFileVisitor
- import com.intellij.openapi.vfs.isFile
- import com.intellij.openapi.vfs.readText
- import com.intellij.serviceContainer.BaseKeyedLazyInstance
- import com.intellij.util.KeyedLazyInstance
- import com.intellij.util.xmlb.annotations.Attribute
- import java.util.ResourceBundle
- import java.util.concurrent.TimeUnit
- import javax.swing.JComponent
- import org.jetbrains.concurrency.runAsync
-
- /**
- * Extensions responsible for creating a [TemplateDescriptor] based on whatever data it is provided in its configuration
- * [UI][setupConfigUi].
- */
- interface TemplateProvider {
-
- val label: String
-
- val hasConfig: Boolean
-
- suspend fun init(indicator: ProgressIndicator, repos: List<MinecraftSettings.TemplateRepo>) = Unit
-
- suspend fun loadTemplates(context: WizardContext, repo: MinecraftSettings.TemplateRepo): Collection<LoadedTemplate>
-
- fun setupConfigUi(data: String, dataSetter: (String) -> Unit): JComponent?
-
- companion object {
-
- private val EP_NAME =
- ExtensionPointName<TemplateProviderBean>("com.demonwav.minecraft-dev.creatorTemplateProvider")
- private val COLLECTOR = KeyedExtensionCollector<TemplateProvider, String>(EP_NAME)
-
- fun get(key: String): TemplateProvider? = COLLECTOR.findSingle(key)
-
- fun getAllKeys() = EP_NAME.extensionList.mapNotNull { it.key }
-
- fun findTemplates(
- modalityState: ModalityState,
- repoRoot: VirtualFile,
- templates: MutableList<VfsLoadedTemplate> = mutableListOf(),
- bundle: ResourceBundle? = loadMessagesBundle(modalityState, repoRoot)
- ): List<VfsLoadedTemplate> {
- val templatesToLoad = mutableListOf<VirtualFile>()
- val visitor = object : VirtualFileVisitor<Unit>() {
- override fun visitFile(file: VirtualFile): Boolean {
- if (!file.isFile || !file.name.endsWith(".mcdev.template.json")) {
- return true
- }
-
- templatesToLoad += file
-
- return true
- }
- }
- VfsUtilCore.visitChildrenRecursively(repoRoot, visitor)
-
- for (file in templatesToLoad) {
- try {
- createVfsLoadedTemplate(modalityState, file.parent, file, bundle = bundle)
- ?.let(templates::add)
- } catch (t: Throwable) {
- if (t is ControlFlowException) {
- throw t
- }
-
- val attachment = runCatching { Attachment(file.name, file.readText()) }.getOrNull()
- if (attachment != null) {
- thisLogger().error("Failed to load template ${file.path}", t, attachment)
- } else {
- thisLogger().error("Failed to load template ${file.path}", t)
- }
- }
- }
-
- return templates
- }
-
- fun loadMessagesBundle(modalityState: ModalityState, repoRoot: VirtualFile): ResourceBundle? = try {
- val locale = DynamicBundle.getLocale()
- // Simplified bundle resolution, but covers all the most common cases
- val baseBundle = doLoadMessageBundle(
- repoRoot.findChild("messages.properties"),
- modalityState,
- null
- )
- val languageBundle = doLoadMessageBundle(
- repoRoot.findChild("messages_${locale.language}.properties"),
- modalityState,
- baseBundle
- )
- doLoadMessageBundle(
- repoRoot.findChild("messages_${locale.language}_${locale.country}.properties"),
- modalityState,
- languageBundle
- )
- } catch (t: Throwable) {
- if (t is ControlFlowException) {
- throw t
- }
-
- thisLogger().error("Failed to load resource bundle of template repository ${repoRoot.path}", t)
- null
- }
-
- private fun doLoadMessageBundle(
- file: VirtualFile?,
- modalityState: ModalityState,
- parent: ResourceBundle?
- ): ResourceBundle? {
- if (file == null) {
- return parent
- }
-
- try {
- return runAsync {
- file.inputStream.reader().use { TemplateResourceBundle(it, parent) }
- }.blockingGet(20, TimeUnit.MILLISECONDS)
- } catch (t: Throwable) {
- if (t is ControlFlowException) {
- return parent
- }
-
- thisLogger().error("Failed to load resource bundle ${file.path}", t)
- }
-
- return parent
- }
-
- fun createVfsLoadedTemplate(
- modalityState: ModalityState,
- templateRoot: VirtualFile,
- descriptorFile: VirtualFile,
- tooltip: String? = null,
- bundle: ResourceBundle? = null
- ): VfsLoadedTemplate? {
- var descriptor = runCatching {
- runAsync {
- descriptorFile.refreshSync(modalityState)
- Gson().fromJson<TemplateDescriptor>(descriptorFile.readText())
- }.blockingGet(100, TimeUnit.MILLISECONDS)
- }.getOrNull() ?: return null
- if (descriptor.version != TemplateDescriptor.FORMAT_VERSION) {
- thisLogger().warn("Cannot handle template ${descriptorFile.path} of version ${descriptor.version}")
- return null
- }
-
- if (descriptor.hidden == true) {
- return null
- }
-
- descriptor.bundle = bundle
-
- val labelKey = descriptor.label
- ?: descriptorFile.name.removeSuffix(".mcdev.template.json").takeIf(String::isNotBlank)
- ?: templateRoot.presentableName
- val label =
- descriptor.translateOrNull("platform.${labelKey.lowercase()}.label") ?: descriptor.translate(labelKey)
-
- if (descriptor.inherit != null) {
- val parent = templateRoot.findFileByRelativePath(descriptor.inherit)
- if (parent != null) {
- parent.refresh(false, false)
- val parentDescriptor = Gson().fromJson<TemplateDescriptor>(parent.readText())
- val mergedProperties = parentDescriptor.properties.orEmpty() + descriptor.properties.orEmpty()
- val mergedFiles = parentDescriptor.files.orEmpty() + descriptor.files.orEmpty()
- descriptor = descriptor.copy(properties = mergedProperties, files = mergedFiles)
- } else {
- thisLogger().error(
- "Could not find inherited template descriptor ${descriptor.inherit} from ${descriptorFile.path}"
- )
- }
- }
-
- if (bundle != null) {
- descriptor.properties?.forEach { property ->
- property.bundle = bundle
- }
- }
-
- return VfsLoadedTemplate(templateRoot, label, tooltip, descriptor, true)
- }
- }
- }
-
- class TemplateProviderBean : BaseKeyedLazyInstance<TemplateProvider>(), KeyedLazyInstance<TemplateProvider> {
-
- @Attribute("key")
- @RequiredElement
- lateinit var name: String
-
- @Attribute("implementation")
- @RequiredElement
- lateinit var implementation: String
-
- override fun getKey(): String = name
-
- override fun getImplementationClassName(): String = implementation
- }