diff --git a/build.gradle.kts b/build.gradle.kts index 2950c2192..6cd13edd8 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")) @@ -298,10 +297,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") 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/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/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")) diff --git a/changelog.md b/changelog.md index a833c4f44..a57d8ded1 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,26 @@ # Minecraft Development for IntelliJ +## [Unreleased] + +### Added + +- 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] This release contains two major features: diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 454bfd4a1..d429ca0c0 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-swing"] -asm = ["asm", "asm-tree", "asm-analysis"] +asm = ["asm", "asm-tree", "asm-analysis", "asm-util"] fuel = ["fuel", "fuel-coroutines"] diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917..2c3521197 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ 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 ########################################################################## 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) ---------------------- 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) 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/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/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/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/CreatorContext.kt b/src/main/kotlin/creator/custom/CreatorContext.kt new file mode 100644 index 000000000..d1f50ec1a --- /dev/null +++ b/src/main/kotlin/creator/custom/CreatorContext.kt @@ -0,0 +1,57 @@ +/* + * 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.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 kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers + +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 682e4256d..73517b8c6 100644 --- a/src/main/kotlin/creator/custom/CustomPlatformStep.kt +++ b/src/main/kotlin/creator/custom/CustomPlatformStep.kt @@ -32,13 +32,16 @@ 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 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 +51,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 +78,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 +91,8 @@ 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 val templateRepoProperty = propertyGraph.property( @@ -109,16 +117,24 @@ 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>() + 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") + } + } override fun setupUI(builder: Panel) { lateinit var templatePropertyPlaceholder: Placeholder @@ -131,16 +147,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,100 +230,69 @@ 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) { properties = mutableMapOf() + creatorContext = creatorContext.copy(properties = properties) if (!template.isValid) { return @@ -321,8 +302,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 ) @@ -422,7 +402,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}") } @@ -435,7 +415,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 } @@ -555,7 +535,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( 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 608864e85..d18154c63 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 { diff --git a/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt index 1c742e80e..fab70421b 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 @@ -50,15 +49,13 @@ 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( 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 +161,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")) @@ -282,7 +279,7 @@ class ArchitecturyVersionsCreatorProperty( updateArchitecturyApiVersions() } - downloadVersions { + downloadVersions(context) { val fabricVersions = fabricVersions if (fabricVersions != null) { loaderVersionModel.removeAllElements() @@ -435,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() } } } @@ -474,8 +470,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..e7745ae48 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 @@ -42,20 +41,17 @@ 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( 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 +131,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")) @@ -222,7 +218,7 @@ class FabricVersionsCreatorProperty( updateFabricApiVersions() } - downloadVersion { + downloadVersion(context) { val fabricVersions = fabricVersions if (fabricVersions != null) { loaderVersionModel.removeAllElements() @@ -309,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() } } } @@ -343,8 +338,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..5503a381a 100644 --- a/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt @@ -22,35 +22,30 @@ 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 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 kotlin.collections.Map import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.swing.Swing +import kotlinx.coroutines.launch 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 +87,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")) @@ -151,7 +146,7 @@ class ForgeVersionsCreatorProperty( } } - downloadVersions { + downloadVersions(context) { reloadMinecraftVersions() loadingVersionsProperty.set(false) @@ -189,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() } } } @@ -212,8 +206,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..9c9016db1 100644 --- a/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt @@ -21,31 +21,27 @@ 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 -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( 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 +51,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) @@ -112,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, @@ -129,6 +126,7 @@ class MavenArtifactVersionCreatorProperty( private var versionsCache = ConcurrentHashMap>() private fun downloadVersions( + context: CreatorContext, key: String, url: String, rawVersionFilter: (String) -> Boolean, @@ -145,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) } } } @@ -170,8 +167,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..e32e15d64 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,29 +32,24 @@ 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 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( 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 +93,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")) @@ -137,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() @@ -166,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() } } } @@ -204,8 +199,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..e8411b8e7 100644 --- a/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt +++ b/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt @@ -22,31 +22,27 @@ 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 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( 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 +88,7 @@ class ParchmentCreatorProperty( ) } - override fun buildUi(panel: Panel, context: WizardContext) { + override fun buildUi(panel: Panel) { panel.row(descriptor.translatedLabel) { checkBox("Use Parchment") .bindSelected(useParchmentProperty) @@ -169,7 +165,7 @@ class ParchmentCreatorProperty( refreshVersionsLists() } - downloadVersions { + downloadVersions(context) { refreshVersionsLists() val minecraftVersion = getPlatformMinecraftVersion() @@ -250,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() } } } @@ -274,8 +269,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 eee7e0ee1..3917ea47f 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) } } 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/platform/mcp/version/McpVersionEntry.kt b/src/main/kotlin/insight/generation/EventListenerGenerationSupport.kt similarity index 57% rename from src/main/kotlin/platform/mcp/version/McpVersionEntry.kt rename to src/main/kotlin/insight/generation/EventListenerGenerationSupport.kt index 52fee57ac..2b8862892 100644 --- a/src/main/kotlin/platform/mcp/version/McpVersionEntry.kt +++ b/src/main/kotlin/insight/generation/EventListenerGenerationSupport.kt @@ -18,22 +18,21 @@ * along with this program. If not, see . */ -package com.demonwav.mcdev.platform.mcp.version +package com.demonwav.mcdev.insight.generation -import com.demonwav.mcdev.platform.mcp.McpVersionPair +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement -class McpVersionEntry(val versionPair: McpVersionPair, val isRed: Boolean = false) { +interface EventListenerGenerationSupport { - override fun toString(): String { - return if (isRed) { - RED_START + versionPair.mcpVersion + RED_END - } else { - versionPair.mcpVersion - } - } + fun canGenerate(context: PsiElement, editor: Editor): Boolean - companion object { - private const val RED_START = "" - private const val RED_END = "" - } + 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/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/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( 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 + } } 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/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/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, 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() 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 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..c886e4338 --- /dev/null +++ b/src/main/kotlin/platform/mixin/framework/MixinIconProvider.kt @@ -0,0 +1,39 @@ +/* + * 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.MinecraftSettings +import com.demonwav.mcdev.asset.MixinAssets +import com.demonwav.mcdev.platform.mixin.util.isMixin +import com.intellij.ide.IconLayerProvider +import com.intellij.openapi.util.Iconable +import com.intellij.psi.PsiClass +import javax.swing.Icon + +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/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 17a917eef..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,6 +102,10 @@ abstract class InjectionPoint { open fun isArgValueList(at: PsiAnnotation, key: String) = false + open val discouragedMessage: String? = null + + open fun isShiftDiscouraged(shift: Int): Boolean = shift != 0 + abstract fun createNavigationVisitor( at: PsiAnnotation, target: MixinSelector?, @@ -144,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 new file mode 100644 index 000000000..ce09d906c --- /dev/null +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/JumpInjectionPoint.kt @@ -0,0 +1,104 @@ +/* + * 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 val discouragedMessage = "Usage of JUMP is discouraged because it is brittle" + + 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/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt index 7ad828179..f85c58681 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?, @@ -103,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 @@ -285,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)) } } } 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/inspection/injector/DiscouragedInjectionPointInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/DiscouragedInjectionPointInspection.kt new file mode 100644 index 000000000..d75bb9ab2 --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/injector/DiscouragedInjectionPointInspection.kt @@ -0,0 +1,48 @@ +/* + * 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 + 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/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/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/reference/InjectionPointReference.kt b/src/main/kotlin/platform/mixin/reference/InjectionPointReference.kt index 4e963eb22..06a427e58 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)?.discouragedMessage == null + } .map { PrioritizedLookupElement.withPriority( LookupElementBuilder.create(it).completeInjectionPoint(context), 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, "...") } 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/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/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/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/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/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 260def521..f99c7a086 100644 --- a/src/main/kotlin/toml/platform/forge/ModsTomlDocumentationProvider.kt +++ b/src/main/kotlin/toml/platform/forge/ModsTomlDocumentationProvider.kt @@ -20,13 +20,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 +56,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 } @@ -79,5 +106,24 @@ 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, + 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..d670e7e45 100644 --- a/src/main/kotlin/toml/platform/forge/completion/ModsTomlCompletionContributor.kt +++ b/src/main/kotlin/toml/platform/forge/completion/ModsTomlCompletionContributor.kt @@ -21,10 +21,12 @@ 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 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 @@ -87,8 +89,9 @@ 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 = when (val parent = key.parent) { + val variants: Collection = when (val parent = key.parent) { is TomlTableHeader -> { if (key != parent.key?.segments?.firstOrNull()) { return @@ -98,22 +101,34 @@ 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).withInsertHandler(TomlKeyInsertionHandler(keyValue)) + } + ) } } 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) diff --git a/src/main/kotlin/toml/toml/TomlKeyInsertionHandler.kt b/src/main/kotlin/toml/toml/TomlKeyInsertionHandler.kt new file mode 100644 index 000000000..e06ee50ea --- /dev/null +++ b/src/main/kotlin/toml/toml/TomlKeyInsertionHandler.kt @@ -0,0 +1,43 @@ +/* + * 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 +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) + } + } +} 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() { 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 362a1fab0..27a38c6a3 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -92,6 +92,10 @@ + + + + @@ -195,9 +199,12 @@ + + + @@ -527,6 +534,8 @@ + + + + + + + diff --git a/src/main/resources/assets/icons/mixin/mixin_mark.svg b/src/main/resources/assets/icons/mixin/mixin_mark.svg new file mode 100644 index 000000000..fa2610055 --- /dev/null +++ b/src/main/resources/assets/icons/mixin/mixin_mark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/messages/MinecraftDevelopment.properties b/src/main/resources/messages/MinecraftDevelopment.properties index 7d8526e81..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 @@ -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 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