From d9c560c208ce4df12beb8fb62f25fa59127f8637 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sun, 14 Jul 2024 18:28:13 +0200 Subject: [PATCH 01/37] Use InjectorUtils instead of the deprecated class --- .../kotlin/platform/mixin/expression/MEExpressionInjector.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionInjector.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionInjector.kt index 22de47c71..ef095fbf6 100644 --- a/src/main/kotlin/platform/mixin/expression/MEExpressionInjector.kt +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionInjector.kt @@ -44,6 +44,7 @@ import com.intellij.psi.util.PsiModificationTracker import com.intellij.psi.util.PsiUtil import com.intellij.psi.util.parentOfType import com.intellij.util.SmartList +import org.intellij.plugins.intelliLang.inject.InjectorUtils class MEExpressionInjector : MultiHostInjector { companion object { @@ -146,8 +147,7 @@ class MEExpressionInjector : MultiHostInjector { ) if (isFrankenstein) { - @Suppress("DEPRECATION") // no replacement for this method - com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil.putInjectedFileUserData( + InjectorUtils.putInjectedFileUserData( context, MEExpressionLanguage, InjectedLanguageManager.FRANKENSTEIN_INJECTION, From 613bb4d5650646c6217083a085632ad7e2018ee0 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Mon, 15 Jul 2024 14:07:22 +0200 Subject: [PATCH 02/37] 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 --- src/main/kotlin/creator/creator-utils.kt | 9 +- .../creator/custom/CustomPlatformStep.kt | 146 ++++++++---------- .../kotlin/creator/custom/TemplateService.kt | 7 +- .../providers/BuiltinTemplateProvider.kt | 4 +- .../custom/providers/LocalTemplateProvider.kt | 2 +- .../providers/RemoteTemplateProvider.kt | 11 +- .../custom/providers/TemplateProvider.kt | 4 +- .../custom/providers/ZipTemplateProvider.kt | 2 +- 8 files changed, 86 insertions(+), 99 deletions(-) diff --git a/src/main/kotlin/creator/creator-utils.kt b/src/main/kotlin/creator/creator-utils.kt index a1ab81512..68416effc 100644 --- a/src/main/kotlin/creator/creator-utils.kt +++ b/src/main/kotlin/creator/creator-utils.kt @@ -37,10 +37,12 @@ import com.intellij.openapi.application.ModalityState 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 @@ fun notifyCreatedProjectNotOpened() { ).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() diff --git a/src/main/kotlin/creator/custom/CustomPlatformStep.kt b/src/main/kotlin/creator/custom/CustomPlatformStep.kt index 682e4256d..3bd19c9b3 100644 --- a/src/main/kotlin/creator/custom/CustomPlatformStep.kt +++ b/src/main/kotlin/creator/custom/CustomPlatformStep.kt @@ -38,7 +38,9 @@ import com.intellij.ide.wizard.AbstractNewProjectWizardStep 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.fileEditor.FileEditorManager 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.component2 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 @@ class CustomPlatformStep( 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 @@ class CustomPlatformStep( 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 @@ class CustomPlatformStep( 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 @@ class CustomPlatformStep( private fun initTemplates() { selectedTemplate = EmptyLoadedTemplate - val task = object : Task.Backgroundable( - context.project, - MCDevBundle("creator.step.generic.init_template_providers.message"), - true, - ALWAYS_BACKGROUND, - ) { - - override fun run(indicator: ProgressIndicator) { - if (project?.isDisposed == true) { - return - } - - application.invokeAndWait({ - ProgressManager.checkCanceled() - templateProvidersLoadingProperty.set(true) - VirtualFileManager.getInstance().syncRefresh() - }, context.modalityState) - - 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()) - } - - ProgressManager.checkCanceled() - application.invokeAndWait({ - ProgressManager.checkCanceled() - templateProvidersLoadingProperty.set(false) - // Force refresh to trigger template loading - templateRepoProperty.set(templateRepo) - }, context.modalityState) - } - } + templateRepoProperty.set(templateRepos.first()) val indicator = CreatorProgressIndicator( templateProvidersLoadingProperty, templateProvidersTextProperty, templateProvidersText2Property ) - ProgressManager.getInstance().runProcessWithProgressAsynchronously(task, indicator) + + templateProvidersTextProperty.set(MCDevBundle("creator.step.generic.init_template_providers.message")) + templateProvidersLoadingProperty.set(true) + + 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 }) { + val provider = TemplateProvider.get(providerKey) + ?: continue + indicator.text = provider.label + runCatching { provider.init(indicator, repos) } + .getOrLogException(logger()) + } + + withContext(uiContext) { + templateProvidersLoadingProperty.set(false) + // Force refresh to trigger template loading + templateRepoProperty.set(templateRepo) + } + } } - 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) + val newTemplates = runCatching { provider() } + .getOrLogException(logger()) + ?: emptyList() - ProgressManager.checkCanceled() - val newTemplates = runCatching { provider() } - .getOrLogException(logger()) - ?: emptyList() - - ProgressManager.checkCanceled() - application.invokeAndWait({ - ProgressManager.checkCanceled() - templateLoadingProperty.set(false) - noTemplatesAvailable.visible(newTemplates.isEmpty()) - availableTemplates = newTemplates - }, context.modalityState) + withContext(uiContext) { + templateLoadingProperty.set(false) + noTemplatesAvailable.visible(newTemplates.isEmpty()) + availableTemplates = newTemplates } } - - templateLoadingIndicator?.cancel() - - val indicator = CreatorProgressIndicator( - templateLoadingProperty, - templateLoadingTextProperty, - templateLoadingText2Property - ) - templateLoadingIndicator = indicator - ProgressManager.getInstance().runProcessWithProgressAsynchronously(task, indicator) } private fun createOptionsPanelInBackground(template: LoadedTemplate, placeholder: Placeholder) { diff --git a/src/main/kotlin/creator/custom/TemplateService.kt b/src/main/kotlin/creator/custom/TemplateService.kt index 28c9094de..6ffe5a831 100644 --- a/src/main/kotlin/creator/custom/TemplateService.kt +++ b/src/main/kotlin/creator/custom/TemplateService.kt @@ -26,9 +26,11 @@ import com.intellij.openapi.diagnostic.thisLogger 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 @@ class TemplateService { 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 diff --git a/src/main/kotlin/creator/custom/providers/BuiltinTemplateProvider.kt b/src/main/kotlin/creator/custom/providers/BuiltinTemplateProvider.kt index 1bc3bc9b6..7a7dc167b 100644 --- a/src/main/kotlin/creator/custom/providers/BuiltinTemplateProvider.kt +++ b/src/main/kotlin/creator/custom/providers/BuiltinTemplateProvider.kt @@ -45,7 +45,7 @@ class BuiltinTemplateProvider : RemoteTemplateProvider() { 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 @@ class BuiltinTemplateProvider : RemoteTemplateProvider() { } } - override fun loadTemplates( + override suspend fun loadTemplates( context: WizardContext, repo: MinecraftSettings.TemplateRepo ): Collection { diff --git a/src/main/kotlin/creator/custom/providers/LocalTemplateProvider.kt b/src/main/kotlin/creator/custom/providers/LocalTemplateProvider.kt index d08fb037c..f18252dcd 100644 --- a/src/main/kotlin/creator/custom/providers/LocalTemplateProvider.kt +++ b/src/main/kotlin/creator/custom/providers/LocalTemplateProvider.kt @@ -46,7 +46,7 @@ class LocalTemplateProvider : TemplateProvider { override val hasConfig: Boolean = true - override fun loadTemplates( + override suspend fun loadTemplates( context: WizardContext, repo: MinecraftSettings.TemplateRepo ): Collection { diff --git a/src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt b/src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt index 2cbc70f16..8bc842d68 100644 --- a/src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt +++ b/src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt @@ -29,6 +29,7 @@ import com.demonwav.mcdev.creator.selectProxy 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 @@ open class RemoteTemplateProvider : TemplateProvider { 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 @@ open class RemoteTemplateProvider : TemplateProvider { } } - protected fun doUpdateRepo( + protected suspend fun doUpdateRepo( indicator: ProgressIndicator, repoName: String, originalRepoUrl: String @@ -88,11 +89,11 @@ open class RemoteTemplateProvider : TemplateProvider { 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 @@ open class RemoteTemplateProvider : TemplateProvider { return false } - override fun loadTemplates( + override suspend fun loadTemplates( context: WizardContext, repo: MinecraftSettings.TemplateRepo ): Collection { diff --git a/src/main/kotlin/creator/custom/providers/TemplateProvider.kt b/src/main/kotlin/creator/custom/providers/TemplateProvider.kt index 30efddbb2..21ee6eb84 100644 --- a/src/main/kotlin/creator/custom/providers/TemplateProvider.kt +++ b/src/main/kotlin/creator/custom/providers/TemplateProvider.kt @@ -57,9 +57,9 @@ interface TemplateProvider { 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? diff --git a/src/main/kotlin/creator/custom/providers/ZipTemplateProvider.kt b/src/main/kotlin/creator/custom/providers/ZipTemplateProvider.kt index 6cae4d3ce..3111e57f6 100644 --- a/src/main/kotlin/creator/custom/providers/ZipTemplateProvider.kt +++ b/src/main/kotlin/creator/custom/providers/ZipTemplateProvider.kt @@ -45,7 +45,7 @@ class ZipTemplateProvider : TemplateProvider { override val hasConfig: Boolean = true - override fun loadTemplates( + override suspend fun loadTemplates( context: WizardContext, repo: MinecraftSettings.TemplateRepo ): Collection { From 6bef7af0d4eacfaa8449cd100d9326acee7aa246 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Mon, 15 Jul 2024 14:28:48 +0200 Subject: [PATCH 03/37] Remove unused McpVersion classes --- .../kotlin/platform/mcp/McpVersionPair.kt | 44 ------ .../kotlin/platform/mcp/version/McpVersion.kt | 131 ------------------ .../platform/mcp/version/McpVersionEntry.kt | 39 ------ 3 files changed, 214 deletions(-) delete mode 100644 src/main/kotlin/platform/mcp/McpVersionPair.kt delete mode 100644 src/main/kotlin/platform/mcp/version/McpVersion.kt delete mode 100644 src/main/kotlin/platform/mcp/version/McpVersionEntry.kt diff --git a/src/main/kotlin/platform/mcp/McpVersionPair.kt b/src/main/kotlin/platform/mcp/McpVersionPair.kt deleted file mode 100644 index c55b36a82..000000000 --- a/src/main/kotlin/platform/mcp/McpVersionPair.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 . - */ - -package com.demonwav.mcdev.platform.mcp - -import com.demonwav.mcdev.util.SemanticVersion - -data class McpVersionPair(val mcpVersion: String, val mcVersion: SemanticVersion) : Comparable { - - override fun compareTo(other: McpVersionPair): Int { - val mcRes = mcVersion.compareTo(other.mcVersion) - if (mcRes != 0) { - return mcRes - } - val thisStable = mcpVersion.startsWith("stable") - val thatStable = other.mcpVersion.startsWith("stable") - return if (thisStable && !thatStable) { - -1 - } else if (!thisStable && thatStable) { - 1 - } else { - val thisNum = mcpVersion.substringAfter('_') - val thatNum = other.mcpVersion.substringAfter('_') - thisNum.toInt().compareTo(thatNum.toInt()) - } - } -} diff --git a/src/main/kotlin/platform/mcp/version/McpVersion.kt b/src/main/kotlin/platform/mcp/version/McpVersion.kt deleted file mode 100644 index f0ad1226b..000000000 --- a/src/main/kotlin/platform/mcp/version/McpVersion.kt +++ /dev/null @@ -1,131 +0,0 @@ -/* - * 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 . - */ - -package com.demonwav.mcdev.platform.mcp.version - -import com.demonwav.mcdev.platform.mcp.McpVersionPair -import com.demonwav.mcdev.util.SemanticVersion -import com.demonwav.mcdev.util.fromJson -import com.demonwav.mcdev.util.sortVersions -import com.google.gson.Gson -import java.io.IOException -import java.net.URL -import kotlin.math.min - -class McpVersion private constructor(private val map: Map>>) { - - val versions: List by lazy { - sortVersions(map.keys) - } - - data class McpVersionSet(val goodVersions: List, val badVersions: List) - - private fun getSnapshot(version: SemanticVersion): McpVersionSet { - return get(version, "snapshot") - } - - private fun getStable(version: SemanticVersion): McpVersionSet { - return get(version, "stable") - } - - private operator fun get(version: SemanticVersion, type: String): McpVersionSet { - val good = ArrayList() - val bad = ArrayList() - - val keySet = map.keys - for (mcVersion in keySet) { - val versions = map[mcVersion] - if (versions != null) { - versions[type]?.let { vers -> - val mcVersionParsed = SemanticVersion.parse(mcVersion) - val pairs = vers.map { McpVersionPair("${type}_$it", mcVersionParsed) } - if (mcVersionParsed.startsWith(version)) { - good.addAll(pairs) - } else { - bad.addAll(pairs) - } - } - } - } - - return McpVersionSet(good, bad) - } - - fun getMcpVersionList(version: SemanticVersion): List { - val limit = 50 - - val result = ArrayList(limit * 4) - - val majorVersion = version.take(2) - val stable = getStable(majorVersion) - val snapshot = getSnapshot(majorVersion) - - fun mapTopTo(source: List, dest: MutableList, limit: Int, isRed: Boolean) { - val tempList = ArrayList(source).apply { sortDescending() } - for (i in 0 until min(limit, tempList.size)) { - dest += McpVersionEntry(tempList[i], isRed) - } - } - - mapTopTo(stable.goodVersions, result, limit, false) - mapTopTo(snapshot.goodVersions, result, limit, false) - - // If we're already at the limit we don't need to go through the bad list - if (result.size >= limit) { - return result.subList(0, min(limit, result.size)) - } - - // The bad pairs don't match the current MC version, but are still available to the user - // We will color them red - mapTopTo(stable.badVersions, result, limit, true) - mapTopTo(snapshot.badVersions, result, limit, true) - - return result.subList(0, min(limit, result.size)) - } - - companion object { - fun downloadData(): McpVersion? { - val bspkrsMappings = try { - val bspkrsText = URL("https://maven.minecraftforge.net/de/oceanlabs/mcp/versions.json").readText() - Gson().fromJson>>>(bspkrsText) - } catch (ignored: IOException) { - mutableMapOf() - } - - val tterragMappings = try { - val tterragText = URL("https://assets.tterrag.com/temp_mappings.json").readText() - Gson().fromJson>>>(tterragText) - } catch (ignored: IOException) { - emptyMap() - } - - // Merge the temporary mappings list into the main one, temporary solution for 1.16 - tterragMappings.forEach { (mcVersion, channels) -> - val existingChannels = bspkrsMappings.getOrPut(mcVersion, ::mutableMapOf) - channels.forEach { (channelName, newVersions) -> - val existingVersions = existingChannels.getOrPut(channelName, ::mutableListOf) - existingVersions.addAll(newVersions) - } - } - - return if (bspkrsMappings.isEmpty()) null else McpVersion(bspkrsMappings) - } - } -} diff --git a/src/main/kotlin/platform/mcp/version/McpVersionEntry.kt b/src/main/kotlin/platform/mcp/version/McpVersionEntry.kt deleted file mode 100644 index 52fee57ac..000000000 --- a/src/main/kotlin/platform/mcp/version/McpVersionEntry.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 . - */ - -package com.demonwav.mcdev.platform.mcp.version - -import com.demonwav.mcdev.platform.mcp.McpVersionPair - -class McpVersionEntry(val versionPair: McpVersionPair, val isRed: Boolean = false) { - - override fun toString(): String { - return if (isRed) { - RED_START + versionPair.mcpVersion + RED_END - } else { - versionPair.mcpVersion - } - } - - companion object { - private const val RED_START = "" - private const val RED_END = "" - } -} From 96a55b2d70305bd6813279cb806f4c008cbad2f7 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Mon, 15 Jul 2024 14:29:19 +0200 Subject: [PATCH 04/37] Replace URL() calls to URI.toURL() as it is deprecated since Java 21 --- buildSrc/src/main/kotlin/mcdev.gradle.kts | 3 ++- src/main/kotlin/util/HttpConnectionFactory.kt | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/buildSrc/src/main/kotlin/mcdev.gradle.kts b/buildSrc/src/main/kotlin/mcdev.gradle.kts index c53450d64..99a58e2eb 100644 --- a/buildSrc/src/main/kotlin/mcdev.gradle.kts +++ b/buildSrc/src/main/kotlin/mcdev.gradle.kts @@ -22,6 +22,7 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken import java.net.HttpURLConnection +import java.net.URI import java.net.URL import java.util.Properties import java.util.zip.ZipFile @@ -94,7 +95,7 @@ tasks.register("resolveIntellijLibSources") { val groupPath = dep.groupId.replace('.', '/') val (_, artifact, ver) = dep val url = "https://repo.maven.apache.org/maven2/$groupPath/$artifact/$ver/$artifact-$ver-sources.jar" - return@filter with(URL(url).openConnection() as HttpURLConnection) { + return@filter with(URI.create(url).toURL().openConnection() as HttpURLConnection) { try { requestMethod = "GET" val code = responseCode diff --git a/src/main/kotlin/util/HttpConnectionFactory.kt b/src/main/kotlin/util/HttpConnectionFactory.kt index ab0fed035..9b2ed87ba 100644 --- a/src/main/kotlin/util/HttpConnectionFactory.kt +++ b/src/main/kotlin/util/HttpConnectionFactory.kt @@ -22,10 +22,10 @@ package com.demonwav.mcdev.util import com.intellij.util.net.HttpConfigurable import java.net.HttpURLConnection -import java.net.URL +import java.net.URI sealed class HttpConnectionFactory { - open fun openHttpConnection(url: String) = URL(url).openConnection() as HttpURLConnection + open fun openHttpConnection(url: String) = URI.create(url).toURL().openConnection() as HttpURLConnection } object ProxyHttpConnectionFactory : HttpConnectionFactory() { From c58995f6b632d2df66c6a2b741e6a46d7fdaa3c2 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Mon, 15 Jul 2024 16:49:37 +0200 Subject: [PATCH 05/37] Reformat created files without keeping line breaks Fixes #2330 --- .../kotlin/creator/custom/CustomPlatformStep.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/creator/custom/CustomPlatformStep.kt b/src/main/kotlin/creator/custom/CustomPlatformStep.kt index 6d54cab04..edf57b332 100644 --- a/src/main/kotlin/creator/custom/CustomPlatformStep.kt +++ b/src/main/kotlin/creator/custom/CustomPlatformStep.kt @@ -32,6 +32,7 @@ import com.demonwav.mcdev.creator.custom.types.ExternalCreatorProperty import com.demonwav.mcdev.creator.modalityState import com.demonwav.mcdev.util.toTypedArray import com.demonwav.mcdev.util.virtualFileOrError +import com.intellij.codeInsight.CodeInsightSettings import com.intellij.codeInsight.actions.ReformatCodeProcessor import com.intellij.ide.projectView.ProjectView import com.intellij.ide.wizard.AbstractNewProjectWizardStep @@ -553,7 +554,18 @@ class CustomPlatformStep( val psiFiles = files.asSequence() .filter { (desc, _) -> desc.reformat != false } .mapNotNull { (_, file) -> psiManager.findFile(file) } - ReformatCodeProcessor(project, psiFiles.toTypedArray(), null, false).run() + + val processor = ReformatCodeProcessor(project, psiFiles.toTypedArray(), null, false) + psiFiles.forEach(processor::setDoNotKeepLineBreaks) + + val insightSettings = CodeInsightSettings.getInstance() + val oldSecondReformat = insightSettings.ENABLE_SECOND_REFORMAT + insightSettings.ENABLE_SECOND_REFORMAT = true + try { + processor.run() + } finally { + insightSettings.ENABLE_SECOND_REFORMAT = oldSecondReformat + } } private fun openFilesInEditor( From 99871564fa50a42027a0a5b49ad00540fedd0508 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Mon, 15 Jul 2024 19:45:33 +0200 Subject: [PATCH 06/37] Fix lexer and parser build cache misses --- buildSrc/src/main/kotlin/JFlexExec.kt | 9 ++++++++- buildSrc/src/main/kotlin/ParserExec.kt | 13 ++++++++----- buildSrc/src/main/kotlin/util.kt | 6 +++--- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/buildSrc/src/main/kotlin/JFlexExec.kt b/buildSrc/src/main/kotlin/JFlexExec.kt index dca469daa..7522ae73c 100644 --- a/buildSrc/src/main/kotlin/JFlexExec.kt +++ b/buildSrc/src/main/kotlin/JFlexExec.kt @@ -26,22 +26,29 @@ import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.FileCollection import org.gradle.api.file.FileSystemOperations import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Internal import org.gradle.api.tasks.JavaExec import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +@CacheableTask abstract class JFlexExec : JavaExec() { @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) abstract val sourceFile: RegularFileProperty - @get:InputFiles + @get:Classpath abstract val jflex: ConfigurableFileCollection @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) abstract val skeletonFile: RegularFileProperty @get:OutputDirectory diff --git a/buildSrc/src/main/kotlin/ParserExec.kt b/buildSrc/src/main/kotlin/ParserExec.kt index adb38256d..ad00c9014 100644 --- a/buildSrc/src/main/kotlin/ParserExec.kt +++ b/buildSrc/src/main/kotlin/ParserExec.kt @@ -24,26 +24,29 @@ import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.FileSystemOperations import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Internal import org.gradle.api.tasks.JavaExec import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +@CacheableTask abstract class ParserExec : JavaExec() { @get:InputFile + @get:PathSensitive(PathSensitivity.RELATIVE) abstract val sourceFile: RegularFileProperty - @get:InputFiles + @get:Classpath abstract val grammarKit: ConfigurableFileCollection - @get:OutputDirectory + @get:Internal abstract val destinationRootDirectory: DirectoryProperty - @get:OutputDirectory - abstract val destinationDirectory: DirectoryProperty - @get:OutputDirectory abstract val psiDirectory: DirectoryProperty diff --git a/buildSrc/src/main/kotlin/util.kt b/buildSrc/src/main/kotlin/util.kt index 7a6623b23..d36de248f 100644 --- a/buildSrc/src/main/kotlin/util.kt +++ b/buildSrc/src/main/kotlin/util.kt @@ -24,6 +24,7 @@ import org.gradle.api.JavaVersion import org.gradle.api.Project import org.gradle.api.tasks.JavaExec import org.gradle.api.tasks.TaskContainer +import org.gradle.api.tasks.TaskProvider import org.gradle.kotlin.dsl.RegisteringDomainObjectDelegateProviderWithTypeAndAction import org.gradle.kotlin.dsl.getValue import org.gradle.kotlin.dsl.provideDelegate @@ -39,8 +40,8 @@ fun Project.lexer(flex: String, pack: String): TaskDelegate { return tasks.registering(JFlexExec::class) { sourceFile.set(layout.projectDirectory.file("src/main/grammars/$flex.flex")) - destinationDirectory.set(layout.buildDirectory.dir("gen/$pack")) - destinationFile.set(layout.buildDirectory.file("gen/$pack/$flex.java")) + destinationDirectory.set(layout.buildDirectory.dir("gen/$pack/lexer")) + destinationFile.set(layout.buildDirectory.file("gen/$pack/lexer/$flex.java")) logFile.set(layout.buildDirectory.file("logs/generate$flex.log")) val jflex by project.configurations @@ -61,7 +62,6 @@ fun Project.parser(bnf: String, pack: String): TaskDelegate { val dest = destRoot.map { it.dir(pack) } sourceFile.set(project.layout.projectDirectory.file("src/main/grammars/$bnf.bnf")) destinationRootDirectory.set(destRoot) - destinationDirectory.set(dest) psiDirectory.set(dest.map { it.dir("psi") }) parserDirectory.set(dest.map { it.dir("parser") }) logFile.set(layout.buildDirectory.file("logs/generate$bnf.log")) From 0a68f5a7fc35d757ccc255c3127e199949320511 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Mon, 15 Jul 2024 19:46:10 +0200 Subject: [PATCH 07/37] Bump to Gradle 8.9 --- gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 43504 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 7 +++++-- gradlew.bat | 2 ++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917707c1f8861d8cb53dd15194d4248596..2c3521197d7c4586c843d1d3e9090525f1898cde 100644 GIT binary patch delta 34463 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 SXRcJ{65?fGI57uQ5&!`B?F@7P delta 34554 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a80b22ce5..09523c0e5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a426..f5feea6d6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30dbd..9d21a2183 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## From 69241a67289807a8303f679ffeb053382a6ee622 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 16 Jul 2024 13:35:33 +0200 Subject: [PATCH 08/37] Move MEExpressionParser to its own package This ensures UP-TO-DATE checks and the build cache won't trip on it --- src/main/grammars/MEExpressionParser.bnf | 2 +- .../platform/mixin/expression/MEExpressionParserDefinition.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/grammars/MEExpressionParser.bnf b/src/main/grammars/MEExpressionParser.bnf index 47d509e01..d4e7f95ac 100644 --- a/src/main/grammars/MEExpressionParser.bnf +++ b/src/main/grammars/MEExpressionParser.bnf @@ -19,7 +19,7 @@ */ { - parserClass="com.demonwav.mcdev.platform.mixin.expression.gen.MEExpressionParser" + parserClass="com.demonwav.mcdev.platform.mixin.expression.gen.parser.MEExpressionParser" extends="com.intellij.extapi.psi.ASTWrapperPsiElement" parserImports = ["static com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionParserUtil.*"] diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionParserDefinition.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionParserDefinition.kt index 7d16e8816..2bfffe87a 100644 --- a/src/main/kotlin/platform/mixin/expression/MEExpressionParserDefinition.kt +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionParserDefinition.kt @@ -20,7 +20,7 @@ package com.demonwav.mcdev.platform.mixin.expression -import com.demonwav.mcdev.platform.mixin.expression.gen.MEExpressionParser +import com.demonwav.mcdev.platform.mixin.expression.gen.parser.MEExpressionParser import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionFile import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionTokenSets From 1579723a3f7834ce8f03a090c6f9e06505da71b6 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 16 Jul 2024 14:20:04 +0200 Subject: [PATCH 09/37] Move graph and properties map into a CreatorContext Also add WizardContext to it so we can access it anytime This is required for more flexibility and a future change to migrate away from Dispatchers.Swing and possibly remove our bundled coroutines lib --- .../kotlin/creator/custom/CreatorContext.kt | 31 +++++++++++++++++++ .../creator/custom/CustomPlatformStep.kt | 9 +++--- .../ArchitecturyVersionsCreatorProperty.kt | 15 ++++----- .../custom/types/BooleanCreatorProperty.kt | 15 ++++----- .../BuildSystemCoordinatesCreatorProperty.kt | 15 ++++----- .../custom/types/ClassFqnCreatorProperty.kt | 15 ++++----- .../creator/custom/types/CreatorProperty.kt | 15 +++++++-- .../custom/types/CreatorPropertyFactory.kt | 13 +++----- .../custom/types/ExternalCreatorProperty.kt | 10 +++--- .../types/FabricVersionsCreatorProperty.kt | 15 ++++----- .../types/ForgeVersionsCreatorProperty.kt | 16 ++++------ .../types/InlineStringListCreatorProperty.kt | 15 ++++----- .../custom/types/IntegerCreatorProperty.kt | 15 ++++----- .../custom/types/JdkCreatorProperty.kt | 17 +++++----- .../custom/types/LicenseCreatorProperty.kt | 15 ++++----- .../MavenArtifactVersionCreatorProperty.kt | 15 ++++----- .../types/NeoForgeVersionsCreatorProperty.kt | 15 ++++----- .../custom/types/ParchmentCreatorProperty.kt | 15 ++++----- .../types/SemanticVersionCreatorProperty.kt | 15 ++++----- .../custom/types/SimpleCreatorProperty.kt | 14 ++++----- .../custom/types/StringCreatorProperty.kt | 15 ++++----- 21 files changed, 153 insertions(+), 167 deletions(-) create mode 100644 src/main/kotlin/creator/custom/CreatorContext.kt diff --git a/src/main/kotlin/creator/custom/CreatorContext.kt b/src/main/kotlin/creator/custom/CreatorContext.kt new file mode 100644 index 000000000..813e9d803 --- /dev/null +++ b/src/main/kotlin/creator/custom/CreatorContext.kt @@ -0,0 +1,31 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.creator.custom + +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.PropertyGraph + +data class CreatorContext( + val graph: PropertyGraph, + val properties: Map>, + val wizardContext: WizardContext, +) diff --git a/src/main/kotlin/creator/custom/CustomPlatformStep.kt b/src/main/kotlin/creator/custom/CustomPlatformStep.kt index edf57b332..2e6000b83 100644 --- a/src/main/kotlin/creator/custom/CustomPlatformStep.kt +++ b/src/main/kotlin/creator/custom/CustomPlatformStep.kt @@ -120,6 +120,7 @@ class CustomPlatformStep( private var hasTemplateErrors: Boolean = true private var properties = mutableMapOf>() + private var creatorContext = CreatorContext(propertyGraph, properties, context) override fun setupUI(builder: Panel) { lateinit var templatePropertyPlaceholder: Placeholder @@ -311,6 +312,7 @@ class CustomPlatformStep( private fun createOptionsPanelInBackground(template: LoadedTemplate, placeholder: Placeholder) { properties = mutableMapOf() + creatorContext = creatorContext.copy(properties = properties) if (!template.isValid) { return @@ -320,8 +322,7 @@ class CustomPlatformStep( ?: return thisLogger().error("Could not find wizard base data") properties["PROJECT_NAME"] = ExternalCreatorProperty( - graph = propertyGraph, - properties = properties, + context = creatorContext, graphProperty = baseData.nameProperty, valueType = String::class.java ) @@ -421,7 +422,7 @@ class CustomPlatformStep( reporter.fatal("Duplicate property name ${descriptor.name}") } - val prop = CreatorPropertyFactory.createFromType(descriptor.type, descriptor, propertyGraph, properties) + val prop = CreatorPropertyFactory.createFromType(descriptor.type, descriptor, creatorContext) if (prop == null) { reporter.fatal("Unknown template property type ${descriptor.type}") } @@ -434,7 +435,7 @@ class CustomPlatformStep( return null } - val factory = Consumer { panel -> prop.buildUi(panel, context) } + val factory = Consumer { panel -> prop.buildUi(panel) } val order = descriptor.order ?: 0 return factory to order } diff --git a/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt index 1c742e80e..d3738f28b 100644 --- a/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt @@ -24,6 +24,7 @@ import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.asset.MCDevBundle.invoke import com.demonwav.mcdev.creator.collectMavenVersions import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.TemplateEvaluator import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.demonwav.mcdev.creator.custom.TemplateValidationReporter @@ -35,9 +36,7 @@ import com.demonwav.mcdev.platform.forge.version.ForgeVersion import com.demonwav.mcdev.platform.neoforge.version.NeoForgeVersion import com.demonwav.mcdev.util.SemanticVersion import com.demonwav.mcdev.util.asyncIO -import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.openapi.observable.properties.GraphProperty -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.openapi.observable.util.not import com.intellij.openapi.observable.util.transform import com.intellij.ui.ComboboxSpeedSearch @@ -56,9 +55,8 @@ import kotlinx.coroutines.withContext class ArchitecturyVersionsCreatorProperty( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> -) : CreatorProperty(descriptor, graph, properties, ArchitecturyVersionsModel::class.java) { + context: CreatorContext +) : CreatorProperty(descriptor, context, ArchitecturyVersionsModel::class.java) { private val emptyVersion = SemanticVersion.release() private val emptyValue = ArchitecturyVersionsModel( @@ -164,7 +162,7 @@ class ArchitecturyVersionsCreatorProperty( ) } - override fun buildUi(panel: Panel, context: WizardContext) { + override fun buildUi(panel: Panel) { panel.row("") { cell(AsyncProcessIcon("ArchitecturyVersions download")) label(MCDevBundle("creator.ui.versions_download.label")) @@ -474,8 +472,7 @@ class ArchitecturyVersionsCreatorProperty( override fun create( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> - ): CreatorProperty<*> = ArchitecturyVersionsCreatorProperty(descriptor, graph, properties) + context: CreatorContext + ): CreatorProperty<*> = ArchitecturyVersionsCreatorProperty(descriptor, context) } } diff --git a/src/main/kotlin/creator/custom/types/BooleanCreatorProperty.kt b/src/main/kotlin/creator/custom/types/BooleanCreatorProperty.kt index 57072d519..f83d098cd 100644 --- a/src/main/kotlin/creator/custom/types/BooleanCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/BooleanCreatorProperty.kt @@ -20,10 +20,9 @@ package com.demonwav.mcdev.creator.custom.types +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.intellij.icons.AllIcons -import com.intellij.ide.util.projectWizard.WizardContext -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.ui.content.AlertIcon import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.RightGap @@ -31,9 +30,8 @@ import com.intellij.ui.dsl.builder.bindSelected class BooleanCreatorProperty( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> -) : SimpleCreatorProperty(descriptor, graph, properties, Boolean::class.java) { + context: CreatorContext +) : SimpleCreatorProperty(descriptor, context, Boolean::class.java) { override fun createDefaultValue(raw: Any?): Boolean = raw as? Boolean ?: false @@ -41,7 +39,7 @@ class BooleanCreatorProperty( override fun deserialize(string: String): Boolean = string.toBoolean() - override fun buildSimpleUi(panel: Panel, context: WizardContext) { + override fun buildSimpleUi(panel: Panel) { val label = descriptor.translatedLabel panel.row(label) { val warning = descriptor.translatedWarning @@ -60,8 +58,7 @@ class BooleanCreatorProperty( class Factory : CreatorPropertyFactory { override fun create( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> - ): CreatorProperty<*> = BooleanCreatorProperty(descriptor, graph, properties) + context: CreatorContext + ): CreatorProperty<*> = BooleanCreatorProperty(descriptor, context) } } diff --git a/src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt b/src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt index 2d70ef5cc..bb1ccc310 100644 --- a/src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt @@ -22,12 +22,11 @@ package com.demonwav.mcdev.creator.custom.types import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.demonwav.mcdev.creator.custom.TemplateValidationReporter import com.demonwav.mcdev.creator.custom.model.BuildSystemCoordinates -import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.openapi.observable.properties.GraphProperty -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.openapi.observable.util.transform import com.intellij.openapi.ui.validation.CHECK_ARTIFACT_ID import com.intellij.openapi.ui.validation.CHECK_GROUP_ID @@ -46,9 +45,8 @@ private val nonExampleValidation = validationErrorIf(MCDevBundle("creato class BuildSystemCoordinatesCreatorProperty( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> -) : CreatorProperty(descriptor, graph, properties, BuildSystemCoordinates::class.java) { + context: CreatorContext +) : CreatorProperty(descriptor, context, BuildSystemCoordinates::class.java) { private val default = createDefaultValue(descriptor.default) @@ -99,7 +97,7 @@ class BuildSystemCoordinatesCreatorProperty( } } - override fun buildUi(panel: Panel, context: WizardContext) { + override fun buildUi(panel: Panel) { panel.collapsibleGroup(MCDevBundle("creator.ui.group.title")) { this.row(MCDevBundle("creator.ui.group.group_id")) { this.textField() @@ -128,8 +126,7 @@ class BuildSystemCoordinatesCreatorProperty( class Factory : CreatorPropertyFactory { override fun create( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> - ): CreatorProperty<*> = BuildSystemCoordinatesCreatorProperty(descriptor, graph, properties) + context: CreatorContext + ): CreatorProperty<*> = BuildSystemCoordinatesCreatorProperty(descriptor, context) } } diff --git a/src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.kt index 5ee2470ad..96b592b94 100644 --- a/src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.kt @@ -21,14 +21,13 @@ package com.demonwav.mcdev.creator.custom.types import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.PropertyDerivation import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.demonwav.mcdev.creator.custom.TemplateValidationReporter import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation import com.demonwav.mcdev.creator.custom.derivation.SuggestClassNamePropertyDerivation import com.demonwav.mcdev.creator.custom.model.ClassFqn -import com.intellij.ide.util.projectWizard.WizardContext -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.ui.dsl.builder.COLUMNS_LARGE import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.bindText @@ -37,9 +36,8 @@ import com.intellij.ui.dsl.builder.textValidation class ClassFqnCreatorProperty( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> -) : SimpleCreatorProperty(descriptor, graph, properties, ClassFqn::class.java) { + context: CreatorContext +) : SimpleCreatorProperty(descriptor, context, ClassFqn::class.java) { override fun createDefaultValue(raw: Any?): ClassFqn = ClassFqn(raw as? String ?: "") @@ -47,7 +45,7 @@ class ClassFqnCreatorProperty( override fun deserialize(string: String): ClassFqn = ClassFqn(string) - override fun buildSimpleUi(panel: Panel, context: WizardContext) { + override fun buildSimpleUi(panel: Panel) { panel.row(descriptor.translatedLabel) { this.textField().bindText(this@ClassFqnCreatorProperty.toStringProperty(graphProperty)) .columns(COLUMNS_LARGE) @@ -71,8 +69,7 @@ class ClassFqnCreatorProperty( class Factory : CreatorPropertyFactory { override fun create( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> - ): CreatorProperty<*> = ClassFqnCreatorProperty(descriptor, graph, properties) + context: CreatorContext + ): CreatorProperty<*> = ClassFqnCreatorProperty(descriptor, context) } } diff --git a/src/main/kotlin/creator/custom/types/CreatorProperty.kt b/src/main/kotlin/creator/custom/types/CreatorProperty.kt index 3e9e1845c..122cb86c6 100644 --- a/src/main/kotlin/creator/custom/types/CreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/CreatorProperty.kt @@ -20,6 +20,7 @@ package com.demonwav.mcdev.creator.custom.types +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.PropertyDerivation import com.demonwav.mcdev.creator.custom.TemplateEvaluator import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor @@ -38,10 +39,18 @@ import com.intellij.ui.dsl.builder.Row abstract class CreatorProperty( val descriptor: TemplatePropertyDescriptor, - val graph: PropertyGraph, - protected val properties: Map>, + protected val context: CreatorContext, val valueType: Class ) { + protected val graph: PropertyGraph + get() = context.graph + + protected val properties + get() = context.properties + + protected val wizardContext: WizardContext + get() = context.wizardContext + private var derivation: PreparedDerivation? = null private lateinit var visibleProperty: GraphProperty @@ -95,7 +104,7 @@ abstract class CreatorProperty( protected open fun convertSelectDerivationResult(original: Any?): Any? = original - abstract fun buildUi(panel: Panel, context: WizardContext) + abstract fun buildUi(panel: Panel) /** * Prepares everything this property needs, like calling [GraphProperty]'s [GraphProperty.afterChange] and diff --git a/src/main/kotlin/creator/custom/types/CreatorPropertyFactory.kt b/src/main/kotlin/creator/custom/types/CreatorPropertyFactory.kt index 8d3689d50..a84c77873 100644 --- a/src/main/kotlin/creator/custom/types/CreatorPropertyFactory.kt +++ b/src/main/kotlin/creator/custom/types/CreatorPropertyFactory.kt @@ -20,10 +20,10 @@ package com.demonwav.mcdev.creator.custom.types +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.extensions.RequiredElement -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.openapi.util.KeyedExtensionCollector import com.intellij.serviceContainer.BaseKeyedLazyInstance import com.intellij.util.KeyedLazyInstance @@ -42,18 +42,13 @@ interface CreatorPropertyFactory { fun createFromType( type: String, descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> + context: CreatorContext ): CreatorProperty<*>? { - return COLLECTOR.findSingle(type)?.create(descriptor, graph, properties) + return COLLECTOR.findSingle(type)?.create(descriptor, context) } } - fun create( - descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> - ): CreatorProperty<*> + fun create(descriptor: TemplatePropertyDescriptor, context: CreatorContext): CreatorProperty<*> } class CreatorPropertyFactoryBean : diff --git a/src/main/kotlin/creator/custom/types/ExternalCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ExternalCreatorProperty.kt index b51b0e58c..aa1118ffd 100644 --- a/src/main/kotlin/creator/custom/types/ExternalCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/ExternalCreatorProperty.kt @@ -20,20 +20,18 @@ package com.demonwav.mcdev.creator.custom.types +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.demonwav.mcdev.creator.custom.TemplateValidationReporter -import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.openapi.observable.properties.GraphProperty -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.ui.dsl.builder.Panel class ExternalCreatorProperty( descriptor: TemplatePropertyDescriptor = TemplatePropertyDescriptor("", "", "", default = ""), - graph: PropertyGraph, - properties: Map>, + context: CreatorContext, override val graphProperty: GraphProperty, valueType: Class, -) : CreatorProperty(descriptor, graph, properties, valueType) { +) : CreatorProperty(descriptor, context, valueType) { override fun setupProperty(reporter: TemplateValidationReporter) = Unit @@ -46,5 +44,5 @@ class ExternalCreatorProperty( override fun deserialize(string: String): T = throw UnsupportedOperationException("Unsupported for external properties") - override fun buildUi(panel: Panel, context: WizardContext) = Unit + override fun buildUi(panel: Panel) = Unit } diff --git a/src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt index 870c470cb..7964d3e44 100644 --- a/src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt @@ -23,6 +23,7 @@ package com.demonwav.mcdev.creator.custom.types import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.creator.collectMavenVersions import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.demonwav.mcdev.creator.custom.TemplateValidationReporter import com.demonwav.mcdev.creator.custom.model.FabricVersionsModel @@ -30,9 +31,7 @@ import com.demonwav.mcdev.platform.fabric.util.FabricApiVersions import com.demonwav.mcdev.platform.fabric.util.FabricVersions import com.demonwav.mcdev.util.SemanticVersion import com.demonwav.mcdev.util.asyncIO -import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.openapi.observable.properties.GraphProperty -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.openapi.observable.util.bindBooleanStorage import com.intellij.openapi.observable.util.not import com.intellij.openapi.observable.util.transform @@ -53,9 +52,8 @@ import kotlinx.coroutines.withContext class FabricVersionsCreatorProperty( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> -) : CreatorProperty(descriptor, graph, properties, FabricVersionsModel::class.java) { + context: CreatorContext +) : CreatorProperty(descriptor, context, FabricVersionsModel::class.java) { private val emptyVersion = SemanticVersion.release() private val emptyValue = FabricVersionsModel( @@ -135,7 +133,7 @@ class FabricVersionsCreatorProperty( ) } - override fun buildUi(panel: Panel, context: WizardContext) { + override fun buildUi(panel: Panel) { panel.row("") { cell(AsyncProcessIcon("FabricVersions download")) label(MCDevBundle("creator.ui.versions_download.label")) @@ -343,8 +341,7 @@ class FabricVersionsCreatorProperty( override fun create( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> - ): CreatorProperty<*> = FabricVersionsCreatorProperty(descriptor, graph, properties) + context: CreatorContext + ): CreatorProperty<*> = FabricVersionsCreatorProperty(descriptor, context) } } diff --git a/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt index de3464fae..5a7518db4 100644 --- a/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt @@ -22,15 +22,14 @@ package com.demonwav.mcdev.creator.custom.types import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.TemplateEvaluator import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.demonwav.mcdev.creator.custom.TemplateValidationReporter import com.demonwav.mcdev.creator.custom.model.ForgeVersions import com.demonwav.mcdev.platform.forge.version.ForgeVersion import com.demonwav.mcdev.util.SemanticVersion -import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.openapi.observable.properties.GraphProperty -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.openapi.observable.util.not import com.intellij.openapi.observable.util.transform import com.intellij.ui.ComboboxSpeedSearch @@ -40,7 +39,6 @@ import com.intellij.ui.dsl.builder.bindItem import com.intellij.util.application import com.intellij.util.ui.AsyncProcessIcon import javax.swing.DefaultComboBoxModel -import kotlin.collections.Map import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.swing.Swing @@ -48,9 +46,8 @@ import kotlinx.coroutines.withContext class ForgeVersionsCreatorProperty( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> -) : CreatorProperty(descriptor, graph, properties, ForgeVersions::class.java) { + context: CreatorContext +) : CreatorProperty(descriptor, context, ForgeVersions::class.java) { private val emptyVersion = SemanticVersion.release() @@ -92,7 +89,7 @@ class ForgeVersionsCreatorProperty( ) } - override fun buildUi(panel: Panel, context: WizardContext) { + override fun buildUi(panel: Panel) { panel.row("") { cell(AsyncProcessIcon("ForgeVersions download")) label(MCDevBundle("creator.ui.versions_download.label")) @@ -212,8 +209,7 @@ class ForgeVersionsCreatorProperty( class Factory : CreatorPropertyFactory { override fun create( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> - ): CreatorProperty<*> = ForgeVersionsCreatorProperty(descriptor, graph, properties) + context: CreatorContext + ): CreatorProperty<*> = ForgeVersionsCreatorProperty(descriptor, context) } } diff --git a/src/main/kotlin/creator/custom/types/InlineStringListCreatorProperty.kt b/src/main/kotlin/creator/custom/types/InlineStringListCreatorProperty.kt index 67e931edf..daa29a221 100644 --- a/src/main/kotlin/creator/custom/types/InlineStringListCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/InlineStringListCreatorProperty.kt @@ -20,10 +20,9 @@ package com.demonwav.mcdev.creator.custom.types +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.demonwav.mcdev.creator.custom.model.StringList -import com.intellij.ide.util.projectWizard.WizardContext -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.ui.dsl.builder.COLUMNS_LARGE import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.bindText @@ -31,9 +30,8 @@ import com.intellij.ui.dsl.builder.columns class InlineStringListCreatorProperty( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> -) : SimpleCreatorProperty(descriptor, graph, properties, StringList::class.java) { + context: CreatorContext +) : SimpleCreatorProperty(descriptor, context, StringList::class.java) { override fun createDefaultValue(raw: Any?): StringList = deserialize(raw as? String ?: "") @@ -44,7 +42,7 @@ class InlineStringListCreatorProperty( .filter(String::isNotBlank) .run(::StringList) - override fun buildSimpleUi(panel: Panel, context: WizardContext) { + override fun buildSimpleUi(panel: Panel) { panel.row(descriptor.translatedLabel) { this.textField().bindText(this@InlineStringListCreatorProperty.toStringProperty(graphProperty)) .columns(COLUMNS_LARGE) @@ -55,8 +53,7 @@ class InlineStringListCreatorProperty( class Factory : CreatorPropertyFactory { override fun create( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> - ): CreatorProperty<*> = InlineStringListCreatorProperty(descriptor, graph, properties) + context: CreatorContext + ): CreatorProperty<*> = InlineStringListCreatorProperty(descriptor, context) } } diff --git a/src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt b/src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt index bcd6edc6b..a0d849aef 100644 --- a/src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt @@ -20,14 +20,13 @@ package com.demonwav.mcdev.creator.custom.types +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.PropertyDerivation import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.demonwav.mcdev.creator.custom.TemplateValidationReporter import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation import com.demonwav.mcdev.creator.custom.derivation.RecommendJavaVersionForMcVersionPropertyDerivation import com.demonwav.mcdev.creator.custom.derivation.SelectPropertyDerivation -import com.intellij.ide.util.projectWizard.WizardContext -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.ui.dsl.builder.COLUMNS_LARGE import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.bindIntText @@ -35,9 +34,8 @@ import com.intellij.ui.dsl.builder.columns class IntegerCreatorProperty( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> -) : SimpleCreatorProperty(descriptor, graph, properties, Int::class.java) { + context: CreatorContext +) : SimpleCreatorProperty(descriptor, context, Int::class.java) { override fun createDefaultValue(raw: Any?): Int = (raw as? Number)?.toInt() ?: 0 @@ -47,7 +45,7 @@ class IntegerCreatorProperty( override fun convertSelectDerivationResult(original: Any?): Any? = (original as? Number)?.toInt() - override fun buildSimpleUi(panel: Panel, context: WizardContext) { + override fun buildSimpleUi(panel: Panel) { panel.row(descriptor.translatedLabel) { this.intTextField().bindIntText(graphProperty) .columns(COLUMNS_LARGE) @@ -75,8 +73,7 @@ class IntegerCreatorProperty( class Factory : CreatorPropertyFactory { override fun create( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> - ): CreatorProperty<*> = IntegerCreatorProperty(descriptor, graph, properties) + context: CreatorContext + ): CreatorProperty<*> = IntegerCreatorProperty(descriptor, context) } } diff --git a/src/main/kotlin/creator/custom/types/JdkCreatorProperty.kt b/src/main/kotlin/creator/custom/types/JdkCreatorProperty.kt index 02608ed5f..a175e71e6 100644 --- a/src/main/kotlin/creator/custom/types/JdkCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/JdkCreatorProperty.kt @@ -21,11 +21,10 @@ package com.demonwav.mcdev.creator.custom.types import com.demonwav.mcdev.creator.JdkComboBoxWithPreference +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.demonwav.mcdev.creator.custom.model.CreatorJdk import com.demonwav.mcdev.creator.jdkComboBoxWithPreference -import com.intellij.ide.util.projectWizard.WizardContext -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.openapi.observable.util.transform import com.intellij.openapi.projectRoots.JavaSdkVersion import com.intellij.openapi.projectRoots.ProjectJdkTable @@ -33,9 +32,8 @@ import com.intellij.ui.dsl.builder.Panel class JdkCreatorProperty( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> -) : SimpleCreatorProperty(descriptor, graph, properties, CreatorJdk::class.java) { + context: CreatorContext +) : SimpleCreatorProperty(descriptor, context, CreatorJdk::class.java) { private lateinit var jdkComboBox: JdkComboBoxWithPreference @@ -46,10 +44,10 @@ class JdkCreatorProperty( override fun deserialize(string: String): CreatorJdk = CreatorJdk(ProjectJdkTable.getInstance().allJdks.find { it.homePath == string }) - override fun buildSimpleUi(panel: Panel, context: WizardContext) { + override fun buildSimpleUi(panel: Panel) { panel.row(descriptor.translatedLabel) { val sdkProperty = graphProperty.transform(CreatorJdk::sdk, ::CreatorJdk) - jdkComboBox = this.jdkComboBoxWithPreference(context, sdkProperty, descriptor.name).component + jdkComboBox = this.jdkComboBoxWithPreference(wizardContext, sdkProperty, descriptor.name).component val minVersionPropName = descriptor.default as? String if (minVersionPropName != null) { @@ -70,8 +68,7 @@ class JdkCreatorProperty( class Factory : CreatorPropertyFactory { override fun create( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> - ): CreatorProperty<*> = JdkCreatorProperty(descriptor, graph, properties) + context: CreatorContext + ): CreatorProperty<*> = JdkCreatorProperty(descriptor, context) } } diff --git a/src/main/kotlin/creator/custom/types/LicenseCreatorProperty.kt b/src/main/kotlin/creator/custom/types/LicenseCreatorProperty.kt index fc6ae05df..e4a45977e 100644 --- a/src/main/kotlin/creator/custom/types/LicenseCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/LicenseCreatorProperty.kt @@ -20,12 +20,11 @@ package com.demonwav.mcdev.creator.custom.types +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.demonwav.mcdev.creator.custom.model.LicenseData import com.demonwav.mcdev.util.License -import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.openapi.observable.properties.GraphProperty -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.openapi.observable.util.transform import com.intellij.ui.ComboboxSpeedSearch import com.intellij.ui.EnumComboBoxModel @@ -35,9 +34,8 @@ import java.time.ZonedDateTime class LicenseCreatorProperty( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> -) : CreatorProperty(descriptor, graph, properties, LicenseData::class.java) { + context: CreatorContext +) : CreatorProperty(descriptor, context, LicenseData::class.java) { override val graphProperty: GraphProperty = graph.property(createDefaultValue(descriptor.default)) @@ -50,7 +48,7 @@ class LicenseCreatorProperty( override fun deserialize(string: String): LicenseData = LicenseData(string, License.byId(string)?.toString() ?: string, ZonedDateTime.now().year.toString()) - override fun buildUi(panel: Panel, context: WizardContext) { + override fun buildUi(panel: Panel) { panel.row(descriptor.translatedLabel) { val model = EnumComboBoxModel(License::class.java) val licenseEnumProperty = graphProperty.transform( @@ -67,8 +65,7 @@ class LicenseCreatorProperty( override fun create( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> - ): CreatorProperty<*> = LicenseCreatorProperty(descriptor, graph, properties) + context: CreatorContext + ): CreatorProperty<*> = LicenseCreatorProperty(descriptor, context) } } diff --git a/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt b/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt index 733af37ae..ff42c2626 100644 --- a/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt @@ -21,15 +21,14 @@ package com.demonwav.mcdev.creator.custom.types import com.demonwav.mcdev.creator.collectMavenVersions +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.TemplateEvaluator import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.demonwav.mcdev.creator.custom.TemplateValidationReporter import com.demonwav.mcdev.util.SemanticVersion -import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.openapi.diagnostic.getOrLogException import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.observable.properties.GraphProperty -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.ui.ComboboxSpeedSearch import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.bindItem @@ -43,9 +42,8 @@ import kotlinx.coroutines.withContext class MavenArtifactVersionCreatorProperty( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> -) : SemanticVersionCreatorProperty(descriptor, graph, properties) { + context: CreatorContext +) : SemanticVersionCreatorProperty(descriptor, context) { lateinit var sourceUrl: String var rawVersionFilter: (String) -> Boolean = { true } @@ -55,7 +53,7 @@ class MavenArtifactVersionCreatorProperty( private val versionsProperty = graph.property>(emptyList()) private val loadingVersionsProperty = graph.property(true) - override fun buildUi(panel: Panel, context: WizardContext) { + override fun buildUi(panel: Panel) { panel.row(descriptor.translatedLabel) { val combobox = comboBox(versionsProperty.get()) .bindItem(graphProperty) @@ -170,8 +168,7 @@ class MavenArtifactVersionCreatorProperty( override fun create( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> - ): CreatorProperty<*> = MavenArtifactVersionCreatorProperty(descriptor, graph, properties) + context: CreatorContext + ): CreatorProperty<*> = MavenArtifactVersionCreatorProperty(descriptor, context) } } diff --git a/src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt index 10925897d..a24edaba6 100644 --- a/src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt @@ -22,6 +22,7 @@ package com.demonwav.mcdev.creator.custom.types import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.TemplateEvaluator import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.demonwav.mcdev.creator.custom.TemplateValidationReporter @@ -31,9 +32,7 @@ import com.demonwav.mcdev.platform.neoforge.version.NeoGradleVersion import com.demonwav.mcdev.platform.neoforge.version.platform.neoforge.version.NeoModDevVersion import com.demonwav.mcdev.util.SemanticVersion import com.demonwav.mcdev.util.asyncIO -import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.openapi.observable.properties.GraphProperty -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.openapi.observable.util.not import com.intellij.openapi.observable.util.transform import com.intellij.ui.ComboboxSpeedSearch @@ -51,9 +50,8 @@ import kotlinx.coroutines.withContext class NeoForgeVersionsCreatorProperty( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> -) : CreatorProperty(descriptor, graph, properties, NeoForgeVersions::class.java) { + context: CreatorContext +) : CreatorProperty(descriptor, context, NeoForgeVersions::class.java) { private val emptyVersion = SemanticVersion.release() @@ -97,7 +95,7 @@ class NeoForgeVersionsCreatorProperty( ) } - override fun buildUi(panel: Panel, context: WizardContext) { + override fun buildUi(panel: Panel) { panel.row("") { cell(AsyncProcessIcon("NeoForgeVersions download")) label(MCDevBundle("creator.ui.versions_download.label")) @@ -204,8 +202,7 @@ class NeoForgeVersionsCreatorProperty( class Factory : CreatorPropertyFactory { override fun create( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> - ): CreatorProperty<*> = NeoForgeVersionsCreatorProperty(descriptor, graph, properties) + context: CreatorContext + ): CreatorProperty<*> = NeoForgeVersionsCreatorProperty(descriptor, context) } } diff --git a/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt index 360c3d2f9..b50ce3844 100644 --- a/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt @@ -22,14 +22,13 @@ package com.demonwav.mcdev.creator.custom.types import com.demonwav.mcdev.creator.ParchmentVersion import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.demonwav.mcdev.creator.custom.TemplateValidationReporter import com.demonwav.mcdev.creator.custom.model.HasMinecraftVersion import com.demonwav.mcdev.creator.custom.model.ParchmentVersions import com.demonwav.mcdev.util.SemanticVersion -import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.openapi.observable.properties.GraphProperty -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.openapi.observable.util.transform import com.intellij.ui.ComboboxSpeedSearch import com.intellij.ui.dsl.builder.Panel @@ -44,9 +43,8 @@ import kotlinx.coroutines.withContext class ParchmentCreatorProperty( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> -) : CreatorProperty(descriptor, graph, properties, ParchmentVersions::class.java) { + context: CreatorContext +) : CreatorProperty(descriptor, context, ParchmentVersions::class.java) { private val emptyVersion = SemanticVersion.release() @@ -92,7 +90,7 @@ class ParchmentCreatorProperty( ) } - override fun buildUi(panel: Panel, context: WizardContext) { + override fun buildUi(panel: Panel) { panel.row(descriptor.translatedLabel) { checkBox("Use Parchment") .bindSelected(useParchmentProperty) @@ -274,8 +272,7 @@ class ParchmentCreatorProperty( class Factory : CreatorPropertyFactory { override fun create( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> - ): CreatorProperty<*> = ParchmentCreatorProperty(descriptor, graph, properties) + context: CreatorContext + ): CreatorProperty<*> = ParchmentCreatorProperty(descriptor, context) } } diff --git a/src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt b/src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt index f500d03d0..0369fd952 100644 --- a/src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt @@ -20,6 +20,7 @@ package com.demonwav.mcdev.creator.custom.types +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.PropertyDerivation import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.demonwav.mcdev.creator.custom.TemplateValidationReporter @@ -27,8 +28,6 @@ import com.demonwav.mcdev.creator.custom.derivation.ExtractVersionMajorMinorProp import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation import com.demonwav.mcdev.creator.custom.derivation.SelectPropertyDerivation import com.demonwav.mcdev.util.SemanticVersion -import com.intellij.ide.util.projectWizard.WizardContext -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.ui.dsl.builder.COLUMNS_SHORT import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.bindText @@ -36,9 +35,8 @@ import com.intellij.ui.dsl.builder.columns open class SemanticVersionCreatorProperty( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> -) : SimpleCreatorProperty(descriptor, graph, properties, SemanticVersion::class.java) { + context: CreatorContext +) : SimpleCreatorProperty(descriptor, context, SemanticVersion::class.java) { override fun createDefaultValue(raw: Any?): SemanticVersion = SemanticVersion.tryParse(raw as? String ?: "") ?: SemanticVersion(emptyList()) @@ -48,7 +46,7 @@ open class SemanticVersionCreatorProperty( override fun deserialize(string: String): SemanticVersion = SemanticVersion.tryParse(string) ?: SemanticVersion(emptyList()) - override fun buildSimpleUi(panel: Panel, context: WizardContext) { + override fun buildSimpleUi(panel: Panel) { panel.row(descriptor.translatedLabel) { this.textField().bindText(this@SemanticVersionCreatorProperty.toStringProperty(graphProperty)) .columns(COLUMNS_SHORT) @@ -79,8 +77,7 @@ open class SemanticVersionCreatorProperty( class Factory : CreatorPropertyFactory { override fun create( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> - ): CreatorProperty<*> = SemanticVersionCreatorProperty(descriptor, graph, properties) + context: CreatorContext + ): CreatorProperty<*> = SemanticVersionCreatorProperty(descriptor, context) } } diff --git a/src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt b/src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt index 7a735d7fb..43654e250 100644 --- a/src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt @@ -22,10 +22,9 @@ package com.demonwav.mcdev.creator.custom.types import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor -import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.openapi.observable.properties.GraphProperty -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.ui.ComboboxSpeedSearch import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.bindItem @@ -35,10 +34,9 @@ import javax.swing.JList abstract class SimpleCreatorProperty( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map>, + context: CreatorContext, valueType: Class -) : CreatorProperty(descriptor, graph, properties, valueType) { +) : CreatorProperty(descriptor, context, valueType) { private val options: Map? = makeOptionsList() @@ -80,7 +78,7 @@ abstract class SimpleCreatorProperty( override val graphProperty: GraphProperty by lazy { graph.property(defaultValue) } - override fun buildUi(panel: Panel, context: WizardContext) { + override fun buildUi(panel: Panel) { if (isDropdown) { if (graphProperty.get() !in options!!.keys) { graphProperty.set(defaultValue) @@ -112,11 +110,11 @@ abstract class SimpleCreatorProperty( } }.propertyVisibility() } else { - buildSimpleUi(panel, context) + buildSimpleUi(panel) } } - abstract fun buildSimpleUi(panel: Panel, context: WizardContext) + abstract fun buildSimpleUi(panel: Panel) private inner class DropdownAutoRenderer : DefaultListCellRenderer() { diff --git a/src/main/kotlin/creator/custom/types/StringCreatorProperty.kt b/src/main/kotlin/creator/custom/types/StringCreatorProperty.kt index 31582bcc7..675a1e4cf 100644 --- a/src/main/kotlin/creator/custom/types/StringCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/StringCreatorProperty.kt @@ -21,15 +21,14 @@ package com.demonwav.mcdev.creator.custom.types import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.CreatorContext import com.demonwav.mcdev.creator.custom.PropertyDerivation import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor import com.demonwav.mcdev.creator.custom.TemplateValidationReporter import com.demonwav.mcdev.creator.custom.derivation.PreparedDerivation import com.demonwav.mcdev.creator.custom.derivation.ReplacePropertyDerivation import com.demonwav.mcdev.creator.custom.derivation.SelectPropertyDerivation -import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.openapi.observable.properties.GraphProperty -import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.ui.dsl.builder.COLUMNS_LARGE import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.bindText @@ -38,9 +37,8 @@ import com.intellij.ui.dsl.builder.textValidation class StringCreatorProperty( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> -) : SimpleCreatorProperty(descriptor, graph, properties, String::class.java) { + context: CreatorContext +) : SimpleCreatorProperty(descriptor, context, String::class.java) { private var validationRegex: Regex? = null @@ -82,7 +80,7 @@ class StringCreatorProperty( else -> null } - override fun buildSimpleUi(panel: Panel, context: WizardContext) { + override fun buildSimpleUi(panel: Panel) { panel.row(descriptor.translatedLabel) { val textField = textField().bindText(this@StringCreatorProperty.toStringProperty(graphProperty)) .columns(COLUMNS_LARGE) @@ -96,8 +94,7 @@ class StringCreatorProperty( class Factory : CreatorPropertyFactory { override fun create( descriptor: TemplatePropertyDescriptor, - graph: PropertyGraph, - properties: Map> - ): CreatorProperty<*> = StringCreatorProperty(descriptor, graph, properties) + context: CreatorContext + ): CreatorProperty<*> = StringCreatorProperty(descriptor, context) } } From 8537ff03ee4f537fd6fcc5d01afc33222c5b284f Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 16 Jul 2024 16:58:49 +0200 Subject: [PATCH 10/37] Migrate creator versions download to coroutines --- .../kotlin/creator/custom/CreatorContext.kt | 28 +++++++++- .../creator/custom/CustomPlatformStep.kt | 4 +- .../ArchitecturyVersionsCreatorProperty.kt | 54 +++++++++---------- .../types/FabricVersionsCreatorProperty.kt | 45 ++++++++-------- .../types/ForgeVersionsCreatorProperty.kt | 21 ++++---- .../MavenArtifactVersionCreatorProperty.kt | 37 +++++++------ .../types/NeoForgeVersionsCreatorProperty.kt | 47 ++++++++-------- .../custom/types/ParchmentCreatorProperty.kt | 23 ++++---- 8 files changed, 136 insertions(+), 123 deletions(-) diff --git a/src/main/kotlin/creator/custom/CreatorContext.kt b/src/main/kotlin/creator/custom/CreatorContext.kt index 813e9d803..c5a4ba3ee 100644 --- a/src/main/kotlin/creator/custom/CreatorContext.kt +++ b/src/main/kotlin/creator/custom/CreatorContext.kt @@ -21,11 +21,37 @@ package com.demonwav.mcdev.creator.custom import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.demonwav.mcdev.creator.modalityState import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.asContextElement import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.util.namedChildScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlin.coroutines.CoroutineContext data class CreatorContext( val graph: PropertyGraph, val properties: Map>, val wizardContext: WizardContext, -) + val scope: CoroutineScope +) { + val modalityState: ModalityState + get() = wizardContext.modalityState + + val coroutineContext: CoroutineContext + get() = modalityState.asContextElement() + + /** + * The CoroutineContext to use when a change has to be made to the creator UI + */ + val uiContext: CoroutineContext + get() = Dispatchers.EDT + coroutineContext + + /** + * A general purpose scope dependent of the main creator scope, cancelled when the creator is closed. + */ + fun childScope(name: String): CoroutineScope = scope.namedChildScope(name) +} diff --git a/src/main/kotlin/creator/custom/CustomPlatformStep.kt b/src/main/kotlin/creator/custom/CustomPlatformStep.kt index b3133a8d4..73517b8c6 100644 --- a/src/main/kotlin/creator/custom/CustomPlatformStep.kt +++ b/src/main/kotlin/creator/custom/CustomPlatformStep.kt @@ -91,6 +91,7 @@ class CustomPlatformStep( parent: NewProjectWizardStep, ) : AbstractNewProjectWizardStep(parent) { + val creatorScope = TemplateService.instance.scope("MinecraftDev Creator") val creatorUiScope = TemplateService.instance.scope("MinecraftDev Creator UI") val templateRepos = MinecraftSettings.instance.creatorTemplateRepos @@ -126,10 +127,11 @@ class CustomPlatformStep( private var hasTemplateErrors: Boolean = true private var properties = mutableMapOf>() - private var creatorContext = CreatorContext(propertyGraph, properties, context) + private var creatorContext = CreatorContext(propertyGraph, properties, context, creatorScope) init { Disposer.register(context.disposable) { + creatorScope.cancel("The creator got disposed") creatorUiScope.cancel("The creator got disposed") } } diff --git a/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt index d3738f28b..fab70421b 100644 --- a/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt @@ -49,8 +49,7 @@ import com.intellij.util.ui.AsyncProcessIcon import javax.swing.DefaultComboBoxModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class ArchitecturyVersionsCreatorProperty( @@ -280,7 +279,7 @@ class ArchitecturyVersionsCreatorProperty( updateArchitecturyApiVersions() } - downloadVersions { + downloadVersions(context) { val fabricVersions = fabricVersions if (fabricVersions != null) { loaderVersionModel.removeAllElements() @@ -433,36 +432,35 @@ class ArchitecturyVersionsCreatorProperty( private var fabricApiVersions: FabricApiVersions? = null private var architecturyVersions: ArchitecturyVersion? = null - private fun downloadVersions(completeCallback: () -> Unit) { + private fun downloadVersions(context: CreatorContext, completeCallback: () -> Unit) { if (hasDownloadedVersions) { completeCallback() return } - application.executeOnPooledThread { - runBlocking { - awaitAll( - asyncIO { ForgeVersion.downloadData().also { forgeVersions = it } }, - asyncIO { NeoForgeVersion.downloadData().also { neoForgeVersions = it } }, - asyncIO { FabricVersions.downloadData().also { fabricVersions = it } }, - asyncIO { - collectMavenVersions( - "https://maven.architectury.dev/dev/architectury/architectury-loom/maven-metadata.xml" - ).also { - loomVersions = it - .mapNotNull(SemanticVersion::tryParse) - .sortedDescending() - } - }, - asyncIO { FabricApiVersions.downloadData().also { fabricApiVersions = it } }, - asyncIO { ArchitecturyVersion.downloadData().also { architecturyVersions = it } }, - ) - - hasDownloadedVersions = true - - withContext(Dispatchers.Swing) { - completeCallback() - } + val scope = context.childScope("ArchitecturyVersionsCreatorProperty") + scope.launch(Dispatchers.Default) { + awaitAll( + asyncIO { ForgeVersion.downloadData().also { forgeVersions = it } }, + asyncIO { NeoForgeVersion.downloadData().also { neoForgeVersions = it } }, + asyncIO { FabricVersions.downloadData().also { fabricVersions = it } }, + asyncIO { + collectMavenVersions( + "https://maven.architectury.dev/dev/architectury/architectury-loom/maven-metadata.xml" + ).also { + loomVersions = it + .mapNotNull(SemanticVersion::tryParse) + .sortedDescending() + } + }, + asyncIO { FabricApiVersions.downloadData().also { fabricApiVersions = it } }, + asyncIO { ArchitecturyVersion.downloadData().also { architecturyVersions = it } }, + ) + + hasDownloadedVersions = true + + withContext(context.uiContext) { + completeCallback() } } } diff --git a/src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt index 7964d3e44..e7745ae48 100644 --- a/src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt @@ -41,13 +41,11 @@ import com.intellij.ui.JBColor import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.bindItem import com.intellij.ui.dsl.builder.bindSelected -import com.intellij.util.application import com.intellij.util.ui.AsyncProcessIcon import javax.swing.DefaultComboBoxModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class FabricVersionsCreatorProperty( @@ -220,7 +218,7 @@ class FabricVersionsCreatorProperty( updateFabricApiVersions() } - downloadVersion { + downloadVersion(context) { val fabricVersions = fabricVersions if (fabricVersions != null) { loaderVersionModel.removeAllElements() @@ -307,31 +305,30 @@ class FabricVersionsCreatorProperty( private var loomVersions: List? = null private var fabricApiVersions: FabricApiVersions? = null - private fun downloadVersion(uiCallback: () -> Unit) { + private fun downloadVersion(context: CreatorContext, uiCallback: () -> Unit) { if (hasDownloadedVersions) { uiCallback() return } - application.executeOnPooledThread { - runBlocking { - awaitAll( - asyncIO { FabricVersions.downloadData().also { fabricVersions = it } }, - asyncIO { - collectMavenVersions( - "https://maven.fabricmc.net/net/fabricmc/fabric-loom/maven-metadata.xml" - ).mapNotNull(SemanticVersion::tryParse) - .sortedDescending() - .also { loomVersions = it } - }, - asyncIO { FabricApiVersions.downloadData().also { fabricApiVersions = it } }, - ) - - hasDownloadedVersions = true - - withContext(Dispatchers.Swing) { - uiCallback() - } + val scope = context.childScope("FabricVersionsCreatorProperty") + scope.launch(Dispatchers.Default) { + awaitAll( + asyncIO { FabricVersions.downloadData().also { fabricVersions = it } }, + asyncIO { + collectMavenVersions( + "https://maven.fabricmc.net/net/fabricmc/fabric-loom/maven-metadata.xml" + ).mapNotNull(SemanticVersion::tryParse) + .sortedDescending() + .also { loomVersions = it } + }, + asyncIO { FabricApiVersions.downloadData().also { fabricApiVersions = it } }, + ) + + hasDownloadedVersions = true + + withContext(context.uiContext) { + uiCallback() } } } diff --git a/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt index 5a7518db4..5503a381a 100644 --- a/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt @@ -36,12 +36,10 @@ import com.intellij.ui.ComboboxSpeedSearch import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.RightGap import com.intellij.ui.dsl.builder.bindItem -import com.intellij.util.application import com.intellij.util.ui.AsyncProcessIcon import javax.swing.DefaultComboBoxModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class ForgeVersionsCreatorProperty( @@ -148,7 +146,7 @@ class ForgeVersionsCreatorProperty( } } - downloadVersions { + downloadVersions(context) { reloadMinecraftVersions() loadingVersionsProperty.set(false) @@ -186,21 +184,20 @@ class ForgeVersionsCreatorProperty( private var forgeVersion: ForgeVersion? = null - private fun downloadVersions(uiCallback: () -> Unit) { + private fun downloadVersions(context: CreatorContext, uiCallback: () -> Unit) { if (hasDownloadedVersions) { uiCallback() return } - application.executeOnPooledThread { - runBlocking { - forgeVersion = ForgeVersion.downloadData() + val scope = context.childScope("ForgeVersionsCreatorProperty") + scope.launch(Dispatchers.IO) { + forgeVersion = ForgeVersion.downloadData() - hasDownloadedVersions = true + hasDownloadedVersions = true - withContext(Dispatchers.Swing) { - uiCallback() - } + withContext(context.uiContext) { + uiCallback() } } } diff --git a/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt b/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt index ff42c2626..9c9016db1 100644 --- a/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt @@ -32,12 +32,10 @@ import com.intellij.openapi.observable.properties.GraphProperty import com.intellij.ui.ComboboxSpeedSearch import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.bindItem -import com.intellij.util.application import com.intellij.util.ui.AsyncProcessIcon import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class MavenArtifactVersionCreatorProperty( @@ -110,6 +108,7 @@ class MavenArtifactVersionCreatorProperty( } downloadVersions( + context, // The key might be a bit too unique, but that'll do the job descriptor.name + "@" + descriptor.hashCode(), sourceUrl, @@ -127,6 +126,7 @@ class MavenArtifactVersionCreatorProperty( private var versionsCache = ConcurrentHashMap>() private fun downloadVersions( + context: CreatorContext, key: String, url: String, rawVersionFilter: (String) -> Boolean, @@ -143,22 +143,21 @@ class MavenArtifactVersionCreatorProperty( return } - application.executeOnPooledThread { - runBlocking { - val versions = collectMavenVersions(url) - .asSequence() - .filter(rawVersionFilter) - .mapNotNull(SemanticVersion::tryParse) - .filter(versionFilter) - .sortedDescending() - .take(limit) - .toList() - - versionsCache[cacheKey] = versions - - withContext(Dispatchers.Swing) { - uiCallback(versions) - } + val scope = context.childScope("MavenArtifactVersionCreatorProperty") + scope.launch(Dispatchers.Default) { + val versions = withContext(Dispatchers.IO) { collectMavenVersions(url) } + .asSequence() + .filter(rawVersionFilter) + .mapNotNull(SemanticVersion::tryParse) + .filter(versionFilter) + .sortedDescending() + .take(limit) + .toList() + + versionsCache[cacheKey] = versions + + withContext(context.uiContext) { + uiCallback(versions) } } } diff --git a/src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt index a24edaba6..e32e15d64 100644 --- a/src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt @@ -39,13 +39,11 @@ import com.intellij.ui.ComboboxSpeedSearch import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.RightGap import com.intellij.ui.dsl.builder.bindItem -import com.intellij.util.application import com.intellij.util.ui.AsyncProcessIcon import javax.swing.DefaultComboBoxModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class NeoForgeVersionsCreatorProperty( @@ -135,7 +133,7 @@ class NeoForgeVersionsCreatorProperty( } val mcVersionFilter = descriptor.parameters?.get("mcVersionFilter") as? String - downloadVersion(mcVersionFilter) { + downloadVersion(context, mcVersionFilter) { val mcVersions = mcVersions ?: return@downloadVersion mcVersionsModel.removeAllElements() @@ -164,36 +162,35 @@ class NeoForgeVersionsCreatorProperty( private var mdVersion: NeoModDevVersion? = null private var mcVersions: List? = null - private fun downloadVersion(mcVersionFilter: String?, uiCallback: () -> Unit) { + private fun downloadVersion(context: CreatorContext, mcVersionFilter: String?, uiCallback: () -> Unit) { if (hasDownloadedVersions) { uiCallback() return } - application.executeOnPooledThread { - runBlocking { - awaitAll( - asyncIO { NeoForgeVersion.downloadData().also { nfVersion = it } }, - asyncIO { NeoGradleVersion.downloadData().also { ngVersion = it } }, - asyncIO { NeoModDevVersion.downloadData().also { mdVersion = it } }, - ) - - mcVersions = nfVersion?.sortedMcVersions?.let { mcVersion -> - if (mcVersionFilter != null) { - mcVersion.filter { version -> - val conditionProps = mapOf("MC_VERSION" to version) - TemplateEvaluator.condition(conditionProps, mcVersionFilter).getOrDefault(true) - } - } else { - mcVersion + val scope = context.childScope("NeoForgeVersionsCreatorProperty") + scope.launch(Dispatchers.Default) { + awaitAll( + asyncIO { NeoForgeVersion.downloadData().also { nfVersion = it } }, + asyncIO { NeoGradleVersion.downloadData().also { ngVersion = it } }, + asyncIO { NeoModDevVersion.downloadData().also { mdVersion = it } }, + ) + + mcVersions = nfVersion?.sortedMcVersions?.let { mcVersion -> + if (mcVersionFilter != null) { + mcVersion.filter { version -> + val conditionProps = mapOf("MC_VERSION" to version) + TemplateEvaluator.condition(conditionProps, mcVersionFilter).getOrDefault(true) } + } else { + mcVersion } + } - hasDownloadedVersions = true + hasDownloadedVersions = true - withContext(Dispatchers.Swing) { - uiCallback() - } + withContext(context.uiContext) { + uiCallback() } } } diff --git a/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt index b50ce3844..e8411b8e7 100644 --- a/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt @@ -34,11 +34,9 @@ import com.intellij.ui.ComboboxSpeedSearch import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.bindItem import com.intellij.ui.dsl.builder.bindSelected -import com.intellij.util.application import javax.swing.DefaultComboBoxModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class ParchmentCreatorProperty( @@ -167,7 +165,7 @@ class ParchmentCreatorProperty( refreshVersionsLists() } - downloadVersions { + downloadVersions(context) { refreshVersionsLists() val minecraftVersion = getPlatformMinecraftVersion() @@ -248,22 +246,21 @@ class ParchmentCreatorProperty( private var allParchmentVersions: List? = null - private fun downloadVersions(uiCallback: () -> Unit) { + private fun downloadVersions(context: CreatorContext, uiCallback: () -> Unit) { if (hasDownloadedVersions) { uiCallback() return } - application.executeOnPooledThread { - runBlocking { - allParchmentVersions = ParchmentVersion.downloadData() - .sortedByDescending(ParchmentVersion::parchmentVersion) + val scope = context.childScope("ParchmentCreatorProperty") + scope.launch(Dispatchers.IO) { + allParchmentVersions = ParchmentVersion.downloadData() + .sortedByDescending(ParchmentVersion::parchmentVersion) - hasDownloadedVersions = true + hasDownloadedVersions = true - withContext(Dispatchers.Swing) { - uiCallback() - } + withContext(context.uiContext) { + uiCallback() } } } From 8d8b3acd4586ca9042c96f5408185ba519eecce9 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Wed, 17 Jul 2024 14:55:52 +0200 Subject: [PATCH 11/37] Fix #2331 Support fabric.mod.json in test resources --- .../FabricModJsonResolveScopeEnlarger.kt | 5 +- .../fabric/reference/ResourceFileReference.kt | 53 ++++++++++++++----- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/platform/fabric/reference/FabricModJsonResolveScopeEnlarger.kt b/src/main/kotlin/platform/fabric/reference/FabricModJsonResolveScopeEnlarger.kt index 46696071f..03490fc11 100644 --- a/src/main/kotlin/platform/fabric/reference/FabricModJsonResolveScopeEnlarger.kt +++ b/src/main/kotlin/platform/fabric/reference/FabricModJsonResolveScopeEnlarger.kt @@ -25,6 +25,7 @@ import com.demonwav.mcdev.platform.mcp.fabricloom.FabricLoomData import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.module.ModuleUtilCore import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ProjectRootManager import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.ResolveScopeEnlarger import com.intellij.psi.search.GlobalSearchScope @@ -49,11 +50,13 @@ class FabricModJsonResolveScopeEnlarger : ResolveScopeEnlarger() { val moduleScopes = mutableListOf() val moduleManager = ModuleManager.getInstance(project) val parentPath = module.name.substringBeforeLast('.') + val rootType = ProjectRootManager.getInstance(project).fileIndex.getContainingSourceRootType(file) + ?: return null for ((_, sourceSets) in modSourceSets) { for (sourceSet in sourceSets) { val childModule = moduleManager.findModuleByName("$parentPath.$sourceSet") if (childModule != null) { - moduleScopes.add(childModule.getModuleScope(false)) + moduleScopes.add(childModule.getModuleScope(rootType.isForTests)) } } } diff --git a/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt b/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt index 1088b03e9..c5dbd702f 100644 --- a/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt +++ b/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt @@ -30,8 +30,9 @@ import com.intellij.json.psi.JsonStringLiteral import com.intellij.openapi.application.runReadAction import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.module.ModuleUtilCore import com.intellij.openapi.project.rootManager -import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.roots.ProjectRootManager import com.intellij.openapi.vfs.findPsiFile import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile @@ -54,24 +55,33 @@ class ResourceFileReference( private inner class Reference(desc: String, element: JsonStringLiteral) : PsiReferenceBase(element), InspectionReference { + + val isInTestSourceSet: Boolean = run { + val containingVFile = element.containingFile.originalFile.virtualFile + val inTestSourceContent = + ProjectRootManager.getInstance(element.project).fileIndex.isInTestSourceContent(containingVFile) + inTestSourceContent + } + override val description = desc override val unresolved = resolve() == null override fun resolve(): PsiElement? { fun findFileIn(module: Module): PsiFile? { val facet = MinecraftFacet.getInstance(module) ?: return null - val virtualFile = facet.findFile(element.value, SourceType.RESOURCE) ?: return null + var virtualFile = facet.findFile(element.value, SourceType.RESOURCE) + if (virtualFile == null && isInTestSourceSet) { + virtualFile = facet.findFile(element.value, SourceType.TEST_RESOURCE) + } + + if (virtualFile == null) { + return null + } + return PsiManager.getInstance(element.project).findFile(virtualFile) } - val module = element.findModule() ?: return null - return findFileIn(module) - ?: ModuleRootManager.getInstance(module) - .getDependencies(false) - .mapFirstNotNull(::findFileIn) - ?: ModuleManager.getInstance(element.project) - .getModuleDependentModules(module) - .mapFirstNotNull(::findFileIn) + return getRelevantModules().mapFirstNotNull(::findFileIn) } override fun bindToElement(newTarget: PsiElement): PsiElement? { @@ -87,13 +97,19 @@ class ResourceFileReference( return emptyArray() } - val module = element.findModule() ?: return emptyArray() val variants = mutableListOf() - val relevantModules = ModuleManager.getInstance(element.project).getModuleDependentModules(module) + module runReadAction { + val relevantModules = getRelevantModules() + + val relevantRootTypes = mutableSetOf(JavaResourceRootType.RESOURCE) + if (isInTestSourceSet) { + relevantRootTypes.add(JavaResourceRootType.TEST_RESOURCE) + } + val relevantRoots = relevantModules.flatMap { - it.rootManager.getSourceRoots(JavaResourceRootType.RESOURCE) + it.rootManager.getSourceRoots(relevantRootTypes) } + for (roots in relevantRoots) { for (child in roots.children) { val relativePath = child.path.removePrefix(roots.path) @@ -107,4 +123,15 @@ class ResourceFileReference( return variants.toTypedArray() } } + + private fun Reference.getRelevantModules(): Set { + val module = element.findModule() ?: return emptySet() + val relevantModules = mutableSetOf() + val moduleManager = ModuleManager.getInstance(element.project) + ModuleUtilCore.getDependencies(module, relevantModules) + relevantModules.flatMapTo(relevantModules) { + moduleManager.getModuleDependentModules(it) + } + return relevantModules + } } From f696271df2a35b9e9b04ad6b3db1c0631090e29e Mon Sep 17 00:00:00 2001 From: RedNesto Date: Wed, 17 Jul 2024 14:56:36 +0200 Subject: [PATCH 12/37] Provide access widener variants --- .../platform/fabric/reference/FabricReferenceContributor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt b/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt index 758e3e890..baebaa80e 100644 --- a/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt +++ b/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt @@ -58,7 +58,7 @@ class FabricReferenceContributor : PsiReferenceContributor() { registrar.registerReferenceProvider( stringInModJson.isPropertyValue("accessWidener"), - ResourceFileReference("access widener '%s'"), + ResourceFileReference("access widener '%s'", Regex("(.+)\\.accesswidener")), ) registrar.registerReferenceProvider( From a3c37a440b60737dbde2553d7bb8af20d300b332 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Wed, 17 Jul 2024 14:58:38 +0200 Subject: [PATCH 13/37] Fix CreatorContext import order --- src/main/kotlin/creator/custom/CreatorContext.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/creator/custom/CreatorContext.kt b/src/main/kotlin/creator/custom/CreatorContext.kt index c5a4ba3ee..d1f50ec1a 100644 --- a/src/main/kotlin/creator/custom/CreatorContext.kt +++ b/src/main/kotlin/creator/custom/CreatorContext.kt @@ -28,9 +28,9 @@ import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.asContextElement import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.util.namedChildScope +import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlin.coroutines.CoroutineContext data class CreatorContext( val graph: PropertyGraph, From e0988592d27d66a165e9e53064bffea506c40de7 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 18 Jul 2024 12:42:36 +0200 Subject: [PATCH 14/37] Add changelog entries for the last few commits --- changelog.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/changelog.md b/changelog.md index a833c4f44..7c0e0c26f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,16 @@ # Minecraft Development for IntelliJ +## [Unreleased] + +### Added + +- Access widener completion in fabric.mod.json + +### Fixed + +- [#2330](https://github.com/minecraft-dev/MinecraftDev/issues/2330) Reformat created files without keeping line breaks. Fixes the Velocity main class annotation's bad formatting. +- [#2331](https://github.com/minecraft-dev/MinecraftDev/issues/2331) Support fabric.mod.json in test resources + ## [1.8.0] This release contains two major features: From df8907cfcc86dbb3df9a0955ed3238c0fc21060a Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 18 Jul 2024 12:43:08 +0200 Subject: [PATCH 15/37] Bump current release in readme --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 6d607a566..073ae7d5f 100644 --- a/readme.md +++ b/readme.md @@ -35,7 +35,7 @@ Minecraft Development for IntelliJ -Info and Documentation [![Current Release](https://img.shields.io/badge/release-1.7.6-orange.svg?style=flat-square)](https://plugins.jetbrains.com/plugin/8327) +Info and Documentation [![Current Release](https://img.shields.io/badge/release-1.8.0-orange.svg?style=flat-square)](https://plugins.jetbrains.com/plugin/8327) ---------------------- From 2293169cc59ae81f90be1ec2fb6216916473dcab Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Sun, 21 Jul 2024 00:29:10 +0100 Subject: [PATCH 16/37] MixinExtras Expressions: Resolve `FlowMap`s sometimes becoming desynced from their `MethodNode`s, causing expression targets to be unresolved until the IDE is restarted. (#2335) --- .../platform/mixin/expression/MEExpressionMatchUtil.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt index 541cdd455..8fc00d95f 100644 --- a/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt @@ -25,7 +25,6 @@ import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor import com.demonwav.mcdev.platform.mixin.util.LocalInfo import com.demonwav.mcdev.platform.mixin.util.MixinConstants -import com.demonwav.mcdev.platform.mixin.util.cached import com.demonwav.mcdev.util.MemberReference import com.demonwav.mcdev.util.computeStringArray import com.demonwav.mcdev.util.constantStringValue @@ -33,6 +32,7 @@ import com.demonwav.mcdev.util.descriptor import com.demonwav.mcdev.util.findAnnotations import com.demonwav.mcdev.util.resolveType import com.demonwav.mcdev.util.resolveTypeArray +import com.github.benmanes.caffeine.cache.Caffeine import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.module.Module import com.intellij.openapi.progress.ProcessCanceledException @@ -78,12 +78,14 @@ object MEExpressionMatchUtil { ExpressionService.offerInstance(MEExpressionService) } - fun getFlowMap(project: Project, classIn: ClassNode, methodIn: MethodNode): FlowMap? { + private val flowCache = Caffeine.newBuilder().weakKeys().build() + + fun getFlowMap(project: Project, classNode: ClassNode, methodIn: MethodNode): FlowMap? { if (methodIn.instructions == null) { return null } - return methodIn.cached(classIn, project) { classNode, methodNode -> + return flowCache.asMap().computeIfAbsent(methodIn) { methodNode -> val interpreter = object : FlowInterpreter(classNode, methodNode, MEFlowContext(project)) { override fun newValue(type: Type?): FlowValue? { ProgressManager.checkCanceled() @@ -147,7 +149,7 @@ object MEExpressionMatchUtil { throw e } LOGGER.warn("MEExpressionMatchUtil.getFlowMap failed", e) - return@cached null + return@computeIfAbsent null } interpreter.finish().asSequence().mapNotNull { flow -> flow.virtualInsnOrNull?.let { it to flow } }.toMap() From 3f2c1217554a95475dca6c60060f0dbfa6506d7a Mon Sep 17 00:00:00 2001 From: joe Date: Sun, 21 Jul 2024 17:22:34 +0100 Subject: [PATCH 17/37] Implement JUMP injection point (without source navigation) --- .../injectionPoint/JumpInjectionPoint.kt | 102 ++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 1 + 2 files changed, 103 insertions(+) create mode 100644 src/main/kotlin/platform/mixin/handlers/injectionPoint/JumpInjectionPoint.kt diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/JumpInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/JumpInjectionPoint.kt new file mode 100644 index 000000000..b55563c8d --- /dev/null +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/JumpInjectionPoint.kt @@ -0,0 +1,102 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint + +import com.demonwav.mcdev.platform.mixin.reference.MixinSelector +import com.demonwav.mcdev.platform.mixin.util.findOrConstructSourceMethod +import com.demonwav.mcdev.util.constantValue +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import org.objectweb.asm.Opcodes +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.JumpInsnNode +import org.objectweb.asm.tree.MethodNode + +class JumpInjectionPoint : InjectionPoint() { + companion object { + private val VALID_OPCODES = setOf( + Opcodes.IFEQ, + Opcodes.IFNE, + Opcodes.IFLT, + Opcodes.IFGE, + Opcodes.IFGT, + Opcodes.IFLE, + Opcodes.IF_ICMPEQ, + Opcodes.IF_ICMPNE, + Opcodes.IF_ICMPLT, + Opcodes.IF_ICMPGE, + Opcodes.IF_ICMPGT, + Opcodes.IF_ICMPLE, + Opcodes.IF_ACMPEQ, + Opcodes.IF_ACMPNE, + Opcodes.GOTO, + Opcodes.JSR, + Opcodes.IFNULL, + Opcodes.IFNONNULL, + ) + } + + override fun createNavigationVisitor( + at: PsiAnnotation, + target: MixinSelector?, + targetClass: PsiClass + ): NavigationVisitor? { + // TODO: jump target source navigation? This would be extremely hard + return null + } + + override fun doCreateCollectVisitor( + at: PsiAnnotation, + target: MixinSelector?, + targetClass: ClassNode, + mode: CollectVisitor.Mode + ): CollectVisitor { + val opcode = (at.findDeclaredAttributeValue("opcode")?.constantValue as? Int) + ?.takeIf { it in VALID_OPCODES } ?: -1 + return MyCollectVisitor(at.project, targetClass, mode, opcode) + } + + override fun createLookup( + targetClass: ClassNode, + result: CollectVisitor.Result + ): LookupElementBuilder? { + return null + } + + private class MyCollectVisitor( + private val project: Project, + private val clazz: ClassNode, + mode: Mode, + private val opcode: Int + ) : CollectVisitor(mode) { + override fun accept(methodNode: MethodNode) { + val insns = methodNode.instructions ?: return + insns.iterator().forEachRemaining { insn -> + if (insn is JumpInsnNode && (opcode == -1 || insn.opcode == opcode)) { + addResult(insn, methodNode.findOrConstructSourceMethod(clazz, project)) + } + } + } + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 362a1fab0..975439497 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -195,6 +195,7 @@ + From faef71f90ea7e46e0f0f483b0ce7ae834ca5059d Mon Sep 17 00:00:00 2001 From: joe Date: Sun, 21 Jul 2024 17:40:24 +0100 Subject: [PATCH 18/37] Discourage usage of JUMP --- .../handlers/injectionPoint/InjectionPoint.kt | 2 + .../injectionPoint/JumpInjectionPoint.kt | 2 + .../DiscouragedInjectionPointInspection.kt | 47 +++++++++++++++++++ .../reference/InjectionPointReference.kt | 3 ++ src/main/resources/META-INF/plugin.xml | 8 ++++ 5 files changed, 62 insertions(+) create mode 100644 src/main/kotlin/platform/mixin/inspection/injector/DiscouragedInjectionPointInspection.kt diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt index 17a917eef..903b129d3 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt @@ -108,6 +108,8 @@ abstract class InjectionPoint { open fun isArgValueList(at: PsiAnnotation, key: String) = false + open val isDiscouraged: Boolean = false + abstract fun createNavigationVisitor( at: PsiAnnotation, target: MixinSelector?, diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/JumpInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/JumpInjectionPoint.kt index b55563c8d..770db81bd 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/JumpInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/JumpInjectionPoint.kt @@ -57,6 +57,8 @@ class JumpInjectionPoint : InjectionPoint() { ) } + override val isDiscouraged = true + override fun createNavigationVisitor( at: PsiAnnotation, target: MixinSelector?, diff --git a/src/main/kotlin/platform/mixin/inspection/injector/DiscouragedInjectionPointInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/DiscouragedInjectionPointInspection.kt new file mode 100644 index 000000000..6989fa9a5 --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/injector/DiscouragedInjectionPointInspection.kt @@ -0,0 +1,47 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.platform.mixin.inspection.injector + +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InjectionPoint +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.constantStringValue +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElementVisitor + +class DiscouragedInjectionPointInspection : MixinInspection() { + override fun getStaticDescription() = "Reports when a discouraged injection point is used" + + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = object : JavaElementVisitor() { + override fun visitAnnotation(annotation: PsiAnnotation) { + if (!annotation.hasQualifiedName(MixinConstants.Annotations.AT)) { + return + } + val atValue = annotation.findDeclaredAttributeValue("value") ?: return + val atCode = atValue.constantStringValue ?: return + if (InjectionPoint.byAtCode(atCode)?.isDiscouraged == true) { + holder.registerProblem(atValue, "Usage of $atCode is discouraged") + } + } + } +} diff --git a/src/main/kotlin/platform/mixin/reference/InjectionPointReference.kt b/src/main/kotlin/platform/mixin/reference/InjectionPointReference.kt index 4e963eb22..2059034fb 100644 --- a/src/main/kotlin/platform/mixin/reference/InjectionPointReference.kt +++ b/src/main/kotlin/platform/mixin/reference/InjectionPointReference.kt @@ -87,6 +87,9 @@ object InjectionPointReference : ReferenceResolver(), MixinReference { override fun collectVariants(context: PsiElement): Array { return ( getAllAtCodes(context.project).keys.asSequence() + .filter { + InjectionPoint.byAtCode(it)?.isDiscouraged != true + } .map { PrioritizedLookupElement.withPriority( LookupElementBuilder.create(it).completeInjectionPoint(context), diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 975439497..562127f59 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1018,6 +1018,14 @@ level="WARNING" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.mixin.inspection.injector.CtorHeadPostInitInspection"/> + From 91d44de7eceeb275ca9a6e1ee2099e3788d245f3 Mon Sep 17 00:00:00 2001 From: joe Date: Sun, 21 Jul 2024 22:46:25 +0100 Subject: [PATCH 19/37] Fix `@ModifyVariable` method signature checking with `STORE`. Closes #2163 --- .../platform/mixin/handlers/ModifyVariableHandler.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt index 42398c23d..ffc8799c0 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt @@ -23,6 +23,7 @@ package com.demonwav.mcdev.platform.mixin.handlers import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AbstractLoadInjectionPoint import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InjectionPoint +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.StoreInjectionPoint import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup import com.demonwav.mcdev.platform.mixin.util.LocalInfo @@ -47,7 +48,9 @@ class ModifyVariableHandler : InjectorAnnotationHandler() { val at = annotation.findAttributeValue("at") as? PsiAnnotation val atCode = at?.findAttributeValue("value")?.constantStringValue - val isLoadStore = atCode != null && InjectionPoint.byAtCode(atCode) is AbstractLoadInjectionPoint + val injectionPoint = atCode?.let { InjectionPoint.byAtCode(atCode) } + val isLoadStore = injectionPoint is AbstractLoadInjectionPoint + val isStore = injectionPoint is StoreInjectionPoint val mode = if (isLoadStore) CollectVisitor.Mode.COMPLETION else CollectVisitor.Mode.MATCH_ALL val targets = resolveInstructions(annotation, targetClass, targetMethod, mode) @@ -63,7 +66,8 @@ class ModifyVariableHandler : InjectorAnnotationHandler() { val possibleTypes = mutableSetOf() for (insn in targets) { - val locals = info.getLocals(module, targetClass, targetMethod, insn.insn) ?: continue + val actualInsn = if (isStore) insn.insn.next ?: insn.insn else insn.insn + val locals = info.getLocals(module, targetClass, targetMethod, actualInsn) ?: continue val matchedLocals = info.matchLocals(locals, CollectVisitor.Mode.COMPLETION, matchType = false) for (local in matchedLocals) { possibleTypes += local.desc!! From 3a055f849c1cacf62cf10a90e8b0298ded082cfe Mon Sep 17 00:00:00 2001 From: joe Date: Sun, 21 Jul 2024 23:17:13 +0100 Subject: [PATCH 20/37] Discourage brittle usages of shifting by n instructions --- .../handlers/injectionPoint/AtResolver.kt | 20 ++++++++ .../ConstantStringMethodInjectionPoint.kt | 5 ++ .../injectionPoint/FieldInjectionPoint.kt | 5 ++ .../handlers/injectionPoint/InjectionPoint.kt | 25 ++-------- .../injectionPoint/InvokeInjectionPoint.kt | 18 +++++++ .../injectionPoint/JumpInjectionPoint.kt | 2 +- .../injectionPoint/LoadInjectionPoint.kt | 18 +++++++ .../DiscouragedInjectionPointInspection.kt | 5 +- .../injector/DiscouragedShiftInspection.kt | 50 +++++++++++++++++++ .../reference/InjectionPointReference.kt | 2 +- src/main/resources/META-INF/plugin.xml | 8 +++ 11 files changed, 133 insertions(+), 25 deletions(-) create mode 100644 src/main/kotlin/platform/mixin/inspection/injector/DiscouragedShiftInspection.kt diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt index ec61f4a4c..c35525bd1 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt @@ -25,24 +25,29 @@ import com.demonwav.mcdev.platform.mixin.reference.isMiscDynamicSelector import com.demonwav.mcdev.platform.mixin.reference.parseMixinSelector import com.demonwav.mcdev.platform.mixin.reference.target.TargetReference import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.SLICE +import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Classes.SHIFT import com.demonwav.mcdev.platform.mixin.util.findSourceElement import com.demonwav.mcdev.util.computeStringArray import com.demonwav.mcdev.util.constantStringValue import com.demonwav.mcdev.util.constantValue +import com.demonwav.mcdev.util.equivalentTo import com.demonwav.mcdev.util.fullQualifiedName import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiAnnotationMemberValue import com.intellij.psi.PsiArrayInitializerMemberValue import com.intellij.psi.PsiClass import com.intellij.psi.PsiClassType import com.intellij.psi.PsiElement +import com.intellij.psi.PsiEnumConstant import com.intellij.psi.PsiExpression import com.intellij.psi.PsiModifierList import com.intellij.psi.PsiQualifiedReference import com.intellij.psi.PsiReference import com.intellij.psi.PsiReferenceExpression import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.util.PsiUtil import com.intellij.psi.util.parentOfType import com.intellij.psi.util.parents import org.objectweb.asm.tree.ClassNode @@ -142,6 +147,21 @@ class AtResolver( .filterIsInstance() .firstOrNull { it.parent is PsiModifierList } } + + fun getShift(at: PsiAnnotation): Int { + val shiftAttr = at.findDeclaredAttributeValue("shift") as? PsiExpression ?: return 0 + val shiftReference = PsiUtil.skipParenthesizedExprDown(shiftAttr) as? PsiReferenceExpression ?: return 0 + val shift = shiftReference.resolve() as? PsiEnumConstant ?: return 0 + val containingClass = shift.containingClass ?: return 0 + val shiftClass = JavaPsiFacade.getInstance(at.project).findClass(SHIFT, at.resolveScope) ?: return 0 + if (!(containingClass equivalentTo shiftClass)) return 0 + return when (shift.name) { + "BEFORE" -> -1 + "AFTER" -> 1 + "BY" -> at.findDeclaredAttributeValue("by")?.constantValue as? Int ?: 0 + else -> 0 + } + } } fun isUnresolved(): InsnResolutionInfo.Failure? { diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt index f2a0a48ba..5a8dc1d7e 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/ConstantStringMethodInjectionPoint.kt @@ -86,6 +86,11 @@ class ConstantStringMethodInjectionPoint : AbstractMethodInjectionPoint() { editor.caretModel.moveToOffset(cursorElement.textRange.endOffset - 1) } + override fun isShiftDiscouraged(shift: Int): Boolean { + // allow shifting after the INVOKE_STRING + return shift != 0 && shift != 1 + } + override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS override fun getArgsValues(at: PsiAnnotation, key: String): Array { diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt index 10cada293..576374721 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/FieldInjectionPoint.kt @@ -59,6 +59,11 @@ class FieldInjectionPoint : QualifiedInjectionPoint() { completeExtraStringAtAttribute(editor, reference, "target") } + override fun isShiftDiscouraged(shift: Int): Boolean { + // allow shift after the field access + return shift != 0 && shift != 1 + } + override fun getArgsKeys(at: PsiAnnotation) = ARGS_KEYS override fun getArgsValues(at: PsiAnnotation, key: String): Array = diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt index 903b129d3..f4b29cb52 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt @@ -22,13 +22,11 @@ package com.demonwav.mcdev.platform.mixin.handlers.injectionPoint import com.demonwav.mcdev.platform.mixin.reference.MixinSelector import com.demonwav.mcdev.platform.mixin.reference.toMixinString -import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Classes.SHIFT import com.demonwav.mcdev.platform.mixin.util.fakeResolve import com.demonwav.mcdev.platform.mixin.util.findOrConstructSourceMethod import com.demonwav.mcdev.util.constantStringValue import com.demonwav.mcdev.util.constantValue import com.demonwav.mcdev.util.createLiteralExpression -import com.demonwav.mcdev.util.equivalentTo import com.demonwav.mcdev.util.findAnnotations import com.demonwav.mcdev.util.fullQualifiedName import com.demonwav.mcdev.util.getQualifiedMemberReference @@ -47,17 +45,13 @@ import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiAnonymousClass import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement -import com.intellij.psi.PsiEnumConstant -import com.intellij.psi.PsiExpression import com.intellij.psi.PsiLambdaExpression import com.intellij.psi.PsiLiteral import com.intellij.psi.PsiMember import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodReferenceExpression -import com.intellij.psi.PsiReferenceExpression import com.intellij.psi.PsiSubstitutor import com.intellij.psi.codeStyle.CodeStyleManager -import com.intellij.psi.util.PsiUtil import com.intellij.psi.util.parentOfType import com.intellij.serviceContainer.BaseKeyedLazyInstance import com.intellij.util.ArrayUtilRt @@ -108,7 +102,9 @@ abstract class InjectionPoint { open fun isArgValueList(at: PsiAnnotation, key: String) = false - open val isDiscouraged: Boolean = false + open val discouragedMessage: String? = null + + open fun isShiftDiscouraged(shift: Int): Boolean = shift != 0 abstract fun createNavigationVisitor( at: PsiAnnotation, @@ -146,20 +142,7 @@ abstract class InjectionPoint { } protected open fun addShiftSupport(at: PsiAnnotation, targetClass: ClassNode, collectVisitor: CollectVisitor<*>) { - val shiftAttr = at.findDeclaredAttributeValue("shift") as? PsiExpression ?: return - val shiftReference = PsiUtil.skipParenthesizedExprDown(shiftAttr) as? PsiReferenceExpression ?: return - val shift = shiftReference.resolve() as? PsiEnumConstant ?: return - val containingClass = shift.containingClass ?: return - val shiftClass = JavaPsiFacade.getInstance(at.project).findClass(SHIFT, at.resolveScope) ?: return - if (!(containingClass equivalentTo shiftClass)) return - when (shift.name) { - "BEFORE" -> collectVisitor.shiftBy = -1 - "AFTER" -> collectVisitor.shiftBy = 1 - "BY" -> { - val by = at.findDeclaredAttributeValue("by")?.constantValue as? Int ?: return - collectVisitor.shiftBy = by - } - } + collectVisitor.shiftBy = AtResolver.getShift(at) } protected open fun addSliceFilter(at: PsiAnnotation, targetClass: ClassNode, collectVisitor: CollectVisitor<*>) { diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt index 6448dfca3..594ed5aff 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InvokeInjectionPoint.kt @@ -44,6 +44,24 @@ abstract class AbstractInvokeInjectionPoint(private val assign: Boolean) : Abstr completeExtraStringAtAttribute(editor, reference, "target") } + override fun isShiftDiscouraged(shift: Int): Boolean { + if (shift == 0) { + return false + } + if (assign) { + // allow shifting before the INVOKE_ASSIGN + if (shift == -1) { + return false + } + } else { + // allow shifting after the INVOKE + if (shift == 1) { + return false + } + } + return true + } + override fun createNavigationVisitor( at: PsiAnnotation, target: MixinSelector?, diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/JumpInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/JumpInjectionPoint.kt index 770db81bd..ce09d906c 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/JumpInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/JumpInjectionPoint.kt @@ -57,7 +57,7 @@ class JumpInjectionPoint : InjectionPoint() { ) } - override val isDiscouraged = true + override val discouragedMessage = "Usage of JUMP is discouraged because it is brittle" override fun createNavigationVisitor( at: PsiAnnotation, diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt index 7ad828179..f99689366 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt @@ -67,6 +67,24 @@ abstract class AbstractLoadInjectionPoint(private val store: Boolean) : Injectio return LocalInfo.fromAnnotation(localType, modifyVariable) } + override fun isShiftDiscouraged(shift: Int): Boolean { + if (shift == 0) { + return false + } + if (store) { + // allow shift before the STORE + if (shift == -1) { + return false + } + } else { + // allow shift after the LOAD + if (shift == 1) { + return false + } + } + return true + } + override fun createNavigationVisitor( at: PsiAnnotation, target: MixinSelector?, diff --git a/src/main/kotlin/platform/mixin/inspection/injector/DiscouragedInjectionPointInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/DiscouragedInjectionPointInspection.kt index 6989fa9a5..d75bb9ab2 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/DiscouragedInjectionPointInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/DiscouragedInjectionPointInspection.kt @@ -39,8 +39,9 @@ class DiscouragedInjectionPointInspection : MixinInspection() { } val atValue = annotation.findDeclaredAttributeValue("value") ?: return val atCode = atValue.constantStringValue ?: return - if (InjectionPoint.byAtCode(atCode)?.isDiscouraged == true) { - holder.registerProblem(atValue, "Usage of $atCode is discouraged") + val discouragedMessage = InjectionPoint.byAtCode(atCode)?.discouragedMessage + if (discouragedMessage != null) { + holder.registerProblem(atValue, discouragedMessage) } } } diff --git a/src/main/kotlin/platform/mixin/inspection/injector/DiscouragedShiftInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/DiscouragedShiftInspection.kt new file mode 100644 index 000000000..e9145d1ca --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/injector/DiscouragedShiftInspection.kt @@ -0,0 +1,50 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.platform.mixin.inspection.injector + +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AtResolver +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InjectionPoint +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.constantStringValue +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElementVisitor + +class DiscouragedShiftInspection : MixinInspection() { + override fun getStaticDescription() = "Reports discouraged usages of shifting in injection points" + + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = object : JavaElementVisitor() { + override fun visitAnnotation(annotation: PsiAnnotation) { + if (!annotation.hasQualifiedName(MixinConstants.Annotations.AT)) { + return + } + val atValue = annotation.findDeclaredAttributeValue("value") ?: return + val atCode = atValue.constantStringValue ?: return + val shift = AtResolver.getShift(annotation) + if (InjectionPoint.byAtCode(atCode)?.isShiftDiscouraged(shift) == true) { + val shiftElement = annotation.findDeclaredAttributeValue("shift") ?: return + holder.registerProblem(shiftElement, "Shifting like this is discouraged because it's brittle") + } + } + } +} diff --git a/src/main/kotlin/platform/mixin/reference/InjectionPointReference.kt b/src/main/kotlin/platform/mixin/reference/InjectionPointReference.kt index 2059034fb..06a427e58 100644 --- a/src/main/kotlin/platform/mixin/reference/InjectionPointReference.kt +++ b/src/main/kotlin/platform/mixin/reference/InjectionPointReference.kt @@ -88,7 +88,7 @@ object InjectionPointReference : ReferenceResolver(), MixinReference { return ( getAllAtCodes(context.project).keys.asSequence() .filter { - InjectionPoint.byAtCode(it)?.isDiscouraged != true + InjectionPoint.byAtCode(it)?.discouragedMessage == null } .map { PrioritizedLookupElement.withPriority( diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 562127f59..c6105cec4 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1026,6 +1026,14 @@ level="WARNING" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.mixin.inspection.injector.DiscouragedInjectionPointInspection"/> + From 3d0a2ca85b757ee723535bc8a12e0b3d5694f221 Mon Sep 17 00:00:00 2001 From: joe Date: Sun, 21 Jul 2024 23:42:01 +0100 Subject: [PATCH 21/37] Apply offset of STORE in the collect visitor --- .../mixin/handlers/ModifyVariableHandler.kt | 8 ++------ .../injectionPoint/LoadInjectionPoint.kt | 16 ++++++++-------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt index ffc8799c0..42398c23d 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt @@ -23,7 +23,6 @@ package com.demonwav.mcdev.platform.mixin.handlers import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AbstractLoadInjectionPoint import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InjectionPoint -import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.StoreInjectionPoint import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup import com.demonwav.mcdev.platform.mixin.util.LocalInfo @@ -48,9 +47,7 @@ class ModifyVariableHandler : InjectorAnnotationHandler() { val at = annotation.findAttributeValue("at") as? PsiAnnotation val atCode = at?.findAttributeValue("value")?.constantStringValue - val injectionPoint = atCode?.let { InjectionPoint.byAtCode(atCode) } - val isLoadStore = injectionPoint is AbstractLoadInjectionPoint - val isStore = injectionPoint is StoreInjectionPoint + val isLoadStore = atCode != null && InjectionPoint.byAtCode(atCode) is AbstractLoadInjectionPoint val mode = if (isLoadStore) CollectVisitor.Mode.COMPLETION else CollectVisitor.Mode.MATCH_ALL val targets = resolveInstructions(annotation, targetClass, targetMethod, mode) @@ -66,8 +63,7 @@ class ModifyVariableHandler : InjectorAnnotationHandler() { val possibleTypes = mutableSetOf() for (insn in targets) { - val actualInsn = if (isStore) insn.insn.next ?: insn.insn else insn.insn - val locals = info.getLocals(module, targetClass, targetMethod, actualInsn) ?: continue + val locals = info.getLocals(module, targetClass, targetMethod, insn.insn) ?: continue val matchedLocals = info.matchLocals(locals, CollectVisitor.Mode.COMPLETION, matchType = false) for (local in matchedLocals) { possibleTypes += local.desc!! diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt index f99689366..f85c58681 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt @@ -121,15 +121,15 @@ abstract class AbstractLoadInjectionPoint(private val store: Boolean) : Injectio val project = at.project val ordinals = mutableMapOf() collectVisitor.addResultFilter("ordinal") { result, method -> - result.originalInsn as? VarInsnNode - ?: throw IllegalStateException("AbstractLoadInjectionPoint returned non-var insn") - val localInsn = if (store) { result.originalInsn.next } else { result.originalInsn } + // store returns the instruction after the variable + val varInsn = (if (store) result.originalInsn.previous ?: result.originalInsn else result.originalInsn) + as? VarInsnNode ?: throw IllegalStateException("AbstractLoadInjectionPoint returned non-var insn") val localType = AsmDfaUtil.getLocalVariableType( project, targetClass, method, - localInsn, - result.originalInsn.`var`, + result.originalInsn, + varInsn.`var`, ) ?: return@addResultFilter true val desc = localType.descriptor val ord = ordinals[desc] ?: 0 @@ -303,13 +303,13 @@ abstract class AbstractLoadInjectionPoint(private val store: Boolean) : Injectio } } - val localLocation = if (store) insn.next ?: insn else insn - val locals = info.getLocals(module, targetClass, methodNode, localLocation) ?: continue + val shiftedInsn = if (store) insn.next ?: insn else insn + val locals = info.getLocals(module, targetClass, methodNode, shiftedInsn) ?: continue val elementFactory = JavaPsiFacade.getElementFactory(module.project) for (result in info.matchLocals(locals, mode)) { - addResult(insn, elementFactory.createExpressionFromText(result.name, null)) + addResult(shiftedInsn, elementFactory.createExpressionFromText(result.name, null)) } } } From 3f58cd903d19a29b4a7100f9fd294f5f00fdd6b8 Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 23 Jul 2024 21:26:20 +0100 Subject: [PATCH 22/37] Add inspection to report usages of LocalCapture.CAPTURE_FAILEXCEPTION --- .../CaptureFailExceptionInspection.kt | 75 +++++++++++++++++++ .../platform/mixin/util/MixinConstants.kt | 1 + src/main/resources/META-INF/plugin.xml | 8 ++ 3 files changed, 84 insertions(+) create mode 100644 src/main/kotlin/platform/mixin/inspection/injector/CaptureFailExceptionInspection.kt diff --git a/src/main/kotlin/platform/mixin/inspection/injector/CaptureFailExceptionInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/CaptureFailExceptionInspection.kt new file mode 100644 index 000000000..ef7e084cc --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/injector/CaptureFailExceptionInspection.kt @@ -0,0 +1,75 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.platform.mixin.inspection.injector + +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.intellij.codeInspection.LocalQuickFixOnPsiElement +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiEnumConstant +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiReferenceExpression + +class CaptureFailExceptionInspection : MixinInspection() { + override fun getStaticDescription() = """ + Usage of LocalCapture.CAPTURE_FAILEXCEPTION is usually a mistake and should be replaced with + LocalCapture.CAPTURE_FAILHARD. CAPTURE_FAILEXCEPTION generates code which throws an + exception when the callback is reached, if the locals do not match. If this is really what you want, you can + suppress this warning. + """.trimIndent() + + override fun buildVisitor(holder: ProblemsHolder) = object : JavaElementVisitor() { + override fun visitReferenceExpression(expression: PsiReferenceExpression) { + if (expression.referenceName != "CAPTURE_FAILEXCEPTION") { + return + } + val resolved = expression.resolve() as? PsiEnumConstant ?: return + if (resolved.containingClass?.qualifiedName != MixinConstants.Classes.LOCAL_CAPTURE) { + return + } + + holder.registerProblem( + expression, + "Suspicious usage of CAPTURE_FAILEXCEPTION", + ReplaceWithCaptureFailHardFix(expression) + ) + } + } + + private class ReplaceWithCaptureFailHardFix( + expression: PsiReferenceExpression + ) : LocalQuickFixOnPsiElement(expression) { + override fun getFamilyName() = "Replace with CAPTURE_FAILHARD" + + override fun getText() = "Replace with CAPTURE_FAILHARD" + + override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { + val captureFailHardText = "${MixinConstants.Classes.LOCAL_CAPTURE}.CAPTURE_FAILHARD" + val captureFailHardExpr = + JavaPsiFacade.getElementFactory(project).createExpressionFromText(captureFailHardText, startElement) + startElement.replace(captureFailHardExpr) + } + } +} diff --git a/src/main/kotlin/platform/mixin/util/MixinConstants.kt b/src/main/kotlin/platform/mixin/util/MixinConstants.kt index 173fc2050..8fe756bdb 100644 --- a/src/main/kotlin/platform/mixin/util/MixinConstants.kt +++ b/src/main/kotlin/platform/mixin/util/MixinConstants.kt @@ -45,6 +45,7 @@ object MixinConstants { const val TARGET_SELECTOR_DYNAMIC = "org.spongepowered.asm.mixin.injection.selectors.ITargetSelectorDynamic" const val SELECTOR_ID = "org.spongepowered.asm.mixin.injection.selectors.ITargetSelectorDynamic.SelectorId" const val SHIFT = "org.spongepowered.asm.mixin.injection.At.Shift" + const val LOCAL_CAPTURE = "org.spongepowered.asm.mixin.injection.callback.LocalCapture" const val SERIALIZED_NAME = "com.google.gson.annotations.SerializedName" const val MIXIN_SERIALIZED_NAME = "org.spongepowered.include.$SERIALIZED_NAME" diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index c6105cec4..ee3a67414 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1034,6 +1034,14 @@ level="WARNING" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.mixin.inspection.injector.DiscouragedShiftInspection"/> + From e0844832bc81a61274f4959ffaeaf7de72c7e96c Mon Sep 17 00:00:00 2001 From: joe Date: Tue, 23 Jul 2024 23:13:48 +0100 Subject: [PATCH 23/37] Inspections for when @Inject local capture is unused and for when they can be replaced with @Local --- .../injector/UnusedLocalCaptureInspection.kt | 125 +++++++++++ ...tLocalCaptureReplaceWithLocalInspection.kt | 205 ++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 16 ++ 3 files changed, 346 insertions(+) create mode 100644 src/main/kotlin/platform/mixin/inspection/injector/UnusedLocalCaptureInspection.kt create mode 100644 src/main/kotlin/platform/mixin/inspection/mixinextras/InjectLocalCaptureReplaceWithLocalInspection.kt diff --git a/src/main/kotlin/platform/mixin/inspection/injector/UnusedLocalCaptureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/UnusedLocalCaptureInspection.kt new file mode 100644 index 000000000..8e54699ec --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/injector/UnusedLocalCaptureInspection.kt @@ -0,0 +1,125 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.platform.mixin.inspection.injector + +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.inspection.fix.AnnotationAttributeFix +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.findContainingMethod +import com.intellij.codeInspection.LocalQuickFixOnPsiElement +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiEnumConstant +import com.intellij.psi.PsiExpression +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiParameter +import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.search.searches.OverridingMethodsSearch +import com.intellij.psi.search.searches.ReferencesSearch +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.PsiUtil + +class UnusedLocalCaptureInspection : MixinInspection() { + companion object { + fun findCallbackInfoParam(parameters: Array): Int { + return parameters.indexOfFirst { param -> + val classType = param.type as? PsiClassType ?: return@indexOfFirst false + val className = classType.className + if (className != "CallbackInfo" && className != "CallbackInfoReturnable") { + return@indexOfFirst false + } + val qualifiedName = classType.resolve()?.qualifiedName ?: return@indexOfFirst false + qualifiedName == MixinConstants.Classes.CALLBACK_INFO || + qualifiedName == MixinConstants.Classes.CALLBACK_INFO_RETURNABLE + } + } + } + + override fun getStaticDescription() = + "Reports when an @Inject local capture is unused" + + override fun buildVisitor(holder: ProblemsHolder) = object : JavaElementVisitor() { + override fun visitAnnotation(annotation: PsiAnnotation) { + if (!annotation.hasQualifiedName(MixinConstants.Annotations.INJECT)) { + return + } + + // check that we are capturing locals + val localsValue = + PsiUtil.skipParenthesizedExprDown( + annotation.findDeclaredAttributeValue("locals") as? PsiExpression + ) as? PsiReferenceExpression ?: return + if (localsValue.referenceName == "NO_CAPTURE") { + return + } + val enumName = (localsValue.resolve() as? PsiEnumConstant)?.containingClass?.qualifiedName + if (enumName != MixinConstants.Classes.LOCAL_CAPTURE) { + return + } + + val method = annotation.findContainingMethod() ?: return + + if (OverridingMethodsSearch.search(method).any()) { + return + } + + // find the start of the locals in the parameter list + val parameters = method.parameterList.parameters + val callbackInfoIndex = findCallbackInfoParam(parameters) + if (callbackInfoIndex == -1) { + return + } + + val hasAnyUsedLocals = parameters.asSequence().drop(callbackInfoIndex + 1).any { param -> + ReferencesSearch.search(param).anyMatch { + !it.isSoft && !PsiTreeUtil.isAncestor(param, it.element, false) + } + } + if (!hasAnyUsedLocals) { + holder.registerProblem( + localsValue, + "Unused @Inject local capture", + RemoveLocalCaptureFix(annotation, callbackInfoIndex) + ) + } + } + } + + private class RemoveLocalCaptureFix( + injectAnnotation: PsiAnnotation, + private val callbackInfoIndex: Int + ) : LocalQuickFixOnPsiElement(injectAnnotation) { + override fun getFamilyName() = "Remove @Inject local capture" + + override fun getText() = "Remove @Inject local capture" + + override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { + val injectAnnotation = startElement as? PsiAnnotation ?: return + val method = injectAnnotation.findContainingMethod() ?: return + method.parameterList.parameters.asSequence().drop(callbackInfoIndex + 1).forEach(PsiElement::delete) + AnnotationAttributeFix(injectAnnotation, "locals" to null).applyFix() + } + } +} diff --git a/src/main/kotlin/platform/mixin/inspection/mixinextras/InjectLocalCaptureReplaceWithLocalInspection.kt b/src/main/kotlin/platform/mixin/inspection/mixinextras/InjectLocalCaptureReplaceWithLocalInspection.kt new file mode 100644 index 000000000..95fb2d66d --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/mixinextras/InjectLocalCaptureReplaceWithLocalInspection.kt @@ -0,0 +1,205 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.platform.mixin.inspection.mixinextras + +import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.inspection.fix.AnnotationAttributeFix +import com.demonwav.mcdev.platform.mixin.inspection.injector.UnusedLocalCaptureInspection +import com.demonwav.mcdev.platform.mixin.util.LocalVariables +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.platform.mixin.util.hasAccess +import com.demonwav.mcdev.util.addAnnotation +import com.demonwav.mcdev.util.descriptor +import com.demonwav.mcdev.util.findContainingMethod +import com.demonwav.mcdev.util.findModule +import com.intellij.codeInsight.intention.FileModifier.SafeFieldForPreview +import com.intellij.codeInspection.LocalQuickFixOnPsiElement +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiEnumConstant +import com.intellij.psi.PsiExpression +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.search.searches.OverridingMethodsSearch +import com.intellij.psi.search.searches.ReferencesSearch +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.PsiUtil +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type + +class InjectLocalCaptureReplaceWithLocalInspection : MixinInspection() { + override fun getStaticDescription() = + "Reports when @Inject local capture can be replaced with @Local, which is less brittle" + + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor { + val localClass = JavaPsiFacade.getInstance(holder.project) + .findClass(MixinConstants.MixinExtras.LOCAL, GlobalSearchScope.allScope(holder.project)) + if (localClass == null) { + return PsiElementVisitor.EMPTY_VISITOR + } + + return object : JavaElementVisitor() { + override fun visitAnnotation(annotation: PsiAnnotation) { + if (!annotation.hasQualifiedName(MixinConstants.Annotations.INJECT)) { + return + } + + // check that we are capturing locals + val localsValue = + PsiUtil.skipParenthesizedExprDown( + annotation.findDeclaredAttributeValue("locals") as? PsiExpression + ) as? PsiReferenceExpression ?: return + if (localsValue.referenceName != "CAPTURE_FAILHARD") { + return + } + val enumName = (localsValue.resolve() as? PsiEnumConstant)?.containingClass?.qualifiedName + if (enumName != MixinConstants.Classes.LOCAL_CAPTURE) { + return + } + + val method = annotation.findContainingMethod() ?: return + + if (OverridingMethodsSearch.search(method).any()) { + return + } + + // find the start of the locals in the parameter list + val parameters = method.parameterList.parameters + val callbackInfoIndex = UnusedLocalCaptureInspection.findCallbackInfoParam(parameters) + if (callbackInfoIndex == -1) { + return + } + + // resolve the local variables at the targets + val handler = MixinAnnotationHandler.forMixinAnnotation(MixinConstants.Annotations.INJECT) + as InjectorAnnotationHandler + val module = annotation.findModule() ?: return + val localsAndParamCountsAtTargets = handler.resolveInstructions(annotation).map { result -> + val locals = LocalVariables.getLocals( + module, + result.method.clazz, + result.method.method, + result.result.insn + ) ?: return + var paramCount = Type.getMethodType(result.method.method.desc).argumentTypes.size + if (!result.method.method.hasAccess(Opcodes.ACC_STATIC)) { + paramCount++ + } + locals to paramCount + } + + // based on the resolved local variables, figure out what @Local specifiers to use + val localSpecifiers = parameters.drop(callbackInfoIndex + 1).withIndex().map { (index, param) -> + val isLocalUsed = ReferencesSearch.search(param).anyMatch { + !it.isSoft && !PsiTreeUtil.isAncestor(param, it.element, false) + } + if (!isLocalUsed) { + return@map UnusedSpecifier + } + + val localType = param.type.descriptor + val canBeImplicit = localsAndParamCountsAtTargets.all { (localsAtTarget, _) -> + localsAtTarget.singleOrNull { it?.desc == localType } != null + } + if (canBeImplicit) { + return@map ImplicitSpecifier + } + + val ordinals = localsAndParamCountsAtTargets.map { (localsAtTarget, paramCount) -> + localsAtTarget.filterNotNull().take(index + paramCount).count { it.desc == localType } + } + if (ordinals.isEmpty()) { + return + } + if (ordinals.all { it == ordinals.first() }) { + return@map OrdinalSpecifier(ordinals.first()) + } + + val indexes = localsAndParamCountsAtTargets.map { (localsAtTarget, paramCount) -> + localsAtTarget.filterNotNull().getOrNull(index + paramCount)?.index ?: return + } + if (indexes.isEmpty()) { + return + } + if (indexes.all { it == indexes.first() }) { + return@map IndexSpecifier(indexes.first()) + } + + return + } + + if (localSpecifiers.isEmpty() || localSpecifiers.all { it is UnusedSpecifier }) { + // this is reported by the redundant local capture inspection + return + } + + holder.registerProblem( + localsValue, + "@Inject local capture can be replaced by @Local", + ReplaceWithLocalFix(annotation, callbackInfoIndex, localSpecifiers) + ) + } + } + } + + private sealed interface LocalSpecifier + private data class OrdinalSpecifier(val ordinal: Int) : LocalSpecifier + private data class IndexSpecifier(val index: Int) : LocalSpecifier + private data object ImplicitSpecifier : LocalSpecifier + private data object UnusedSpecifier : LocalSpecifier + + private class ReplaceWithLocalFix( + injectAnnotation: PsiAnnotation, + private val callbackInfoIndex: Int, + @SafeFieldForPreview private val localSpecifiers: List + ) : LocalQuickFixOnPsiElement(injectAnnotation) { + override fun getFamilyName() = "Replace @Inject local capture with @Local" + + override fun getText() = "Replace @Inject local capture with @Local" + + override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { + val injectAnnotation = startElement as? PsiAnnotation ?: return + val method = injectAnnotation.findContainingMethod() ?: return + val paramsForLocals = method.parameterList.parameters.asSequence().drop(callbackInfoIndex + 1) + for ((param, specifier) in paramsForLocals.zip(localSpecifiers.asSequence())) { + val localAnnotationText = when (specifier) { + ImplicitSpecifier -> "@${MixinConstants.MixinExtras.LOCAL}" + is IndexSpecifier -> "@${MixinConstants.MixinExtras.LOCAL}(index = ${specifier.index})" + is OrdinalSpecifier -> "@${MixinConstants.MixinExtras.LOCAL}(ordinal = ${specifier.ordinal})" + is UnusedSpecifier -> { + param.delete() + continue + } + } + param.addAnnotation(localAnnotationText) + } + AnnotationAttributeFix(injectAnnotation, "locals" to null).applyFix() + } + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index ee3a67414..6a3488694 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1042,6 +1042,22 @@ level="WARNING" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.mixin.inspection.injector.CaptureFailExceptionInspection"/> + + From c74216fc93e4ea449abf8c9eb0aa30f6ab47a959 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 24 Jul 2024 17:44:03 +0100 Subject: [PATCH 24/37] Use mixin icon for mixin classes, closes #2306 --- .../platform/mixin/framework/MixinIconProvider.kt | 12 ++++++++++++ src/main/resources/META-INF/plugin.xml | 2 ++ 2 files changed, 14 insertions(+) create mode 100644 src/main/kotlin/platform/mixin/framework/MixinIconProvider.kt diff --git a/src/main/kotlin/platform/mixin/framework/MixinIconProvider.kt b/src/main/kotlin/platform/mixin/framework/MixinIconProvider.kt new file mode 100644 index 000000000..9f64aec52 --- /dev/null +++ b/src/main/kotlin/platform/mixin/framework/MixinIconProvider.kt @@ -0,0 +1,12 @@ +package com.demonwav.mcdev.platform.mixin.framework + +import com.demonwav.mcdev.asset.PlatformAssets +import com.demonwav.mcdev.platform.mixin.util.isMixin +import com.intellij.ide.IconProvider +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement + +class MixinIconProvider : IconProvider() { + override fun getIcon(element: PsiElement, flags: Int) = + PlatformAssets.MIXIN_ICON.takeIf { element is PsiClass && element.isMixin } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 6a3488694..cf1cdd934 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -528,6 +528,8 @@ + + Date: Wed, 24 Jul 2024 17:52:00 +0100 Subject: [PATCH 25/37] Add option to disable mixin class icon --- src/main/kotlin/MinecraftConfigurable.kt | 7 +++++++ src/main/kotlin/MinecraftSettings.kt | 8 ++++++++ .../mixin/framework/MixinIconProvider.kt | 20 +++++++++++++++++++ .../messages/MinecraftDevelopment.properties | 1 + 4 files changed, 36 insertions(+) diff --git a/src/main/kotlin/MinecraftConfigurable.kt b/src/main/kotlin/MinecraftConfigurable.kt index 12fc11567..b75ff0c0c 100644 --- a/src/main/kotlin/MinecraftConfigurable.kt +++ b/src/main/kotlin/MinecraftConfigurable.kt @@ -86,6 +86,13 @@ class MinecraftConfigurable : Configurable { } } + group(MCDevBundle("minecraft.settings.mixin")) { + row { + checkBox(MCDevBundle("minecraft.settings.mixin.mixin_class_icon")) + .bindSelected(settings::mixinClassIcon) + } + } + group(MCDevBundle("minecraft.settings.creator")) { row(MCDevBundle("minecraft.settings.creator.repos")) {} diff --git a/src/main/kotlin/MinecraftSettings.kt b/src/main/kotlin/MinecraftSettings.kt index 18ae02acf..8733f50fb 100644 --- a/src/main/kotlin/MinecraftSettings.kt +++ b/src/main/kotlin/MinecraftSettings.kt @@ -40,6 +40,8 @@ class MinecraftSettings : PersistentStateComponent { var isShowChatColorUnderlines: Boolean = false, var underlineType: UnderlineType = UnderlineType.DOTTED, + var mixinClassIcon: Boolean = true, + var creatorTemplateRepos: List = listOf(TemplateRepo.makeBuiltinRepo()), ) @@ -106,6 +108,12 @@ class MinecraftSettings : PersistentStateComponent { state.underlineType = underlineType } + var mixinClassIcon: Boolean + get() = state.mixinClassIcon + set(mixinClassIcon) { + state.mixinClassIcon = mixinClassIcon + } + var creatorTemplateRepos: List get() = state.creatorTemplateRepos.map { it.copy() } set(creatorTemplateRepos) { diff --git a/src/main/kotlin/platform/mixin/framework/MixinIconProvider.kt b/src/main/kotlin/platform/mixin/framework/MixinIconProvider.kt index 9f64aec52..1e6dfb744 100644 --- a/src/main/kotlin/platform/mixin/framework/MixinIconProvider.kt +++ b/src/main/kotlin/platform/mixin/framework/MixinIconProvider.kt @@ -1,3 +1,23 @@ +/* + * 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 . + */ + package com.demonwav.mcdev.platform.mixin.framework import com.demonwav.mcdev.asset.PlatformAssets diff --git a/src/main/resources/messages/MinecraftDevelopment.properties b/src/main/resources/messages/MinecraftDevelopment.properties index 7d8526e81..2e79f30a3 100644 --- a/src/main/resources/messages/MinecraftDevelopment.properties +++ b/src/main/resources/messages/MinecraftDevelopment.properties @@ -278,6 +278,7 @@ minecraft.settings.lang_template.comment=You may edit the template used fo
    Each line may be empty, a comment (with #) or a glob pattern for matching translation keys (like "item.*").\
    Note: Empty lines are respected and will be put into the sorting result. minecraft.settings.mixin.definition_pos_relative_to_expression=@Definition position relative to @Expression +minecraft.settings.mixin.mixin_class_icon=Show Mixin class icon minecraft.settings.mixin.shadow_annotation_same_line=@Shadow annotations on same line minecraft.settings.mixin=Mixin minecraft.settings.project.display_name=Project-Specific Settings From 7d1cd3a2811670fb0d18b8a5a96d2099c3767453 Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 24 Jul 2024 22:22:15 +0100 Subject: [PATCH 26/37] Actually use the mixin class icon setting --- .../kotlin/platform/mixin/framework/MixinIconProvider.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/platform/mixin/framework/MixinIconProvider.kt b/src/main/kotlin/platform/mixin/framework/MixinIconProvider.kt index 1e6dfb744..646b68f02 100644 --- a/src/main/kotlin/platform/mixin/framework/MixinIconProvider.kt +++ b/src/main/kotlin/platform/mixin/framework/MixinIconProvider.kt @@ -20,6 +20,7 @@ package com.demonwav.mcdev.platform.mixin.framework +import com.demonwav.mcdev.MinecraftSettings import com.demonwav.mcdev.asset.PlatformAssets import com.demonwav.mcdev.platform.mixin.util.isMixin import com.intellij.ide.IconProvider @@ -28,5 +29,7 @@ import com.intellij.psi.PsiElement class MixinIconProvider : IconProvider() { override fun getIcon(element: PsiElement, flags: Int) = - PlatformAssets.MIXIN_ICON.takeIf { element is PsiClass && element.isMixin } + PlatformAssets.MIXIN_ICON.takeIf { + MinecraftSettings.instance.mixinClassIcon && element is PsiClass && element.isMixin + } } From 88a475af50d03711db5afb579b6b53d3d5876381 Mon Sep 17 00:00:00 2001 From: Joe Date: Sun, 28 Jul 2024 18:19:09 +0100 Subject: [PATCH 27/37] Prevent replacement of $ with . in more places, fixes #2282 --- .../kotlin/platform/mixin/reference/DescReference.kt | 7 ++++--- src/main/kotlin/platform/mixin/util/AsmUtil.kt | 11 ++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/platform/mixin/reference/DescReference.kt b/src/main/kotlin/platform/mixin/reference/DescReference.kt index 95155671a..694e130d2 100644 --- a/src/main/kotlin/platform/mixin/reference/DescReference.kt +++ b/src/main/kotlin/platform/mixin/reference/DescReference.kt @@ -21,6 +21,7 @@ package com.demonwav.mcdev.platform.mixin.reference import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.DESC +import com.demonwav.mcdev.platform.mixin.util.canonicalName import com.demonwav.mcdev.platform.mixin.util.findClassNodeByQualifiedName import com.demonwav.mcdev.util.MemberReference import com.demonwav.mcdev.util.findModule @@ -114,11 +115,11 @@ object DescReference : AbstractMethodReference() { val argTypes = Type.getArgumentTypes(desc) if (argTypes.isNotEmpty()) { val argsText = if (argTypes.size == 1) { - "${argTypes[0].className.replace('$', '.')}.class" + "${argTypes[0].canonicalName}.class" } else { "{${ argTypes.joinToString(", ") { type -> - "${type.className.replace('$', '.')}.class" + "${type.canonicalName}.class" } }}" } @@ -134,7 +135,7 @@ object DescReference : AbstractMethodReference() { descAnnotation.setDeclaredAttributeValue( "ret", elementFactory.createAnnotationMemberValueFromText( - "${returnType.className.replace('$', '.')}.class", + "${returnType.canonicalName}.class", descAnnotation, ), ) diff --git a/src/main/kotlin/platform/mixin/util/AsmUtil.kt b/src/main/kotlin/platform/mixin/util/AsmUtil.kt index 981be7320..0f57c5961 100644 --- a/src/main/kotlin/platform/mixin/util/AsmUtil.kt +++ b/src/main/kotlin/platform/mixin/util/AsmUtil.kt @@ -146,16 +146,17 @@ fun Type.toPsiType(elementFactory: PsiElementFactory, context: PsiElement? = nul if (this == ExpressionASMUtils.INTLIKE_TYPE) { return PsiTypes.intType() } - val javaClassName = className.replace("(\\$)(\\D)".toRegex()) { "." + it.groupValues[2] } - return elementFactory.createTypeFromText(javaClassName, context) + return elementFactory.createTypeFromText(canonicalName, context) } val Type.canonicalName get() = computeCanonicalName(this) +private val DOLLAR_TO_DOT_REGEX = "\\$(?!\\d)".toRegex() + private fun computeCanonicalName(type: Type): String { return when (type.sort) { Type.ARRAY -> computeCanonicalName(type.elementType) + "[]".repeat(type.dimensions) - Type.OBJECT -> type.className.replace('$', '.') + Type.OBJECT -> type.className.replace(DOLLAR_TO_DOT_REGEX, ".") else -> type.className } } @@ -817,7 +818,7 @@ fun MethodNode.findOrConstructSourceMethod( } append(name) } else { - append(returnType.className.replace('$', '.')) + append(returnType.canonicalName) append(' ') append(this@findOrConstructSourceMethod.name.toJavaIdentifier()) } @@ -827,7 +828,7 @@ fun MethodNode.findOrConstructSourceMethod( if (index != 0) { append(", ") } - var typeName = param.className.replace('$', '.') + var typeName = param.canonicalName if (index == params.size - 1 && hasAccess(Opcodes.ACC_VARARGS) && typeName.endsWith("[]")) { typeName = typeName.replaceRange(typeName.length - 2, typeName.length, "...") } From 03fb693644f3cfc2dd2e0fa46a67e2ab2aca2a94 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Sat, 27 Jul 2024 21:32:34 +0200 Subject: [PATCH 28/37] Fix recent NeoModDev import errors --- .../NeoModDevGradleModelBuilderImpl.groovy | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelBuilderImpl.groovy b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelBuilderImpl.groovy index 02ef8e305..46dc6669a 100644 --- a/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelBuilderImpl.groovy +++ b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelBuilderImpl.groovy @@ -22,6 +22,7 @@ package com.demonwav.mcdev.platform.mcp.gradle.tooling.neomoddev import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelNMD import org.gradle.api.Project +import org.gradle.api.provider.ListProperty import org.jetbrains.annotations.NotNull import org.jetbrains.plugins.gradle.tooling.ErrorMessageBuilder import org.jetbrains.plugins.gradle.tooling.ModelBuilderService @@ -51,16 +52,26 @@ final class NeoModDevGradleModelBuilderImpl implements ModelBuilderService { return null } - def accessTransformers = extension.accessTransformers.get().collect { project.file(it) } + def accessTransformersRaw = extension.accessTransformers + List accessTransformers + if (accessTransformersRaw instanceof ListProperty) { + accessTransformers = accessTransformersRaw.get().collect { project.file(it) } + } else { + accessTransformers = accessTransformersRaw.files.files.toList() + } - // Hacky way to guess where the mappings file is, but I could not find a proper way to find it - def neoformDir = project.buildDir.toPath().resolve("neoForm") - def mappingsFile = Files.list(neoformDir) - .map { it.resolve("config/joined.tsrg") } - .filter { Files.exists(it) } - .findFirst() - .orElse(null) - ?.toFile() + File mappingsFile = null + try { + // Hacky way to guess where the mappings file is, but I could not find a proper way to find it + def neoformDir = project.buildDir.toPath().resolve("neoForm") + mappingsFile = Files.list(neoformDir) + .map { it.resolve("config/joined.tsrg") } + .filter { Files.exists(it) } + .findFirst() + .orElse(null) + ?.toFile() + } catch (Exception ignore) { + } //noinspection GroovyAssignabilityCheck return new NeoModDevGradleModelImpl(neoforgeVersion, mappingsFile, accessTransformers) From 7198d5f7c00898e8f561a29725279d1f85bcef27 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 30 Jul 2024 13:59:44 +0200 Subject: [PATCH 29/37] Support Kotlin event listener generation --- changelog.md | 1 + .../insight/generation/EventGenHelper.kt | 122 ++++++++++++ .../EventListenerGenerationSupport.kt | 38 ++++ .../generation/GenerateEventListenerAction.kt | 26 ++- .../GenerateEventListenerHandler.kt | 128 +++---------- .../insight/generation/MethodRenderer.kt | 173 ++++++++++++++++++ ...ererBasedEventListenerGenerationSupport.kt | 94 ++++++++++ .../generation/ui/EventGenerationPanel.kt | 6 +- src/main/kotlin/platform/AbstractModule.kt | 13 +- .../BukkitEventListenerGenerationSupport.kt | 81 ++++++++ .../kotlin/platform/bukkit/BukkitModule.kt | 44 +---- ...ungeeCordEventListenerGenerationSupport.kt | 77 ++++++++ .../platform/bungeecord/BungeeCordModule.kt | 50 +---- .../ForgeEventListenerGenerationSupport.kt | 59 ++++++ src/main/kotlin/platform/forge/ForgeModule.kt | 32 +--- .../NeoForgeEventListenerGenerationSupport.kt | 52 ++++++ .../platform/neoforge/NeoForgeModule.kt | 19 +- .../SpongeEventListenerGenerationSupport.kt | 78 ++++++++ .../kotlin/platform/sponge/SpongeModule.kt | 41 +---- .../platform/sponge/util/SpongeConstants.kt | 1 + .../VelocityEventListenerGenerationSupport.kt | 66 +++++++ .../platform/velocity/VelocityModule.kt | 34 +--- .../velocity/util/VelocityConstants.kt | 1 + src/main/resources/META-INF/mcdev-kotlin.xml | 2 + src/main/resources/META-INF/plugin.xml | 6 + .../messages/MinecraftDevelopment.properties | 2 +- .../MinecraftDevelopment_fr.properties | 2 +- 27 files changed, 922 insertions(+), 326 deletions(-) create mode 100644 src/main/kotlin/insight/generation/EventGenHelper.kt create mode 100644 src/main/kotlin/insight/generation/EventListenerGenerationSupport.kt create mode 100644 src/main/kotlin/insight/generation/MethodRenderer.kt create mode 100644 src/main/kotlin/insight/generation/MethodRendererBasedEventListenerGenerationSupport.kt create mode 100644 src/main/kotlin/platform/bukkit/BukkitEventListenerGenerationSupport.kt create mode 100644 src/main/kotlin/platform/bungeecord/BungeeCordEventListenerGenerationSupport.kt create mode 100644 src/main/kotlin/platform/forge/ForgeEventListenerGenerationSupport.kt create mode 100644 src/main/kotlin/platform/neoforge/NeoForgeEventListenerGenerationSupport.kt create mode 100644 src/main/kotlin/platform/sponge/SpongeEventListenerGenerationSupport.kt create mode 100644 src/main/kotlin/platform/velocity/VelocityEventListenerGenerationSupport.kt diff --git a/changelog.md b/changelog.md index 7c0e0c26f..c7b74ffd6 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,7 @@ ### Added - Access widener completion in fabric.mod.json +- Event listener generation for Kotlin ### Fixed diff --git a/src/main/kotlin/insight/generation/EventGenHelper.kt b/src/main/kotlin/insight/generation/EventGenHelper.kt new file mode 100644 index 000000000..5ee687e8b --- /dev/null +++ b/src/main/kotlin/insight/generation/EventGenHelper.kt @@ -0,0 +1,122 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.insight.generation + +import com.demonwav.mcdev.util.addImplements +import com.intellij.core.CoreJavaCodeStyleManager +import com.intellij.lang.LanguageExtension +import com.intellij.lang.LanguageExtensionPoint +import com.intellij.openapi.editor.RangeMarker +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.psi.util.parentOfType +import org.jetbrains.kotlin.idea.core.ShortenReferences +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtPsiFactory + +interface EventGenHelper { + + fun addImplements(context: PsiElement, fqn: String) + + fun reformatAndShortenRefs(file: PsiFile, startOffset: Int, endOffset: Int) + + companion object { + + val EP_NAME = ExtensionPointName.create>( + "com.demonwav.minecraft-dev.eventGenHelper" + ) + val COLLECTOR = LanguageExtension(EP_NAME, JvmEventGenHelper()) + } +} + +open class JvmEventGenHelper : EventGenHelper { + + override fun addImplements(context: PsiElement, fqn: String) {} + + override fun reformatAndShortenRefs(file: PsiFile, startOffset: Int, endOffset: Int) { + val project = file.project + + val marker = doReformat(project, file, startOffset, endOffset) ?: return + + CoreJavaCodeStyleManager.getInstance(project).shortenClassReferences(file, marker.startOffset, marker.endOffset) + } + + companion object { + + fun doReformat(project: Project, file: PsiFile, startOffset: Int, endOffset: Int): RangeMarker? { + val documentManager = PsiDocumentManager.getInstance(project) + val document = documentManager.getDocument(file) ?: return null + + val marker = document.createRangeMarker(startOffset, endOffset).apply { + isGreedyToLeft = true + isGreedyToRight = true + } + + CodeStyleManager.getInstance(project).reformatText(file, startOffset, endOffset) + documentManager.commitDocument(document) + + return marker + } + } +} + +class JavaEventGenHelper : JvmEventGenHelper() { + + override fun addImplements(context: PsiElement, fqn: String) { + val psiClass = context.parentOfType(true) ?: return + psiClass.addImplements(fqn) + } +} + +class KotlinEventGenHelper : EventGenHelper { + + private fun hasSuperType(ktClass: KtClassOrObject, fqn: String): Boolean { + val names = setOf(fqn, fqn.substringAfterLast('.')) + return ktClass.superTypeListEntries.any { it.text in names } + } + + override fun addImplements(context: PsiElement, fqn: String) { + val ktClass = context.parentOfType(true) ?: return + if (hasSuperType(ktClass, fqn)) { + return + } + + val factory = KtPsiFactory.contextual(context) + val entry = factory.createSuperTypeEntry(fqn) + val insertedEntry = ktClass.addSuperTypeListEntry(entry) + ShortenReferences.DEFAULT.process(insertedEntry) + } + + override fun reformatAndShortenRefs(file: PsiFile, startOffset: Int, endOffset: Int) { + file as? KtFile ?: return + val project = file.project + + val marker = JvmEventGenHelper.doReformat(project, file, startOffset, endOffset) ?: return + + ShortenReferences.DEFAULT.process(file, marker.startOffset, marker.endOffset) + } +} diff --git a/src/main/kotlin/insight/generation/EventListenerGenerationSupport.kt b/src/main/kotlin/insight/generation/EventListenerGenerationSupport.kt new file mode 100644 index 000000000..2b8862892 --- /dev/null +++ b/src/main/kotlin/insight/generation/EventListenerGenerationSupport.kt @@ -0,0 +1,38 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.insight.generation + +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement + +interface EventListenerGenerationSupport { + + fun canGenerate(context: PsiElement, editor: Editor): Boolean + + fun generateEventListener( + context: PsiElement, + listenerName: String, + eventClass: PsiClass, + data: GenerationData?, + editor: Editor + ) +} diff --git a/src/main/kotlin/insight/generation/GenerateEventListenerAction.kt b/src/main/kotlin/insight/generation/GenerateEventListenerAction.kt index c8397e0ca..b53442ec7 100644 --- a/src/main/kotlin/insight/generation/GenerateEventListenerAction.kt +++ b/src/main/kotlin/insight/generation/GenerateEventListenerAction.kt @@ -21,13 +21,35 @@ package com.demonwav.mcdev.insight.generation import com.demonwav.mcdev.asset.MCDevBundle -import com.intellij.codeInsight.generation.actions.BaseGenerateAction +import com.demonwav.mcdev.facet.MinecraftFacet +import com.demonwav.mcdev.util.findModule +import com.intellij.codeInsight.CodeInsightActionHandler +import com.intellij.codeInsight.actions.CodeInsightAction import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile -class GenerateEventListenerAction : BaseGenerateAction(GenerateEventListenerHandler()) { +class GenerateEventListenerAction : CodeInsightAction() { + + private val handler = GenerateEventListenerHandler() + + override fun getHandler(): CodeInsightActionHandler = handler override fun update(e: AnActionEvent) { super.update(e) e.presentation.text = MCDevBundle("generate.event_listener.title") } + + override fun isValidForFile( + project: Project, + editor: Editor, + file: PsiFile + ): Boolean { + val module = file.findModule() ?: return false + val minecraftFacet = MinecraftFacet.getInstance(module) ?: return false + val support = minecraftFacet.modules.firstNotNullOfOrNull { it.eventListenerGenSupport } ?: return false + val caretElement = file.findElementAt(editor.caretModel.offset) ?: return false + return support.canGenerate(caretElement, editor) + } } diff --git a/src/main/kotlin/insight/generation/GenerateEventListenerHandler.kt b/src/main/kotlin/insight/generation/GenerateEventListenerHandler.kt index dd6695fb2..cfd8e45a7 100644 --- a/src/main/kotlin/insight/generation/GenerateEventListenerHandler.kt +++ b/src/main/kotlin/insight/generation/GenerateEventListenerHandler.kt @@ -20,72 +20,45 @@ package com.demonwav.mcdev.insight.generation -import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.facet.MinecraftFacet import com.demonwav.mcdev.insight.generation.ui.EventGenerationDialog import com.demonwav.mcdev.platform.AbstractModule -import com.demonwav.mcdev.util.castNotNull -import com.intellij.codeInsight.generation.ClassMember -import com.intellij.codeInsight.generation.GenerateMembersHandlerBase -import com.intellij.codeInsight.generation.GenerationInfo -import com.intellij.codeInsight.generation.PsiGenerationInfo +import com.demonwav.mcdev.util.findModule +import com.demonwav.mcdev.util.mapFirstNotNull +import com.intellij.codeInsight.CodeInsightActionHandler import com.intellij.ide.util.TreeClassChooserFactory -import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.editor.CaretModel import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.LogicalPosition -import com.intellij.openapi.module.ModuleUtilCore import com.intellij.openapi.project.Project import com.intellij.psi.PsiClass import com.intellij.psi.PsiFile -import com.intellij.psi.PsiMethod import com.intellij.psi.search.GlobalSearchScope -import com.intellij.psi.util.PsiTreeUtil import com.intellij.refactoring.RefactoringBundle -/** - * The standard handler to generate a new event listener as a method. - * Note that this is a psuedo generator as it relies on a wizard and the - * [.cleanup] to complete - */ -class GenerateEventListenerHandler : GenerateMembersHandlerBase(MCDevBundle("generate.event_listener.title")) { - - private data class GenerateData( - var editor: Editor, - var position: LogicalPosition, - var method: PsiMethod?, - var model: CaretModel, - var data: GenerationData?, - var chosenClass: PsiClass, - var chosenName: String, - var relevantModule: AbstractModule, - ) - - private var data: GenerateData? = null - - override fun getHelpId() = "Generate Event Listener Dialog" +class GenerateEventListenerHandler : CodeInsightActionHandler { - override fun chooseOriginalMembers(aClass: PsiClass, project: Project, editor: Editor): Array? { - val moduleForPsiElement = ModuleUtilCore.findModuleForPsiElement(aClass) ?: return null - - val facet = MinecraftFacet.getInstance(moduleForPsiElement) ?: return null + override fun invoke(project: Project, editor: Editor, file: PsiFile) { + val module = file.findModule() ?: return + val facet = MinecraftFacet.getInstance(module) ?: return + val eventListenerGenSupport = facet.modules.mapFirstNotNull { it.eventListenerGenSupport } ?: return + val caretElement = file.findElementAt(editor.caretModel.offset) ?: return + val context = caretElement.context ?: return val chooser = TreeClassChooserFactory.getInstance(project) .createWithInnerClassesScopeChooser( RefactoringBundle.message("choose.destination.class"), - GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(moduleForPsiElement, false), + GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module, false), { aClass1 -> isSuperEventListenerAllowed(aClass1, facet) }, null, ) chooser.showDialog() - val chosenClass = chooser.selected ?: return null + val chosenClass = chooser.selected ?: return val relevantModule = facet.modules.asSequence() .filter { m -> isSuperEventListenerAllowed(chosenClass, m) } - .firstOrNull() ?: return null + .firstOrNull() ?: return - val chosenClassName = chosenClass.nameIdentifier?.text ?: return null + val chosenClassName = chosenClass.nameIdentifier?.text ?: return val generationDialog = EventGenerationDialog( editor, @@ -94,71 +67,20 @@ class GenerateEventListenerHandler : GenerateMembersHandlerBase(MCDevBundle("gen relevantModule.moduleType.getDefaultListenerName(chosenClass), ) - val okay = generationDialog.showAndGet() - - if (!okay) { - return null + if (!generationDialog.showAndGet()) { + return } - val dialogDAta = generationDialog.data - val chosenName = generationDialog.chosenName - - val position = editor.caretModel.logicalPosition - - val method = PsiTreeUtil.getParentOfType( - aClass.containingFile.findElementAt(editor.caretModel.offset), - PsiMethod::class.java, - ) - - this.data = GenerateData( - editor, - position, - method, - editor.caretModel, - dialogDAta, + eventListenerGenSupport.generateEventListener( + context, + generationDialog.chosenName, chosenClass, - chosenName, - relevantModule, + generationDialog.data, + editor ) - - return DUMMY_RESULT - } - - override fun getAllOriginalMembers(aClass: PsiClass) = null - - override fun generateMemberPrototypes(aClass: PsiClass, originalMember: ClassMember?): Array? { - if (data == null) { - return null - } - - data?.let { data -> - data.relevantModule.doPreEventGenerate(aClass, data.data) - - data.model.moveToLogicalPosition(data.position) - - val newMethod = - data.relevantModule.generateEventListenerMethod(aClass, data.chosenClass, data.chosenName, data.data) - - if (newMethod != null) { - val info = PsiGenerationInfo(newMethod) - info.positionCaret(data.editor, true) - if (data.method != null) { - info.insert(aClass, data.method, false) - } - - return arrayOf(info) - } - } - - return null } - override fun isAvailableForQuickList(editor: Editor, file: PsiFile, dataContext: DataContext): Boolean { - val module = ModuleUtilCore.findModuleForPsiElement(file) ?: return false - - val instance = MinecraftFacet.getInstance(module) - return instance != null && instance.isEventGenAvailable - } + override fun startInWriteAction(): Boolean = false private fun isSuperEventListenerAllowed(eventClass: PsiClass, module: AbstractModule): Boolean { val supers = eventClass.supers @@ -185,10 +107,4 @@ class GenerateEventListenerHandler : GenerateMembersHandlerBase(MCDevBundle("gen } return false } - - companion object { - private val DUMMY_RESULT = - // cannot return empty array, but this result won't be used anyway - arrayOfNulls(1).castNotNull() - } } diff --git a/src/main/kotlin/insight/generation/MethodRenderer.kt b/src/main/kotlin/insight/generation/MethodRenderer.kt new file mode 100644 index 000000000..7444f62ed --- /dev/null +++ b/src/main/kotlin/insight/generation/MethodRenderer.kt @@ -0,0 +1,173 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.insight.generation + +import com.intellij.lang.jvm.JvmModifier +import com.intellij.lang.jvm.actions.AnnotationAttributeRequest +import com.intellij.lang.jvm.actions.AnnotationAttributeValueRequest +import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypes + +interface MethodRenderer { + + fun renderMethod( + name: String, + parameters: List>, + modifiers: Set, + returnType: PsiType, + annotations: List>> + ): String + + companion object { + + val byLanguage = mapOf( + "JAVA" to JavaRenderer, + "kotlin" to KotlinRenderer, + ) + } + + private object JavaRenderer : MethodRenderer { + + override fun renderMethod( + name: String, + parameters: List>, + modifiers: Set, + returnType: PsiType, + annotations: List>> + ): String = buildString { + for ((fqn, attributes) in annotations) { + renderAnnotation(fqn, attributes) + appendLine() + } + + when { + JvmModifier.PUBLIC in modifiers -> append("public ") + JvmModifier.PRIVATE in modifiers -> append("private ") + JvmModifier.PROTECTED in modifiers -> append("protected ") + } + + when { + JvmModifier.STATIC in modifiers -> append("static ") + JvmModifier.ABSTRACT in modifiers -> append("abstract ") + JvmModifier.FINAL in modifiers -> append("final ") + } + + append(returnType.canonicalText) + append(' ') + append(name) + parameters.joinTo(this, prefix = "(", postfix = ")") { (paramName, paramType) -> + paramType.canonicalText + " " + paramName + } + appendLine("{}") + } + + fun StringBuilder.renderAnnotation(fqn: String, attributes: List) { + append('@') + append(fqn) + if (attributes.isNotEmpty()) { + attributes.joinTo(this, prefix = "(", postfix = ")") { attribute -> + attribute.name + " = " + renderAnnotationValue(attribute.value) + } + } + } + + fun renderAnnotationValue(value: AnnotationAttributeValueRequest): String = when (value) { + is AnnotationAttributeValueRequest.PrimitiveValue -> value.value.toString() + is AnnotationAttributeValueRequest.StringValue -> '"' + value.value + '"' + is AnnotationAttributeValueRequest.ClassValue -> value.classFqn + ".class" + is AnnotationAttributeValueRequest.ConstantValue -> value.text + is AnnotationAttributeValueRequest.NestedAnnotation -> buildString { + renderAnnotation(value.annotationRequest.qualifiedName, value.annotationRequest.attributes) + } + + is AnnotationAttributeValueRequest.ArrayValue -> + value.members.joinToString(prefix = "{", postfix = "}", transform = ::renderAnnotationValue) + } + } + + private object KotlinRenderer : MethodRenderer { + + override fun renderMethod( + name: String, + parameters: List>, + modifiers: Set, + returnType: PsiType, + annotations: List>> + ): String = buildString { + for ((fqn, attributes) in annotations) { + renderAnnotation(fqn, attributes) + appendLine() + } + + if (JvmModifier.STATIC in modifiers) { + appendLine("@JvmStatic") + } + + when { + // Skipping public as it is the default visibility + JvmModifier.PRIVATE in modifiers -> append("private ") + JvmModifier.PROTECTED in modifiers -> append("protected ") + JvmModifier.PACKAGE_LOCAL in modifiers -> append("internal ") // Close enough + } + + when { + JvmModifier.ABSTRACT in modifiers -> append("abstract ") + JvmModifier.FINAL in modifiers -> append("final ") + } + + append("fun ") + append(name) + parameters.joinTo(this, prefix = "(", postfix = ")") { (paramName, paramType) -> + paramName + ": " + paramType.canonicalText + } + + if (returnType != PsiTypes.voidType()) { + append(": ") + append(returnType.canonicalText) + } + + appendLine("{}") + } + + fun StringBuilder.renderAnnotation(fqn: String, attributes: List) { + append('@') + append(fqn) + if (attributes.isNotEmpty()) { + attributes.joinTo(this, prefix = "(", postfix = ")") { attribute -> + attribute.name + " = " + renderAnnotationValue(attribute.value) + } + } + } + + fun renderAnnotationValue(value: AnnotationAttributeValueRequest): String = when (value) { + is AnnotationAttributeValueRequest.PrimitiveValue -> value.value.toString() + is AnnotationAttributeValueRequest.StringValue -> '"' + value.value + '"' + is AnnotationAttributeValueRequest.ClassValue -> value.classFqn + "::class" + is AnnotationAttributeValueRequest.ConstantValue -> value.text + is AnnotationAttributeValueRequest.NestedAnnotation -> buildString { + renderAnnotation(value.annotationRequest.qualifiedName, value.annotationRequest.attributes) + } + + is AnnotationAttributeValueRequest.ArrayValue -> + value.members.joinToString(prefix = "[", postfix = "]", transform = ::renderAnnotationValue) + } + } +} diff --git a/src/main/kotlin/insight/generation/MethodRendererBasedEventListenerGenerationSupport.kt b/src/main/kotlin/insight/generation/MethodRendererBasedEventListenerGenerationSupport.kt new file mode 100644 index 000000000..5766bf9e4 --- /dev/null +++ b/src/main/kotlin/insight/generation/MethodRendererBasedEventListenerGenerationSupport.kt @@ -0,0 +1,94 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.insight.generation + +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import org.jetbrains.uast.UClass +import org.jetbrains.uast.UDeclaration +import org.jetbrains.uast.getUastParentOfType + +abstract class MethodRendererBasedEventListenerGenerationSupport : EventListenerGenerationSupport { + + override fun canGenerate(context: PsiElement, editor: Editor): Boolean { + if (context.language.id !in MethodRenderer.byLanguage) { + return false + } + + return adjustOffset(context, editor) != null + } + + override fun generateEventListener( + context: PsiElement, + listenerName: String, + eventClass: PsiClass, + data: GenerationData?, + editor: Editor + ) = runWriteAction { + val document = editor.document + + preGenerationProcess(context, data) + PsiDocumentManager.getInstance(context.project).doPostponedOperationsAndUnblockDocument(document) + + val renderer = MethodRenderer.byLanguage[context.language.id]!! + val offset = adjustOffset(context, editor) ?: return@runWriteAction + val text = invokeRenderer(renderer, context, listenerName, eventClass, data, editor) + + document.insertString(offset, text) + PsiDocumentManager.getInstance(context.project).commitDocument(document) + + val file = context.containingFile + editor.caretModel.moveToOffset(offset + text.length - 2) + + EventGenHelper.COLLECTOR.forLanguage(file.language) + .reformatAndShortenRefs(file, offset, offset + text.length) + } + + private fun adjustOffset(context: PsiElement, editor: Editor): Int? { + val declaration = context.getUastParentOfType() + if (declaration == null) { + return null + } + + if (declaration is UClass) { + return editor.caretModel.offset + } + + return declaration.sourcePsi?.textRange?.endOffset + } + + protected open fun preGenerationProcess( + context: PsiElement, + data: GenerationData?, + ) = Unit + + protected abstract fun invokeRenderer( + renderer: MethodRenderer, + context: PsiElement, + listenerName: String, + eventClass: PsiClass, + data: GenerationData?, + editor: Editor + ): String +} diff --git a/src/main/kotlin/insight/generation/ui/EventGenerationPanel.kt b/src/main/kotlin/insight/generation/ui/EventGenerationPanel.kt index 68559fcd2..e8a294c5d 100644 --- a/src/main/kotlin/insight/generation/ui/EventGenerationPanel.kt +++ b/src/main/kotlin/insight/generation/ui/EventGenerationPanel.kt @@ -43,12 +43,10 @@ open class EventGenerationPanel(val chosenClass: PsiClass) { /** * This is called when the dialog is closing from the OK action. The platform should fill in their [GenerationData] object as * needed for whatever information their panel provides. The state of the panel can be assumed to be valid, since this will only be - * called if [.doValidate] has passed successfully. + * called if [doValidate] has passed successfully. * @return The [GenerationData] object which will be passed to the - * * [AbstractModule#doPreEventGenerate()][com.demonwav.mcdev.platform.AbstractModule.doPreEventGenerate] and - * * [AbstractModule#generateEventListenerMethod][com.demonwav.mcdev.platform.AbstractModule.generateEventListenerMethod] - * * methods. + * [EventListenerGenerationSupport][com.demonwav.mcdev.insight.generation.EventListenerGenerationSupport] */ open fun gatherData(): GenerationData? { return null diff --git a/src/main/kotlin/platform/AbstractModule.kt b/src/main/kotlin/platform/AbstractModule.kt index 86ad41590..a546e432c 100644 --- a/src/main/kotlin/platform/AbstractModule.kt +++ b/src/main/kotlin/platform/AbstractModule.kt @@ -21,7 +21,7 @@ package com.demonwav.mcdev.platform import com.demonwav.mcdev.facet.MinecraftFacet -import com.demonwav.mcdev.insight.generation.GenerationData +import com.demonwav.mcdev.insight.generation.EventListenerGenerationSupport import com.demonwav.mcdev.inspection.IsCancelled import com.intellij.openapi.module.Module import com.intellij.psi.PsiClass @@ -44,6 +44,8 @@ abstract class AbstractModule(protected val facet: MinecraftFacet) { open val icon: Icon? get() = moduleType.icon + open val eventListenerGenSupport: EventListenerGenerationSupport? = null + /** * By default, this method is provided in the case that a specific platform has no * listener handling whatsoever, or simply accepts event listeners with random @@ -63,15 +65,6 @@ abstract class AbstractModule(protected val facet: MinecraftFacet) { open fun writeErrorMessageForEventParameter(eventClass: PsiClass, method: PsiMethod) = "Parameter does not extend the proper Event Class!" - open fun doPreEventGenerate(psiClass: PsiClass, data: GenerationData?) {} - - open fun generateEventListenerMethod( - containingClass: PsiClass, - chosenClass: PsiClass, - chosenName: String, - data: GenerationData?, - ): PsiMethod? = null - open fun shouldShowPluginIcon(element: PsiElement?) = false open fun checkUselessCancelCheck(expression: PsiMethodCallExpression): IsCancelled? { diff --git a/src/main/kotlin/platform/bukkit/BukkitEventListenerGenerationSupport.kt b/src/main/kotlin/platform/bukkit/BukkitEventListenerGenerationSupport.kt new file mode 100644 index 000000000..7e2ddfe67 --- /dev/null +++ b/src/main/kotlin/platform/bukkit/BukkitEventListenerGenerationSupport.kt @@ -0,0 +1,81 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.platform.bukkit + +import com.demonwav.mcdev.insight.generation.EventGenHelper +import com.demonwav.mcdev.insight.generation.GenerationData +import com.demonwav.mcdev.insight.generation.MethodRenderer +import com.demonwav.mcdev.insight.generation.MethodRendererBasedEventListenerGenerationSupport +import com.demonwav.mcdev.platform.bukkit.generation.BukkitGenerationData +import com.demonwav.mcdev.platform.bukkit.util.BukkitConstants +import com.demonwav.mcdev.util.psiType +import com.intellij.lang.jvm.JvmModifier +import com.intellij.lang.jvm.actions.AnnotationAttributeRequest +import com.intellij.lang.jvm.actions.constantAttribute +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiTypes + +class BukkitEventListenerGenerationSupport : MethodRendererBasedEventListenerGenerationSupport() { + + override fun preGenerationProcess( + context: PsiElement, + data: GenerationData? + ) { + require(data is BukkitGenerationData) + + EventGenHelper.COLLECTOR.forLanguage(context.language) + .addImplements(context, BukkitConstants.LISTENER_CLASS) + } + + override fun invokeRenderer( + renderer: MethodRenderer, + context: PsiElement, + listenerName: String, + eventClass: PsiClass, + data: GenerationData?, + editor: Editor + ): String { + require(data is BukkitGenerationData) + + val handlerAttributes = mutableListOf() + if (data.eventPriority != "NORMAL") { + handlerAttributes.add( + constantAttribute("priority", BukkitConstants.EVENT_PRIORITY_CLASS + '.' + data.eventPriority) + ) + } + + if (data.isIgnoreCanceled) { + handlerAttributes.add(constantAttribute("ignoreCancelled", "true")) + } + + return renderer.renderMethod( + listenerName, + listOf("event" to eventClass.psiType), + setOf(JvmModifier.PUBLIC), + PsiTypes.voidType(), + listOf( + BukkitConstants.HANDLER_ANNOTATION to handlerAttributes + ) + ) + } +} diff --git a/src/main/kotlin/platform/bukkit/BukkitModule.kt b/src/main/kotlin/platform/bukkit/BukkitModule.kt index a1380b36d..9639e5b6d 100644 --- a/src/main/kotlin/platform/bukkit/BukkitModule.kt +++ b/src/main/kotlin/platform/bukkit/BukkitModule.kt @@ -21,16 +21,14 @@ package com.demonwav.mcdev.platform.bukkit import com.demonwav.mcdev.facet.MinecraftFacet -import com.demonwav.mcdev.insight.generation.GenerationData +import com.demonwav.mcdev.insight.generation.EventListenerGenerationSupport import com.demonwav.mcdev.inspection.IsCancelled import com.demonwav.mcdev.platform.AbstractModule import com.demonwav.mcdev.platform.AbstractModuleType import com.demonwav.mcdev.platform.PlatformType -import com.demonwav.mcdev.platform.bukkit.generation.BukkitGenerationData import com.demonwav.mcdev.platform.bukkit.util.BukkitConstants import com.demonwav.mcdev.platform.bukkit.util.PaperConstants import com.demonwav.mcdev.util.SourceType -import com.demonwav.mcdev.util.addImplements import com.demonwav.mcdev.util.createVoidMethodWithParameterType import com.demonwav.mcdev.util.extendsOrImplements import com.demonwav.mcdev.util.findContainingMethod @@ -66,6 +64,8 @@ class BukkitModule>(facet: MinecraftFacet, type: T override val moduleType: T = type + override val eventListenerGenSupport: EventListenerGenerationSupport? = BukkitEventListenerGenerationSupport() + private val pluginParentClasses = listOf( BukkitConstants.PLUGIN, PaperConstants.PLUGIN_BOOTSTRAP, @@ -81,44 +81,6 @@ class BukkitModule>(facet: MinecraftFacet, type: T "Parameter is not a subclass of org.bukkit.event.Event\n" + "Compiling and running this listener may result in a runtime exception" - override fun doPreEventGenerate(psiClass: PsiClass, data: GenerationData?) { - if (!psiClass.extendsOrImplements(BukkitConstants.LISTENER_CLASS)) { - psiClass.addImplements(BukkitConstants.LISTENER_CLASS) - } - } - - override fun generateEventListenerMethod( - containingClass: PsiClass, - chosenClass: PsiClass, - chosenName: String, - data: GenerationData?, - ): PsiMethod? { - val bukkitData = data as BukkitGenerationData - - val method = generateBukkitStyleEventListenerMethod( - chosenClass, - chosenName, - project, - BukkitConstants.HANDLER_ANNOTATION, - bukkitData.isIgnoreCanceled, - ) ?: return null - - if (bukkitData.eventPriority != "NORMAL") { - val list = method.modifierList - val annotation = list.findAnnotation(BukkitConstants.HANDLER_ANNOTATION) ?: return method - - val value = JavaPsiFacade.getElementFactory(project) - .createExpressionFromText( - BukkitConstants.EVENT_PRIORITY_CLASS + "." + bukkitData.eventPriority, - annotation, - ) - - annotation.setDeclaredAttributeValue("priority", value) - } - - return method - } - override fun checkUselessCancelCheck(expression: PsiMethodCallExpression): IsCancelled? { val method = expression.findContainingMethod() ?: return null diff --git a/src/main/kotlin/platform/bungeecord/BungeeCordEventListenerGenerationSupport.kt b/src/main/kotlin/platform/bungeecord/BungeeCordEventListenerGenerationSupport.kt new file mode 100644 index 000000000..39ec4e5e2 --- /dev/null +++ b/src/main/kotlin/platform/bungeecord/BungeeCordEventListenerGenerationSupport.kt @@ -0,0 +1,77 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.platform.bungeecord + +import com.demonwav.mcdev.insight.generation.EventGenHelper +import com.demonwav.mcdev.insight.generation.GenerationData +import com.demonwav.mcdev.insight.generation.MethodRenderer +import com.demonwav.mcdev.insight.generation.MethodRendererBasedEventListenerGenerationSupport +import com.demonwav.mcdev.platform.bungeecord.generation.BungeeCordGenerationData +import com.demonwav.mcdev.platform.bungeecord.util.BungeeCordConstants +import com.demonwav.mcdev.util.psiType +import com.intellij.lang.jvm.JvmModifier +import com.intellij.lang.jvm.actions.AnnotationAttributeRequest +import com.intellij.lang.jvm.actions.constantAttribute +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiTypes + +class BungeeCordEventListenerGenerationSupport : MethodRendererBasedEventListenerGenerationSupport() { + + override fun preGenerationProcess( + context: PsiElement, + data: GenerationData? + ) { + require(data is BungeeCordGenerationData) + + EventGenHelper.COLLECTOR.forLanguage(context.language) + .addImplements(context, BungeeCordConstants.LISTENER_CLASS) + } + + override fun invokeRenderer( + renderer: MethodRenderer, + context: PsiElement, + listenerName: String, + eventClass: PsiClass, + data: GenerationData?, + editor: Editor + ): String { + require(data is BungeeCordGenerationData) + + val handlerAttributes = mutableListOf() + if (data.eventPriority != "NORMAL") { + handlerAttributes.add( + constantAttribute("priority", BungeeCordConstants.EVENT_PRIORITY_CLASS + '.' + data.eventPriority) + ) + } + + return renderer.renderMethod( + listenerName, + listOf("event" to eventClass.psiType), + setOf(JvmModifier.PUBLIC), + PsiTypes.voidType(), + listOf( + BungeeCordConstants.HANDLER_ANNOTATION to handlerAttributes + ) + ) + } +} diff --git a/src/main/kotlin/platform/bungeecord/BungeeCordModule.kt b/src/main/kotlin/platform/bungeecord/BungeeCordModule.kt index e43a520f8..77710b505 100644 --- a/src/main/kotlin/platform/bungeecord/BungeeCordModule.kt +++ b/src/main/kotlin/platform/bungeecord/BungeeCordModule.kt @@ -21,19 +21,15 @@ package com.demonwav.mcdev.platform.bungeecord import com.demonwav.mcdev.facet.MinecraftFacet -import com.demonwav.mcdev.insight.generation.GenerationData +import com.demonwav.mcdev.insight.generation.EventListenerGenerationSupport import com.demonwav.mcdev.platform.AbstractModule import com.demonwav.mcdev.platform.AbstractModuleType import com.demonwav.mcdev.platform.PlatformType -import com.demonwav.mcdev.platform.bukkit.BukkitModule import com.demonwav.mcdev.platform.bukkit.BukkitModuleType import com.demonwav.mcdev.platform.bukkit.PaperModuleType import com.demonwav.mcdev.platform.bukkit.SpigotModuleType -import com.demonwav.mcdev.platform.bungeecord.generation.BungeeCordGenerationData import com.demonwav.mcdev.platform.bungeecord.util.BungeeCordConstants import com.demonwav.mcdev.util.SourceType -import com.demonwav.mcdev.util.addImplements -import com.demonwav.mcdev.util.extendsOrImplements import com.demonwav.mcdev.util.nullable import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions import com.intellij.lang.jvm.JvmModifier @@ -65,6 +61,8 @@ class BungeeCordModule>(facet: MinecraftFacet, typ override val moduleType: T = type + override val eventListenerGenSupport: EventListenerGenerationSupport? = BungeeCordEventListenerGenerationSupport() + override fun isEventClassValid(eventClass: PsiClass, method: PsiMethod?) = BungeeCordConstants.EVENT_CLASS == eventClass.qualifiedName @@ -72,48 +70,6 @@ class BungeeCordModule>(facet: MinecraftFacet, typ "Parameter is not a subclass of net.md_5.bungee.api.plugin.Event\n" + "Compiling and running this listener may result in a runtime exception" - override fun doPreEventGenerate(psiClass: PsiClass, data: GenerationData?) { - val bungeeCordListenerClass = BungeeCordConstants.LISTENER_CLASS - - if (!psiClass.extendsOrImplements(bungeeCordListenerClass)) { - psiClass.addImplements(bungeeCordListenerClass) - } - } - - override fun generateEventListenerMethod( - containingClass: PsiClass, - chosenClass: PsiClass, - chosenName: String, - data: GenerationData?, - ): PsiMethod? { - val method = BukkitModule.generateBukkitStyleEventListenerMethod( - chosenClass, - chosenName, - project, - BungeeCordConstants.HANDLER_ANNOTATION, - false, - ) ?: return null - - val generationData = data as BungeeCordGenerationData? ?: return method - - val modifierList = method.modifierList - val annotation = modifierList.findAnnotation(BungeeCordConstants.HANDLER_ANNOTATION) ?: return method - - if (generationData.eventPriority == "NORMAL") { - return method - } - - val value = JavaPsiFacade.getElementFactory(project) - .createExpressionFromText( - BungeeCordConstants.EVENT_PRIORITY_CLASS + "." + generationData.eventPriority, - annotation, - ) - - annotation.setDeclaredAttributeValue("priority", value) - - return method - } - override fun shouldShowPluginIcon(element: PsiElement?): Boolean { val identifier = element?.toUElementOfType() ?: return false diff --git a/src/main/kotlin/platform/forge/ForgeEventListenerGenerationSupport.kt b/src/main/kotlin/platform/forge/ForgeEventListenerGenerationSupport.kt new file mode 100644 index 000000000..7bf687067 --- /dev/null +++ b/src/main/kotlin/platform/forge/ForgeEventListenerGenerationSupport.kt @@ -0,0 +1,59 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.platform.forge + +import com.demonwav.mcdev.insight.generation.GenerationData +import com.demonwav.mcdev.insight.generation.MethodRenderer +import com.demonwav.mcdev.insight.generation.MethodRendererBasedEventListenerGenerationSupport +import com.demonwav.mcdev.platform.forge.util.ForgeConstants +import com.demonwav.mcdev.util.extendsOrImplements +import com.demonwav.mcdev.util.psiType +import com.intellij.lang.jvm.JvmModifier +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiTypes + +class ForgeEventListenerGenerationSupport : MethodRendererBasedEventListenerGenerationSupport() { + + override fun invokeRenderer( + renderer: MethodRenderer, + context: PsiElement, + listenerName: String, + eventClass: PsiClass, + data: GenerationData?, + editor: Editor + ): String { + val annotationFqn = if (eventClass.extendsOrImplements(ForgeConstants.FML_EVENT)) { + ForgeConstants.EVENT_HANDLER_ANNOTATION + } else { + ForgeConstants.EVENTBUS_SUBSCRIBE_EVENT_ANNOTATION + } + + return renderer.renderMethod( + listenerName, + listOf("event" to eventClass.psiType), + setOf(JvmModifier.PUBLIC), + PsiTypes.voidType(), + listOf(annotationFqn to emptyList()) + ) + } +} diff --git a/src/main/kotlin/platform/forge/ForgeModule.kt b/src/main/kotlin/platform/forge/ForgeModule.kt index ee426dc1c..57dba30b3 100644 --- a/src/main/kotlin/platform/forge/ForgeModule.kt +++ b/src/main/kotlin/platform/forge/ForgeModule.kt @@ -22,7 +22,7 @@ package com.demonwav.mcdev.platform.forge import com.demonwav.mcdev.asset.PlatformAssets import com.demonwav.mcdev.facet.MinecraftFacet -import com.demonwav.mcdev.insight.generation.GenerationData +import com.demonwav.mcdev.insight.generation.EventListenerGenerationSupport import com.demonwav.mcdev.inspection.IsCancelled import com.demonwav.mcdev.platform.AbstractModule import com.demonwav.mcdev.platform.PlatformType @@ -31,8 +31,6 @@ import com.demonwav.mcdev.platform.forge.util.ForgeConstants import com.demonwav.mcdev.platform.mcp.McpModuleSettings import com.demonwav.mcdev.util.SemanticVersion import com.demonwav.mcdev.util.SourceType -import com.demonwav.mcdev.util.createVoidMethodWithParameterType -import com.demonwav.mcdev.util.extendsOrImplements import com.demonwav.mcdev.util.nullable import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions import com.demonwav.mcdev.util.runWriteTaskLater @@ -62,6 +60,8 @@ class ForgeModule internal constructor(facet: MinecraftFacet) : AbstractModule(f override val type = PlatformType.FORGE override val icon = PlatformAssets.FORGE_ICON + override val eventListenerGenSupport: EventListenerGenerationSupport = ForgeEventListenerGenerationSupport() + override fun init() { ApplicationManager.getApplication().executeOnPooledThread { waitForAllSmart() @@ -158,32 +158,6 @@ class ForgeModule internal constructor(facet: MinecraftFacet) : AbstractModule(f override fun isStaticListenerSupported(method: PsiMethod) = true - override fun generateEventListenerMethod( - containingClass: PsiClass, - chosenClass: PsiClass, - chosenName: String, - data: GenerationData?, - ): PsiMethod? { - val isFmlEvent = chosenClass.extendsOrImplements(ForgeConstants.FML_EVENT) - - val method = createVoidMethodWithParameterType(project, chosenName, chosenClass) ?: return null - val modifierList = method.modifierList - - if (isFmlEvent) { - modifierList.addAnnotation(ForgeConstants.EVENT_HANDLER_ANNOTATION) - } else { - val mcVersion = McpModuleSettings.getInstance(module).state.minecraftVersion - ?.let { SemanticVersion.parse(it) } - if (mcVersion != null && mcVersion >= ForgeModuleType.FG3_MC_VERSION) { - modifierList.addAnnotation(ForgeConstants.EVENTBUS_SUBSCRIBE_EVENT_ANNOTATION) - } else { - modifierList.addAnnotation(ForgeConstants.SUBSCRIBE_EVENT_ANNOTATION) - } - } - - return method - } - override fun shouldShowPluginIcon(element: PsiElement?): Boolean { val identifier = element?.toUElementOfType() ?: return false diff --git a/src/main/kotlin/platform/neoforge/NeoForgeEventListenerGenerationSupport.kt b/src/main/kotlin/platform/neoforge/NeoForgeEventListenerGenerationSupport.kt new file mode 100644 index 000000000..ce7d11e5b --- /dev/null +++ b/src/main/kotlin/platform/neoforge/NeoForgeEventListenerGenerationSupport.kt @@ -0,0 +1,52 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.platform.neoforge + +import com.demonwav.mcdev.insight.generation.GenerationData +import com.demonwav.mcdev.insight.generation.MethodRenderer +import com.demonwav.mcdev.insight.generation.MethodRendererBasedEventListenerGenerationSupport +import com.demonwav.mcdev.platform.neoforge.util.NeoForgeConstants +import com.demonwav.mcdev.util.psiType +import com.intellij.lang.jvm.JvmModifier +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiTypes + +class NeoForgeEventListenerGenerationSupport : MethodRendererBasedEventListenerGenerationSupport() { + + override fun invokeRenderer( + renderer: MethodRenderer, + context: PsiElement, + listenerName: String, + eventClass: PsiClass, + data: GenerationData?, + editor: Editor + ): String { + return renderer.renderMethod( + listenerName, + listOf("event" to eventClass.psiType), + setOf(JvmModifier.PUBLIC), + PsiTypes.voidType(), + listOf(NeoForgeConstants.SUBSCRIBE_EVENT to emptyList()) + ) + } +} diff --git a/src/main/kotlin/platform/neoforge/NeoForgeModule.kt b/src/main/kotlin/platform/neoforge/NeoForgeModule.kt index 56341c4a1..0209e2bbd 100644 --- a/src/main/kotlin/platform/neoforge/NeoForgeModule.kt +++ b/src/main/kotlin/platform/neoforge/NeoForgeModule.kt @@ -22,13 +22,12 @@ package com.demonwav.mcdev.platform.neoforge import com.demonwav.mcdev.asset.PlatformAssets import com.demonwav.mcdev.facet.MinecraftFacet -import com.demonwav.mcdev.insight.generation.GenerationData +import com.demonwav.mcdev.insight.generation.EventListenerGenerationSupport import com.demonwav.mcdev.inspection.IsCancelled import com.demonwav.mcdev.platform.AbstractModule import com.demonwav.mcdev.platform.PlatformType import com.demonwav.mcdev.platform.neoforge.util.NeoForgeConstants import com.demonwav.mcdev.util.SourceType -import com.demonwav.mcdev.util.createVoidMethodWithParameterType import com.demonwav.mcdev.util.nullable import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions import com.demonwav.mcdev.util.runWriteTaskLater @@ -54,6 +53,8 @@ class NeoForgeModule internal constructor(facet: MinecraftFacet) : AbstractModul override val type = PlatformType.NEOFORGE override val icon = PlatformAssets.NEOFORGE_ICON + override val eventListenerGenSupport: EventListenerGenerationSupport = NeoForgeEventListenerGenerationSupport() + override fun init() { ApplicationManager.getApplication().executeOnPooledThread { waitForAllSmart() @@ -97,20 +98,6 @@ class NeoForgeModule internal constructor(facet: MinecraftFacet) : AbstractModul override fun isStaticListenerSupported(method: PsiMethod) = true - override fun generateEventListenerMethod( - containingClass: PsiClass, - chosenClass: PsiClass, - chosenName: String, - data: GenerationData?, - ): PsiMethod? { - val method = createVoidMethodWithParameterType(project, chosenName, chosenClass) ?: return null - val modifierList = method.modifierList - - modifierList.addAnnotation(NeoForgeConstants.SUBSCRIBE_EVENT) - - return method - } - override fun shouldShowPluginIcon(element: PsiElement?): Boolean { val identifier = element?.toUElementOfType() ?: return false diff --git a/src/main/kotlin/platform/sponge/SpongeEventListenerGenerationSupport.kt b/src/main/kotlin/platform/sponge/SpongeEventListenerGenerationSupport.kt new file mode 100644 index 000000000..e5ab7c3e6 --- /dev/null +++ b/src/main/kotlin/platform/sponge/SpongeEventListenerGenerationSupport.kt @@ -0,0 +1,78 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.platform.sponge + +import com.demonwav.mcdev.insight.generation.GenerationData +import com.demonwav.mcdev.insight.generation.MethodRenderer +import com.demonwav.mcdev.insight.generation.MethodRendererBasedEventListenerGenerationSupport +import com.demonwav.mcdev.platform.sponge.generation.SpongeGenerationData +import com.demonwav.mcdev.platform.sponge.util.SpongeConstants +import com.demonwav.mcdev.util.psiType +import com.intellij.lang.jvm.JvmModifier +import com.intellij.lang.jvm.actions.AnnotationAttributeRequest +import com.intellij.lang.jvm.actions.constantAttribute +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiTypes + +class SpongeEventListenerGenerationSupport : MethodRendererBasedEventListenerGenerationSupport() { + + override fun invokeRenderer( + renderer: MethodRenderer, + context: PsiElement, + listenerName: String, + eventClass: PsiClass, + data: GenerationData?, + editor: Editor + ): String { + require(data is SpongeGenerationData) + + val handlerAnnotations = mutableListOf>>() + + val handlerAttributes = mutableListOf() + if (data.eventOrder != "DEFAULT") { + handlerAttributes.add( + constantAttribute("order", SpongeConstants.ORDER + '.' + data.eventOrder) + ) + } + + handlerAnnotations.add(SpongeConstants.LISTENER_ANNOTATION to handlerAttributes) + + if (!data.isIgnoreCanceled) { + handlerAnnotations.add( + SpongeConstants.IS_CANCELLED_ANNOTATION to listOf( + constantAttribute("value", "org.spongepowered.api.util.Tristate.UNDEFINED") + ) + ) + } + + return renderer.renderMethod( + listenerName, + listOf("event" to eventClass.psiType), + setOf(JvmModifier.PUBLIC), + PsiTypes.voidType(), + listOf( + SpongeConstants.LISTENER_ANNOTATION to handlerAttributes + ) + ) + } +} diff --git a/src/main/kotlin/platform/sponge/SpongeModule.kt b/src/main/kotlin/platform/sponge/SpongeModule.kt index 6ad51c3d1..5f59430f9 100644 --- a/src/main/kotlin/platform/sponge/SpongeModule.kt +++ b/src/main/kotlin/platform/sponge/SpongeModule.kt @@ -22,19 +22,16 @@ package com.demonwav.mcdev.platform.sponge import com.demonwav.mcdev.asset.PlatformAssets import com.demonwav.mcdev.facet.MinecraftFacet -import com.demonwav.mcdev.insight.generation.GenerationData +import com.demonwav.mcdev.insight.generation.EventListenerGenerationSupport import com.demonwav.mcdev.inspection.IsCancelled import com.demonwav.mcdev.platform.AbstractModule import com.demonwav.mcdev.platform.PlatformType -import com.demonwav.mcdev.platform.sponge.generation.SpongeGenerationData import com.demonwav.mcdev.platform.sponge.util.SpongeConstants -import com.demonwav.mcdev.util.createVoidMethodWithParameterType import com.demonwav.mcdev.util.extendsOrImplements import com.demonwav.mcdev.util.findContainingMethod import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions import com.intellij.lang.jvm.JvmModifier import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.PsiAnnotationMemberValue import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement import com.intellij.psi.PsiMethod @@ -49,6 +46,8 @@ class SpongeModule(facet: MinecraftFacet) : AbstractModule(facet) { override val type = PlatformType.SPONGE override val icon = PlatformAssets.SPONGE_ICON + override val eventListenerGenSupport: EventListenerGenerationSupport = SpongeEventListenerGenerationSupport() + override fun isEventClassValid(eventClass: PsiClass, method: PsiMethod?) = "org.spongepowered.api.event.Event" == eventClass.qualifiedName @@ -56,40 +55,6 @@ class SpongeModule(facet: MinecraftFacet) : AbstractModule(facet) { "Parameter is not an instance of org.spongepowered.api.event.Event\n" + "Compiling and running this listener may result in a runtime exception" - override fun generateEventListenerMethod( - containingClass: PsiClass, - chosenClass: PsiClass, - chosenName: String, - data: GenerationData?, - ): PsiMethod? { - val method = createVoidMethodWithParameterType(project, chosenName, chosenClass) ?: return null - val modifierList = method.modifierList - - val listenerAnnotation = modifierList.addAnnotation("org.spongepowered.api.event.Listener") - - val generationData = (data as SpongeGenerationData?)!! - - if (!generationData.isIgnoreCanceled) { - val annotation = modifierList.addAnnotation("org.spongepowered.api.event.filter.IsCancelled") - val value = JavaPsiFacade.getElementFactory(project) - .createExpressionFromText("org.spongepowered.api.util.Tristate.UNDEFINED", annotation) - - annotation.setDeclaredAttributeValue("value", value) - } - - if (generationData.eventOrder != "DEFAULT") { - val value = JavaPsiFacade.getElementFactory(project) - .createExpressionFromText( - "org.spongepowered.api.event.Order." + generationData.eventOrder, - listenerAnnotation, - ) - - listenerAnnotation.setDeclaredAttributeValue("order", value) - } - - return method - } - override fun shouldShowPluginIcon(element: PsiElement?): Boolean { val identifier = element?.toUElementOfType() ?: return false diff --git a/src/main/kotlin/platform/sponge/util/SpongeConstants.kt b/src/main/kotlin/platform/sponge/util/SpongeConstants.kt index f242bfa4d..6cb88cf8c 100644 --- a/src/main/kotlin/platform/sponge/util/SpongeConstants.kt +++ b/src/main/kotlin/platform/sponge/util/SpongeConstants.kt @@ -31,6 +31,7 @@ object SpongeConstants { const val TEXT_COLORS = "org.spongepowered.api.text.format.TextColors" const val EVENT = "org.spongepowered.api.event.Event" const val LISTENER_ANNOTATION = "org.spongepowered.api.event.Listener" + const val ORDER = "org.spongepowered.api.event.Order" const val GETTER_ANNOTATION = "org.spongepowered.api.event.filter.Getter" const val IS_CANCELLED_ANNOTATION = "org.spongepowered.api.event.filter.IsCancelled" const val CANCELLABLE = "org.spongepowered.api.event.Cancellable" diff --git a/src/main/kotlin/platform/velocity/VelocityEventListenerGenerationSupport.kt b/src/main/kotlin/platform/velocity/VelocityEventListenerGenerationSupport.kt new file mode 100644 index 000000000..02109af9f --- /dev/null +++ b/src/main/kotlin/platform/velocity/VelocityEventListenerGenerationSupport.kt @@ -0,0 +1,66 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.platform.velocity + +import com.demonwav.mcdev.insight.generation.GenerationData +import com.demonwav.mcdev.insight.generation.MethodRenderer +import com.demonwav.mcdev.insight.generation.MethodRendererBasedEventListenerGenerationSupport +import com.demonwav.mcdev.platform.velocity.generation.VelocityGenerationData +import com.demonwav.mcdev.platform.velocity.util.VelocityConstants +import com.demonwav.mcdev.util.psiType +import com.intellij.lang.jvm.JvmModifier +import com.intellij.lang.jvm.actions.AnnotationAttributeRequest +import com.intellij.lang.jvm.actions.constantAttribute +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiTypes + +class VelocityEventListenerGenerationSupport : MethodRendererBasedEventListenerGenerationSupport() { + + override fun invokeRenderer( + renderer: MethodRenderer, + context: PsiElement, + listenerName: String, + eventClass: PsiClass, + data: GenerationData?, + editor: Editor + ): String { + require(data is VelocityGenerationData) + + val handlerAttributes = mutableListOf() + if (data.eventOrder != "NORMAL") { + handlerAttributes.add( + constantAttribute("order", VelocityConstants.POST_ORDER + '.' + data.eventOrder) + ) + } + + return renderer.renderMethod( + listenerName, + listOf("event" to eventClass.psiType), + setOf(JvmModifier.PUBLIC), + PsiTypes.voidType(), + listOf( + VelocityConstants.SUBSCRIBE_ANNOTATION to handlerAttributes + ) + ) + } +} diff --git a/src/main/kotlin/platform/velocity/VelocityModule.kt b/src/main/kotlin/platform/velocity/VelocityModule.kt index 84d4ba245..7624971be 100644 --- a/src/main/kotlin/platform/velocity/VelocityModule.kt +++ b/src/main/kotlin/platform/velocity/VelocityModule.kt @@ -22,16 +22,12 @@ package com.demonwav.mcdev.platform.velocity import com.demonwav.mcdev.asset.PlatformAssets import com.demonwav.mcdev.facet.MinecraftFacet -import com.demonwav.mcdev.insight.generation.GenerationData +import com.demonwav.mcdev.insight.generation.EventListenerGenerationSupport import com.demonwav.mcdev.platform.AbstractModule import com.demonwav.mcdev.platform.PlatformType -import com.demonwav.mcdev.platform.velocity.generation.VelocityGenerationData import com.demonwav.mcdev.platform.velocity.util.VelocityConstants -import com.demonwav.mcdev.platform.velocity.util.VelocityConstants.SUBSCRIBE_ANNOTATION -import com.demonwav.mcdev.util.createVoidMethodWithParameterType import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions import com.intellij.lang.jvm.JvmModifier -import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement import com.intellij.psi.PsiMethod @@ -44,33 +40,9 @@ class VelocityModule(facet: MinecraftFacet) : AbstractModule(facet) { override val type = PlatformType.VELOCITY override val icon = PlatformAssets.VELOCITY_ICON - override fun isEventClassValid(eventClass: PsiClass, method: PsiMethod?): Boolean = true - - override fun generateEventListenerMethod( - containingClass: PsiClass, - chosenClass: PsiClass, - chosenName: String, - data: GenerationData?, - ): PsiMethod? { - val method = createVoidMethodWithParameterType(project, chosenName, chosenClass) ?: return null - val modifierList = method.modifierList - - val subscribeAnnotation = modifierList.addAnnotation(SUBSCRIBE_ANNOTATION) - - val generationData = data as VelocityGenerationData + override val eventListenerGenSupport: EventListenerGenerationSupport = VelocityEventListenerGenerationSupport() - if (generationData.eventOrder != "NORMAL") { - val value = JavaPsiFacade.getElementFactory(project) - .createExpressionFromText( - "com.velocitypowered.api.event.PostOrder." + generationData.eventOrder, - subscribeAnnotation, - ) - - subscribeAnnotation.setDeclaredAttributeValue("order", value) - } - - return method - } + override fun isEventClassValid(eventClass: PsiClass, method: PsiMethod?): Boolean = true override fun shouldShowPluginIcon(element: PsiElement?): Boolean { val identifier = element?.toUElementOfType() diff --git a/src/main/kotlin/platform/velocity/util/VelocityConstants.kt b/src/main/kotlin/platform/velocity/util/VelocityConstants.kt index a7fd51180..38165d479 100644 --- a/src/main/kotlin/platform/velocity/util/VelocityConstants.kt +++ b/src/main/kotlin/platform/velocity/util/VelocityConstants.kt @@ -26,6 +26,7 @@ object VelocityConstants { const val PLUGIN_ANNOTATION = "com.velocitypowered.api.plugin.Plugin" const val SUBSCRIBE_ANNOTATION = "com.velocitypowered.api.event.Subscribe" + const val POST_ORDER = "com.velocitypowered.api.event.PostOrder" const val KYORI_TEXT_COLOR = "net.kyori.text.format.TextColor" val API_2 = SemanticVersion.release(2) diff --git a/src/main/resources/META-INF/mcdev-kotlin.xml b/src/main/resources/META-INF/mcdev-kotlin.xml index e49b5a467..2ff77caa7 100644 --- a/src/main/resources/META-INF/mcdev-kotlin.xml +++ b/src/main/resources/META-INF/mcdev-kotlin.xml @@ -21,5 +21,7 @@ + + diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index cf1cdd934..978d84018 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -92,6 +92,10 @@ + + + + @@ -199,6 +203,8 @@ + + diff --git a/src/main/resources/messages/MinecraftDevelopment.properties b/src/main/resources/messages/MinecraftDevelopment.properties index 2e79f30a3..e1abcbe96 100644 --- a/src/main/resources/messages/MinecraftDevelopment.properties +++ b/src/main/resources/messages/MinecraftDevelopment.properties @@ -159,7 +159,7 @@ facet.reimport.failed.title=Minecraft facet refresh facet.reimport.failed.content.no_error=Failed to start project refresh, please refresh your project manually. facet.reimport.failed.content.with_error=Failed to start project refresh, please refresh your project manually. Cause: {0} -generate.event_listener.title=Generate Event Listener +generate.event_listener.title=Event Listener generate.event_listener.settings=Event Listener Settings generate.event_listener.event_priority=Event Priority generate.event_listener.event_order=Event Order diff --git a/src/main/resources/messages/MinecraftDevelopment_fr.properties b/src/main/resources/messages/MinecraftDevelopment_fr.properties index 618dee4ea..d9492a1b5 100644 --- a/src/main/resources/messages/MinecraftDevelopment_fr.properties +++ b/src/main/resources/messages/MinecraftDevelopment_fr.properties @@ -18,5 +18,5 @@ # along with this program. If not, see . # -generate.event_listener.title=Générer un Event Listener +generate.event_listener.title=Event Listener generate.event_listener.settings=Configuration du Listener From 32278d5e44bba28c44c739b51a782a5b20fd9aa9 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 30 Jul 2024 14:11:50 +0200 Subject: [PATCH 30/37] Fill the changelog a fair bit --- changelog.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/changelog.md b/changelog.md index c7b74ffd6..a57d8ded1 100644 --- a/changelog.md +++ b/changelog.md @@ -6,11 +6,20 @@ - Access widener completion in fabric.mod.json - Event listener generation for Kotlin +- `JUMP` injection point support (without source navigation) +- Inspection highlighting that `JUMP` usages are discouraged +- Inspection highlighting discouraged instruction shifts +- Inspections for when @Inject local capture is unused and for when they can be replaced with @Local +- [#2306](https://github.com/minecraft-dev/MinecraftDev/issues/2306) Use mixin icon for mixin classes ### Fixed - [#2330](https://github.com/minecraft-dev/MinecraftDev/issues/2330) Reformat created files without keeping line breaks. Fixes the Velocity main class annotation's bad formatting. - [#2331](https://github.com/minecraft-dev/MinecraftDev/issues/2331) Support fabric.mod.json in test resources +- MixinExtras occasional cache desync ([#2335](https://github.com/minecraft-dev/MinecraftDev/pull/2335)) +- [#2163](https://github.com/minecraft-dev/MinecraftDev/issues/2163) `@ModifyVariable` method signature checking with `STORE` +- [#2282](https://github.com/minecraft-dev/MinecraftDev/issues/2282) Mixin support confusion with `$` and `.` separators in class names +- Recent NeoModDev version import errors ## [1.8.0] From e54877a7ab68d6ad87f32d951ec94ed428279d55 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 30 Jul 2024 15:04:03 +0200 Subject: [PATCH 31/37] Add testLibs system properties directly in the task configuration This fixes a configuration cache error and works the same. The last config cache issue seems to be from the licenser plugin, which is already fixed in master but not released yet -_- --- build.gradle.kts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index cce2ac752..ced14b1b5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -296,10 +296,9 @@ tasks.processResources { tasks.test { dependsOn(tasks.jar, testLibs) useJUnitPlatform() - doFirst { - testLibs.resolvedConfiguration.resolvedArtifacts.forEach { - systemProperty("testLibs.${it.name}", it.file.absolutePath) - } + + testLibs.resolvedConfiguration.resolvedArtifacts.forEach { + systemProperty("testLibs.${it.name}", it.file.absolutePath) } systemProperty("NO_FS_ROOTS_ACCESS_CHECK", "true") systemProperty("java.awt.headless", "true") From 93be50594da1105874d960a8a873e301404c39ea Mon Sep 17 00:00:00 2001 From: KikuGie Date: Thu, 1 Aug 2024 13:08:22 +0200 Subject: [PATCH 32/37] Use `LayeredIcon` for mixin classes (#2340) * Use layered icons for mixin classes * Resize icons * Remove outlines * Use `IconLayerProvider` * Clean up svg --- src/main/kotlin/asset/MixinAssets.kt | 2 ++ .../mixin/framework/MixinIconProvider.kt | 16 ++++++++++------ src/main/resources/META-INF/plugin.xml | 2 +- .../resources/assets/icons/mixin/mixin_mark.svg | 3 +++ 4 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 src/main/resources/assets/icons/mixin/mixin_mark.svg diff --git a/src/main/kotlin/asset/MixinAssets.kt b/src/main/kotlin/asset/MixinAssets.kt index 67dde8e19..8a93d0c91 100644 --- a/src/main/kotlin/asset/MixinAssets.kt +++ b/src/main/kotlin/asset/MixinAssets.kt @@ -27,4 +27,6 @@ object MixinAssets : Assets() { val MIXIN_CLASS_ICON = loadIcon("/assets/icons/mixin/mixin_class_gutter.png") val MIXIN_CLASS_ICON_DARK = loadIcon("/assets/icons/mixin/mixin_class_gutter_dark.png") + + val MIXIN_MARK = loadIcon("/assets/icons/mixin/mixin_mark.svg") } diff --git a/src/main/kotlin/platform/mixin/framework/MixinIconProvider.kt b/src/main/kotlin/platform/mixin/framework/MixinIconProvider.kt index 646b68f02..c886e4338 100644 --- a/src/main/kotlin/platform/mixin/framework/MixinIconProvider.kt +++ b/src/main/kotlin/platform/mixin/framework/MixinIconProvider.kt @@ -21,15 +21,19 @@ package com.demonwav.mcdev.platform.mixin.framework import com.demonwav.mcdev.MinecraftSettings -import com.demonwav.mcdev.asset.PlatformAssets +import com.demonwav.mcdev.asset.MixinAssets import com.demonwav.mcdev.platform.mixin.util.isMixin -import com.intellij.ide.IconProvider +import com.intellij.ide.IconLayerProvider +import com.intellij.openapi.util.Iconable import com.intellij.psi.PsiClass -import com.intellij.psi.PsiElement +import javax.swing.Icon -class MixinIconProvider : IconProvider() { - override fun getIcon(element: PsiElement, flags: Int) = - PlatformAssets.MIXIN_ICON.takeIf { +class MixinIconProvider : IconLayerProvider { + override fun getLayerIcon(element: Iconable, isLocked: Boolean): Icon? = + MixinAssets.MIXIN_MARK.takeIf { MinecraftSettings.instance.mixinClassIcon && element is PsiClass && element.isMixin } + + override fun getLayerDescription(): String = + "Mixin class" } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 978d84018..27a38c6a3 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -534,7 +534,7 @@ - + + + From 23821336cf07f577b4058eb057250b806adb2bc5 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 1 Aug 2024 16:34:30 +0200 Subject: [PATCH 33/37] Move asm-util dep to existing asm bundle --- build.gradle.kts | 1 - gradle/libs.versions.toml | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index ced14b1b5..8dda80673 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -118,7 +118,6 @@ dependencies { implementation(libs.mixinExtras.expressions) testLibs(libs.mixinExtras.common) - implementation("org.ow2.asm:asm-util:9.3") // Kotlin implementation(kotlin("stdlib-jdk8")) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 941db8f7c..9c1c92308 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,6 +26,7 @@ groovy = "org.codehaus.groovy:groovy-all:2.5.18" asm = { module = "org.ow2.asm:asm", version.ref = "asm" } asm-tree = { module = "org.ow2.asm:asm-tree", version.ref = "asm" } asm-analysis = { module = "org.ow2.asm:asm-analysis", version.ref = "asm" } +asm-util = { module = "org.ow2.asm:asm-util", version.ref = "asm" } fuel = { module = "com.github.kittinunf.fuel:fuel", version.ref = "fuel" } fuel-coroutines = { module = "com.github.kittinunf.fuel:fuel-coroutines", version.ref = "fuel" } @@ -45,5 +46,5 @@ mixinExtras-common = "io.github.llamalad7:mixinextras-common:0.5.0-beta.1" [bundles] coroutines = ["coroutines-core", "coroutines-jdk8", "coroutines-swing"] -asm = ["asm", "asm-tree", "asm-analysis"] +asm = ["asm", "asm-tree", "asm-analysis", "asm-util"] fuel = ["fuel", "fuel-coroutines"] From 1801be074a48212f6a2c201299deb2a103737be2 Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 1 Aug 2024 16:45:29 +0200 Subject: [PATCH 34/37] Provide documentation in mods.toml key completion --- src/main/kotlin/toml/TomlSchema.kt | 6 +++ .../forge/ModsTomlDocumentationProvider.kt | 47 +++++++++++++++++++ .../ModsTomlCompletionContributor.kt | 15 ++++-- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/toml/TomlSchema.kt b/src/main/kotlin/toml/TomlSchema.kt index 6c040f8e1..26bdb0193 100644 --- a/src/main/kotlin/toml/TomlSchema.kt +++ b/src/main/kotlin/toml/TomlSchema.kt @@ -42,9 +42,15 @@ class TomlSchema private constructor( fun topLevelKeys(isArray: Boolean): Set = tables.filter { it.isArray == isArray }.mapTo(mutableSetOf()) { it.name } + fun topLevelEntries(isArray: Boolean): Set = + tables.filter { it.isArray == isArray }.flatMapTo(mutableSetOf()) { it.entries } + fun keysForTable(tableName: String): Set = tableSchema(tableName)?.entries?.mapTo(mutableSetOf()) { it.key }.orEmpty() + fun entriesForTable(tableName: String): Set = + tableSchema(tableName)?.entries.orEmpty() + fun tableEntry(tableName: String, key: String): TomlSchemaEntry? = tableSchema(tableName)?.entries?.find { it.key == key } diff --git a/src/main/kotlin/toml/platform/forge/ModsTomlDocumentationProvider.kt b/src/main/kotlin/toml/platform/forge/ModsTomlDocumentationProvider.kt index 260def521..13b9a3b9b 100644 --- a/src/main/kotlin/toml/platform/forge/ModsTomlDocumentationProvider.kt +++ b/src/main/kotlin/toml/platform/forge/ModsTomlDocumentationProvider.kt @@ -21,12 +21,19 @@ package com.demonwav.mcdev.toml.platform.forge import com.demonwav.mcdev.platform.forge.util.ForgeConstants +import com.demonwav.mcdev.toml.TomlSchemaEntry import com.intellij.lang.documentation.DocumentationMarkup import com.intellij.lang.documentation.DocumentationProvider +import com.intellij.navigation.ItemPresentation import com.intellij.openapi.editor.Editor +import com.intellij.openapi.util.NlsSafe import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile +import com.intellij.psi.PsiManager +import com.intellij.psi.impl.FakePsiElement +import com.intellij.psi.impl.source.DummyHolderFactory import com.intellij.psi.util.parentOfType +import javax.swing.Icon import org.toml.lang.psi.TomlHeaderOwner import org.toml.lang.psi.TomlKey import org.toml.lang.psi.TomlKeySegment @@ -50,7 +57,28 @@ class ModsTomlDocumentationProvider : DocumentationProvider { ?: contextElement?.parentOfType() } + override fun getDocumentationElementForLookupItem( + psiManager: PsiManager, + entry: Any?, + element: PsiElement? + ): PsiElement? { + if (entry !is TomlSchemaEntry) { + return null + } + + val description = entry.description.joinToString("\n") + return if (description.isNotBlank()) { + TomlSchemaKeyElement(entry.key, description, psiManager) + } else { + null + } + } + override fun generateDoc(element: PsiElement, originalElement: PsiElement?): String? { + if (element is TomlSchemaKeyElement) { + return element.description + } + if (element !is TomlKeySegment || !isModsToml(originalElement)) { return null } @@ -80,4 +108,23 @@ class ModsTomlDocumentationProvider : DocumentationProvider { private fun isModsToml(element: PsiElement?): Boolean = element?.containingFile?.virtualFile?.name == ForgeConstants.MODS_TOML + + private class TomlSchemaKeyElement( + val key: String, + val description: String, + val psiManager: PsiManager + ) : FakePsiElement() { + + private val dummyHolder = DummyHolderFactory.createHolder(psiManager, null) + + override fun getParent(): PsiElement? = dummyHolder + + override fun getManager(): PsiManager? = psiManager + + override fun getPresentation(): ItemPresentation? = object : ItemPresentation { + override fun getPresentableText(): @NlsSafe String? = key + + override fun getIcon(unused: Boolean): Icon? = null + } + } } diff --git a/src/main/kotlin/toml/platform/forge/completion/ModsTomlCompletionContributor.kt b/src/main/kotlin/toml/platform/forge/completion/ModsTomlCompletionContributor.kt index 45de2e1f2..e28c50f7d 100644 --- a/src/main/kotlin/toml/platform/forge/completion/ModsTomlCompletionContributor.kt +++ b/src/main/kotlin/toml/platform/forge/completion/ModsTomlCompletionContributor.kt @@ -21,6 +21,7 @@ package com.demonwav.mcdev.toml.platform.forge.completion import com.demonwav.mcdev.platform.forge.util.ForgeConstants +import com.demonwav.mcdev.toml.TomlSchemaEntry import com.demonwav.mcdev.toml.TomlStringValueInsertionHandler import com.demonwav.mcdev.toml.inModsTomlKey import com.demonwav.mcdev.toml.inModsTomlValueWithKey @@ -88,7 +89,7 @@ object ModsTomlKeyCompletionProvider : CompletionProvider( val keySegment = parameters.position.parent as? TomlKeySegment ?: return val key = keySegment.parent as? TomlKey ?: return val table = key.parentOfType() - val variants = when (val parent = key.parent) { + val variants: Collection = when (val parent = key.parent) { is TomlTableHeader -> { if (key != parent.key?.segments?.firstOrNull()) { return @@ -98,22 +99,26 @@ object ModsTomlKeyCompletionProvider : CompletionProvider( is TomlTable -> false else -> return } - schema.topLevelKeys(isArray) - table.entries.mapTo(HashSet()) { it.key.text } + val existingKeys = table.entries.mapTo(HashSet()) { it.key.text } + schema.topLevelEntries(isArray).filter { it.key !in existingKeys } } is TomlKeyValue -> when (table) { null -> { - schema.topLevelEntries.map { it.key } - + val existingKeys = key.containingFile.children.filterIsInstance().mapTo(HashSet()) { it.key.text } + schema.topLevelEntries.filter { it.key !in existingKeys } } is TomlHeaderOwner -> { val tableName = table.header.key?.segments?.firstOrNull()?.text ?: return - schema.keysForTable(tableName) - table.entries.mapTo(HashSet()) { it.key.text } + val existingKeys = table.entries.mapTo(HashSet()) { it.key.text } + schema.entriesForTable(tableName).filter { it.key !in existingKeys } } else -> return } else -> return } - result.addAllElements(variants.map(LookupElementBuilder::create)) + + result.addAllElements(variants.map { entry -> LookupElementBuilder.create(entry, entry.key) }) } } From d74d95e782ef9ac5d8ee4dd54224e61f518561fb Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 1 Aug 2024 17:08:13 +0200 Subject: [PATCH 35/37] Make mods.toml support work with neoforge.mods.toml too --- .../neoforge/util/NeoForgeConstants.kt | 1 + .../toml/platform/forge/ForgeTomlConstants.kt | 29 +++++++++++++++++++ .../forge/ModsTomlDocumentationProvider.kt | 3 +- .../ModsTomlValidationInspection.kt | 5 ++-- src/main/kotlin/toml/toml-patterns.kt | 6 ++-- 5 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/toml/platform/forge/ForgeTomlConstants.kt diff --git a/src/main/kotlin/platform/neoforge/util/NeoForgeConstants.kt b/src/main/kotlin/platform/neoforge/util/NeoForgeConstants.kt index a84ee784a..dafe4681b 100644 --- a/src/main/kotlin/platform/neoforge/util/NeoForgeConstants.kt +++ b/src/main/kotlin/platform/neoforge/util/NeoForgeConstants.kt @@ -30,4 +30,5 @@ object NeoForgeConstants { const val NETWORK_MESSAGE = "net.neoforged.neoforge.network.simple.SimpleMessage" const val MCMOD_INFO = "mcmod.info" const val PACK_MCMETA = "pack.mcmeta" + const val MODS_TOML = "neoforge.mods.toml" } diff --git a/src/main/kotlin/toml/platform/forge/ForgeTomlConstants.kt b/src/main/kotlin/toml/platform/forge/ForgeTomlConstants.kt new file mode 100644 index 000000000..498fe2214 --- /dev/null +++ b/src/main/kotlin/toml/platform/forge/ForgeTomlConstants.kt @@ -0,0 +1,29 @@ +/* + * 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 . + */ + +package com.demonwav.mcdev.toml.platform.forge + +import com.demonwav.mcdev.platform.forge.util.ForgeConstants +import com.demonwav.mcdev.platform.neoforge.util.NeoForgeConstants + +object ForgeTomlConstants { + + val FILE_NAMES = setOf(ForgeConstants.MODS_TOML, NeoForgeConstants.MODS_TOML) +} diff --git a/src/main/kotlin/toml/platform/forge/ModsTomlDocumentationProvider.kt b/src/main/kotlin/toml/platform/forge/ModsTomlDocumentationProvider.kt index 13b9a3b9b..f99c7a086 100644 --- a/src/main/kotlin/toml/platform/forge/ModsTomlDocumentationProvider.kt +++ b/src/main/kotlin/toml/platform/forge/ModsTomlDocumentationProvider.kt @@ -20,7 +20,6 @@ package com.demonwav.mcdev.toml.platform.forge -import com.demonwav.mcdev.platform.forge.util.ForgeConstants import com.demonwav.mcdev.toml.TomlSchemaEntry import com.intellij.lang.documentation.DocumentationMarkup import com.intellij.lang.documentation.DocumentationProvider @@ -107,7 +106,7 @@ class ModsTomlDocumentationProvider : DocumentationProvider { } private fun isModsToml(element: PsiElement?): Boolean = - element?.containingFile?.virtualFile?.name == ForgeConstants.MODS_TOML + element?.containingFile?.virtualFile?.name in ForgeTomlConstants.FILE_NAMES private class TomlSchemaKeyElement( val key: String, diff --git a/src/main/kotlin/toml/platform/forge/inspections/ModsTomlValidationInspection.kt b/src/main/kotlin/toml/platform/forge/inspections/ModsTomlValidationInspection.kt index d5490af52..b08f66c5e 100644 --- a/src/main/kotlin/toml/platform/forge/inspections/ModsTomlValidationInspection.kt +++ b/src/main/kotlin/toml/platform/forge/inspections/ModsTomlValidationInspection.kt @@ -22,6 +22,7 @@ package com.demonwav.mcdev.toml.platform.forge.inspections import com.demonwav.mcdev.platform.forge.util.ForgeConstants import com.demonwav.mcdev.toml.TomlElementVisitor +import com.demonwav.mcdev.toml.platform.forge.ForgeTomlConstants import com.demonwav.mcdev.toml.platform.forge.ModsTomlSchema import com.demonwav.mcdev.toml.stringValue import com.demonwav.mcdev.toml.tomlType @@ -53,14 +54,14 @@ class ModsTomlValidationInspection : LocalInspectionTool() { override fun getStaticDescription(): String = "Checks mods.toml files for errors" override fun processFile(file: PsiFile, manager: InspectionManager): MutableList { - if (file.virtualFile.name == ForgeConstants.MODS_TOML) { + if (file.virtualFile.name in ForgeTomlConstants.FILE_NAMES) { return super.processFile(file, manager) } return mutableListOf() } override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { - if (holder.file.virtualFile.name == ForgeConstants.MODS_TOML) { + if (holder.file.virtualFile.name in ForgeTomlConstants.FILE_NAMES) { return Visitor(holder) } return PsiElementVisitor.EMPTY_VISITOR diff --git a/src/main/kotlin/toml/toml-patterns.kt b/src/main/kotlin/toml/toml-patterns.kt index 8186462d0..a8d308452 100644 --- a/src/main/kotlin/toml/toml-patterns.kt +++ b/src/main/kotlin/toml/toml-patterns.kt @@ -20,9 +20,10 @@ package com.demonwav.mcdev.toml -import com.demonwav.mcdev.platform.forge.util.ForgeConstants +import com.demonwav.mcdev.toml.platform.forge.ForgeTomlConstants import com.intellij.patterns.PlatformPatterns import com.intellij.patterns.PsiElementPattern +import com.intellij.patterns.StandardPatterns import com.intellij.patterns.VirtualFilePattern import com.intellij.psi.PsiElement import org.toml.lang.psi.TomlKey @@ -33,7 +34,8 @@ import org.toml.lang.psi.TomlTableHeader inline fun inModsToml(): PsiElementPattern.Capture = inModsToml(E::class.java) fun inModsToml(clazz: Class): PsiElementPattern.Capture = - PlatformPatterns.psiElement(clazz).inVirtualFile(VirtualFilePattern().withName(ForgeConstants.MODS_TOML)) + PlatformPatterns.psiElement(clazz) + .inVirtualFile(VirtualFilePattern().withName(StandardPatterns.string().oneOf(ForgeTomlConstants.FILE_NAMES))) fun inModsTomlKey(): PsiElementPattern.Capture = inModsToml().withParent(TomlKeySegment::class.java) From f3d3406edb8696170933af5f0043545bab40bfbc Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 1 Aug 2024 17:23:45 +0200 Subject: [PATCH 36/37] Insert equals and invoke completion automatically when completing toml key --- .../ModsTomlCompletionContributor.kt | 6 ++++- .../toml/toml/TomlKeyInsertionHandler.kt | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/toml/toml/TomlKeyInsertionHandler.kt diff --git a/src/main/kotlin/toml/platform/forge/completion/ModsTomlCompletionContributor.kt b/src/main/kotlin/toml/platform/forge/completion/ModsTomlCompletionContributor.kt index e28c50f7d..c41653703 100644 --- a/src/main/kotlin/toml/platform/forge/completion/ModsTomlCompletionContributor.kt +++ b/src/main/kotlin/toml/platform/forge/completion/ModsTomlCompletionContributor.kt @@ -26,6 +26,7 @@ import com.demonwav.mcdev.toml.TomlStringValueInsertionHandler import com.demonwav.mcdev.toml.inModsTomlKey import com.demonwav.mcdev.toml.inModsTomlValueWithKey import com.demonwav.mcdev.toml.platform.forge.ModsTomlSchema +import com.demonwav.mcdev.toml.toml.TomlKeyInsertionHandler import com.demonwav.mcdev.util.isAncestorOf import com.intellij.codeInsight.completion.CompletionContributor import com.intellij.codeInsight.completion.CompletionParameters @@ -88,6 +89,7 @@ object ModsTomlKeyCompletionProvider : CompletionProvider( val keySegment = parameters.position.parent as? TomlKeySegment ?: return val key = keySegment.parent as? TomlKey ?: return + val keyValue = key.parent as? TomlKeyValue ?: return val table = key.parentOfType() val variants: Collection = when (val parent = key.parent) { is TomlTableHeader -> { @@ -118,7 +120,9 @@ object ModsTomlKeyCompletionProvider : CompletionProvider( else -> return } - result.addAllElements(variants.map { entry -> LookupElementBuilder.create(entry, entry.key) }) + result.addAllElements(variants.map { entry -> + LookupElementBuilder.create(entry, entry.key).withInsertHandler(TomlKeyInsertionHandler(keyValue)) + }) } } diff --git a/src/main/kotlin/toml/toml/TomlKeyInsertionHandler.kt b/src/main/kotlin/toml/toml/TomlKeyInsertionHandler.kt new file mode 100644 index 000000000..c69caddd9 --- /dev/null +++ b/src/main/kotlin/toml/toml/TomlKeyInsertionHandler.kt @@ -0,0 +1,23 @@ +package com.demonwav.mcdev.toml.toml + +import com.intellij.codeInsight.AutoPopupController +import com.intellij.codeInsight.completion.InsertHandler +import com.intellij.codeInsight.completion.InsertionContext +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.psi.PsiDocumentManager +import org.toml.lang.psi.TomlElementTypes +import org.toml.lang.psi.TomlKeyValue +import org.toml.lang.psi.ext.elementType + +/** Inserts `=` after the completed key if missing and invokes the completion popup for the value automatically */ +class TomlKeyInsertionHandler(private val keyValue: TomlKeyValue) : InsertHandler { + override fun handleInsert(context: InsertionContext, item: LookupElement) { + val hasEq = keyValue.children.any { it.elementType == TomlElementTypes.EQ } + if (!hasEq) { + context.document.insertString(context.tailOffset, " = ") + PsiDocumentManager.getInstance(context.project).commitDocument(context.document) + context.editor.caretModel.moveToOffset(context.tailOffset) // The tail offset is tracked automatically + AutoPopupController.getInstance(context.project).scheduleAutoPopup(context.editor); + } + } +} From 40d6b84a3fa360e29658da71a9c557ac5cf7923d Mon Sep 17 00:00:00 2001 From: RedNesto Date: Thu, 1 Aug 2024 17:29:01 +0200 Subject: [PATCH 37/37] Add missing license and fix style --- .../ModsTomlCompletionContributor.kt | 12 +++++++--- .../toml/toml/TomlKeyInsertionHandler.kt | 22 ++++++++++++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/toml/platform/forge/completion/ModsTomlCompletionContributor.kt b/src/main/kotlin/toml/platform/forge/completion/ModsTomlCompletionContributor.kt index c41653703..d670e7e45 100644 --- a/src/main/kotlin/toml/platform/forge/completion/ModsTomlCompletionContributor.kt +++ b/src/main/kotlin/toml/platform/forge/completion/ModsTomlCompletionContributor.kt @@ -104,25 +104,31 @@ object ModsTomlKeyCompletionProvider : CompletionProvider( val existingKeys = table.entries.mapTo(HashSet()) { it.key.text } schema.topLevelEntries(isArray).filter { it.key !in existingKeys } } + is TomlKeyValue -> when (table) { null -> { val existingKeys = key.containingFile.children.filterIsInstance().mapTo(HashSet()) { it.key.text } schema.topLevelEntries.filter { it.key !in existingKeys } } + is TomlHeaderOwner -> { val tableName = table.header.key?.segments?.firstOrNull()?.text ?: return val existingKeys = table.entries.mapTo(HashSet()) { it.key.text } schema.entriesForTable(tableName).filter { it.key !in existingKeys } } + else -> return } + else -> return } - result.addAllElements(variants.map { entry -> - LookupElementBuilder.create(entry, entry.key).withInsertHandler(TomlKeyInsertionHandler(keyValue)) - }) + result.addAllElements( + variants.map { entry -> + LookupElementBuilder.create(entry, entry.key).withInsertHandler(TomlKeyInsertionHandler(keyValue)) + } + ) } } diff --git a/src/main/kotlin/toml/toml/TomlKeyInsertionHandler.kt b/src/main/kotlin/toml/toml/TomlKeyInsertionHandler.kt index c69caddd9..e06ee50ea 100644 --- a/src/main/kotlin/toml/toml/TomlKeyInsertionHandler.kt +++ b/src/main/kotlin/toml/toml/TomlKeyInsertionHandler.kt @@ -1,3 +1,23 @@ +/* + * 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 . + */ + package com.demonwav.mcdev.toml.toml import com.intellij.codeInsight.AutoPopupController @@ -17,7 +37,7 @@ class TomlKeyInsertionHandler(private val keyValue: TomlKeyValue) : InsertHandle context.document.insertString(context.tailOffset, " = ") PsiDocumentManager.getInstance(context.project).commitDocument(context.document) context.editor.caretModel.moveToOffset(context.tailOffset) // The tail offset is tracked automatically - AutoPopupController.getInstance(context.project).scheduleAutoPopup(context.editor); + AutoPopupController.getInstance(context.project).scheduleAutoPopup(context.editor) } } }