⁠
rednesto: 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
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
- /*
- * Minecraft Development for IntelliJ
- *
- * https://mcdev.io/
- *
- * Copyright (C) 2024 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
- fun init(indicator: ProgressIndicator, repos: List<MinecraftSettings.TemplateRepo>) = Unit
- 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 visitor = object : VirtualFileVisitor<Unit>() {
- override fun visitFile(file: VirtualFile): Boolean {
- if (!file.isFile || !file.name.endsWith(".mcdev.template.json")) {
- return true
- }
- 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 true
- }
- }
- VfsUtilCore.visitChildrenRecursively(repoRoot, visitor)
- 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) 2024 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 visitor = object : VirtualFileVisitor<Unit>() {
- override fun visitFile(file: VirtualFile): Boolean {
- if (!file.isFile || !file.name.endsWith(".mcdev.template.json")) {
- return true
- }
- 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 true
- }
- }
- VfsUtilCore.visitChildrenRecursively(repoRoot, visitor)
- 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
- }