diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f58ca5f03..b0908e67e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,6 +10,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + with: + submodules: true + - name: Fetch latest submodule updates + run: git submodule update --remote - uses: actions/setup-java@v3 with: distribution: 'zulu' diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..3bcb1c8f6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "templates"] + path = templates + branch = v1 + url = https://github.com/minecraft-dev/templates diff --git a/build.gradle.kts b/build.gradle.kts index 33eaf9475..2950c2192 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,6 +21,7 @@ import org.cadixdev.gradle.licenser.header.HeaderStyle import org.cadixdev.gradle.licenser.tasks.LicenseUpdate import org.gradle.internal.jvm.Jvm +import org.jetbrains.changelog.Changelog import org.jetbrains.gradle.ext.settings import org.jetbrains.gradle.ext.taskTriggers import org.jetbrains.intellij.tasks.PrepareSandboxTask @@ -37,6 +38,7 @@ plugins { id("org.jetbrains.intellij") version "1.17.2" id("org.cadixdev.licenser") id("org.jlleitschuh.gradle.ktlint") version "10.3.0" + id("org.jetbrains.changelog") version "2.2.0" } val ideaVersionName: String by project @@ -72,6 +74,26 @@ val gradleToolingExtensionJar = tasks.register(gradleToolingExtensionSource archiveClassifier.set("gradle-tooling-extension") } +val templatesSourceSet: SourceSet = sourceSets.create("templates") { + resources { + srcDir("templates") + compileClasspath += sourceSets.main.get().output + } +} + +val templateSourceSets: List = (file("templates").listFiles() ?: emptyArray()).mapNotNull { file -> + if (file.isDirectory() && (file.listFiles() ?: emptyArray()).any { it.name.endsWith(".mcdev.template.json") }) { + sourceSets.create("templates-${file.name}") { + resources { + srcDir(file) + compileClasspath += sourceSets.main.get().output + } + } + } else { + null + } +} + val externalAnnotationsJar = tasks.register("externalAnnotationsJar") { from("externalAnnotations") destinationDirectory.set(layout.buildDirectory.dir("externalAnnotations")) @@ -83,15 +105,21 @@ repositories { maven("https://maven.fabricmc.net/") { content { includeModule("net.fabricmc", "mapping-io") + includeModule("net.fabricmc", "fabric-loader") } } mavenCentral() + maven("https://repo.spongepowered.org/maven/") } dependencies { // Add tools.jar for the JDI API implementation(files(Jvm.current().toolsJar)) + implementation(libs.mixinExtras.expressions) + testLibs(libs.mixinExtras.common) + implementation("org.ow2.asm:asm-util:9.3") + // Kotlin implementation(kotlin("stdlib-jdk8")) implementation(kotlin("reflect")) @@ -121,6 +149,7 @@ dependencies { classifier = "shaded" } } + testLibs(libs.test.fabricloader) testLibs(libs.test.nbt) { artifact { extension = "nbt" @@ -166,6 +195,12 @@ configurations.compileClasspath { attributes.attribute(filtered, true) } +changelog { + version = coreVersion + groups.empty() + path = "changelog.md" +} + intellij { // IntelliJ IDEA dependency version.set(providers.gradleProperty("ideaVersion")) @@ -178,6 +213,7 @@ intellij { "Kotlin", "org.toml.lang:$pluginTomlVersion", "ByteCodeViewer", + "org.intellij.intelliLang", "properties", // needed dependencies for unit tests "junit" @@ -191,6 +227,11 @@ intellij { sandboxDir.set(layout.projectDirectory.dir(".sandbox").toString()) } +tasks.patchPluginXml { + val changelog = project.changelog + changeNotes = changelog.render(Changelog.OutputType.HTML) +} + tasks.publishPlugin { // Build numbers are used for properties["buildNumber"]?.let { buildNumber -> @@ -343,7 +384,13 @@ val generateNbttParser by parser("NbttParser", "com/demonwav/mcdev/nbt/lang/gen" val generateLangLexer by lexer("LangLexer", "com/demonwav/mcdev/translations/lang/gen") val generateLangParser by parser("LangParser", "com/demonwav/mcdev/translations/lang/gen") -val generateTranslationTemplateLexer by lexer("TranslationTemplateLexer", "com/demonwav/mcdev/translations/lang/gen") +val generateMEExpressionLexer by lexer("MEExpressionLexer", "com/demonwav/mcdev/platform/mixin/expression/gen") +val generateMEExpressionParser by parser("MEExpressionParser", "com/demonwav/mcdev/platform/mixin/expression/gen") + +val generateTranslationTemplateLexer by lexer( + "TranslationTemplateLexer", + "com/demonwav/mcdev/translations/template/gen" +) val generate by tasks.registering { group = "minecraft" @@ -358,6 +405,8 @@ val generate by tasks.registering { generateNbttParser, generateLangLexer, generateLangParser, + generateMEExpressionLexer, + generateMEExpressionParser, generateTranslationTemplateLexer, ) } @@ -374,9 +423,14 @@ tasks.register("cleanSandbox", Delete::class) { } tasks.withType { + pluginJar.set(tasks.jar.get().archiveFile) from(externalAnnotationsJar) { into("Minecraft Development/lib/resources") } + from("templates") { + exclude(".git") + into("Minecraft Development/lib/resources/builtin-templates") + } } tasks.runIde { @@ -387,11 +441,19 @@ tasks.runIde { systemProperty("idea.debug.mode", "true") } // Set these properties to test different languages - // systemProperty("user.language", "en") - // systemProperty("user.country", "US") + // systemProperty("user.language", "fr") + // systemProperty("user.country", "FR") } tasks.buildSearchableOptions { // not working atm enabled = false } + +tasks.instrumentCode { + enabled = false +} + +tasks.instrumentedJar { + enabled = false +} diff --git a/buildSrc/src/main/kotlin/JFlexExec.kt b/buildSrc/src/main/kotlin/JFlexExec.kt new file mode 100644 index 000000000..dca469daa --- /dev/null +++ b/buildSrc/src/main/kotlin/JFlexExec.kt @@ -0,0 +1,84 @@ +/* + * 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 . + */ + +import java.io.ByteArrayOutputStream +import javax.inject.Inject +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.ConfigurableFileTree +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.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 + +abstract class JFlexExec : JavaExec() { + + @get:InputFile + abstract val sourceFile: RegularFileProperty + + @get:InputFiles + abstract val jflex: ConfigurableFileCollection + + @get:InputFile + abstract val skeletonFile: RegularFileProperty + + @get:OutputDirectory + abstract val destinationDirectory: DirectoryProperty + + @get:OutputFile + abstract val destinationFile: RegularFileProperty + + @get:Internal + abstract val logFile: RegularFileProperty + + @get:Inject + abstract val fs: FileSystemOperations + + init { + mainClass.set("jflex.Main") + } + + override fun exec() { + classpath = jflex + + args( + "--skel", skeletonFile.get().asFile.absolutePath, + "-d", destinationDirectory.get().asFile.absolutePath, + sourceFile.get().asFile.absolutePath + ) + + fs.delete { delete(destinationDirectory) } + + val taskOutput = ByteArrayOutputStream() + standardOutput = taskOutput + errorOutput = taskOutput + + super.exec() + + val log = logFile.get().asFile + log.parentFile.mkdirs() + log.writeBytes(taskOutput.toByteArray()) + } +} diff --git a/buildSrc/src/main/kotlin/ParserExec.kt b/buildSrc/src/main/kotlin/ParserExec.kt new file mode 100644 index 000000000..adb38256d --- /dev/null +++ b/buildSrc/src/main/kotlin/ParserExec.kt @@ -0,0 +1,88 @@ +/* + * 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 . + */ + +import java.io.ByteArrayOutputStream +import javax.inject.Inject +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.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 + +abstract class ParserExec : JavaExec() { + + @get:InputFile + abstract val sourceFile: RegularFileProperty + + @get:InputFiles + abstract val grammarKit: ConfigurableFileCollection + + @get:OutputDirectory + abstract val destinationRootDirectory: DirectoryProperty + + @get:OutputDirectory + abstract val destinationDirectory: DirectoryProperty + + @get:OutputDirectory + abstract val psiDirectory: DirectoryProperty + + @get:OutputDirectory + abstract val parserDirectory: DirectoryProperty + + @get:Internal + abstract val logFile: RegularFileProperty + + @get:Inject + abstract val fs: FileSystemOperations + + init { + mainClass.set("org.intellij.grammar.Main") + + jvmArgs( + "--add-opens", "java.base/java.lang=ALL-UNNAMED", + "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED", + "--add-opens", "java.base/java.util=ALL-UNNAMED" + ) + } + + override fun exec() { + classpath = grammarKit + args( + destinationRootDirectory.get().asFile, + sourceFile.get().asFile + ) + + fs.delete { delete(psiDirectory, parserDirectory) } + + val taskOutput = ByteArrayOutputStream() + standardOutput = taskOutput + errorOutput = taskOutput + + super.exec() + + val log = logFile.get().asFile + log.parentFile.mkdirs() + log.writeBytes(taskOutput.toByteArray()) + } +} diff --git a/buildSrc/src/main/kotlin/util.kt b/buildSrc/src/main/kotlin/util.kt index 054a24cf7..7a6623b23 100644 --- a/buildSrc/src/main/kotlin/util.kt +++ b/buildSrc/src/main/kotlin/util.kt @@ -32,94 +32,42 @@ import org.gradle.kotlin.dsl.configure typealias TaskDelegate = RegisteringDomainObjectDelegateProviderWithTypeAndAction -fun Project.lexer(flex: String, pack: String): TaskDelegate { +fun Project.lexer(flex: String, pack: String): TaskDelegate { configure { exclude(pack.removeSuffix("/") + "/**") } - return tasks.registering(JavaExec::class) { - val src = layout.projectDirectory.file("src/main/grammars/$flex.flex") - val dst = layout.buildDirectory.dir("gen/$pack") - val output = layout.buildDirectory.file("gen/$pack/$flex.java") - val logOutout = layout.buildDirectory.file("logs/generate$flex.log") + 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")) + logFile.set(layout.buildDirectory.file("logs/generate$flex.log")) val jflex by project.configurations - val jflexSkeleton by project.configurations - - classpath = jflex - mainClass.set("jflex.Main") - - val taskOutput = ByteArrayOutputStream() - standardOutput = taskOutput - errorOutput = taskOutput - - doFirst { - args( - "--skel", jflexSkeleton.singleFile.absolutePath, - "-d", dst.get().asFile.absolutePath, - src.asFile.absolutePath - ) - - // Delete current lexer - project.delete(output) - logOutout.get().asFile.parentFile.mkdirs() - } - - doLast { - logOutout.get().asFile.writeBytes(taskOutput.toByteArray()) - } + this.jflex.setFrom(jflex) - inputs.files(src, jflexSkeleton) - outputs.file(output) + val jflexSkeleton by project.configurations + skeletonFile.set(jflexSkeleton.singleFile) } } -fun Project.parser(bnf: String, pack: String): TaskDelegate { +fun Project.parser(bnf: String, pack: String): TaskDelegate { configure { exclude(pack.removeSuffix("/") + "/**") } - return tasks.registering(JavaExec::class) { - val src = project.layout.projectDirectory.file("src/main/grammars/$bnf.bnf") - val dstRoot = project.layout.buildDirectory.dir("gen") - val dst = dstRoot.map { it.dir(pack) } - val psiDir = dst.map { it.dir("psi") } - val parserDir = dst.map { it.dir("parser") } - val logOutout = layout.buildDirectory.file("logs/generate$bnf.log") + return tasks.registering(ParserExec::class) { + val destRoot = project.layout.buildDirectory.dir("gen") + 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")) val grammarKit by project.configurations - - val taskOutput = ByteArrayOutputStream() - standardOutput = taskOutput - errorOutput = taskOutput - - classpath = grammarKit - mainClass.set("org.intellij.grammar.Main") - - if (JavaVersion.current().isJava9Compatible) { - jvmArgs( - "--add-opens", "java.base/java.lang=ALL-UNNAMED", - "--add-opens", "java.base/java.lang.reflect=ALL-UNNAMED", - "--add-opens", "java.base/java.util=ALL-UNNAMED" - ) - } - - doFirst { - project.delete(psiDir, parserDir) - args(dstRoot.get().asFile, src.asFile) - logOutout.get().asFile.parentFile.mkdirs() - } - doLast { - logOutout.get().asFile.writeBytes(taskOutput.toByteArray()) - } - - inputs.file(src) - outputs.dirs( - mapOf( - "psi" to psiDir, - "parser" to parserDir - ) - ) + this.grammarKit.setFrom(grammarKit) } } diff --git a/changelog.md b/changelog.md new file mode 100644 index 000000000..48b69e895 --- /dev/null +++ b/changelog.md @@ -0,0 +1,40 @@ +# Minecraft Development for IntelliJ + +This release contains two major features: +- Support for MixinExtras expressions ([#2274](https://github.com/minecraft-dev/MinecraftDev/pull/2274)) +- A rewritten project creator ([#2304](https://github.com/minecraft-dev/MinecraftDev/pull/2304)) + +### About the new project creator + +The new project creator is very similar to the previous one but has a few advantages: +- The templates are now stored on a separate repository and updated the first time you open the creator. This allows us to release template updates independently of plugin releases. +- You can create your own custom templates in their own repositories, which can be: + - flat directories + - local ZIP archives + - remote ZIP archives (like the built-in templates) +- Kotlin templates were added to all platforms except Forge and Architectury (couldn't get the Forge one to work, will look into it later) +- Fabric now has a split sources option +- Some niche options like the plugins dependencies fields were removed as their use was quite limited +- Remembered field values won't be ported over to the new creator, so make sure to configure your Group ID under Build System Properties! +- The old creator will be kept for a few months to give us the time to fix the new creator, please report any issues on the [issue tracker](https://github.com/minecraft-dev/MinecraftDev/issues) + +### Added + +- Initial support for NeoForge's ModDevGradle +- Option to force json translation and configurable default i18n call ([#2292](https://github.com/minecraft-dev/MinecraftDev/pull/2292)) +- Minecraft version detection for Loom-based projects +- Other JVM languages support for translation references, inspections and code folding +- Repo-based project creator templates ([#2304](https://github.com/minecraft-dev/MinecraftDev/pull/2304)) +- Support for MixinExtras expressions ([#2274](https://github.com/minecraft-dev/MinecraftDev/pull/2274)) + +### Changed + +- [#2296](https://github.com/minecraft-dev/MinecraftDev/issues/2296) Support entry point container objects in fabric.mod.json +- [#2325](https://github.com/minecraft-dev/MinecraftDev/issues/2325) Make lang annotator fixes bulk compatible +- Migrated the remaining legacy forms to the Kotlin UI DSL + +### Fixed + +- [#2316](https://github.com/minecraft-dev/MinecraftDev/issues/2316) Sponge's injection inspection isn't aware of the Configurate 4 classes ([#2317](https://github.com/minecraft-dev/MinecraftDev/pull/2317)) +- [#2310](https://github.com/minecraft-dev/MinecraftDev/issues/2310) Translations aren't detected for enum constructors +- [#2260](https://github.com/minecraft-dev/MinecraftDev/issues/2260) Vararg return type expected in a @ModifyArg method diff --git a/gradle.properties b/gradle.properties index 0118710f7..93f97d634 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,7 +24,7 @@ kotlin.code.style=official ideaVersion = 2024.1 ideaVersionName = 2024.1 -coreVersion = 1.7.6 +coreVersion = 1.8.0 downloadIdeaSources = true pluginTomlVersion = 241.14494.150 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9227c1821..454bfd4a1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ coroutines-jdk8 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", ve coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } mappingIo = "net.fabricmc:mapping-io:0.2.1" +mixinExtras-expressions = "io.github.llamalad7:mixinextras-expressions:0.0.1" # GrammarKit jflex-lib = "org.jetbrains.idea:jflex:1.7.0-b7f882a" @@ -33,12 +34,15 @@ fuel-coroutines = { module = "com.github.kittinunf.fuel:fuel-coroutines", versio test-mockJdk = "org.jetbrains.idea:mock-jdk:1.7-4d76c50" test-mixin = "org.spongepowered:mixin:0.8.5" test-spongeapi = "org.spongepowered:spongeapi:7.4.0" +test-fabricloader = "net.fabricmc:fabric-loader:0.15.11" test-nbt = "com.demonwav.mcdev:all-types-nbt:1.0" junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-entine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit-platform" } +mixinExtras-common = "io.github.llamalad7:mixinextras-common:0.5.0-beta.1" + [bundles] coroutines = ["coroutines-swing"] asm = ["asm", "asm-tree", "asm-analysis"] diff --git a/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData.java b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData.java new file mode 100644 index 000000000..c5efbe9a6 --- /dev/null +++ b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData.java @@ -0,0 +1,84 @@ +/* + * 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.mixintestdata.meExpression; + +import java.util.ArrayList; +import java.util.stream.Stream; + +public class MEExpressionTestData { + private static final SynchedData STINGER_COUNT = null; + private SynchedDataManager synchedData; + + public void complexFunction() { + int one = 1; + String local1 = "Hello"; + String local2 = "World"; + + System.out.println(new StringBuilder(local1).append(", ").append(local2)); + System.out.println(one); + + new ArrayList<>(10); + + InaccessibleType varOfInaccessibleType = new InaccessibleType(); + acceptInaccessibleType(varOfInaccessibleType); + noArgMethod(); + + String[] strings1 = new String[] { local1, local2 }; + String[] strings2 = new String[one]; + + Stream.empty().map(this::nonStaticMapper).map(MEExpressionTestData::staticMapper).map(ConstructedByMethodReference::new); + } + + private static void acceptInaccessibleType(InaccessibleType type) { + } + + private static void noArgMethod() { + } + + public int getStingerCount() { + return (Integer) this.synchedData.get(STINGER_COUNT); + } + + private Object nonStaticMapper(Object arg) { + return arg; + } + + private static Object staticMapper(Object arg) { + return arg; + } + + private static class InaccessibleType { + + } + + public static class SynchedDataManager { + public V get(SynchedData data) { + return null; + } + } + + public static class SynchedData { + } + + public static class ConstructedByMethodReference { + public ConstructedByMethodReference(Object bar) {} + } +} diff --git a/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModelBuilderImpl.groovy b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModelBuilderImpl.groovy index 9cfd8f225..c751e8998 100644 --- a/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModelBuilderImpl.groovy +++ b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModelBuilderImpl.groovy @@ -50,6 +50,7 @@ class FabricLoomModelBuilderImpl implements ModelBuilderService { } FabricLoomModel build(Project project, Object loomExtension) { + def minecraftVersion = loomExtension.minecraftProvider.minecraftVersion() def tinyMappings = loomExtension.mappingsFile def splitMinecraftJar = loomExtension.areEnvironmentSourceSetsSplit() @@ -70,7 +71,7 @@ class FabricLoomModelBuilderImpl implements ModelBuilderService { } //noinspection GroovyAssignabilityCheck - return new FabricLoomModelImpl(tinyMappings, decompilers, splitMinecraftJar, modSourceSets) + return new FabricLoomModelImpl(minecraftVersion, tinyMappings, decompilers, splitMinecraftJar, modSourceSets) } List getDecompilers(Object loomExtension, boolean client) { diff --git a/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModelImpl.groovy b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModelImpl.groovy index 1a04fab1e..d54149dbf 100644 --- a/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModelImpl.groovy +++ b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModelImpl.groovy @@ -24,6 +24,7 @@ import groovy.transform.Immutable @Immutable(knownImmutableClasses = [File]) class FabricLoomModelImpl implements FabricLoomModel, Serializable { + String minecraftVersion File tinyMappings Map> decompilers boolean splitMinecraftJar 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 new file mode 100644 index 000000000..02ef8e305 --- /dev/null +++ b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelBuilderImpl.groovy @@ -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.mcp.gradle.tooling.neomoddev + +import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelNMD +import org.gradle.api.Project +import org.jetbrains.annotations.NotNull +import org.jetbrains.plugins.gradle.tooling.ErrorMessageBuilder +import org.jetbrains.plugins.gradle.tooling.ModelBuilderService + +import java.nio.file.Files + +final class NeoModDevGradleModelBuilderImpl implements ModelBuilderService { + + @Override + boolean canBuild(String modelName) { + return McpModelNMD.name == modelName + } + + @Override + Object buildAll(String modelName, Project project) { + def extension = project.extensions.findByName('neoForge') + if (extension == null) { + return null + } + + if (!project.plugins.findPlugin("net.neoforged.moddev")) { + return null + } + + def neoforgeVersion = extension.version.get() + if (neoforgeVersion == null) { + return null + } + + def accessTransformers = extension.accessTransformers.get().collect { project.file(it) } + + // 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() + + //noinspection GroovyAssignabilityCheck + return new NeoModDevGradleModelImpl(neoforgeVersion, mappingsFile, accessTransformers) + } + + @Override + ErrorMessageBuilder getErrorMessageBuilder(@NotNull Project project, @NotNull Exception e) { + return ErrorMessageBuilder.create( + project, e, "MinecraftDev import errors" + ).withDescription("Unable to build MinecraftDev MCP project configuration") + } +} diff --git a/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelImpl.groovy b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelImpl.groovy new file mode 100644 index 000000000..5640af6fd --- /dev/null +++ b/src/gradle-tooling-extension/groovy/com/demonwav/mcdev/platform/mcp/gradle/tooling/neomoddev/NeoModDevGradleModelImpl.groovy @@ -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.platform.mcp.gradle.tooling.neomoddev + + +import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelNMD +import groovy.transform.CompileStatic + +@CompileStatic +final class NeoModDevGradleModelImpl implements McpModelNMD, Serializable { + + final String neoForgeVersion + final File mappingsFile + final List accessTransformers + + NeoModDevGradleModelImpl( + final String neoForgeVersion, + final File mappingsFile, + final List accessTransformers + ) { + this.neoForgeVersion = neoForgeVersion + this.mappingsFile = mappingsFile + this.accessTransformers = accessTransformers + } +} diff --git a/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/McpModelNMD.java b/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/McpModelNMD.java new file mode 100644 index 000000000..beff8af28 --- /dev/null +++ b/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/McpModelNMD.java @@ -0,0 +1,30 @@ +/* + * 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.gradle.tooling; + +import java.io.File; +import java.util.List; + +public interface McpModelNMD { + String getNeoForgeVersion(); + File getMappingsFile(); + List getAccessTransformers(); +} diff --git a/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModel.java b/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModel.java index 6198402bf..e984864a2 100644 --- a/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModel.java +++ b/src/gradle-tooling-extension/java/com/demonwav/mcdev/platform/mcp/gradle/tooling/fabricloom/FabricLoomModel.java @@ -26,6 +26,8 @@ public interface FabricLoomModel { + String getMinecraftVersion(); + File getTinyMappings(); Map> getDecompilers(); diff --git a/src/gradle-tooling-extension/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService b/src/gradle-tooling-extension/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService index 382ea8362..9746eaf54 100644 --- a/src/gradle-tooling-extension/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService +++ b/src/gradle-tooling-extension/resources/META-INF/services/org.jetbrains.plugins.gradle.tooling.ModelBuilderService @@ -1,6 +1,7 @@ com.demonwav.mcdev.platform.mcp.gradle.tooling.archloom.ArchitecturyModelBuilderImpl com.demonwav.mcdev.platform.mcp.gradle.tooling.fabricloom.FabricLoomModelBuilderImpl com.demonwav.mcdev.platform.mcp.gradle.tooling.neogradle.NeoGradle7ModelBuilderImpl +com.demonwav.mcdev.platform.mcp.gradle.tooling.neomoddev.NeoModDevGradleModelBuilderImpl com.demonwav.mcdev.platform.mcp.gradle.tooling.vanillagradle.VanillaGradleModelBuilderImpl com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelFG2BuilderImpl com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelFG3BuilderImpl diff --git a/src/main/grammars/MEExpressionLexer.flex b/src/main/grammars/MEExpressionLexer.flex new file mode 100644 index 000000000..c7d9fad8a --- /dev/null +++ b/src/main/grammars/MEExpressionLexer.flex @@ -0,0 +1,146 @@ +/* + * 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.expression; + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes; +import com.intellij.lexer.FlexLexer; +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.TokenType; + +%% + +%public +%class MEExpressionLexer +%implements FlexLexer +%function advance +%type IElementType + +%state STRING + +%unicode + +WHITE_SPACE = [\ \n\t\r] +RESERVED = assert|break|case|catch|const|continue|default|else|finally|for|goto|if|switch|synchronized|try|while|yield|_ +WILDCARD = "?" +NEW = new +INSTANCEOF = instanceof +BOOL_LIT = true|false +NULL_LIT = null +DO = do +RETURN = return +THROW = throw +THIS = this +SUPER = super +CLASS = class +IDENTIFIER = [A-Za-z_][A-Za-z0-9_]* +INT_LIT = ( [0-9]+ | 0x[0-9a-fA-F]+ ) +DEC_LIT = [0-9]*\.[0-9]+ +PLUS = "+" +MINUS = - +MULT = "*" +DIV = "/" +MOD = % +BITWISE_NOT = "~" +DOT = "." +COMMA = , +LEFT_PAREN = "(" +RIGHT_PAREN = ")" +LEFT_BRACKET = "[" +RIGHT_BRACKET = "]" +LEFT_BRACE = "{" +RIGHT_BRACE = "}" +AT = @ +SHL = << +SHR = >> +USHR = >>> +LT = < +LE = <= +GT = > +GE = >= +EQ = == +NE = "!=" +BITWISE_AND = & +BITWISE_XOR = "^" +BITWISE_OR = "|" +ASSIGN = = +METHOD_REF = :: + +STRING_TERMINATOR = ' +STRING_ESCAPE = \\'|\\\\ + +%% + + { + {WHITE_SPACE}+ { return TokenType.WHITE_SPACE; } + {RESERVED} { return MEExpressionTypes.TOKEN_RESERVED; } + {WILDCARD} { return MEExpressionTypes.TOKEN_WILDCARD; } + {NEW} { return MEExpressionTypes.TOKEN_NEW; } + {INSTANCEOF} { return MEExpressionTypes.TOKEN_INSTANCEOF; } + {BOOL_LIT} { return MEExpressionTypes.TOKEN_BOOL_LIT; } + {NULL_LIT} { return MEExpressionTypes.TOKEN_NULL_LIT; } + {DO} { return MEExpressionTypes.TOKEN_DO; } + {RETURN} { return MEExpressionTypes.TOKEN_RETURN; } + {THROW} { return MEExpressionTypes.TOKEN_THROW; } + {THIS} { return MEExpressionTypes.TOKEN_THIS; } + {SUPER} { return MEExpressionTypes.TOKEN_SUPER; } + {CLASS} { return MEExpressionTypes.TOKEN_CLASS; } + {IDENTIFIER} { return MEExpressionTypes.TOKEN_IDENTIFIER; } + {INT_LIT} { return MEExpressionTypes.TOKEN_INT_LIT; } + {DEC_LIT} { return MEExpressionTypes.TOKEN_DEC_LIT; } + {PLUS} { return MEExpressionTypes.TOKEN_PLUS; } + {MINUS} { return MEExpressionTypes.TOKEN_MINUS; } + {MULT} { return MEExpressionTypes.TOKEN_MULT; } + {DIV} { return MEExpressionTypes.TOKEN_DIV; } + {MOD} { return MEExpressionTypes.TOKEN_MOD; } + {BITWISE_NOT} { return MEExpressionTypes.TOKEN_BITWISE_NOT; } + {DOT} { return MEExpressionTypes.TOKEN_DOT; } + {COMMA} { return MEExpressionTypes.TOKEN_COMMA; } + {LEFT_PAREN} { return MEExpressionTypes.TOKEN_LEFT_PAREN; } + {RIGHT_PAREN} { return MEExpressionTypes.TOKEN_RIGHT_PAREN; } + {LEFT_BRACKET} { return MEExpressionTypes.TOKEN_LEFT_BRACKET; } + {RIGHT_BRACKET} { return MEExpressionTypes.TOKEN_RIGHT_BRACKET; } + {LEFT_BRACE} { return MEExpressionTypes.TOKEN_LEFT_BRACE; } + {RIGHT_BRACE} { return MEExpressionTypes.TOKEN_RIGHT_BRACE; } + {AT} { return MEExpressionTypes.TOKEN_AT; } + {SHL} { return MEExpressionTypes.TOKEN_SHL; } + {SHR} { return MEExpressionTypes.TOKEN_SHR; } + {USHR} { return MEExpressionTypes.TOKEN_USHR; } + {LT} { return MEExpressionTypes.TOKEN_LT; } + {LE} { return MEExpressionTypes.TOKEN_LE; } + {GT} { return MEExpressionTypes.TOKEN_GT; } + {GE} { return MEExpressionTypes.TOKEN_GE; } + {EQ} { return MEExpressionTypes.TOKEN_EQ; } + {NE} { return MEExpressionTypes.TOKEN_NE; } + {BITWISE_AND} { return MEExpressionTypes.TOKEN_BITWISE_AND; } + {BITWISE_XOR} { return MEExpressionTypes.TOKEN_BITWISE_XOR; } + {BITWISE_OR} { return MEExpressionTypes.TOKEN_BITWISE_OR; } + {ASSIGN} { return MEExpressionTypes.TOKEN_ASSIGN; } + {METHOD_REF} { return MEExpressionTypes.TOKEN_METHOD_REF; } + {STRING_TERMINATOR} { yybegin(STRING); return MEExpressionTypes.TOKEN_STRING_TERMINATOR; } +} + + { + {STRING_ESCAPE} { return MEExpressionTypes.TOKEN_STRING_ESCAPE; } + {STRING_TERMINATOR} { yybegin(YYINITIAL); return MEExpressionTypes.TOKEN_STRING_TERMINATOR; } + [^'\\]+ { return MEExpressionTypes.TOKEN_STRING; } +} + +[^] { return TokenType.BAD_CHARACTER; } diff --git a/src/main/grammars/MEExpressionParser.bnf b/src/main/grammars/MEExpressionParser.bnf new file mode 100644 index 000000000..47d509e01 --- /dev/null +++ b/src/main/grammars/MEExpressionParser.bnf @@ -0,0 +1,335 @@ +/* + * 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 . + */ + +{ + parserClass="com.demonwav.mcdev.platform.mixin.expression.gen.MEExpressionParser" + extends="com.intellij.extapi.psi.ASTWrapperPsiElement" + parserImports = ["static com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionParserUtil.*"] + + psiClassPrefix="ME" + psiImplClassSuffix="Impl" + psiPackage="com.demonwav.mcdev.platform.mixin.expression.gen.psi" + psiImplPackage="com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl" + + elementTypeHolderClass="com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes" + elementTypeClass="com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionElementType" + tokenTypeClass="com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionTokenType" + + tokens = [ + TOKEN_RESERVED = "TOKEN_RESERVED" + ] + + extends(".+Expression") = expression + extends(".+Statement") = statement +} + +meExpressionFile ::= item* <> + +item ::= declarationItem | statementItem + +declarationItem ::= TOKEN_CLASS TOKEN_BOOL_LIT declaration { + pin = 1 + extends = item + implements = [ + "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEDeclarationItemMixin" + ] + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEDeclarationItemImplMixin" +} + +declaration ::= TOKEN_IDENTIFIER { + implements = [ + "com.intellij.psi.PsiNamedElement" + "com.intellij.psi.PsiNameIdentifierOwner" + "com.intellij.psi.NavigatablePsiElement" + ] + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEDeclarationImplMixin" +} + +statementItem ::= TOKEN_DO TOKEN_LEFT_BRACE statement TOKEN_RIGHT_BRACE { + pin = 1 + extends = item +} + +private statementRecover ::= !TOKEN_RIGHT_BRACE + +statement ::= assignStatement | + returnStatement | + throwStatement | + expressionStatement { + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.MEMatchableElement" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEStatementImplMixin" + recoverWhile = statementRecover +} + +assignStatement ::= assignableExpression TOKEN_ASSIGN expression { + pin = 2 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEAssignStatementImplMixin" + methods = [ + targetExpr = "expression[0]" + rightExpr = "expression[1]" + ] +} + +private assignableExpression ::= arrayAccessExpression | memberAccessExpression | nameExpression + +returnStatement ::= TOKEN_RETURN expression { + pin = 1 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEReturnStatementImplMixin" + methods = [ + valueExpr = "expression" + ] +} + +throwStatement ::= TOKEN_THROW expression { + pin = 1 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.METhrowStatementImplMixin" + methods = [ + valueExpr = "expression" + ] +} + +expressionStatement ::= expression { + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEExpressionStatementImplMixin" +} + +private exprRecover ::= !( TOKEN_COMMA | TOKEN_RIGHT_PAREN | TOKEN_RIGHT_BRACKET | TOKEN_RIGHT_BRACE ) + +expression ::= capturingExpression | + superCallExpression | + staticMethodCallExpression | + classConstantExpression | + unaryExpression | + binaryExpression | + castExpression | + parenthesizedExpression | + methodCallExpression | + boundMethodReferenceExpression | + freeMethodReferenceExpression | + constructorReferenceExpression | + arrayAccessExpression | + memberAccessExpression | + newExpression | + litExpression | + thisExpression | + nameExpression { + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.MEMatchableElement" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEExpressionImplMixin" + recoverWhile = exprRecover +} + +external rightParen ::= parseToRightBracket exprRecover TOKEN_RIGHT_PAREN +external rightBracket ::= parseToRightBracket exprRecover TOKEN_RIGHT_BRACKET +external rightBrace ::= parseToRightBracket exprRecover TOKEN_RIGHT_BRACE + +capturingExpression ::= TOKEN_AT TOKEN_LEFT_PAREN expression rightParen { + pin = 1 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MECapturingExpressionImplMixin" +} + +parenthesizedExpression ::= TOKEN_LEFT_PAREN expression rightParen { + pin = 1 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEParenthesizedExpressionImplMixin" +} + +superCallExpression ::= TOKEN_SUPER TOKEN_DOT name TOKEN_LEFT_PAREN arguments rightParen { + pin = 1 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MESuperCallExpressionImplMixin" + methods = [ + memberName = "name" + ] +} + +methodCallExpression ::= expression TOKEN_DOT name TOKEN_LEFT_PAREN arguments rightParen { + pin = 4 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEMethodCallExpressionImplMixin" + methods = [ + receiverExpr = "expression" + memberName = "name" + ] +} + +staticMethodCallExpression ::= name TOKEN_LEFT_PAREN arguments rightParen { + pin = 2 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEStaticMethodCallExpressionImplMixin" + methods = [ + memberName = "name" + ] +} + +boundMethodReferenceExpression ::= expression !(TOKEN_METHOD_REF TOKEN_NEW) TOKEN_METHOD_REF name { + pin = 3 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEBoundReferenceExpressionImplMixin" + methods = [ + receiverExpr = "expression" + memberName = "name" + ] +} + +freeMethodReferenceExpression ::= TOKEN_METHOD_REF name { + pin = 1 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEFreeMethodReferenceExpressionImplMixin" + methods = [ + memberName = "name" + ] +} + +constructorReferenceExpression ::= type TOKEN_METHOD_REF TOKEN_NEW { + pin = 3 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEConstructorReferenceExpressionImplMixin" + methods = [ + className = "type" + ] +} + +arrayAccessExpression ::= expression TOKEN_LEFT_BRACKET expression? rightBracket { + pin = 2 + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEArrayAccessExpressionMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEArrayAccessExpressionImplMixin" + methods = [ + arrayExpr = "expression[0]" + indexExpr = "expression[1]" + ] +} + +classConstantExpression ::= type TOKEN_DOT TOKEN_CLASS { + pin = 3 + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEClassConstantExpressionImplMixin" + methods = [ + className = "name" + ] +} + +memberAccessExpression ::= expression TOKEN_DOT name { + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEMemberAccessExpressionImplMixin" + methods = [ + receiverExpr = "expression" + memberName = "name" + ] +} + +unaryExpression ::= ((TOKEN_MINUS !(TOKEN_DEC_LIT | TOKEN_INT_LIT)) | TOKEN_BITWISE_NOT) expression { + pin = 2 + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEUnaryExpressionMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEUnaryExpressionImplMixin" +} + +castExpression ::= parenthesizedExpression expression { + rightAssociative = true + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MECastExpressionMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MECastExpressionImplMixin" +} + +binaryExpression ::= expression binaryOp expression { + pin = 2 + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEBinaryExpressionMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEBinaryExpressionImplMixin" + methods = [ + leftExpr = "expression[0]" + rightExpr = "expression[1]" + ] +} + +private binaryOp ::= multiplicativeOp | + additiveOp | + shiftOp | + comparisonOp | + TOKEN_INSTANCEOF | + equalityOp | + TOKEN_BITWISE_AND | + TOKEN_BITWISE_XOR | + TOKEN_BITWISE_OR + +private multiplicativeOp ::= TOKEN_MULT | TOKEN_DIV | TOKEN_MOD +private additiveOp ::= TOKEN_PLUS | TOKEN_MINUS +private shiftOp ::= TOKEN_SHL | TOKEN_SHR | TOKEN_USHR +private comparisonOp ::= TOKEN_LT | TOKEN_LE | TOKEN_GT | TOKEN_GE +private equalityOp ::= TOKEN_EQ | TOKEN_NE + +newExpression ::= TOKEN_NEW name ( + (TOKEN_LEFT_PAREN arguments rightParen) | + ( + TOKEN_LEFT_BRACKET expression? rightBracket + ( TOKEN_LEFT_BRACKET expression? rightBracket )* + ( TOKEN_LEFT_BRACE arguments rightBrace )? + ) +) { + pin = 1 + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MENewExpressionMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MENewExpressionImplMixin" + methods = [ + type = "name" + dimExprs = "expression" + ] +} + +litExpression ::= decimalLitExpression | intLitExpression | stringLitExpression | boolLitExpression | nulLLitExpression { + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MELitExpressionMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MELitExpressionImplMixin" +} + +private decimalLitExpression ::= TOKEN_MINUS? TOKEN_DEC_LIT { + extends = litExpression +} + +private intLitExpression ::= TOKEN_MINUS? TOKEN_INT_LIT { + extends = litExpression +} + +private stringLitExpression ::= TOKEN_STRING_TERMINATOR ( TOKEN_STRING | TOKEN_STRING_ESCAPE )* TOKEN_STRING_TERMINATOR { + pin = 1 + extends = litExpression +} + +private boolLitExpression ::= TOKEN_BOOL_LIT { + extends = litExpression +} + +private nulLLitExpression ::= TOKEN_NULL_LIT { + extends = litExpression +} + +thisExpression ::= TOKEN_THIS { + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.METhisExpressionImplMixin" +} + +nameExpression ::= name { + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MENameExpressionImplMixin" + methods = [ + MEName = "name" + ] +} + +type ::= name ( TOKEN_LEFT_BRACKET TOKEN_RIGHT_BRACKET )* { + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.METypeMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.METypeImplMixin" + methods = [ + MEName = "name" + ] +} + +name ::= TOKEN_IDENTIFIER | TOKEN_WILDCARD { + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MENameMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MENameImplMixin" +} + +arguments ::= (expression (TOKEN_COMMA expression)*)? { + implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEArgumentsMixin" + mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEArgumentsImplMixin" +} diff --git a/src/main/grammars/TranslationTemplateLexer.flex b/src/main/grammars/TranslationTemplateLexer.flex index b71ef2bc8..ba1831dcb 100644 --- a/src/main/grammars/TranslationTemplateLexer.flex +++ b/src/main/grammars/TranslationTemplateLexer.flex @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package com.demonwav.mcdev.translations.lang.gen; +package com.demonwav.mcdev.translations.template.gen; import com.intellij.lexer.*; import com.intellij.psi.tree.IElementType; diff --git a/src/main/kotlin/MinecraftConfigurable.kt b/src/main/kotlin/MinecraftConfigurable.kt index 815131f7f..12fc11567 100644 --- a/src/main/kotlin/MinecraftConfigurable.kt +++ b/src/main/kotlin/MinecraftConfigurable.kt @@ -22,6 +22,7 @@ package com.demonwav.mcdev import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.asset.PlatformAssets +import com.demonwav.mcdev.creator.custom.templateRepoTable import com.demonwav.mcdev.update.ConfigurePluginUpdatesDialog import com.intellij.ide.projectView.ProjectView import com.intellij.openapi.options.Configurable @@ -31,6 +32,7 @@ import com.intellij.ui.EnumComboBoxModel import com.intellij.ui.components.Label import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.MutableProperty import com.intellij.ui.dsl.builder.bindItem import com.intellij.ui.dsl.builder.bindSelected import com.intellij.ui.dsl.builder.panel @@ -84,11 +86,17 @@ class MinecraftConfigurable : Configurable { } } - group(MCDevBundle("minecraft.settings.mixin")) { + group(MCDevBundle("minecraft.settings.creator")) { + row(MCDevBundle("minecraft.settings.creator.repos")) {} + row { - checkBox(MCDevBundle("minecraft.settings.mixin.shadow_annotation_same_line")) - .bindSelected(settings::isShadowAnnotationsSameLine) - } + templateRepoTable( + MutableProperty( + { settings.creatorTemplateRepos.toMutableList() }, + { settings.creatorTemplateRepos = it } + ) + ) + }.resizableRow() } onApply { diff --git a/src/main/kotlin/MinecraftProjectConfigurable.kt b/src/main/kotlin/MinecraftProjectConfigurable.kt new file mode 100644 index 000000000..0e676f0b7 --- /dev/null +++ b/src/main/kotlin/MinecraftProjectConfigurable.kt @@ -0,0 +1,64 @@ +/* + * 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 + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.util.BeforeOrAfter +import com.intellij.openapi.options.Configurable +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogPanel +import com.intellij.ui.EnumComboBoxModel +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.panel +import javax.swing.JComponent +import org.jetbrains.annotations.Nls + +class MinecraftProjectConfigurable(private val project: Project) : Configurable { + private lateinit var panel: DialogPanel + + @Nls + override fun getDisplayName() = MCDevBundle("minecraft.settings.project.display_name") + + override fun createComponent(): JComponent = panel { + val settings = MinecraftProjectSettings.getInstance(project) + + group(MCDevBundle("minecraft.settings.mixin")) { + row { + checkBox(MCDevBundle("minecraft.settings.mixin.shadow_annotation_same_line")) + .bindSelected(settings::isShadowAnnotationsSameLine) + } + row { + label(MCDevBundle("minecraft.settings.mixin.definition_pos_relative_to_expression")) + comboBox(EnumComboBoxModel(BeforeOrAfter::class.java)) + .bindItem(settings::definitionPosRelativeToExpression) { + settings.definitionPosRelativeToExpression = it ?: BeforeOrAfter.BEFORE + } + } + } + }.also { panel = it } + + override fun isModified(): Boolean = panel.isModified() + + override fun apply() = panel.apply() + + override fun reset() = panel.reset() +} diff --git a/src/main/kotlin/MinecraftProjectSettings.kt b/src/main/kotlin/MinecraftProjectSettings.kt new file mode 100644 index 000000000..f22bf178b --- /dev/null +++ b/src/main/kotlin/MinecraftProjectSettings.kt @@ -0,0 +1,46 @@ +/* + * 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 + +import com.demonwav.mcdev.util.BeforeOrAfter +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.util.xmlb.XmlSerializerUtil + +@Service(Service.Level.PROJECT) +@State(name = "MinecraftSettings", storages = [Storage("minecraft_dev.xml")]) +class MinecraftProjectSettings : PersistentStateComponent { + var isShadowAnnotationsSameLine = true + var definitionPosRelativeToExpression = BeforeOrAfter.BEFORE + + override fun getState() = this + override fun loadState(state: MinecraftProjectSettings) { + XmlSerializerUtil.copyBean(state, this) + } + + companion object { + fun getInstance(project: Project) = project.service() + } +} diff --git a/src/main/kotlin/MinecraftSettings.kt b/src/main/kotlin/MinecraftSettings.kt index b4b596114..18ae02acf 100644 --- a/src/main/kotlin/MinecraftSettings.kt +++ b/src/main/kotlin/MinecraftSettings.kt @@ -20,11 +20,15 @@ package com.demonwav.mcdev +import com.demonwav.mcdev.asset.MCDevBundle import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.openapi.editor.markup.EffectType +import com.intellij.util.xmlb.annotations.Attribute +import com.intellij.util.xmlb.annotations.Tag +import com.intellij.util.xmlb.annotations.Text @State(name = "MinecraftSettings", storages = [Storage("minecraft_dev.xml")]) class MinecraftSettings : PersistentStateComponent { @@ -36,9 +40,28 @@ class MinecraftSettings : PersistentStateComponent { var isShowChatColorUnderlines: Boolean = false, var underlineType: UnderlineType = UnderlineType.DOTTED, - var isShadowAnnotationsSameLine: Boolean = true, + var creatorTemplateRepos: List = listOf(TemplateRepo.makeBuiltinRepo()), ) + @Tag("repo") + data class TemplateRepo( + @get:Attribute("name") + var name: String, + @get:Attribute("provider") + var provider: String, + @get:Text + var data: String + ) { + constructor() : this("", "", "") + + companion object { + + fun makeBuiltinRepo(): TemplateRepo { + return TemplateRepo(MCDevBundle("minecraft.settings.creator.repo.builtin_name"), "builtin", "true") + } + } + } + private var state = State() override fun getState(): State { @@ -47,6 +70,9 @@ class MinecraftSettings : PersistentStateComponent { override fun loadState(state: State) { this.state = state + if (state.creatorTemplateRepos.isEmpty()) { + state.creatorTemplateRepos = listOf() + } } // State mappings @@ -80,10 +106,10 @@ class MinecraftSettings : PersistentStateComponent { state.underlineType = underlineType } - var isShadowAnnotationsSameLine: Boolean - get() = state.isShadowAnnotationsSameLine - set(shadowAnnotationsSameLine) { - state.isShadowAnnotationsSameLine = shadowAnnotationsSameLine + var creatorTemplateRepos: List + get() = state.creatorTemplateRepos.map { it.copy() } + set(creatorTemplateRepos) { + state.creatorTemplateRepos = creatorTemplateRepos.map { it.copy() } } enum class UnderlineType(private val regular: String, val effectType: EffectType) { diff --git a/src/main/kotlin/TranslationSettings.kt b/src/main/kotlin/TranslationSettings.kt new file mode 100644 index 000000000..f2b5cf483 --- /dev/null +++ b/src/main/kotlin/TranslationSettings.kt @@ -0,0 +1,71 @@ +/* + * 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 + +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project + +@State(name = "TranslationSettings", storages = [Storage("minecraft_dev.xml")]) +class TranslationSettings : PersistentStateComponent { + + data class State( + var isForceJsonTranslationFile: Boolean = false, + var isUseCustomConvertToTranslationTemplate: Boolean = false, + var convertToTranslationTemplate: String = "net.minecraft.client.resources.I18n.format(\"\$key\")", + ) + + private var state = State() + + override fun getState(): State { + return state + } + + override fun loadState(state: State) { + this.state = state + } + + // State mappings + var isForceJsonTranslationFile: Boolean + get() = state.isForceJsonTranslationFile + set(forceJsonTranslationFile) { + state.isForceJsonTranslationFile = forceJsonTranslationFile + } + + var isUseCustomConvertToTranslationTemplate: Boolean + get() = state.isUseCustomConvertToTranslationTemplate + set(useCustomConvertToTranslationTemplate) { + state.isUseCustomConvertToTranslationTemplate = useCustomConvertToTranslationTemplate + } + + var convertToTranslationTemplate: String + get() = state.convertToTranslationTemplate + set(convertToTranslationTemplate) { + state.convertToTranslationTemplate = convertToTranslationTemplate + } + + companion object { + @JvmStatic + fun getInstance(project: Project): TranslationSettings = project.service() + } +} diff --git a/src/main/kotlin/asset/MCDevBundle.kt b/src/main/kotlin/asset/MCDevBundle.kt index 04b9b3d0b..6246c2bae 100644 --- a/src/main/kotlin/asset/MCDevBundle.kt +++ b/src/main/kotlin/asset/MCDevBundle.kt @@ -21,6 +21,7 @@ package com.demonwav.mcdev.asset import com.intellij.DynamicBundle +import java.util.function.Supplier import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.PropertyKey @@ -36,4 +37,9 @@ object MCDevBundle : DynamicBundle(BUNDLE) { operator fun invoke(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any?): String { return getMessage(key, *params) } + + fun pointer(@PropertyKey(resourceBundle = BUNDLE) key: String) = Supplier { invoke(key) } + + fun pointer(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any?) = + Supplier { invoke(key, params) } } diff --git a/src/main/kotlin/creator/MinecraftModuleBuilder.kt b/src/main/kotlin/creator/MinecraftModuleBuilder.kt index 7b3f2318c..a847ccf14 100644 --- a/src/main/kotlin/creator/MinecraftModuleBuilder.kt +++ b/src/main/kotlin/creator/MinecraftModuleBuilder.kt @@ -35,7 +35,7 @@ import com.intellij.openapi.roots.ModifiableRootModel class MinecraftModuleBuilder : AbstractNewProjectWizardBuilder() { - override fun getPresentableName() = "Minecraft" + override fun getPresentableName() = "Minecraft (Old Wizard)" override fun getNodeIcon() = PlatformAssets.MINECRAFT_ICON override fun getGroupName() = "Minecraft" override fun getBuilderId() = "MINECRAFT_MODULE" diff --git a/src/main/kotlin/creator/ProjectSetupFinalizerWizardStep.kt b/src/main/kotlin/creator/ProjectSetupFinalizerWizardStep.kt index 6aa8694bb..6c3b70902 100644 --- a/src/main/kotlin/creator/ProjectSetupFinalizerWizardStep.kt +++ b/src/main/kotlin/creator/ProjectSetupFinalizerWizardStep.kt @@ -126,7 +126,9 @@ class JdkProjectSetupFinalizer( private var preferredJdkLabel: Placeholder? = null private var preferredJdkReason = MCDevBundle("creator.validation.jdk_preferred_default_reason") - var preferredJdk: JavaSdkVersion = JavaSdkVersion.JDK_17 + val preferredJdkProperty = propertyGraph.property(JavaSdkVersion.JDK_17) + + var preferredJdk: JavaSdkVersion by preferredJdkProperty private set fun setPreferredJdk(value: JavaSdkVersion, reason: String) { diff --git a/src/main/kotlin/creator/buildsystem/AbstractBuildSystemStep.kt b/src/main/kotlin/creator/buildsystem/AbstractBuildSystemStep.kt index 51614b1fb..887682753 100644 --- a/src/main/kotlin/creator/buildsystem/AbstractBuildSystemStep.kt +++ b/src/main/kotlin/creator/buildsystem/AbstractBuildSystemStep.kt @@ -49,7 +49,7 @@ abstract class AbstractBuildSystemStep( override val self get() = this override val label - get() = MCDevBundle("creator.ui.build_system.label.generic") + get() = MCDevBundle("creator.ui.build_system.label") override fun initSteps(): LinkedHashMap { context.putUserData(PLATFORM_NAME_KEY, platformName) diff --git a/src/main/kotlin/creator/buildsystem/BuildSystemPropertiesStep.kt b/src/main/kotlin/creator/buildsystem/BuildSystemPropertiesStep.kt index bc6324f54..67cc8a3ef 100644 --- a/src/main/kotlin/creator/buildsystem/BuildSystemPropertiesStep.kt +++ b/src/main/kotlin/creator/buildsystem/BuildSystemPropertiesStep.kt @@ -52,7 +52,7 @@ class BuildSystemPropertiesStep(private val parent: ParentStep) : Ab val groupIdProperty = propertyGraph.property("org.example") .bindStorage("${javaClass.name}.groupId") val artifactIdProperty = propertyGraph.lazyProperty(::suggestArtifactId) - private val versionProperty = propertyGraph.property("1.0-SNAPSHOT") + val versionProperty = propertyGraph.property("1.0-SNAPSHOT") .bindStorage("${javaClass.name}.version") var groupId by groupIdProperty diff --git a/src/main/kotlin/creator/creator-utils.kt b/src/main/kotlin/creator/creator-utils.kt index 687793ddf..a1ab81512 100644 --- a/src/main/kotlin/creator/creator-utils.kt +++ b/src/main/kotlin/creator/creator-utils.kt @@ -26,11 +26,15 @@ import com.demonwav.mcdev.creator.step.LicenseStep import com.demonwav.mcdev.util.MinecraftTemplates import com.intellij.ide.fileTemplates.FileTemplateManager import com.intellij.ide.starters.local.GeneratorTemplateFile +import com.intellij.ide.util.projectWizard.WizardContext import com.intellij.ide.wizard.AbstractNewProjectWizardStep +import com.intellij.ide.wizard.AbstractWizard import com.intellij.ide.wizard.GitNewProjectWizardData import com.intellij.ide.wizard.NewProjectWizardStep import com.intellij.notification.Notification import com.intellij.notification.NotificationType +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.project.Project @@ -160,3 +164,15 @@ fun notifyCreatedProjectNotOpened() { NotificationType.ERROR, ).notify(null) } + +val WizardContext.modalityState: ModalityState + get() { + val contentPanel = this.getUserData(AbstractWizard.KEY)?.contentPanel + + if (contentPanel == null) { + thisLogger().error("Wizard content panel is null, using default modality state") + return ModalityState.defaultModalityState() + } + + return ModalityState.stateForComponent(contentPanel) + } diff --git a/src/main/kotlin/creator/custom/BuiltinValidations.kt b/src/main/kotlin/creator/custom/BuiltinValidations.kt new file mode 100644 index 000000000..8fd1a8401 --- /dev/null +++ b/src/main/kotlin/creator/custom/BuiltinValidations.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.creator.custom + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.platform.fabric.util.FabricVersions +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.ui.validation.DialogValidation +import com.intellij.openapi.ui.validation.validationErrorIf +import com.intellij.openapi.util.text.StringUtil +import javax.swing.JComponent + +object BuiltinValidations { + val nonBlank = validationErrorIf(MCDevBundle("creator.validation.blank")) { it.isBlank() } + + val validVersion = validationErrorIf(MCDevBundle("creator.validation.semantic_version")) { + SemanticVersion.tryParse(it) == null + } + + val nonEmptyVersion = DialogValidation.WithParameter> { combobox -> + DialogValidation { + if (combobox.item?.parts.isNullOrEmpty()) { + ValidationInfo(MCDevBundle("creator.validation.semantic_version")) + } else { + null + } + } + } + + val nonEmptyYarnVersion = DialogValidation.WithParameter> { combobox -> + DialogValidation { + if (combobox.item == null) { + ValidationInfo(MCDevBundle("creator.validation.semantic_version")) + } else { + null + } + } + } + + val validClassFqn = validationErrorIf(MCDevBundle("creator.validation.class_fqn")) { + it.isBlank() || it.split('.').any { part -> !StringUtil.isJavaIdentifier(part) } + } + + fun byRegex(regex: Regex): DialogValidation.WithParameter<() -> String> = + validationErrorIf(MCDevBundle("creator.validation.regex", regex)) { !it.matches(regex) } + + fun isAnyOf( + selectionGetter: () -> T, + options: Collection, + component: JComponent? = null + ): DialogValidation = DialogValidation { + if (selectionGetter() !in options) { + return@DialogValidation ValidationInfo(MCDevBundle("creator.validation.invalid_option"), component) + } + + return@DialogValidation null + } +} diff --git a/src/main/kotlin/creator/custom/CreatorProgressIndicator.kt b/src/main/kotlin/creator/custom/CreatorProgressIndicator.kt new file mode 100644 index 000000000..2bad9bf12 --- /dev/null +++ b/src/main/kotlin/creator/custom/CreatorProgressIndicator.kt @@ -0,0 +1,58 @@ +/* + * 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.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.progress.TaskInfo +import com.intellij.openapi.progress.util.ProgressIndicatorBase + +class CreatorProgressIndicator( + val loadingProperty: GraphProperty? = null, + val textProperty: GraphProperty? = null, + val text2Property: GraphProperty? = null, +) : ProgressIndicatorBase(false, false) { + + init { + loadingProperty?.set(false) + textProperty?.set("") + text2Property?.set("") + } + + override fun start() { + super.start() + loadingProperty?.set(true) + } + + override fun finish(task: TaskInfo) { + super.finish(task) + loadingProperty?.set(false) + } + + override fun setText(text: String?) { + super.setText(text) + textProperty?.set(text ?: "") + } + + override fun setText2(text: String?) { + super.setText2(text) + text2Property?.set(text ?: "") + } +} diff --git a/src/main/kotlin/creator/custom/CustomMinecraftModuleBuilder.kt b/src/main/kotlin/creator/custom/CustomMinecraftModuleBuilder.kt new file mode 100644 index 000000000..65a95052e --- /dev/null +++ b/src/main/kotlin/creator/custom/CustomMinecraftModuleBuilder.kt @@ -0,0 +1,58 @@ +/* + * 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.asset.MCDevBundle +import com.demonwav.mcdev.asset.PlatformAssets +import com.demonwav.mcdev.creator.step.NewProjectWizardChainStep.Companion.nextStep +import com.intellij.ide.projectWizard.ProjectSettingsStep +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.ide.wizard.AbstractNewProjectWizardBuilder +import com.intellij.ide.wizard.GitNewProjectWizardStep +import com.intellij.ide.wizard.NewProjectWizardBaseStep +import com.intellij.ide.wizard.RootNewProjectWizardStep +import com.intellij.openapi.roots.ModifiableRootModel + +class CustomMinecraftModuleBuilder : AbstractNewProjectWizardBuilder() { + + override fun getPresentableName() = "Minecraft" + override fun getNodeIcon() = PlatformAssets.MINECRAFT_ICON + override fun getGroupName() = "Minecraft" + override fun getBuilderId() = "CUSTOM_MINECRAFT_MODULE" + override fun getDescription() = MCDevBundle("creator.ui.create_minecraft_project") + + override fun setupRootModel(modifiableRootModel: ModifiableRootModel) { + if (moduleJdk != null) { + modifiableRootModel.sdk = moduleJdk + } else { + modifiableRootModel.inheritSdk() + } + } + + override fun getParentGroup() = "Minecraft" + + override fun createStep(context: WizardContext) = RootNewProjectWizardStep(context) + .nextStep(::NewProjectWizardBaseStep) + .nextStep(::GitNewProjectWizardStep) + .nextStep(::CustomPlatformStep) + + override fun getIgnoredSteps() = listOf(ProjectSettingsStep::class.java) +} diff --git a/src/main/kotlin/creator/custom/CustomPlatformStep.kt b/src/main/kotlin/creator/custom/CustomPlatformStep.kt new file mode 100644 index 000000000..682e4256d --- /dev/null +++ b/src/main/kotlin/creator/custom/CustomPlatformStep.kt @@ -0,0 +1,574 @@ +/* + * 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.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.finalizers.CreatorFinalizer +import com.demonwav.mcdev.creator.custom.providers.EmptyLoadedTemplate +import com.demonwav.mcdev.creator.custom.providers.LoadedTemplate +import com.demonwav.mcdev.creator.custom.providers.TemplateProvider +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.demonwav.mcdev.creator.custom.types.CreatorPropertyFactory +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.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.WriteAction +import com.intellij.openapi.diagnostic.Attachment +import com.intellij.openapi.diagnostic.ControlFlowException +import com.intellij.openapi.diagnostic.getOrLogException +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.diagnostic.thisLogger +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.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.refreshAndFindVirtualFile +import com.intellij.psi.PsiManager +import com.intellij.ui.JBColor +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.Placeholder +import com.intellij.ui.dsl.builder.SegmentedButton +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.application +import com.intellij.util.ui.AsyncProcessIcon +import java.nio.file.Path +import java.util.function.Consumer +import javax.swing.JLabel +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.set +import kotlin.io.path.createDirectories +import kotlin.io.path.writeText + +/** + * The step to select a custom template repo. + */ +class CustomPlatformStep( + parent: NewProjectWizardStep, +) : AbstractNewProjectWizardStep(parent) { + + val templateRepos = MinecraftSettings.instance.creatorTemplateRepos + + val templateRepoProperty = propertyGraph.property( + templateRepos.firstOrNull() ?: MinecraftSettings.TemplateRepo.makeBuiltinRepo() + ) + var templateRepo by templateRepoProperty + + val availableGroupsProperty = propertyGraph.property>(emptyList()) + var availableGroups by availableGroupsProperty + val availableTemplatesProperty = propertyGraph.property>(emptyList()) + var availableTemplates by availableTemplatesProperty + lateinit var availableGroupsSegmentedButton: SegmentedButton + lateinit var availableTemplatesSegmentedButton: SegmentedButton + + val selectedGroupProperty = propertyGraph.property("") + var selectedGroup by selectedGroupProperty + val selectedTemplateProperty = propertyGraph.property(EmptyLoadedTemplate) + var selectedTemplate by selectedTemplateProperty + + val templateProvidersLoadingProperty = propertyGraph.property(true) + val templateProvidersTextProperty = propertyGraph.property("") + val templateProvidersText2Property = propertyGraph.property("") + lateinit var templateProvidersProcessIcon: Cell + + val templateLoadingProperty = propertyGraph.property(true) + val templateLoadingTextProperty = propertyGraph.property("") + val templateLoadingText2Property = propertyGraph.property("") + lateinit var templatePropertiesProcessIcon: Cell + lateinit var noTemplatesAvailable: Cell + var templateLoadingIndicator: ProgressIndicator? = null + + private var hasTemplateErrors: Boolean = true + + private var properties = mutableMapOf>() + + override fun setupUI(builder: Panel) { + lateinit var templatePropertyPlaceholder: Placeholder + + builder.row(MCDevBundle("creator.ui.custom.repos.label")) { + segmentedButton(templateRepos) { text = it.name } + .bind(templateRepoProperty) + }.visible(templateRepos.size > 1) + + 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) + } + + templateRepoProperty.afterChange { templateRepo -> + templatePropertyPlaceholder.component = null + availableTemplates = emptyList() + loadTemplatesInBackground { + val provider = TemplateProvider.get(templateRepo.provider) + provider?.loadTemplates(context, templateRepo).orEmpty() + } + } + + builder.row(MCDevBundle("creator.ui.custom.groups.label")) { + availableGroupsSegmentedButton = + segmentedButton(emptyList()) { text = it } + .bind(selectedGroupProperty) + }.visibleIf( + availableGroupsProperty.transform { it.size > 1 } + ) + + builder.row(MCDevBundle("creator.ui.custom.templates.label")) { + availableTemplatesSegmentedButton = + segmentedButton(emptyList()) { template: LoadedTemplate -> + text = template.label + toolTipText = template.tooltip + }.bind(selectedTemplateProperty) + .validation { + addApplyRule("", condition = ::hasTemplateErrors) + } + }.visibleIf( + availableTemplatesProperty.transform { it.size > 1 } + ) + + availableTemplatesProperty.afterChange { newTemplates -> + val groups = newTemplates.mapTo(linkedSetOf()) { it.descriptor.translatedGroup } + availableGroupsSegmentedButton.items = groups + // availableGroupsSegmentedButton.visible(groups.size > 1) + availableGroups = groups + selectedGroup = groups.firstOrNull() ?: "empty" + } + + selectedGroupProperty.afterChange { group -> + val templates = availableTemplates.filter { it.descriptor.translatedGroup == group } + availableTemplatesSegmentedButton.items = templates + // Force visiblity because the component might become hidden and not show up again + // when the segmented button switches between dropdown and buttons + availableTemplatesSegmentedButton.visible(true) + templatePropertyPlaceholder.component = null + selectedTemplate = templates.firstOrNull() ?: EmptyLoadedTemplate + } + + selectedTemplateProperty.afterChange { template -> + createOptionsPanelInBackground(template, templatePropertyPlaceholder) + } + + builder.row { + templatePropertiesProcessIcon = + cell(AsyncProcessIcon("Templates loading")) + .visibleIf(templateLoadingProperty) + label(MCDevBundle("creator.step.generic.load_template.message")) + .visibleIf(templateLoadingProperty) + label("") + .bindText(templateLoadingTextProperty) + .visibleIf(templateLoadingProperty) + label("") + .bindText(templateLoadingText2Property) + .visibleIf(templateLoadingProperty) + noTemplatesAvailable = label(MCDevBundle("creator.step.generic.no_templates_available.message")) + .visible(false) + .apply { component.foreground = JBColor.RED } + templatePropertyPlaceholder = placeholder().align(AlignX.FILL) + }.topGap(TopGap.SMALL) + + initTemplates() + } + + 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) + } + } + + val indicator = CreatorProgressIndicator( + templateProvidersLoadingProperty, + templateProvidersTextProperty, + templateProvidersText2Property + ) + ProgressManager.getInstance().runProcessWithProgressAsynchronously(task, indicator) + } + + private fun loadTemplatesInBackground(provider: () -> Collection) { + selectedTemplate = EmptyLoadedTemplate + + val task = object : Task.Backgroundable( + context.project, + MCDevBundle("creator.step.generic.load_template.message"), + true, + ALWAYS_BACKGROUND, + ) { + + override fun run(indicator: ProgressIndicator) { + if (project?.isDisposed == true) { + return + } + + application.invokeAndWait({ + ProgressManager.checkCanceled() + templateLoadingProperty.set(true) + VirtualFileManager.getInstance().syncRefresh() + }, context.modalityState) + + 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) + } + } + + templateLoadingIndicator?.cancel() + + val indicator = CreatorProgressIndicator( + templateLoadingProperty, + templateLoadingTextProperty, + templateLoadingText2Property + ) + templateLoadingIndicator = indicator + ProgressManager.getInstance().runProcessWithProgressAsynchronously(task, indicator) + } + + private fun createOptionsPanelInBackground(template: LoadedTemplate, placeholder: Placeholder) { + properties = mutableMapOf() + + if (!template.isValid) { + return + } + + val baseData = data.getUserData(NewProjectWizardBaseData.KEY) + ?: return thisLogger().error("Could not find wizard base data") + + properties["PROJECT_NAME"] = ExternalCreatorProperty( + graph = propertyGraph, + properties = properties, + graphProperty = baseData.nameProperty, + valueType = String::class.java + ) + + placeholder.component = panel { + val reporter = TemplateValidationReporterImpl() + val uiFactories = setupTemplate(template, reporter) + if (uiFactories.isEmpty() && !reporter.hasErrors) { + row { + label(MCDevBundle("creator.ui.warn.no_properties")) + .component.foreground = JBColor.YELLOW + } + } else { + hasTemplateErrors = reporter.hasErrors + reporter.display(this) + + if (!reporter.hasErrors) { + for (uiFactory in uiFactories) { + uiFactory.accept(this) + } + } + } + } + } + + private fun setupTemplate( + template: LoadedTemplate, + reporter: TemplateValidationReporterImpl + ): List> { + return try { + val properties = template.descriptor.properties.orEmpty() + .mapNotNull { + reporter.subject = it.name + setupProperty(it, reporter) + } + .sortedBy { (_, order) -> order } + .map { it.first } + + val finalizers = template.descriptor.finalizers + if (finalizers != null) { + CreatorFinalizer.validateAll(reporter, finalizers) + } + + properties + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + + thisLogger().error( + "Unexpected error during template setup", + t, + template.label, + template.descriptor.toString() + ) + + emptyList() + } finally { + reporter.subject = null + } + } + + private fun setupProperty( + descriptor: TemplatePropertyDescriptor, + reporter: TemplateValidationReporter + ): Pair, Int>? { + if (!descriptor.groupProperties.isNullOrEmpty()) { + val childrenUiFactories = descriptor.groupProperties + .mapNotNull { setupProperty(it, reporter) } + .sortedBy { (_, order) -> order } + .map { it.first } + + val factory = Consumer { panel -> + val label = descriptor.translatedLabel + if (descriptor.collapsible == false) { + panel.group(label) { + for (childFactory in childrenUiFactories) { + childFactory.accept(this@group) + } + } + } else { + val group = panel.collapsibleGroup(label) { + for (childFactory in childrenUiFactories) { + childFactory.accept(this@collapsibleGroup) + } + } + + group.expanded = descriptor.default as? Boolean ?: false + } + } + + val order = descriptor.order ?: 0 + return factory to order + } + + if (descriptor.name in properties.keys) { + reporter.fatal("Duplicate property name ${descriptor.name}") + } + + val prop = CreatorPropertyFactory.createFromType(descriptor.type, descriptor, propertyGraph, properties) + if (prop == null) { + reporter.fatal("Unknown template property type ${descriptor.type}") + } + + prop.setupProperty(reporter) + + properties[descriptor.name] = prop + + if (descriptor.visible == false) { + return null + } + + val factory = Consumer { panel -> prop.buildUi(panel, context) } + val order = descriptor.order ?: 0 + return factory to order + } + + override fun setupProject(project: Project) { + val template = selectedTemplate + if (template is EmptyLoadedTemplate) { + return + } + + val projectPath = context.projectDirectory + val templateProperties = collectTemplateProperties() + thisLogger().debug("Template properties: $templateProperties") + + val generatedFiles = mutableListOf>() + for (file in template.descriptor.files.orEmpty()) { + if (file.condition != null && + !TemplateEvaluator.condition(templateProperties, file.condition).getOrElse { false } + ) { + continue + } + + val relativeTemplate = TemplateEvaluator.template(templateProperties, file.template).getOrNull() + ?: continue + val relativeDest = TemplateEvaluator.template(templateProperties, file.destination).getOrNull() + ?: continue + + try { + val templateContents = template.loadTemplateContents(relativeTemplate) + ?: continue + + val destPath = projectPath.resolve(relativeDest).toAbsolutePath() + if (!destPath.startsWith(projectPath)) { + // We want to make sure template files aren't 'escaping' the project directory + continue + } + + var fileTemplateProperties = templateProperties + if (file.properties != null) { + fileTemplateProperties = templateProperties.toMutableMap() + fileTemplateProperties.putAll(file.properties) + } + + val processedContent = TemplateEvaluator.template(fileTemplateProperties, templateContents) + .onFailure { t -> + val attachment = Attachment(relativeTemplate, templateContents) + thisLogger().error("Failed evaluate template '$relativeTemplate'", t, attachment) + } + .getOrNull() + ?: continue + + destPath.parent.createDirectories() + destPath.writeText(processedContent) + + val virtualFile = destPath.refreshAndFindVirtualFile() + if (virtualFile != null) { + generatedFiles.add(file to virtualFile) + } else { + thisLogger().warn("Could not find VirtualFile for file generated at $destPath (descriptor: $file)") + } + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + + thisLogger().error("Failed to process template file $file", t) + } + } + + val finalizeAction = { + WriteAction.runAndWait { + LocalFileSystem.getInstance().refresh(false) + // Apparently a module root is required for the reformat to work + setupTempRootModule(project, projectPath) + + reformatFiles(project, generatedFiles) + openFilesInEditor(project, generatedFiles) + } + + val finalizers = selectedTemplate.descriptor.finalizers + if (!finalizers.isNullOrEmpty()) { + CreatorFinalizer.executeAll(context, project, finalizers, templateProperties) + } + } + if (context.isCreatingNewProject) { + TemplateService.instance.registerFinalizerAction(project, finalizeAction) + } else { + application.executeOnPooledThread { finalizeAction() } + } + } + + private fun setupTempRootModule(project: Project, projectPath: Path) { + val modifiableModel = ModuleManager.getInstance(project).getModifiableModel() + val module = modifiableModel.newNonPersistentModule("mcdev-temp-root", ModuleTypeId.JAVA_MODULE) + val rootsModel = ModuleRootManager.getInstance(module).modifiableModel + rootsModel.addContentEntry(projectPath.virtualFileOrError) + rootsModel.commit() + modifiableModel.commit() + } + + private fun collectTemplateProperties(): MutableMap { + val into = mutableMapOf() + + into.putAll(TemplateEvaluator.baseProperties) + + val gitData = data.getUserData(GitNewProjectWizardData.KEY) + into["USE_GIT"] = gitData?.git == true + + return properties.mapValuesTo(into) { (_, prop) -> prop.get() } + } + + private fun reformatFiles( + project: Project, + files: MutableList> + ) { + val psiManager = PsiManager.getInstance(project) + val psiFiles = files.asSequence() + .filter { (desc, _) -> desc.reformat != false } + .mapNotNull { (_, file) -> psiManager.findFile(file) } + ReformatCodeProcessor(project, psiFiles.toTypedArray(), null, false).run() + } + + private fun openFilesInEditor( + project: Project, + files: MutableList> + ) { + val fileEditorManager = FileEditorManager.getInstance(project) + val projectView = ProjectView.getInstance(project) + for ((desc, file) in files) { + if (desc.openInEditor == true) { + fileEditorManager.openFile(file, true) + projectView.select(null, file, false) + } + } + } +} diff --git a/src/main/kotlin/creator/custom/EvaluateTemplateExpressionAction.kt b/src/main/kotlin/creator/custom/EvaluateTemplateExpressionAction.kt new file mode 100644 index 000000000..d2fc2c771 --- /dev/null +++ b/src/main/kotlin/creator/custom/EvaluateTemplateExpressionAction.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.creator.custom + +import com.demonwav.mcdev.creator.custom.model.ClassFqn +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.util.Disposer +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.panel +import javax.swing.JComponent + +class EvaluateTemplateExpressionAction : AnAction() { + override fun actionPerformed(e: AnActionEvent) { + + val dialog = EvaluateDialog() + dialog.isModal = false + dialog.show() + } + + private class EvaluateDialog : DialogWrapper(null, false, IdeModalityType.IDE) { + val document = EditorFactory.getInstance().createDocument("") + val editor = EditorFactory.getInstance().createEditor(document) as EditorEx + + lateinit var field: JBTextField + + init { + title = "Evaluate Template Expression" + isOKActionEnabled = true + setValidationDelay(0) + + Disposer.register(disposable) { + EditorFactory.getInstance().releaseEditor(editor) + } + + init() + } + + override fun createCenterPanel(): JComponent = panel { + row { + cell(editor.component).align(Align.FILL) + } + + row("Result:") { + field = textField().align(Align.FILL).component + field.isEditable = false + } + } + + override fun doOKAction() { + val props = mapOf( + "BUILD_SYSTEM" to "gradle", + "USE_PAPER_MANIFEST" to false, + "MAIN_CLASS" to ClassFqn("io.github.rednesto.test.Test") + ) + field.text = TemplateEvaluator.evaluate(props, document.text).toString() + } + } +} diff --git a/src/main/kotlin/creator/custom/ResourceBundleTranslator.kt b/src/main/kotlin/creator/custom/ResourceBundleTranslator.kt new file mode 100644 index 000000000..c90077f45 --- /dev/null +++ b/src/main/kotlin/creator/custom/ResourceBundleTranslator.kt @@ -0,0 +1,47 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom + +import com.demonwav.mcdev.asset.MCDevBundle +import com.intellij.openapi.util.text.StringUtil +import java.util.MissingResourceException +import java.util.ResourceBundle +import org.jetbrains.annotations.Nls +import org.jetbrains.annotations.NonNls + +abstract class ResourceBundleTranslator { + + abstract val bundle: ResourceBundle? + + fun translate(key: @NonNls String): @Nls String { + return translateOrNull(key) ?: StringUtil.escapeMnemonics(key) + } + + fun translateOrNull(key: @NonNls String): @Nls String? { + if (bundle != null) { + try { + return bundle!!.getString(key) + } catch (_: MissingResourceException) { + } + } + return MCDevBundle.messageOrNull(key) + } +} diff --git a/src/main/kotlin/creator/custom/TemplateDescriptor.kt b/src/main/kotlin/creator/custom/TemplateDescriptor.kt new file mode 100644 index 000000000..abed981b6 --- /dev/null +++ b/src/main/kotlin/creator/custom/TemplateDescriptor.kt @@ -0,0 +1,99 @@ +/* + * 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 java.util.ResourceBundle + +data class TemplateDescriptor( + val version: Int, + val label: String? = null, + val group: String? = null, + val inherit: String? = null, + val hidden: Boolean? = null, + val properties: List? = null, + val files: List? = null, + val finalizers: List>? = null, +) : ResourceBundleTranslator() { + @Transient + override var bundle: ResourceBundle? = null + + val translatedGroup: String + get() = translate("creator.ui.group.${(group ?: "default").lowercase()}.label") + + companion object { + + const val FORMAT_VERSION = 1 + } +} + +data class TemplatePropertyDescriptor( + val name: String, + val type: String, + val label: String? = null, + val order: Int? = null, + val options: Any? = null, + val limit: Int? = null, + val maxSegmentedButtonsCount: Int? = null, + val forceDropdown: Boolean? = null, + val groupProperties: List? = null, + val remember: Any? = null, + val visible: Any? = null, + val editable: Boolean? = null, + val collapsible: Boolean? = null, + val warning: String? = null, + val default: Any?, + val nullIfDefault: Boolean? = null, + val derives: PropertyDerivation? = null, + val inheritFrom: String? = null, + val parameters: Map? = null, + val validator: Any? = null +) : ResourceBundleTranslator() { + @Transient + override var bundle: ResourceBundle? = null + + val translatedLabel: String + get() = translate(label ?: "creator.ui.${name.lowercase()}.label") + val translatedWarning: String? + get() = translateOrNull(label ?: "creator.ui.${name.lowercase()}.warning") ?: warning +} + +data class PropertyDerivation( + val parents: List? = null, + val method: String? = null, + val select: List? = null, + val default: Any? = null, + val whenModified: Boolean? = null, + val parameters: Map? = null, +) + +data class PropertyDerivationSelect( + val condition: String, + val value: Any +) + +data class TemplateFile( + val template: String, + val destination: String, + val condition: String? = null, + val properties: Map? = null, + val reformat: Boolean? = null, + val openInEditor: Boolean? = null, +) diff --git a/src/main/kotlin/creator/custom/TemplateEvaluator.kt b/src/main/kotlin/creator/custom/TemplateEvaluator.kt new file mode 100644 index 000000000..b717fe712 --- /dev/null +++ b/src/main/kotlin/creator/custom/TemplateEvaluator.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.creator.custom + +import com.demonwav.mcdev.util.MinecraftVersions +import com.demonwav.mcdev.util.SemanticVersion +import org.apache.velocity.VelocityContext +import org.apache.velocity.app.Velocity +import org.apache.velocity.util.StringBuilderWriter + +object TemplateEvaluator { + + val baseProperties = mapOf( + "semver" to SemanticVersion.Companion, + "mcver" to MinecraftVersions + ) + + fun evaluate(properties: Map, template: String): Result> { + val context = VelocityContext(baseProperties + properties) + val stringWriter = StringBuilderWriter() + return runCatching { + Velocity.evaluate(context, stringWriter, "McDevTplExpr", template) to stringWriter.toString() + } + } + + fun template(properties: Map, template: String): Result { + return evaluate(properties, template).map { it.second } + } + + fun condition(properties: Map, condition: String): Result { + val actualCondition = "#if ($condition) true #else false #end" + return evaluate(properties, actualCondition).map { it.second.trim().toBoolean() } + } +} diff --git a/src/main/kotlin/creator/custom/TemplateRepoTable.kt b/src/main/kotlin/creator/custom/TemplateRepoTable.kt new file mode 100644 index 000000000..d14f023f0 --- /dev/null +++ b/src/main/kotlin/creator/custom/TemplateRepoTable.kt @@ -0,0 +1,133 @@ +/* + * 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.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.providers.TemplateProvider +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.ComboBoxTableCellRenderer +import com.intellij.ui.ToolbarDecorator +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.MutableProperty +import com.intellij.ui.dsl.builder.Row +import com.intellij.ui.table.TableView +import com.intellij.util.ListWithSelection +import com.intellij.util.ui.ColumnInfo +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.ListTableModel +import com.intellij.util.ui.table.ComboBoxTableCellEditor +import java.awt.Dimension +import javax.swing.JComponent +import javax.swing.JPanel +import javax.swing.table.TableCellEditor +import javax.swing.table.TableCellRenderer + +private object NameColumn : ColumnInfo( + MCDevBundle("minecraft.settings.creator.repos.column.name") +) { + override fun valueOf(item: MinecraftSettings.TemplateRepo?): String? { + return item?.name + } + + override fun setValue(item: MinecraftSettings.TemplateRepo?, value: String?) { + item?.name = value ?: MCDevBundle("minecraft.settings.creator.repo.default_name") + } + + override fun isCellEditable(item: MinecraftSettings.TemplateRepo?): Boolean = true +} + +private object ProviderColumn : ColumnInfo( + MCDevBundle("minecraft.settings.creator.repos.column.provider") +) { + override fun valueOf(item: MinecraftSettings.TemplateRepo?): ListWithSelection? { + val providers = TemplateProvider.getAllKeys() + val list = ListWithSelection(providers) + list.select(item?.provider?.takeIf(providers::contains)) + + return list + } + + override fun setValue(item: MinecraftSettings.TemplateRepo?, value: Any?) { + item?.provider = value as? String ?: "local" + } + + override fun isCellEditable(item: MinecraftSettings.TemplateRepo?): Boolean = true + + override fun getRenderer(item: MinecraftSettings.TemplateRepo?): TableCellRenderer? { + return ComboBoxTableCellRenderer.INSTANCE + } + + override fun getEditor(item: MinecraftSettings.TemplateRepo?): TableCellEditor? { + return ComboBoxTableCellEditor.INSTANCE + } +} + +fun Row.templateRepoTable( + prop: MutableProperty> +): Cell { + val model = object : ListTableModel(NameColumn, ProviderColumn) { + override fun addRow() { + val defaultName = MCDevBundle("minecraft.settings.creator.repo.default_name") + addRow(MinecraftSettings.TemplateRepo(defaultName, "local", "")) + } + } + + val table = TableView(model) + table.setShowGrid(true) + table.tableHeader.reorderingAllowed = false + + val decoratedTable = ToolbarDecorator.createDecorator(table) + .setPreferredSize(Dimension(JBUI.scale(300), JBUI.scale(200))) + .setEditActionUpdater { + val selectedRepo = table.selection.firstOrNull() + ?: return@setEditActionUpdater false + val provider = TemplateProvider.get(selectedRepo.provider) + ?: return@setEditActionUpdater false + return@setEditActionUpdater provider.hasConfig + } + .setEditAction { + val selectedRepo = table.selection.firstOrNull() + ?: return@setEditAction + val provider = TemplateProvider.get(selectedRepo.provider) + ?: return@setEditAction + val dataConsumer = { data: String -> selectedRepo.data = data } + val configPanel = provider.setupConfigUi(selectedRepo.data, dataConsumer) + ?: return@setEditAction + + val dialog = object : DialogWrapper(table, true) { + init { + init() + } + + override fun createCenterPanel(): JComponent = configPanel + } + dialog.title = MCDevBundle("minecraft.settings.creator.repo_config.title", selectedRepo.name) + dialog.show() + } + .createPanel() + return cell(decoratedTable) + .bind( + { _ -> model.items }, + { _, repos -> model.items = repos; }, + prop + ) +} diff --git a/src/main/kotlin/creator/custom/TemplateResourceBundle.kt b/src/main/kotlin/creator/custom/TemplateResourceBundle.kt new file mode 100644 index 000000000..ecb4d4bf0 --- /dev/null +++ b/src/main/kotlin/creator/custom/TemplateResourceBundle.kt @@ -0,0 +1,32 @@ +/* + * 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 java.io.Reader +import java.util.PropertyResourceBundle +import java.util.ResourceBundle + +class TemplateResourceBundle(val reader: Reader, parent: ResourceBundle?) : PropertyResourceBundle(reader) { + + init { + this.parent = parent + } +} diff --git a/src/main/kotlin/creator/custom/TemplateService.kt b/src/main/kotlin/creator/custom/TemplateService.kt new file mode 100644 index 000000000..28c9094de --- /dev/null +++ b/src/main/kotlin/creator/custom/TemplateService.kt @@ -0,0 +1,60 @@ +/* + * 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.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity +import com.intellij.util.application + +@Service +class TemplateService { + + private val pendingActions: MutableMap Unit> = mutableMapOf() + + fun registerFinalizerAction(project: Project, action: suspend () -> Unit) { + if (pendingActions.containsKey(project.name)) { + thisLogger().error("More than one finalizer action registered for project $project") + return + } + + pendingActions[project.locationHash] = action + } + + suspend fun executeFinalizer(project: Project) { + pendingActions.remove(project.locationHash)?.invoke() + } + + companion object { + + val instance: TemplateService + get() = application.service() + } +} + +class TemplateProjectFinalizerActivity : ProjectActivity { + + override suspend fun execute(project: Project) { + TemplateService.instance.executeFinalizer(project) + } +} diff --git a/src/main/kotlin/creator/custom/TemplateValidationReporter.kt b/src/main/kotlin/creator/custom/TemplateValidationReporter.kt new file mode 100644 index 000000000..b953eb16e --- /dev/null +++ b/src/main/kotlin/creator/custom/TemplateValidationReporter.kt @@ -0,0 +1,106 @@ +/* + * 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.asset.MCDevBundle +import com.intellij.ui.JBColor +import com.intellij.ui.dsl.builder.Panel + +interface TemplateValidationReporter { + + fun warn(message: String) + + fun error(message: String) + + fun fatal(message: String, cause: Throwable? = null): Nothing +} + +class TemplateValidationReporterImpl : TemplateValidationReporter { + + private val validationItems: MutableMap> = linkedMapOf() + var hasErrors = false + private set + var hasWarns = false + private set + + var subject: String? = null + + override fun warn(message: String) { + check(subject != null) { "No subject is being validated" } + hasWarns = true + validationItems.getOrPut(subject!!, ::mutableListOf).add(TemplateValidationItem.Warn(message)) + } + + override fun error(message: String) { + check(subject != null) { "No subject is being validated" } + hasErrors = true + validationItems.getOrPut(subject!!, ::mutableListOf).add(TemplateValidationItem.Error(message)) + } + + override fun fatal(message: String, cause: Throwable?): Nothing { + error("Fatal validation error: $message") + throw TemplateValidationException(message, cause) + } + + fun display(panel: Panel) { + if (!hasErrors && !hasWarns) { + return + } + + panel.row { + when { + hasWarns && hasErrors -> label(MCDevBundle("creator.ui.error.template_warns_and_errors")).apply { + component.foreground = JBColor.RED + } + + hasWarns -> label(MCDevBundle("creator.ui.error.template_warns")).apply { + component.foreground = JBColor.YELLOW + } + + hasErrors -> label(MCDevBundle("creator.ui.error.template_errors")).apply { + component.foreground = JBColor.RED + } + } + } + + for ((subjectName, items) in validationItems) { + panel.row { + label("$subjectName:") + } + + panel.indent { + for (item in items) { + row { + label(item.message).component.foreground = item.color + } + } + } + } + } +} + +class TemplateValidationException(message: String?, cause: Throwable? = null) : Exception(message, cause) + +private sealed class TemplateValidationItem(val message: String, val color: JBColor) { + + class Warn(message: String) : TemplateValidationItem(message, JBColor.YELLOW) + class Error(message: String) : TemplateValidationItem(message, JBColor.RED) +} diff --git a/src/main/kotlin/creator/custom/derivation/ExtractVersionMajorMinorPropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/ExtractVersionMajorMinorPropertyDerivation.kt new file mode 100644 index 000000000..186117050 --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/ExtractVersionMajorMinorPropertyDerivation.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.creator.custom.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.demonwav.mcdev.util.SemanticVersion + +class ExtractVersionMajorMinorPropertyDerivation : PreparedDerivation { + + override fun derive(parentValues: List): Any? { + val from = parentValues[0] as SemanticVersion + if (from.parts.size < 2) { + return SemanticVersion(emptyList()) + } + + val (part1, part2) = from.parts + if (part1 is SemanticVersion.Companion.VersionPart.ReleasePart && + part2 is SemanticVersion.Companion.VersionPart.ReleasePart + ) { + return SemanticVersion(listOf(part1, part2)) + } + + return SemanticVersion(emptyList()) + } + + companion object : PropertyDerivationFactory { + + override fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? { + if (parents.isNullOrEmpty()) { + reporter.error("Expected a parent") + return null + } + + if (!parents[0]!!.acceptsType(SemanticVersion::class.java)) { + reporter.error("First parent must produce a semantic version") + return null + } + + return ExtractVersionMajorMinorPropertyDerivation() + } + } +} diff --git a/src/main/kotlin/creator/custom/derivation/PropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/PropertyDerivation.kt new file mode 100644 index 000000000..8d1aaf1dd --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/PropertyDerivation.kt @@ -0,0 +1,38 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.types.CreatorProperty + +fun interface PreparedDerivation { + fun derive(parentValues: List): Any? +} + +interface PropertyDerivationFactory { + + fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? +} diff --git a/src/main/kotlin/creator/custom/derivation/RecommendJavaVersionForMcVersionPropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/RecommendJavaVersionForMcVersionPropertyDerivation.kt new file mode 100644 index 000000000..c762c09b5 --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/RecommendJavaVersionForMcVersionPropertyDerivation.kt @@ -0,0 +1,74 @@ +/* + * 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.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.HasMinecraftVersion +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.demonwav.mcdev.util.MinecraftVersions +import com.demonwav.mcdev.util.SemanticVersion + +class RecommendJavaVersionForMcVersionPropertyDerivation(val default: Int) : PreparedDerivation { + + override fun derive(parentValues: List): Any? { + val mcVersion: SemanticVersion = when (val version = parentValues[0]) { + is SemanticVersion -> version + is HasMinecraftVersion -> version.minecraftVersion + else -> return default + } + return MinecraftVersions.requiredJavaVersion(mcVersion).ordinal + } + + companion object : PropertyDerivationFactory { + + override fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? { + if (parents.isNullOrEmpty()) { + reporter.error("Expected one parent") + return null + } + + if (parents.size > 1) { + reporter.warn("More than one parent defined") + } + + val parentValue = parents[0]!! + if (!parentValue.acceptsType(SemanticVersion::class.java) && + !parentValue.acceptsType(HasMinecraftVersion::class.java) + ) { + reporter.error("Parent must produce a semantic version or a value that has a Minecraft version") + return null + } + + val default = (derivation.default as? Number)?.toInt() + if (default == null) { + reporter.error("Default value is required and must be an integer") + return null + } + + return RecommendJavaVersionForMcVersionPropertyDerivation(default) + } + } +} diff --git a/src/main/kotlin/creator/custom/derivation/ReplacePropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/ReplacePropertyDerivation.kt new file mode 100644 index 000000000..c3688f9ad --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/ReplacePropertyDerivation.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.creator.custom.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.types.CreatorProperty + +class ReplacePropertyDerivation( + val regex: Regex, + val replacement: String, + val maxLength: Int?, +) : PreparedDerivation { + + override fun derive(parentValues: List): Any? { + val projectName = parentValues.first() as? String + ?: return null + + val sanitized = projectName.lowercase().replace(regex, replacement) + if (maxLength != null && sanitized.length > maxLength) { + return sanitized.substring(0, maxLength) + } + + return sanitized + } + + companion object : PropertyDerivationFactory { + + override fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? { + if (derivation.parameters == null) { + reporter.error("Missing parameters") + return null + } + + if (parents.isNullOrEmpty()) { + reporter.error("Missing parent value") + return null + } + + if (parents.size > 2) { + reporter.warn("More than one parent defined") + } + + if (!parents[0]!!.acceptsType(String::class.java)) { + reporter.error("Parent property must produce a string value") + return null + } + + val regexString = derivation.parameters["regex"] as? String + if (regexString == null) { + reporter.error("Missing 'regex' string parameter") + return null + } + + val regex = try { + Regex(regexString) + } catch (t: Throwable) { + reporter.error("Invalid regex: '$regexString': ${t.message}") + return null + } + + val replacement = derivation.parameters["replacement"] as? String + if (replacement == null) { + reporter.error("Missing 'replacement' string parameter") + return null + } + + val maxLength = (derivation.parameters["maxLength"] as? Number)?.toInt() + return ReplacePropertyDerivation(regex, replacement, maxLength) + } + } +} diff --git a/src/main/kotlin/creator/custom/derivation/SelectPropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/SelectPropertyDerivation.kt new file mode 100644 index 000000000..88444bb5d --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/SelectPropertyDerivation.kt @@ -0,0 +1,67 @@ +/* + * 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.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.PropertyDerivationSelect +import com.demonwav.mcdev.creator.custom.TemplateEvaluator +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.intellij.openapi.diagnostic.getOrLogException +import com.intellij.openapi.diagnostic.thisLogger + +class SelectPropertyDerivation( + val parents: List?, + val options: List, + val default: Any?, +) : PreparedDerivation { + + override fun derive(parentValues: List): Any? { + val properties = if (!parents.isNullOrEmpty()) { + parentValues.mapIndexed { i, value -> parents[i] to value }.toMap() + } else { + emptyMap() + } + for (option in options) { + if (TemplateEvaluator.condition(properties, option.condition).getOrLogException(thisLogger()) == true) { + return option.value + } + } + + return default + } + + companion object : PropertyDerivationFactory { + + override fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? { + if (derivation.select == null) { + reporter.error("Missing select options") + return null + } + + return SelectPropertyDerivation(derivation.parents, derivation.select, derivation.default) + } + } +} diff --git a/src/main/kotlin/creator/custom/derivation/SuggestClassNamePropertyDerivation.kt b/src/main/kotlin/creator/custom/derivation/SuggestClassNamePropertyDerivation.kt new file mode 100644 index 000000000..8362a7b4a --- /dev/null +++ b/src/main/kotlin/creator/custom/derivation/SuggestClassNamePropertyDerivation.kt @@ -0,0 +1,68 @@ +/* + * 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.derivation + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.BuildSystemCoordinates +import com.demonwav.mcdev.creator.custom.model.ClassFqn +import com.demonwav.mcdev.creator.custom.types.CreatorProperty +import com.demonwav.mcdev.util.capitalize +import com.demonwav.mcdev.util.decapitalize + +class SuggestClassNamePropertyDerivation : PreparedDerivation { + + override fun derive(parentValues: List): Any? { + val coords = parentValues[0] as BuildSystemCoordinates + val name = parentValues[1] as String + return ClassFqn("${coords.groupId}.${name.decapitalize()}.${name.capitalize()}") + } + + companion object : PropertyDerivationFactory { + + override fun create( + reporter: TemplateValidationReporter, + parents: List?>?, + derivation: PropertyDerivation + ): PreparedDerivation? { + if (parents == null || parents.size < 2) { + reporter.error("Expected 2 parents") + return null + } + + if (parents.size > 2) { + reporter.warn("More than two parents defined") + } + + if (!parents[0]!!.acceptsType(BuildSystemCoordinates::class.java)) { + reporter.error("First parent must produce a build system coordinates") + return null + } + + if (!parents[1]!!.acceptsType(String::class.java)) { + reporter.error("Second parent must produce a string value") + return null + } + + return SuggestClassNamePropertyDerivation() + } + } +} diff --git a/src/main/kotlin/creator/custom/finalizers/CreatorFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/CreatorFinalizer.kt new file mode 100644 index 000000000..4442f33f7 --- /dev/null +++ b/src/main/kotlin/creator/custom/finalizers/CreatorFinalizer.kt @@ -0,0 +1,128 @@ +/* + * 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.finalizers + +import com.demonwav.mcdev.creator.custom.TemplateEvaluator +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.TemplateValidationReporterImpl +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.diagnostic.ControlFlowException +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.extensions.RequiredElement +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.KeyedExtensionCollector +import com.intellij.serviceContainer.BaseKeyedLazyInstance +import com.intellij.util.KeyedLazyInstance +import com.intellij.util.xmlb.annotations.Attribute + +interface CreatorFinalizer { + + fun validate(reporter: TemplateValidationReporter, properties: Map) = Unit + + fun execute( + context: WizardContext, + project: Project, + properties: Map, + templateProperties: Map + ) + + companion object { + private val EP_NAME = + ExtensionPointName.create("com.demonwav.minecraft-dev.creatorFinalizer") + private val COLLECTOR = KeyedExtensionCollector(EP_NAME) + + fun validateAll( + reporter: TemplateValidationReporterImpl, + finalizers: List>, + ) { + for ((index, properties) in finalizers.withIndex()) { + reporter.subject = "Finalizer #$index" + + val type = properties["type"] as? String + if (type == null) { + reporter.error("Missing required 'type' value") + } + + val condition = properties["condition"] + if (condition != null && condition !is String) { + reporter.error("'condition' must be a string") + } + + if (type != null) { + val finalizer = COLLECTOR.findSingle(type) + if (finalizer == null) { + reporter.error("Unknown finalizer of type $type") + } else { + try { + finalizer.validate(reporter, properties) + } catch (t: Throwable) { + reporter.error("Unexpected error during finalizer validation: ${t.message}") + thisLogger().error("Unexpected error during finalizer validation", t) + } + } + } + } + } + + fun executeAll( + context: WizardContext, + project: Project, + finalizers: List>, + templateProperties: Map + ) { + for ((index, properties) in finalizers.withIndex()) { + val type = properties["type"] as String + val condition = properties["condition"] as? String + if (condition != null && + !TemplateEvaluator.condition(templateProperties, condition).getOrElse { false } + ) { + continue + } + + val finalizer = COLLECTOR.findSingle(type)!! + try { + finalizer.execute(context, project, properties, templateProperties) + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + thisLogger().error("Unhandled exception in finalizer #$index ($type)", t) + } + } + } + } +} + +class CreatorFinalizerBean : BaseKeyedLazyInstance(), KeyedLazyInstance { + + @Attribute("type") + @RequiredElement + lateinit var type: String + + @Attribute("implementation") + @RequiredElement + lateinit var implementation: String + + override fun getKey(): String? = type + + override fun getImplementationClassName(): String? = implementation +} diff --git a/src/main/kotlin/creator/custom/finalizers/GitAddAllFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/GitAddAllFinalizer.kt new file mode 100644 index 000000000..e2ebd6863 --- /dev/null +++ b/src/main/kotlin/creator/custom/finalizers/GitAddAllFinalizer.kt @@ -0,0 +1,38 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.finalizers + +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.util.ExecUtil +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.project.Project + +class GitAddAllFinalizer : CreatorFinalizer { + + override fun execute( + context: WizardContext, + project: Project, + properties: Map, + templateProperties: Map + ) { + ExecUtil.execAndGetOutput(GeneralCommandLine("git", "add", ".").withWorkDirectory(context.projectFileDirectory)) + } +} diff --git a/src/main/kotlin/creator/custom/finalizers/ImportGradleProjectFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/ImportGradleProjectFinalizer.kt new file mode 100644 index 000000000..2115dc3b8 --- /dev/null +++ b/src/main/kotlin/creator/custom/finalizers/ImportGradleProjectFinalizer.kt @@ -0,0 +1,45 @@ +/* + * 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.finalizers + +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project +import org.jetbrains.plugins.gradle.service.project.open.canLinkAndRefreshGradleProject +import org.jetbrains.plugins.gradle.service.project.open.linkAndRefreshGradleProject + +class ImportGradleProjectFinalizer : CreatorFinalizer { + + override fun execute( + context: WizardContext, + project: Project, + properties: Map, + templateProperties: Map + ) { + val projectDir = context.projectFileDirectory + val canLink = canLinkAndRefreshGradleProject(projectDir, project, showValidationDialog = false) + thisLogger().info("canLink = $canLink projectDir = $projectDir") + if (canLink) { + linkAndRefreshGradleProject(projectDir, project) + thisLogger().info("Linking done") + } + } +} diff --git a/src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt new file mode 100644 index 000000000..38aefd975 --- /dev/null +++ b/src/main/kotlin/creator/custom/finalizers/ImportMavenProjectFinalizer.kt @@ -0,0 +1,53 @@ +/* + * 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.finalizers + +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VfsUtil +import java.nio.file.Path +import kotlinx.coroutines.runBlocking +import org.jetbrains.idea.maven.buildtool.MavenImportSpec +import org.jetbrains.idea.maven.project.MavenProjectsManager + +class ImportMavenProjectFinalizer : CreatorFinalizer { + + override fun execute( + context: WizardContext, + project: Project, + properties: Map, + templateProperties: Map + ) { + val projectDir = context.projectFileDirectory + val pomFile = VfsUtil.findFile(Path.of(projectDir).resolve("pom.xml"), true) + ?: return + + thisLogger().info("Invoking import on EDT pomFile = ${pomFile.path}") + val projectsManager = MavenProjectsManager.getInstance(project) + projectsManager.addManagedFiles(listOf(pomFile)) + runBlocking { + projectsManager.updateAllMavenProjects(MavenImportSpec(true, true, false)) + } + + thisLogger().info("Import finished") + } +} diff --git a/src/main/kotlin/creator/custom/finalizers/RunGradleTasksFinalizer.kt b/src/main/kotlin/creator/custom/finalizers/RunGradleTasksFinalizer.kt new file mode 100644 index 000000000..210920426 --- /dev/null +++ b/src/main/kotlin/creator/custom/finalizers/RunGradleTasksFinalizer.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.creator.custom.finalizers + +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.util.runGradleTaskAndWait +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project + +class RunGradleTasksFinalizer : CreatorFinalizer { + + override fun validate( + reporter: TemplateValidationReporter, + properties: Map + ) { + @Suppress("UNCHECKED_CAST") + val tasks = properties["tasks"] as? List + if (tasks == null) { + reporter.warn("Missing list of 'tasks' to execute") + } + } + + override fun execute( + context: WizardContext, + project: Project, + properties: Map, + templateProperties: Map + ) { + @Suppress("UNCHECKED_CAST") + val tasks = properties["tasks"] as List + val projectDir = context.projectDirectory + + thisLogger().info("tasks = $tasks projectDir = $projectDir") + runGradleTaskAndWait(project, projectDir) { settings -> + settings.taskNames = tasks + } + + thisLogger().info("Done running tasks") + } +} diff --git a/src/main/kotlin/creator/custom/model/ArchitecturyVersionsModel.kt b/src/main/kotlin/creator/custom/model/ArchitecturyVersionsModel.kt new file mode 100644 index 000000000..d77d9e22e --- /dev/null +++ b/src/main/kotlin/creator/custom/model/ArchitecturyVersionsModel.kt @@ -0,0 +1,60 @@ +/* + * 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.model + +import com.demonwav.mcdev.platform.fabric.util.FabricVersions +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +data class ArchitecturyVersionsModel( + val minecraft: SemanticVersion, + val forge: SemanticVersion?, + val neoforge: SemanticVersion?, + val loom: SemanticVersion, + val loader: SemanticVersion, + val yarn: FabricVersions.YarnVersion, + val useFabricApi: Boolean, + val fabricApi: SemanticVersion, + val useOfficialMappings: Boolean, + val useArchitecturyApi: Boolean, + val architecturyApi: SemanticVersion, +) : HasMinecraftVersion { + + override val minecraftVersion: SemanticVersion = minecraft + + val minecraftNext by lazy { + val mcNext = when (val part = minecraft.parts.getOrNull(1)) { + // Mimics the code used to get the next Minecraft version in Forge's MDK + // https://github.com/MinecraftForge/MinecraftForge/blob/0ff8a596fc1ef33d4070be89dd5cb4851f93f731/build.gradle#L884 + is SemanticVersion.Companion.VersionPart.ReleasePart -> (part.version + 1).toString() + null -> "?" + else -> part.versionString + } + + "1.$mcNext" + } + + val hasForge: Boolean by lazy { !forge?.parts.isNullOrEmpty() } + val forgeSpec: String? by lazy { forge?.parts?.getOrNull(0)?.versionString } + + val hasNeoforge: Boolean by lazy { !neoforge?.parts.isNullOrEmpty() } + val neoforgeSpec: String? by lazy { neoforge?.parts?.getOrNull(0)?.versionString } +} diff --git a/src/main/kotlin/creator/custom/model/BuildSystemCoordinates.kt b/src/main/kotlin/creator/custom/model/BuildSystemCoordinates.kt new file mode 100644 index 000000000..0eeab3c6e --- /dev/null +++ b/src/main/kotlin/creator/custom/model/BuildSystemCoordinates.kt @@ -0,0 +1,27 @@ +/* + * 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.model + +@TemplateApi +data class BuildSystemCoordinates(val groupId: String, val artifactId: String, val version: String) { + + override fun toString(): String = "$groupId:$artifactId:$version" +} diff --git a/src/main/kotlin/creator/custom/model/ClassFqn.kt b/src/main/kotlin/creator/custom/model/ClassFqn.kt new file mode 100644 index 000000000..5383f3fac --- /dev/null +++ b/src/main/kotlin/creator/custom/model/ClassFqn.kt @@ -0,0 +1,51 @@ +/* + * 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.model + +@TemplateApi +data class ClassFqn(val fqn: String) { + + /** + * The [Class.simpleName] of this class. + */ + val className by lazy { fqn.substringAfterLast('.') } + + /** + * The relative filesystem path to this class, without extension. + */ + val path by lazy { fqn.replace('.', '/') } + + /** + * The package name of this FQN as it would appear in source code. + */ + val packageName by lazy { fqn.substringBeforeLast('.') } + + /** + * The package path of this FQN reflected as a local filesystem path + */ + val packagePath by lazy { packageName.replace('.', '/') } + + fun withClassName(className: String) = copy("$packageName.$className") + + fun withSubPackage(name: String) = copy("$packageName.$name.$className") + + override fun toString(): String = fqn +} diff --git a/src/main/kotlin/creator/custom/model/CreatorJdk.kt b/src/main/kotlin/creator/custom/model/CreatorJdk.kt new file mode 100644 index 000000000..1e442b19b --- /dev/null +++ b/src/main/kotlin/creator/custom/model/CreatorJdk.kt @@ -0,0 +1,31 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.model + +import com.intellij.openapi.projectRoots.JavaSdk +import com.intellij.openapi.projectRoots.Sdk + +@TemplateApi +data class CreatorJdk(val sdk: Sdk?) { + + val javaVersion: Int + get() = sdk?.let { JavaSdk.getInstance().getVersion(it)?.ordinal } ?: 17 +} diff --git a/src/main/kotlin/creator/custom/model/FabricVersionsModel.kt b/src/main/kotlin/creator/custom/model/FabricVersionsModel.kt new file mode 100644 index 000000000..c5111c7c5 --- /dev/null +++ b/src/main/kotlin/creator/custom/model/FabricVersionsModel.kt @@ -0,0 +1,35 @@ +/* + * 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.model + +import com.demonwav.mcdev.platform.fabric.util.FabricVersions +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +data class FabricVersionsModel( + override val minecraftVersion: SemanticVersion, + val loom: SemanticVersion, + val loader: SemanticVersion, + val yarn: FabricVersions.YarnVersion, + val useFabricApi: Boolean, + val fabricApi: SemanticVersion, + val useOfficialMappings: Boolean, +) : HasMinecraftVersion diff --git a/src/main/kotlin/creator/custom/model/ForgeVersions.kt b/src/main/kotlin/creator/custom/model/ForgeVersions.kt new file mode 100644 index 000000000..a308f4787 --- /dev/null +++ b/src/main/kotlin/creator/custom/model/ForgeVersions.kt @@ -0,0 +1,44 @@ +/* + * 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.model + +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +data class ForgeVersions( + val minecraft: SemanticVersion, + val forge: SemanticVersion, +) : HasMinecraftVersion { + override val minecraftVersion = minecraft + + val minecraftNext by lazy { + val mcNext = when (val part = minecraft.parts.getOrNull(1)) { + // Mimics the code used to get the next Minecraft version in Forge's MDK + // https://github.com/MinecraftForge/MinecraftForge/blob/0ff8a596fc1ef33d4070be89dd5cb4851f93f731/build.gradle#L884 + is SemanticVersion.Companion.VersionPart.ReleasePart -> (part.version + 1).toString() + null -> "?" + else -> part.versionString + } + + "1.$mcNext" + } + val forgeSpec by lazy { forge.parts[0].versionString } +} diff --git a/src/main/kotlin/creator/custom/model/HasMinecraftVersion.kt b/src/main/kotlin/creator/custom/model/HasMinecraftVersion.kt new file mode 100644 index 000000000..c33cd9676 --- /dev/null +++ b/src/main/kotlin/creator/custom/model/HasMinecraftVersion.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.creator.custom.model + +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +interface HasMinecraftVersion { + + val minecraftVersion: SemanticVersion +} diff --git a/src/main/kotlin/creator/custom/model/LicenseData.kt b/src/main/kotlin/creator/custom/model/LicenseData.kt new file mode 100644 index 000000000..ddbf7932c --- /dev/null +++ b/src/main/kotlin/creator/custom/model/LicenseData.kt @@ -0,0 +1,30 @@ +/* + * 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.model + +import java.time.ZonedDateTime + +@TemplateApi +data class LicenseData( + val id: String, + val name: String, + val year: String = ZonedDateTime.now().year.toString(), +) diff --git a/src/main/kotlin/creator/custom/model/NeoForgeVersions.kt b/src/main/kotlin/creator/custom/model/NeoForgeVersions.kt new file mode 100644 index 000000000..c5a9bb2a2 --- /dev/null +++ b/src/main/kotlin/creator/custom/model/NeoForgeVersions.kt @@ -0,0 +1,46 @@ +/* + * 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.model + +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +data class NeoForgeVersions( + val minecraft: SemanticVersion, + val neoforge: SemanticVersion, + val neogradle: SemanticVersion, + val moddev: SemanticVersion, +) : HasMinecraftVersion { + override val minecraftVersion = minecraft + + val minecraftNext by lazy { + val mcNext = when (val part = minecraft.parts.getOrNull(1)) { + // Mimics the code used to get the next Minecraft version in Forge's MDK + // https://github.com/MinecraftForge/MinecraftForge/blob/0ff8a596fc1ef33d4070be89dd5cb4851f93f731/build.gradle#L884 + is SemanticVersion.Companion.VersionPart.ReleasePart -> (part.version + 1).toString() + null -> "?" + else -> part.versionString + } + + "1.$mcNext" + } + val neoforgeSpec by lazy { neoforge.parts[0].versionString } +} diff --git a/src/main/kotlin/creator/custom/model/ParchmentVersions.kt b/src/main/kotlin/creator/custom/model/ParchmentVersions.kt new file mode 100644 index 000000000..0d11a3c74 --- /dev/null +++ b/src/main/kotlin/creator/custom/model/ParchmentVersions.kt @@ -0,0 +1,32 @@ +/* + * 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.model + +import com.demonwav.mcdev.util.SemanticVersion + +@TemplateApi +data class ParchmentVersions( + val use: Boolean, + val version: SemanticVersion, + override val minecraftVersion: SemanticVersion, + val includeOlderMcVersions: Boolean, + val includeSnapshots: Boolean, +) : HasMinecraftVersion diff --git a/src/main/kotlin/creator/custom/model/StringList.kt b/src/main/kotlin/creator/custom/model/StringList.kt new file mode 100644 index 000000000..d2b3bf09c --- /dev/null +++ b/src/main/kotlin/creator/custom/model/StringList.kt @@ -0,0 +1,31 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.creator.custom.model + +@TemplateApi +data class StringList(val values: List) : List by values { + + override fun toString(): String = values.joinToString() + + @JvmOverloads + fun toString(separator: String, prefix: String = "", postfix: String = ""): String = + values.joinToString(separator, prefix, postfix) +} diff --git a/src/main/kotlin/creator/custom/model/TemplateApi.kt b/src/main/kotlin/creator/custom/model/TemplateApi.kt new file mode 100644 index 000000000..fb053db2d --- /dev/null +++ b/src/main/kotlin/creator/custom/model/TemplateApi.kt @@ -0,0 +1,30 @@ +/* + * 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.model + +/** + * Marker annotation indicating classes exposed to templates. + * + * Be careful of not breaking source or binary compatibility of those APIs without a good reason. + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.SOURCE) +annotation class TemplateApi diff --git a/src/main/kotlin/creator/custom/providers/BuiltinTemplateProvider.kt b/src/main/kotlin/creator/custom/providers/BuiltinTemplateProvider.kt new file mode 100644 index 000000000..1bc3bc9b6 --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/BuiltinTemplateProvider.kt @@ -0,0 +1,92 @@ +/* + * 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.providers + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.TemplateDescriptor +import com.demonwav.mcdev.creator.modalityState +import com.demonwav.mcdev.update.PluginUtil +import com.demonwav.mcdev.util.refreshSync +import com.demonwav.mcdev.util.virtualFile +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.panel +import javax.swing.JComponent + +class BuiltinTemplateProvider : RemoteTemplateProvider() { + + private val builtinRepoUrl = "https://github.com/minecraft-dev/templates/archive/refs/heads/v\$version.zip" + private val builtinTemplatesPath = PluginUtil.plugin.pluginPath.resolve("lib/resources/builtin-templates") + private val builtinTemplatesInnerPath = "templates-${TemplateDescriptor.FORMAT_VERSION}" + private var repoUpdated: Boolean = false + + override val label: String = MCDevBundle("template.provider.builtin.label") + + override val hasConfig: Boolean = true + + override fun init(indicator: ProgressIndicator, repos: List) { + if (repoUpdated || repos.none { it.data.toBoolean() }) { + // Auto update is disabled + return + } + + if (doUpdateRepo(indicator, label, builtinRepoUrl)) { + repoUpdated = true + } + } + + override fun loadTemplates( + context: WizardContext, + repo: MinecraftSettings.TemplateRepo + ): Collection { + val remoteTemplates = doLoadTemplates(context, repo, builtinTemplatesInnerPath) + if (remoteTemplates.isNotEmpty()) { + return remoteTemplates + } + + val repoRoot = builtinTemplatesPath.virtualFile + ?: return emptyList() + repoRoot.refreshSync(context.modalityState) + return TemplateProvider.findTemplates(context.modalityState, repoRoot) + } + + override fun setupConfigUi( + data: String, + dataSetter: (String) -> Unit + ): JComponent? { + val propertyGraph = PropertyGraph("BuiltinTemplateProvider config") + val autoUpdateProperty = propertyGraph.property(data.toBooleanStrictOrNull() != false) + + return panel { + row { + checkBox(MCDevBundle("creator.ui.custom.remote.auto_update.label")) + .bindSelected(autoUpdateProperty) + } + + onApply { + dataSetter(autoUpdateProperty.get().toString()) + } + } + } +} diff --git a/src/main/kotlin/creator/custom/providers/EmptyLoadedTemplate.kt b/src/main/kotlin/creator/custom/providers/EmptyLoadedTemplate.kt new file mode 100644 index 000000000..6f0cd2f45 --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/EmptyLoadedTemplate.kt @@ -0,0 +1,40 @@ +/* + * 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.providers + +import com.demonwav.mcdev.creator.custom.TemplateDescriptor + +/** + * Placeholder template + */ +object EmptyLoadedTemplate : LoadedTemplate { + + override val label: String = "Empty template" + override val tooltip: String = "Empty template tooltip" + + override val descriptor: TemplateDescriptor + get() = throw UnsupportedOperationException("The empty template can't have a descriptor") + + override val isValid: Boolean = false + + override fun loadTemplateContents(path: String): String? = + throw UnsupportedOperationException("The empty template can't have contents") +} diff --git a/src/main/kotlin/creator/custom/providers/LoadedTemplate.kt b/src/main/kotlin/creator/custom/providers/LoadedTemplate.kt new file mode 100644 index 000000000..186d58f40 --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/LoadedTemplate.kt @@ -0,0 +1,33 @@ +/* + * 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.providers + +import com.demonwav.mcdev.creator.custom.TemplateDescriptor + +interface LoadedTemplate { + + val label: String + val tooltip: String? + val descriptor: TemplateDescriptor + val isValid: Boolean + + fun loadTemplateContents(path: String): String? +} diff --git a/src/main/kotlin/creator/custom/providers/LocalTemplateProvider.kt b/src/main/kotlin/creator/custom/providers/LocalTemplateProvider.kt new file mode 100644 index 000000000..d08fb037c --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/LocalTemplateProvider.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.creator.custom.providers + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.modalityState +import com.demonwav.mcdev.util.refreshSync +import com.demonwav.mcdev.util.virtualFile +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.ui.validation.validationErrorIf +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.textValidation +import java.nio.file.Path +import javax.swing.JComponent +import kotlin.io.path.absolute + +class LocalTemplateProvider : TemplateProvider { + + override val label: String = MCDevBundle("template.provider.local.label") + + override val hasConfig: Boolean = true + + override fun loadTemplates( + context: WizardContext, + repo: MinecraftSettings.TemplateRepo + ): Collection { + val rootPath = Path.of(repo.data.trim()).absolute() + val repoRoot = rootPath.virtualFile + ?: return emptyList() + val modalityState = context.modalityState + repoRoot.refreshSync(modalityState) + return TemplateProvider.findTemplates(modalityState, repoRoot) + } + + override fun setupConfigUi( + data: String, + dataSetter: (String) -> Unit + ): JComponent? { + val propertyGraph = PropertyGraph("LocalTemplateProvider config") + val pathProperty = propertyGraph.property(data) + return panel { + row(MCDevBundle("creator.ui.custom.path.label")) { + val pathChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor().apply { + description = MCDevBundle("creator.ui.custom.path.dialog.description") + } + textFieldWithBrowseButton( + MCDevBundle("creator.ui.custom.path.dialog.title"), + null, + pathChooserDescriptor + ).align(AlignX.FILL) + .columns(COLUMNS_LARGE) + .bindText(pathProperty) + .textValidation( + validationErrorIf(MCDevBundle("creator.validation.custom.path_not_a_directory")) { value -> + val file = kotlin.runCatching { + VirtualFileManager.getInstance().findFileByNioPath(Path.of(value)) + }.getOrNull() + file == null || !file.isDirectory + } + ) + } + + onApply { + dataSetter(pathProperty.get()) + } + } + } +} diff --git a/src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt b/src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt new file mode 100644 index 000000000..2cbc70f16 --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/RemoteTemplateProvider.kt @@ -0,0 +1,228 @@ +/* + * 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.providers + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.BuiltinValidations +import com.demonwav.mcdev.creator.custom.TemplateDescriptor +import com.demonwav.mcdev.creator.modalityState +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.result.getOrNull +import com.github.kittinunf.result.onError +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.diagnostic.ControlFlowException +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.trim +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.vfs.JarFileSystem +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.textValidation +import com.intellij.util.io.createDirectories +import java.nio.file.Path +import javax.swing.JComponent +import kotlin.io.path.absolutePathString +import kotlin.io.path.exists +import kotlin.io.path.writeBytes + +open class RemoteTemplateProvider : TemplateProvider { + + private var updatedTemplates = mutableSetOf() + + override val label: String = MCDevBundle("template.provider.remote.label") + + override val hasConfig: Boolean = true + + override fun init(indicator: ProgressIndicator, repos: List) { + for (repo in repos) { + ProgressManager.checkCanceled() + val remote = RemoteTemplateRepo.deserialize(repo.data) + ?: continue + if (!remote.autoUpdate || remote.url in updatedTemplates) { + continue + } + + if (doUpdateRepo(indicator, repo.name, remote.url)) { + updatedTemplates.add(remote.url) + } + } + } + + protected fun doUpdateRepo( + indicator: ProgressIndicator, + repoName: String, + originalRepoUrl: String + ): Boolean { + indicator.text2 = "Updating remote repository $repoName" + + val repoUrl = replaceVariables(originalRepoUrl) + + val manager = FuelManager() + manager.proxy = selectProxy(repoUrl) + val (_, _, result) = manager.get(repoUrl) + .header("User-Agent", "github_org/minecraft-dev/${PluginUtil.pluginVersion}") + .header("Accepts", "application/json") + .timeout(10000) + .response() + + val data = result.onError { + thisLogger().warn("Could not fetch remote templates repository update at $repoUrl", it) + }.getOrNull() ?: return false + + try { + val zipPath = RemoteTemplateRepo.getDestinationZip(repoName) + zipPath.parent.createDirectories() + zipPath.writeBytes(data) + + thisLogger().info("Remote templates repository update applied successfully") + return true + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + thisLogger().error("Failed to apply remote templates repository update of $repoName", t) + } + return false + } + + override fun loadTemplates( + context: WizardContext, + repo: MinecraftSettings.TemplateRepo + ): Collection { + val remoteRepo = RemoteTemplateRepo.deserialize(repo.data) + ?: return emptyList() + return doLoadTemplates(context, repo, remoteRepo.innerPath) + } + + protected fun doLoadTemplates( + context: WizardContext, + repo: MinecraftSettings.TemplateRepo, + rawInnerPath: String + ): List { + val remoteRootPath = RemoteTemplateRepo.getDestinationZip(repo.name) + if (!remoteRootPath.exists()) { + return emptyList() + } + + val archiveRoot = remoteRootPath.absolutePathString() + JarFileSystem.JAR_SEPARATOR + + val fs = JarFileSystem.getInstance() + val rootFile = fs.refreshAndFindFileByPath(archiveRoot) + ?: return emptyList() + val modalityState = context.modalityState + rootFile.refreshSync(modalityState) + + val innerPath = replaceVariables(rawInnerPath) + val repoRoot = if (innerPath.isNotBlank()) { + rootFile.findFileByRelativePath(innerPath) + } else { + rootFile + } + + if (repoRoot == null) { + return emptyList() + } + + return TemplateProvider.findTemplates(modalityState, repoRoot) + } + + private fun replaceVariables(originalRepoUrl: String): String = + originalRepoUrl.replace("\$version", TemplateDescriptor.FORMAT_VERSION.toString()) + + override fun setupConfigUi( + data: String, + dataSetter: (String) -> Unit + ): JComponent? { + val propertyGraph = PropertyGraph("RemoteTemplateProvider config") + val defaultRepo = RemoteTemplateRepo.deserialize(data) + val urlProperty = propertyGraph.property(defaultRepo?.url ?: "").trim() + val autoUpdateProperty = propertyGraph.property(defaultRepo?.autoUpdate != false) + val innerPathProperty = propertyGraph.property(defaultRepo?.innerPath ?: "").trim() + + return panel { + row(MCDevBundle("creator.ui.custom.remote.url.label")) { + textField() + .comment(MCDevBundle("creator.ui.custom.remote.url.comment")) + .align(AlignX.FILL) + .columns(COLUMNS_LARGE) + .bindText(urlProperty) + .textValidation(BuiltinValidations.nonBlank) + } + + row(MCDevBundle("creator.ui.custom.remote.inner_path.label")) { + textField() + .comment(MCDevBundle("creator.ui.custom.remote.inner_path.comment")) + .align(AlignX.FILL) + .columns(COLUMNS_LARGE) + .bindText(innerPathProperty) + } + + row { + checkBox(MCDevBundle("creator.ui.custom.remote.auto_update.label")) + .bindSelected(autoUpdateProperty) + } + + onApply { + val repo = RemoteTemplateRepo(urlProperty.get(), autoUpdateProperty.get(), innerPathProperty.get()) + dataSetter(repo.serialize()) + } + } + } + + data class RemoteTemplateRepo(val url: String, val autoUpdate: Boolean, val innerPath: String) { + + fun serialize(): String = "$url\n$autoUpdate\n$innerPath" + + companion object { + + val templatesBaseDir: Path + get() = PathManager.getSystemDir().resolve("mcdev-templates") + + fun getDestinationZip(repoName: String): Path { + return templatesBaseDir.resolve("$repoName.zip") + } + + fun deserialize(data: String): RemoteTemplateRepo? { + if (data.isBlank()) { + return null + } + + val lines = data.lines() + return RemoteTemplateRepo( + lines[0], + lines.getOrNull(1).toBoolean(), + lines.getOrNull(2) ?: "", + ) + } + } + } +} diff --git a/src/main/kotlin/creator/custom/providers/TemplateProvider.kt b/src/main/kotlin/creator/custom/providers/TemplateProvider.kt new file mode 100644 index 000000000..30efddbb2 --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/TemplateProvider.kt @@ -0,0 +1,226 @@ +/* + * 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.providers + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.creator.custom.TemplateDescriptor +import com.demonwav.mcdev.creator.custom.TemplateResourceBundle +import com.demonwav.mcdev.util.fromJson +import com.demonwav.mcdev.util.refreshSync +import com.google.gson.Gson +import com.intellij.DynamicBundle +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.diagnostic.Attachment +import com.intellij.openapi.diagnostic.ControlFlowException +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.extensions.RequiredElement +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.util.KeyedExtensionCollector +import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileVisitor +import com.intellij.openapi.vfs.isFile +import com.intellij.openapi.vfs.readText +import com.intellij.serviceContainer.BaseKeyedLazyInstance +import com.intellij.util.KeyedLazyInstance +import com.intellij.util.xmlb.annotations.Attribute +import java.util.ResourceBundle +import javax.swing.JComponent + +/** + * Extensions responsible for creating a [TemplateDescriptor] based on whatever data it is provided in its configuration + * [UI][setupConfigUi]. + */ +interface TemplateProvider { + + val label: String + + val hasConfig: Boolean + + fun init(indicator: ProgressIndicator, repos: List) = Unit + + fun loadTemplates(context: WizardContext, repo: MinecraftSettings.TemplateRepo): Collection + + fun setupConfigUi(data: String, dataSetter: (String) -> Unit): JComponent? + + companion object { + + private val EP_NAME = + ExtensionPointName("com.demonwav.minecraft-dev.creatorTemplateProvider") + private val COLLECTOR = KeyedExtensionCollector(EP_NAME) + + fun get(key: String): TemplateProvider? = COLLECTOR.findSingle(key) + + fun getAllKeys() = EP_NAME.extensionList.mapNotNull { it.key } + + fun findTemplates( + modalityState: ModalityState, + repoRoot: VirtualFile, + templates: MutableList = mutableListOf(), + bundle: ResourceBundle? = loadMessagesBundle(modalityState, repoRoot) + ): List { + val visitor = object : VirtualFileVisitor() { + override fun visitFile(file: VirtualFile): Boolean { + if (!file.isFile || !file.name.endsWith(".mcdev.template.json")) { + return true + } + + try { + createVfsLoadedTemplate(modalityState, file.parent, file, bundle = bundle) + ?.let(templates::add) + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + + val attachment = runCatching { Attachment(file.name, file.readText()) }.getOrNull() + if (attachment != null) { + thisLogger().error("Failed to load template ${file.path}", t, attachment) + } else { + thisLogger().error("Failed to load template ${file.path}", t) + } + } + + return true + } + } + VfsUtilCore.visitChildrenRecursively(repoRoot, visitor) + return templates + } + + fun loadMessagesBundle(modalityState: ModalityState, repoRoot: VirtualFile): ResourceBundle? = try { + val locale = DynamicBundle.getLocale() + // Simplified bundle resolution, but covers all the most common cases + val baseBundle = doLoadMessageBundle( + repoRoot.findChild("messages.properties"), + modalityState, + null + ) + val languageBundle = doLoadMessageBundle( + repoRoot.findChild("messages_${locale.language}.properties"), + modalityState, + baseBundle + ) + doLoadMessageBundle( + repoRoot.findChild("messages_${locale.language}_${locale.country}.properties"), + modalityState, + languageBundle + ) + } catch (t: Throwable) { + if (t is ControlFlowException) { + throw t + } + + thisLogger().error("Failed to load resource bundle of template repository ${repoRoot.path}", t) + null + } + + private fun doLoadMessageBundle( + file: VirtualFile?, + modalityState: ModalityState, + parent: ResourceBundle? + ): ResourceBundle? { + if (file == null) { + return parent + } + + try { + return file.refreshSync(modalityState) + ?.inputStream?.reader()?.use { TemplateResourceBundle(it, parent) } + } catch (t: Throwable) { + if (t is ControlFlowException) { + return parent + } + + thisLogger().error("Failed to load resource bundle ${file.path}", t) + } + + return parent + } + + fun createVfsLoadedTemplate( + modalityState: ModalityState, + templateRoot: VirtualFile, + descriptorFile: VirtualFile, + tooltip: String? = null, + bundle: ResourceBundle? = null + ): VfsLoadedTemplate? { + descriptorFile.refreshSync(modalityState) + var descriptor = Gson().fromJson(descriptorFile.readText()) + if (descriptor.version != TemplateDescriptor.FORMAT_VERSION) { + thisLogger().warn("Cannot handle template ${descriptorFile.path} of version ${descriptor.version}") + return null + } + + if (descriptor.hidden == true) { + return null + } + + descriptor.bundle = bundle + + val labelKey = descriptor.label + ?: descriptorFile.name.removeSuffix(".mcdev.template.json").takeIf(String::isNotBlank) + ?: templateRoot.presentableName + val label = + descriptor.translateOrNull("platform.${labelKey.lowercase()}.label") ?: descriptor.translate(labelKey) + + if (descriptor.inherit != null) { + val parent = templateRoot.findFileByRelativePath(descriptor.inherit!!) + if (parent != null) { + parent.refresh(false, false) + val parentDescriptor = Gson().fromJson(parent.readText()) + val mergedProperties = parentDescriptor.properties.orEmpty() + descriptor.properties.orEmpty() + val mergedFiles = parentDescriptor.files.orEmpty() + descriptor.files.orEmpty() + descriptor = descriptor.copy(properties = mergedProperties, files = mergedFiles) + } else { + thisLogger().error( + "Could not find inherited template descriptor ${descriptor.inherit} from ${descriptorFile.path}" + ) + } + } + + if (bundle != null) { + descriptor.properties?.forEach { property -> + property.bundle = bundle + } + } + + return VfsLoadedTemplate(templateRoot, label, tooltip, descriptor, true) + } + } +} + +class TemplateProviderBean : BaseKeyedLazyInstance(), KeyedLazyInstance { + + @Attribute("key") + @RequiredElement + lateinit var name: String + + @Attribute("implementation") + @RequiredElement + lateinit var implementation: String + + override fun getKey(): String? = name + + override fun getImplementationClassName(): String? = implementation +} diff --git a/src/main/kotlin/creator/custom/providers/VfsLoadedTemplate.kt b/src/main/kotlin/creator/custom/providers/VfsLoadedTemplate.kt new file mode 100644 index 000000000..aa34b456c --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/VfsLoadedTemplate.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.creator.custom.providers + +import com.demonwav.mcdev.creator.custom.TemplateDescriptor +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.readText +import java.io.FileNotFoundException + +class VfsLoadedTemplate( + val templateRoot: VirtualFile, + override val label: String, + override val tooltip: String? = null, + override val descriptor: TemplateDescriptor, + override val isValid: Boolean, +) : LoadedTemplate { + + override fun loadTemplateContents(path: String): String? { + templateRoot.refresh(false, true) + val virtualFile = templateRoot.findFileByRelativePath(path) + ?: throw FileNotFoundException("Could not find file $path in template root ${templateRoot.path}") + virtualFile.refresh(false, false) + return virtualFile.readText() + } +} diff --git a/src/main/kotlin/creator/custom/providers/ZipTemplateProvider.kt b/src/main/kotlin/creator/custom/providers/ZipTemplateProvider.kt new file mode 100644 index 000000000..6cae4d3ce --- /dev/null +++ b/src/main/kotlin/creator/custom/providers/ZipTemplateProvider.kt @@ -0,0 +1,92 @@ +/* + * 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.providers + +import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.modalityState +import com.demonwav.mcdev.util.refreshSync +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.ui.validation.validationErrorIf +import com.intellij.openapi.vfs.JarFileSystem +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.textValidation +import java.nio.file.Path +import javax.swing.JComponent +import kotlin.io.path.isRegularFile + +class ZipTemplateProvider : TemplateProvider { + + override val label: String = MCDevBundle("template.provider.zip.label") + + override val hasConfig: Boolean = true + + override fun loadTemplates( + context: WizardContext, + repo: MinecraftSettings.TemplateRepo + ): Collection { + val archiveRoot = repo.data + JarFileSystem.JAR_SEPARATOR + val fs = JarFileSystem.getInstance() + val rootFile = fs.refreshAndFindFileByPath(archiveRoot) + ?: return emptyList() + val modalityState = context.modalityState + rootFile.refreshSync(modalityState) + return TemplateProvider.findTemplates(modalityState, rootFile) + } + + override fun setupConfigUi( + data: String, + dataSetter: (String) -> Unit + ): JComponent { + val propertyGraph = PropertyGraph("ZipTemplateProvider config") + val pathProperty = propertyGraph.property(data) + + return panel { + row(MCDevBundle("creator.ui.custom.path.label")) { + val pathChooserDescriptor = FileChooserDescriptorFactory.createSingleLocalFileDescriptor() + .withFileFilter { it.extension == "zip" } + .apply { description = MCDevBundle("creator.ui.custom.archive.dialog.description") } + textFieldWithBrowseButton( + MCDevBundle("creator.ui.custom.archive.dialog.title"), + null, + pathChooserDescriptor + ).align(AlignX.FILL) + .columns(COLUMNS_LARGE) + .bindText(pathProperty) + .textValidation( + validationErrorIf(MCDevBundle("creator.validation.custom.path_not_a_file")) { value -> + runCatching { !Path.of(value).isRegularFile() }.getOrDefault(true) + } + ) + } + + onApply { + dataSetter(pathProperty.get()) + } + } + } +} diff --git a/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt new file mode 100644 index 000000000..1c742e80e --- /dev/null +++ b/src/main/kotlin/creator/custom/types/ArchitecturyVersionsCreatorProperty.kt @@ -0,0 +1,481 @@ +/* + * 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.types + +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.TemplateEvaluator +import com.demonwav.mcdev.creator.custom.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.ArchitecturyVersionsModel +import com.demonwav.mcdev.platform.architectury.ArchitecturyVersion +import com.demonwav.mcdev.platform.fabric.util.FabricApiVersions +import com.demonwav.mcdev.platform.fabric.util.FabricVersions +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 +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.withContext + +class ArchitecturyVersionsCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, ArchitecturyVersionsModel::class.java) { + + private val emptyVersion = SemanticVersion.release() + private val emptyValue = ArchitecturyVersionsModel( + emptyVersion, + emptyVersion, + emptyVersion, + emptyVersion, + emptyVersion, + FabricVersions.YarnVersion("", -1), + true, + emptyVersion, + true, + true, + emptyVersion, + ) + private val defaultValue = createDefaultValue(descriptor.default) + + private val loadingVersionsProperty = graph.property(true) + override val graphProperty: GraphProperty = graph.property(defaultValue) + var model: ArchitecturyVersionsModel by graphProperty + + val mcVersionProperty = graphProperty.transform({ it.minecraft }, { model.copy(minecraft = it) }) + val mcVersionModel = DefaultComboBoxModel() + + val forgeVersionProperty = graphProperty.transform({ it.forge }, { model.copy(forge = it) }) + val forgeVersionsModel = DefaultComboBoxModel() + val isForgeAvailableProperty = forgeVersionProperty.transform { !it?.parts.isNullOrEmpty() } + + val nfVersionProperty = graphProperty.transform({ it.neoforge }, { model.copy(neoforge = it) }) + val nfVersionsModel = DefaultComboBoxModel() + val isNfAvailableProperty = nfVersionProperty.transform { !it?.parts.isNullOrEmpty() } + + val loomVersionProperty = graphProperty.transform({ it.loom }, { model.copy(loom = it) }) + val loomVersionModel = DefaultComboBoxModel() + + val loaderVersionProperty = graphProperty.transform({ it.loader }, { model.copy(loader = it) }) + val loaderVersionModel = DefaultComboBoxModel() + + val yarnVersionProperty = graphProperty.transform({ it.yarn }, { model.copy(yarn = it) }) + val yarnVersionModel = DefaultComboBoxModel() + val yarnHasMatchingGameVersion = mcVersionProperty.transform { mcVersion -> + val versions = fabricVersions + ?: return@transform true + val mcVersionString = mcVersion.toString() + versions.mappings.any { it.gameVersion == mcVersionString } + } + + val fabricApiVersionProperty = graphProperty.transform({ it.fabricApi }, { model.copy(fabricApi = it) }) + val fabricApiVersionModel = DefaultComboBoxModel() + val useFabricApiVersionProperty = graphProperty.transform({ it.useFabricApi }, { model.copy(useFabricApi = it) }) + val fabricApiHasMatchingGameVersion = mcVersionProperty.transform { mcVersion -> + val apiVersions = fabricApiVersions + ?: return@transform true + val mcVersionString = mcVersion.toString() + apiVersions.versions.any { mcVersionString in it.gameVersions } + } + + val useOfficialMappingsProperty = + graphProperty.transform({ it.useOfficialMappings }, { model.copy(useOfficialMappings = it) }) + + val architecturyApiVersionProperty = + graphProperty.transform({ it.architecturyApi }, { model.copy(architecturyApi = it) }) + val architecturyApiVersionModel = DefaultComboBoxModel() + val useArchitecturyApiVersionProperty = + graphProperty.transform({ it.useArchitecturyApi }, { model.copy(useArchitecturyApi = it) }) + val architecturyApiHasMatchingGameVersion = mcVersionProperty.transform { mcVersion -> + val apiVersions = architecturyVersions + ?: return@transform true + apiVersions.versions.containsKey(mcVersion) + } + + override fun createDefaultValue(raw: Any?): ArchitecturyVersionsModel = when (raw) { + is String -> deserialize(raw) + else -> emptyValue + } + + override fun serialize(value: ArchitecturyVersionsModel): String { + return "${value.minecraft} ${value.forge} ${value.neoforge} ${value.loom} ${value.loader} ${value.yarn}" + + " ${value.useFabricApi} ${value.fabricApi} ${value.useOfficialMappings} ${value.useArchitecturyApi}" + + " ${value.architecturyApi}" + } + + override fun deserialize(string: String): ArchitecturyVersionsModel { + val segments = string.split(' ') + val yarnSegments = segments.getOrNull(5)?.split(':') + val yarnVersion = if (yarnSegments != null && yarnSegments.size == 2) { + FabricVersions.YarnVersion(yarnSegments[0], yarnSegments[1].toInt()) + } else { + emptyValue.yarn + } + return ArchitecturyVersionsModel( + segments.getOrNull(0)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(1)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(2)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(3)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(4)?.let(SemanticVersion::tryParse) ?: emptyVersion, + yarnVersion, + segments.getOrNull(6)?.toBoolean() != false, + segments.getOrNull(7)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(8)?.toBoolean() != false, + segments.getOrNull(9)?.toBoolean() != false, + segments.getOrNull(10)?.let(SemanticVersion::tryParse) ?: emptyVersion, + ) + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row("") { + cell(AsyncProcessIcon("ArchitecturyVersions download")) + label(MCDevBundle("creator.ui.versions_download.label")) + }.visibleIf(loadingVersionsProperty) + + panel.row("Minecraft Version:") { + comboBox(mcVersionModel) + .bindItem(mcVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row("Forge Version:") { + comboBox(forgeVersionsModel) + .bindItem(forgeVersionProperty) + .enabledIf(isForgeAvailableProperty) + .also { ComboboxSpeedSearch.installOn(it.component) } + .component + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row("NeoForge Version:") { + comboBox(nfVersionsModel) + .bindItem(nfVersionProperty) + .enabledIf(isNfAvailableProperty) + .also { ComboboxSpeedSearch.installOn(it.component) } + .component + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + // + // panel.row("Loom Version:") { + // comboBox(loomVersionModel) + // .bindItem(loomVersionProperty) + // .validationOnInput(BuiltinValidations.nonEmptyVersion) + // .validationOnApply(BuiltinValidations.nonEmptyVersion) + // .also { ComboboxSpeedSearch.installOn(it.component) } + // }.enabled(descriptor.editable != false) + + panel.row("Loader Version:") { + comboBox(loaderVersionModel) + .bindItem(loaderVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + // Official mappings forced currently, yarn mappings are not handled yet + // panel.row("Yarn Version:") { + // comboBox(yarnVersionModel) + // .bindItem(yarnVersionProperty) + // .enabledIf(useOfficialMappingsProperty.not()) + // .validationOnInput(BuiltinValidations.nonEmptyYarnVersion) + // .validationOnApply(BuiltinValidations.nonEmptyYarnVersion) + // .also { ComboboxSpeedSearch.installOn(it.component) } + // + // checkBox("Use official mappings") + // .bindSelected(useOfficialMappingsProperty) + // + // label("Unable to match Yarn versions to Minecraft version") + // .visibleIf(yarnHasMatchingGameVersion.not()) + // .component.foreground = JBColor.YELLOW + // }.enabled(descriptor.editable != false) + + panel.row("Fabric API Version:") { + comboBox(fabricApiVersionModel) + .bindItem(fabricApiVersionProperty) + .enabledIf(useFabricApiVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + checkBox("Use Fabric API") + .bindSelected(useFabricApiVersionProperty) + label("Unable to match API versions to Minecraft version") + .visibleIf(fabricApiHasMatchingGameVersion.not()) + .component.foreground = JBColor.YELLOW + }.visibleIf(!loadingVersionsProperty) + + panel.row("Architectury API Version:") { + comboBox(architecturyApiVersionModel) + .bindItem(architecturyApiVersionProperty) + .enabledIf(useArchitecturyApiVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + checkBox("Use Architectury API") + .bindSelected(useArchitecturyApiVersionProperty) + label("Unable to match API versions to Minecraft version") + .visibleIf(architecturyApiHasMatchingGameVersion.not()) + .component.foreground = JBColor.YELLOW + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + var previousMcVersion: SemanticVersion? = null + mcVersionProperty.afterChange { mcVersion -> + if (previousMcVersion == mcVersion) { + return@afterChange + } + + previousMcVersion = mcVersion + + updateForgeVersions() + updateNeoForgeVersions() + updateYarnVersions() + updateFabricApiVersions() + updateArchitecturyApiVersions() + } + + downloadVersions { + val fabricVersions = fabricVersions + if (fabricVersions != null) { + loaderVersionModel.removeAllElements() + loaderVersionModel.addAll(fabricVersions.loader) + loaderVersionProperty.set(fabricVersions.loader.firstOrNull() ?: emptyVersion) + } + + val loomVersions = loomVersions + if (loomVersions != null) { + loomVersionModel.removeAllElements() + loomVersionModel.addAll(loomVersions) + val defaultValue = loomVersions.find { + it.parts.any { it is SemanticVersion.Companion.VersionPart.PreReleasePart } + } ?: loomVersions.firstOrNull() ?: emptyVersion + + loomVersionProperty.set(defaultValue) + } + + updateMcVersionsList() + + loadingVersionsProperty.set(false) + } + } + + private fun updateMcVersionsList() { + val architecturyVersions = architecturyVersions + ?: return + + val mcVersions = architecturyVersions.versions.keys.sortedDescending() + mcVersionModel.removeAllElements() + mcVersionModel.addAll(mcVersions) + + val selectedMcVersion = when { + mcVersionProperty.get() in mcVersions -> mcVersionProperty.get() + defaultValue.minecraft in mcVersions -> defaultValue.minecraft + else -> mcVersions.first() + } + mcVersionProperty.set(selectedMcVersion) + } + + private fun updateForgeVersions() { + val mcVersion = mcVersionProperty.get() + + val filterExpr = descriptor.parameters?.get("forgeMcVersionFilter") as? String + if (filterExpr != null) { + val conditionProps = mapOf("MC_VERSION" to mcVersion) + if (!TemplateEvaluator.condition(conditionProps, filterExpr).getOrDefault(true)) { + forgeVersionsModel.removeAllElements() + application.invokeLater { + // For some reason we have to set those properties later for the values to actually be set + // and the enabled state to be set appropriately + forgeVersionProperty.set(null) + } + return + } + } + + val availableForgeVersions = forgeVersions!!.getForgeVersions(mcVersion) + .take(descriptor.limit ?: 50) + forgeVersionsModel.removeAllElements() + forgeVersionsModel.addAll(availableForgeVersions) + application.invokeLater { + forgeVersionProperty.set(availableForgeVersions.firstOrNull()) + } + } + + private fun updateNeoForgeVersions() { + val mcVersion = mcVersionProperty.get() + + val filterExpr = descriptor.parameters?.get("neoForgeMcVersionFilter") as? String + if (filterExpr != null) { + val conditionProps = mapOf("MC_VERSION" to mcVersion) + if (!TemplateEvaluator.condition(conditionProps, filterExpr).getOrDefault(true)) { + nfVersionsModel.removeAllElements() + application.invokeLater { + nfVersionProperty.set(null) + } + return + } + } + + val availableNeoForgeVersions = neoForgeVersions!!.getNeoForgeVersions(mcVersion) + .take(descriptor.limit ?: 50) + nfVersionsModel.removeAllElements() + nfVersionsModel.addAll(availableNeoForgeVersions) + application.invokeLater { + nfVersionProperty.set(availableNeoForgeVersions.firstOrNull()) + } + } + + private fun updateYarnVersions() { + val fabricVersions = fabricVersions + ?: return + + val mcVersion = mcVersionProperty.get() + val mcVersionString = mcVersion.toString() + + val yarnVersions = if (yarnHasMatchingGameVersion.get()) { + fabricVersions.mappings.asSequence() + .filter { it.gameVersion == mcVersionString } + .map { it.version } + .toList() + } else { + fabricVersions.mappings.map { it.version } + } + yarnVersionModel.removeAllElements() + yarnVersionModel.addAll(yarnVersions) + yarnVersionProperty.set(yarnVersions.firstOrNull() ?: emptyValue.yarn) + } + + private fun updateFabricApiVersions() { + val fabricApiVersions = fabricApiVersions + ?: return + + val mcVersion = mcVersionProperty.get() + val mcVersionString = mcVersion.toString() + + val apiVersions = if (fabricApiHasMatchingGameVersion.get()) { + fabricApiVersions.versions.asSequence() + .filter { mcVersionString in it.gameVersions } + .map(FabricApiVersions.Version::version) + .toList() + } else { + fabricApiVersions.versions.map(FabricApiVersions.Version::version) + } + fabricApiVersionModel.removeAllElements() + fabricApiVersionModel.addAll(apiVersions) + fabricApiVersionProperty.set(apiVersions.firstOrNull() ?: emptyVersion) + } + + private fun updateArchitecturyApiVersions() { + val architecturyVersions = architecturyVersions + ?: return + + val mcVersion = mcVersionProperty.get() + val availableArchitecturyApiVersions = architecturyVersions.getArchitecturyVersions(mcVersion) + architecturyApiVersionModel.removeAllElements() + architecturyApiVersionModel.addAll(availableArchitecturyApiVersions) + + architecturyApiVersionProperty.set(availableArchitecturyApiVersions.firstOrNull() ?: emptyVersion) + } + + companion object { + private var hasDownloadedVersions = false + + private var forgeVersions: ForgeVersion? = null + private var neoForgeVersions: NeoForgeVersion? = null + private var fabricVersions: FabricVersions? = null + private var loomVersions: List? = null + private var fabricApiVersions: FabricApiVersions? = null + private var architecturyVersions: ArchitecturyVersion? = null + + private fun downloadVersions(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() + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = ArchitecturyVersionsCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/BooleanCreatorProperty.kt b/src/main/kotlin/creator/custom/types/BooleanCreatorProperty.kt new file mode 100644 index 000000000..57072d519 --- /dev/null +++ b/src/main/kotlin/creator/custom/types/BooleanCreatorProperty.kt @@ -0,0 +1,67 @@ +/* + * 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.types + +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 +import com.intellij.ui.dsl.builder.bindSelected + +class BooleanCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, Boolean::class.java) { + + override fun createDefaultValue(raw: Any?): Boolean = raw as? Boolean ?: false + + override fun serialize(value: Boolean): String = value.toString() + + override fun deserialize(string: String): Boolean = string.toBoolean() + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + val label = descriptor.translatedLabel + panel.row(label) { + val warning = descriptor.translatedWarning + if (warning != null) { + icon(AlertIcon(AllIcons.General.Warning)) + .gap(RightGap.SMALL) + .comment(descriptor.translate(warning)) + } + + this.checkBox(label.removeSuffix(":").trim()) + .bindSelected(graphProperty) + .enabled(descriptor.editable != false) + }.propertyVisibility() + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = BooleanCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt b/src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt new file mode 100644 index 000000000..2d70ef5cc --- /dev/null +++ b/src/main/kotlin/creator/custom/types/BuildSystemCoordinatesCreatorProperty.kt @@ -0,0 +1,135 @@ +/* + * 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.types + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.BuiltinValidations +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 +import com.intellij.openapi.ui.validation.CHECK_NON_EMPTY +import com.intellij.openapi.ui.validation.WHEN_GRAPH_PROPAGATION_FINISHED +import com.intellij.openapi.ui.validation.validationErrorIf +import com.intellij.ui.dsl.builder.COLUMNS_MEDIUM +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.textValidation + +private val nonExampleValidation = validationErrorIf(MCDevBundle("creator.validation.group_id_non_example")) { + it == "org.example" +} + +class BuildSystemCoordinatesCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, BuildSystemCoordinates::class.java) { + + private val default = createDefaultValue(descriptor.default) + + override val graphProperty: GraphProperty = graph.property(default) + var coords: BuildSystemCoordinates by graphProperty + + private val groupIdProperty = graphProperty.transform({ it.groupId }, { coords.copy(groupId = it) }) + private val artifactIdProperty = graphProperty.transform({ it.artifactId }, { coords.copy(artifactId = it) }) + private val versionProperty = graphProperty.transform({ it.version }, { coords.copy(version = it) }) + + override fun createDefaultValue(raw: Any?): BuildSystemCoordinates { + val str = (raw as? String) ?: return createDefaultValue() + return deserialize(str) + } + + private fun createDefaultValue() = BuildSystemCoordinates("org.example", "", "1.0-SNAPSHOT") + + override fun serialize(value: BuildSystemCoordinates): String = + "${value.groupId}:${value.artifactId}:${value.version}" + + override fun deserialize(string: String): BuildSystemCoordinates { + val segments = string.split(':') + + val groupId = segments.getOrElse(0) { "" } + val artifactId = segments.getOrElse(1) { "" } + val version = segments.getOrElse(2) { "" } + return BuildSystemCoordinates(groupId, artifactId, version) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + val projectNameProperty = properties["PROJECT_NAME"]?.graphProperty + if (projectNameProperty != null) { + val projectName = projectNameProperty.get() + if (projectName is String) { + coords = coords.copy(artifactId = projectName) + } + + graphProperty.dependsOn(projectNameProperty, false) { + val newProjectName = projectNameProperty.get() + if (newProjectName is String) { + coords.copy(artifactId = newProjectName) + } else { + coords + } + } + } + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.collapsibleGroup(MCDevBundle("creator.ui.group.title")) { + this.row(MCDevBundle("creator.ui.group.group_id")) { + this.textField() + .bindText(this@BuildSystemCoordinatesCreatorProperty.groupIdProperty) + .columns(COLUMNS_MEDIUM) + .validationRequestor(WHEN_GRAPH_PROPAGATION_FINISHED(graph)) + .textValidation(CHECK_NON_EMPTY, CHECK_GROUP_ID, nonExampleValidation) + } + this.row(MCDevBundle("creator.ui.group.artifact_id")) { + this.textField() + .bindText(this@BuildSystemCoordinatesCreatorProperty.artifactIdProperty) + .columns(COLUMNS_MEDIUM) + .validationRequestor(WHEN_GRAPH_PROPAGATION_FINISHED(graph)) + .textValidation(CHECK_NON_EMPTY, CHECK_ARTIFACT_ID) + } + this.row(MCDevBundle("creator.ui.group.version")) { + this.textField() + .bindText(this@BuildSystemCoordinatesCreatorProperty.versionProperty) + .columns(COLUMNS_MEDIUM) + .validationRequestor(WHEN_GRAPH_PROPAGATION_FINISHED(graph)) + .textValidation(BuiltinValidations.validVersion) + } + }.expanded = true + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = BuildSystemCoordinatesCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.kt new file mode 100644 index 000000000..5ee2470ad --- /dev/null +++ b/src/main/kotlin/creator/custom/types/ClassFqnCreatorProperty.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.creator.custom.types + +import com.demonwav.mcdev.creator.custom.BuiltinValidations +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 +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.textValidation + +class ClassFqnCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, ClassFqn::class.java) { + + override fun createDefaultValue(raw: Any?): ClassFqn = ClassFqn(raw as? String ?: "") + + override fun serialize(value: ClassFqn): String = value.toString() + + override fun deserialize(string: String): ClassFqn = ClassFqn(string) + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + this.textField().bindText(this@ClassFqnCreatorProperty.toStringProperty(graphProperty)) + .columns(COLUMNS_LARGE) + .textValidation(BuiltinValidations.validClassFqn) + .enabled(descriptor.editable != false) + }.propertyVisibility() + } + + override fun setupDerivation( + reporter: TemplateValidationReporter, + derives: PropertyDerivation + ): PreparedDerivation? = when (derives.method) { + "suggestClassName" -> { + val parents = collectDerivationParents(reporter) + SuggestClassNamePropertyDerivation.create(reporter, parents, derives) + } + + else -> null + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = ClassFqnCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/CreatorProperty.kt b/src/main/kotlin/creator/custom/types/CreatorProperty.kt new file mode 100644 index 000000000..3e9e1845c --- /dev/null +++ b/src/main/kotlin/creator/custom/types/CreatorProperty.kt @@ -0,0 +1,288 @@ +/* + * 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.types + +import com.demonwav.mcdev.creator.custom.PropertyDerivation +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.derivation.PreparedDerivation +import com.demonwav.mcdev.creator.custom.derivation.SelectPropertyDerivation +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.observable.properties.GraphProperty +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.bindStorage +import com.intellij.openapi.observable.util.transform +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.Row + +abstract class CreatorProperty( + val descriptor: TemplatePropertyDescriptor, + val graph: PropertyGraph, + protected val properties: Map>, + val valueType: Class +) { + private var derivation: PreparedDerivation? = null + private lateinit var visibleProperty: GraphProperty + + abstract val graphProperty: GraphProperty + + abstract fun createDefaultValue(raw: Any?): T + + abstract fun serialize(value: T): String + + abstract fun deserialize(string: String): T + + open fun toStringProperty(graphProperty: GraphProperty): ObservableMutableProperty = + graphProperty.transform(::serialize, ::deserialize) + + open fun get(): T? { + val value = graphProperty.get() + if (descriptor.nullIfDefault == true) { + val default = createDefaultValue(descriptor.default) + if (value == default) { + return null + } + } + + return value + } + + fun acceptsType(type: Class<*>): Boolean = type.isAssignableFrom(valueType) + + /** + * Produces a new value based on the provided [parentValues] and the template-defined [derivation] configuration. + * + * You must **NOT** [set][GraphProperty.set] the value of [graphProperty] in the process. You may however [get][GraphProperty.get] it at will. + * + * @param parentValues the values of the properties this [graphProperty] depends on + * @param derivation the configuration of the desired derivation + * + * @see GraphProperty.dependsOn + */ + open fun derive(parentValues: List?, derivation: PropertyDerivation): Any? { + if (this.derivation == null) { + throw IllegalStateException("This property has not been configured with a derivation") + } + + val result = this.derivation!!.derive(parentValues.orEmpty()) + if (this.derivation is SelectPropertyDerivation) { + return convertSelectDerivationResult(result) + } + + return result + } + + protected open fun convertSelectDerivationResult(original: Any?): Any? = original + + abstract fun buildUi(panel: Panel, context: WizardContext) + + /** + * Prepares everything this property needs, like calling [GraphProperty]'s [GraphProperty.afterChange] and + * [GraphProperty.dependsOn] on this property or other properties declared before this one. + * + * [properties] contains all the properties declared in the descriptor + * up to this one, forward references are not permitted. + * + * This is also where you should validate the [descriptor] values you want to use, and report all validation errors + * or warnings through the [reporter], use [TemplateValidationReporter.fatal] if the error is a show-stopper and + * the validation cannot even proceed further. + */ + open fun setupProperty(reporter: TemplateValidationReporter) { + if (descriptor.remember != false && descriptor.derives == null) { + val storageKey = when (val remember = descriptor.remember) { + null, true -> makeStorageKey() + is String -> makeCustomStorageKey(remember) + else -> { + reporter.error("Invalid 'remember' value. Must be a boolean or a string") + null + } + } + + if (storageKey != null) { + toStringProperty(graphProperty).bindStorage(storageKey) + } + } + + visibleProperty = setupVisibleProperty(reporter, descriptor.visible) + + if (descriptor.derives != null) { + val parents = descriptor.derives.parents + ?: return reporter.error("No parents specified in derivation") + for (parent in parents) { + if (!properties.containsKey(parent)) { + return reporter.error("Unknown parent property '$parent' in derivation") + } + } + + derivation = setupDerivation(reporter, descriptor.derives) + if (derivation == null) { + reporter.fatal("Unknown method derivation: ${descriptor.derives}") + } + + @Suppress("UNCHECKED_CAST") + graphProperty.set(derive(collectDerivationParentValues(reporter), descriptor.derives) as T) + for (parent in parents) { + val parentProperty = properties[parent]!! + graphProperty.dependsOn(parentProperty.graphProperty, descriptor.derives.whenModified != false) { + @Suppress("UNCHECKED_CAST") + derive(collectDerivationParentValues(), descriptor.derives) as T + } + } + } + + if (descriptor.inheritFrom != null) { + val parentProperty = properties[descriptor.inheritFrom] + ?: return reporter.error("Unknown parent property '${descriptor.inheritFrom}' in derivation") + + @Suppress("UNCHECKED_CAST") + graphProperty.set(parentProperty.graphProperty.get() as T) + graphProperty.dependsOn(parentProperty.graphProperty, true) { + @Suppress("UNCHECKED_CAST") + parentProperty.graphProperty.get() as T + } + } + } + + protected open fun setupDerivation( + reporter: TemplateValidationReporter, + derives: PropertyDerivation + ): PreparedDerivation? = null + + protected fun makeStorageKey(discriminator: String? = null): String { + val base = "${javaClass.name}.property.${descriptor.name}.${descriptor.type}" + if (discriminator == null) { + return base + } + + return "$base.$discriminator" + } + + protected fun makeCustomStorageKey(key: String): String { + return "${javaClass.name}.property.$key" + } + + protected fun collectPropertiesValues(names: List? = null): MutableMap { + val into = mutableMapOf() + + into.putAll(TemplateEvaluator.baseProperties) + + return if (names == null) { + properties.mapValuesTo(into) { (_, prop) -> prop.get() } + } else { + names.associateWithTo(mutableMapOf()) { properties[it]?.get() } + } + } + + protected fun collectDerivationParents(reporter: TemplateValidationReporter? = null): List?>? = + descriptor.derives?.parents?.map { parentName -> + val property = properties[parentName] + if (property == null) { + reporter?.error("Unknown parent property: $parentName") + } + return@map property + } + + protected fun collectDerivationParentValues(reporter: TemplateValidationReporter? = null): List? = + descriptor.derives?.parents?.map { parentName -> + val property = properties[parentName] + if (property == null) { + reporter?.error("Unknown parent property: $parentName") + } + return@map property?.get() + } + + protected fun Row.propertyVisibility(): Row = this.visibleIf(visibleProperty) + + private fun setupVisibleProperty( + reporter: TemplateValidationReporter, + visibility: Any? + ): GraphProperty { + val prop = graph.property(true) + if (visibility == null || visibility is Boolean) { + prop.set(visibility != false) + return prop + } + + if (visibility !is Map<*, *>) { + reporter.error("Visibility can only be a boolean or an object") + return prop + } + + var dependsOn = visibility["dependsOn"] + if (dependsOn !is String && (dependsOn !is List<*> || dependsOn.any { it !is String })) { + reporter.error( + "Expected 'visible' to have a 'dependsOn' value that is either a string or a list of strings" + ) + return prop + } + + val dependenciesNames = when (dependsOn) { + is String -> setOf(dependsOn) + is Collection<*> -> dependsOn.filterIsInstance().toSet() + else -> throw IllegalStateException("Should not be reached") + } + val dependencies = dependenciesNames.mapNotNull { + val dependency = this.properties[it] + if (dependency == null) { + reporter.error("Visibility dependency '$it' does not exist") + } + dependency + } + if (dependencies.size != dependenciesNames.size) { + // Errors have already been reported + return prop + } + + val condition = visibility["condition"] + if (condition !is String) { + reporter.error("Expected 'visible' to have a 'condition' string") + return prop + } + + var didInitialUpdate = false + val update: () -> Boolean = { + val conditionProperties = dependencies.associate { prop -> prop.descriptor.name to prop.get() } + val result = TemplateEvaluator.condition(conditionProperties, condition) + val exception = result.exceptionOrNull() + if (exception != null) { + if (!didInitialUpdate) { + didInitialUpdate = true + reporter.error("Failed to compute initial visibility: ${exception.message}") + thisLogger().info("Failed to compute initial visibility: ${exception.message}", exception) + } else { + thisLogger().error("Failed to compute initial visibility: ${exception.message}", exception) + } + } + + result.getOrDefault(true) + } + + prop.set(update()) + for (dependency in dependencies) { + prop.dependsOn(dependency.graphProperty, deleteWhenModified = false, update) + } + + return prop + } +} diff --git a/src/main/kotlin/creator/custom/types/CreatorPropertyFactory.kt b/src/main/kotlin/creator/custom/types/CreatorPropertyFactory.kt new file mode 100644 index 000000000..8d3689d50 --- /dev/null +++ b/src/main/kotlin/creator/custom/types/CreatorPropertyFactory.kt @@ -0,0 +1,73 @@ +/* + * 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.types + +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 +import com.intellij.util.xmlb.annotations.Attribute + +interface CreatorPropertyFactory { + + companion object { + + private val EP_NAME = ExtensionPointName>( + "com.demonwav.minecraft-dev.creatorPropertyType" + ) + + private val COLLECTOR = KeyedExtensionCollector(EP_NAME) + + fun createFromType( + type: String, + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*>? { + return COLLECTOR.findSingle(type)?.create(descriptor, graph, properties) + } + } + + fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> +} + +class CreatorPropertyFactoryBean : + BaseKeyedLazyInstance(), KeyedLazyInstance { + + @Attribute("type") + @RequiredElement + lateinit var type: String + + @Attribute("implementation") + @RequiredElement + lateinit var implementation: String + + override fun getImplementationClassName(): String = implementation + + override fun getKey(): String = type +} diff --git a/src/main/kotlin/creator/custom/types/ExternalCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ExternalCreatorProperty.kt new file mode 100644 index 000000000..b51b0e58c --- /dev/null +++ b/src/main/kotlin/creator/custom/types/ExternalCreatorProperty.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.creator.custom.types + +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>, + override val graphProperty: GraphProperty, + valueType: Class, +) : CreatorProperty(descriptor, graph, properties, valueType) { + + override fun setupProperty(reporter: TemplateValidationReporter) = Unit + + override fun createDefaultValue(raw: Any?): T = + throw UnsupportedOperationException("Unsupported for external properties") + + override fun serialize(value: T): String = + throw UnsupportedOperationException("Unsupported for external properties") + + override fun deserialize(string: String): T = + throw UnsupportedOperationException("Unsupported for external properties") + + override fun buildUi(panel: Panel, context: WizardContext) = Unit +} diff --git a/src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt new file mode 100644 index 000000000..870c470cb --- /dev/null +++ b/src/main/kotlin/creator/custom/types/FabricVersionsCreatorProperty.kt @@ -0,0 +1,350 @@ +/* + * 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.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.TemplatePropertyDescriptor +import com.demonwav.mcdev.creator.custom.TemplateValidationReporter +import com.demonwav.mcdev.creator.custom.model.FabricVersionsModel +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 +import com.intellij.openapi.ui.validation.WHEN_GRAPH_PROPAGATION_FINISHED +import com.intellij.ui.ComboboxSpeedSearch +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.withContext + +class FabricVersionsCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, FabricVersionsModel::class.java) { + + private val emptyVersion = SemanticVersion.release() + private val emptyValue = FabricVersionsModel( + emptyVersion, + emptyVersion, + emptyVersion, + FabricVersions.YarnVersion("", -1), + true, + emptyVersion, + false, + ) + private val defaultValue = createDefaultValue(descriptor.default) + + private val loadingVersionsProperty = graph.property(true) + override val graphProperty: GraphProperty = graph.property(defaultValue) + var model: FabricVersionsModel by graphProperty + + val mcVersionProperty = graphProperty.transform({ it.minecraftVersion }, { model.copy(minecraftVersion = it) }) + val mcVersionModel = DefaultComboBoxModel() + val showMcSnapshotsProperty = graph.property(false) + .bindBooleanStorage(makeStorageKey("showMcSnapshots")) + + val loomVersionProperty = graphProperty.transform({ it.loom }, { model.copy(loom = it) }) + val loomVersionModel = DefaultComboBoxModel() + + val loaderVersionProperty = graphProperty.transform({ it.loader }, { model.copy(loader = it) }) + val loaderVersionModel = DefaultComboBoxModel() + + val yarnVersionProperty = graphProperty.transform({ it.yarn }, { model.copy(yarn = it) }) + val yarnVersionModel = DefaultComboBoxModel() + val yarnHasMatchingGameVersion = mcVersionProperty.transform { mcVersion -> + val versions = fabricVersions + ?: return@transform true + val mcVersionString = mcVersion.toString() + versions.mappings.any { it.gameVersion == mcVersionString } + } + + val fabricApiVersionProperty = graphProperty.transform({ it.fabricApi }, { model.copy(fabricApi = it) }) + val fabricApiVersionModel = DefaultComboBoxModel() + val useFabricApiVersionProperty = graphProperty.transform({ it.useFabricApi }, { model.copy(useFabricApi = it) }) + val fabricApiHasMatchingGameVersion = mcVersionProperty.transform { mcVersion -> + val apiVersions = fabricApiVersions + ?: return@transform true + val mcVersionString = mcVersion.toString() + apiVersions.versions.any { mcVersionString in it.gameVersions } + } + + val useOfficialMappingsProperty = + graphProperty.transform({ it.useOfficialMappings }, { model.copy(useOfficialMappings = it) }) + + override fun createDefaultValue(raw: Any?): FabricVersionsModel = when (raw) { + is String -> deserialize(raw) + else -> emptyValue + } + + override fun serialize(value: FabricVersionsModel): String { + return "${value.minecraftVersion} ${value.loom} ${value.loader} ${value.yarn}" + + " ${value.useFabricApi} ${value.fabricApi} ${value.useOfficialMappings}" + } + + override fun deserialize(string: String): FabricVersionsModel { + val segments = string.split(' ') + val yarnSegments = segments.getOrNull(3)?.split(':') + val yarnVersion = if (yarnSegments != null && yarnSegments.size == 2) { + FabricVersions.YarnVersion(yarnSegments[0], yarnSegments[1].toInt()) + } else { + emptyValue.yarn + } + return FabricVersionsModel( + segments.getOrNull(0)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(1)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(2)?.let(SemanticVersion::tryParse) ?: emptyVersion, + yarnVersion, + segments.getOrNull(4).toBoolean(), + segments.getOrNull(5)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(6).toBoolean(), + ) + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row("") { + cell(AsyncProcessIcon("FabricVersions download")) + label(MCDevBundle("creator.ui.versions_download.label")) + }.visibleIf(loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.mc_version.label")) { + comboBox(mcVersionModel) + .bindItem(mcVersionProperty) + .validationRequestor(WHEN_GRAPH_PROPAGATION_FINISHED(graph)) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + checkBox(MCDevBundle("creator.ui.show_snapshots.label")) + .bindSelected(showMcSnapshotsProperty) + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.loom_version.label")) { + comboBox(loomVersionModel) + .bindItem(loomVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.loader_version.label")) { + comboBox(loaderVersionModel) + .bindItem(loaderVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.yarn_version.label")) { + comboBox(yarnVersionModel) + .bindItem(yarnVersionProperty) + .enabledIf(useOfficialMappingsProperty.not()) + .validationOnInput(BuiltinValidations.nonEmptyYarnVersion) + .validationOnApply(BuiltinValidations.nonEmptyYarnVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + checkBox(MCDevBundle("creator.ui.use_official_mappings.label")) + .bindSelected(useOfficialMappingsProperty) + + label(MCDevBundle("creator.ui.warn.no_yarn_to_mc_match")) + .visibleIf(yarnHasMatchingGameVersion.not()) + .component.foreground = JBColor.YELLOW + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.fabricapi_version.label")) { + comboBox(fabricApiVersionModel) + .bindItem(fabricApiVersionProperty) + .enabledIf(useFabricApiVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + checkBox(MCDevBundle("creator.ui.use_fabricapi.label")) + .bindSelected(useFabricApiVersionProperty) + label(MCDevBundle("creator.ui.warn.no_fabricapi_to_mc_match")) + .visibleIf(fabricApiHasMatchingGameVersion.not()) + .component.foreground = JBColor.YELLOW + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + showMcSnapshotsProperty.afterChange { updateMcVersionsList() } + + var previousMcVersion: SemanticVersion? = null + mcVersionProperty.afterChange { mcVersion -> + if (previousMcVersion == mcVersion) { + return@afterChange + } + + previousMcVersion = mcVersion + updateYarnVersions() + updateFabricApiVersions() + } + + downloadVersion { + val fabricVersions = fabricVersions + if (fabricVersions != null) { + loaderVersionModel.removeAllElements() + loaderVersionModel.addAll(fabricVersions.loader) + loaderVersionProperty.set(fabricVersions.loader.firstOrNull() ?: emptyVersion) + + updateMcVersionsList() + } + + val loomVersions = loomVersions + if (loomVersions != null) { + loomVersionModel.removeAllElements() + loomVersionModel.addAll(loomVersions) + val defaultValue = loomVersions.firstOrNull { it.toString().endsWith("-SNAPSHOT") } + ?: loomVersions.firstOrNull() + ?: emptyVersion + + loomVersionProperty.set(defaultValue) + } + + loadingVersionsProperty.set(false) + } + } + + private fun updateMcVersionsList() { + val versions = fabricVersions + ?: return + + val showSnapshots = showMcSnapshotsProperty.get() + val mcVersions = versions.game.asSequence() + .filter { showSnapshots || it.stable } + .mapNotNull { version -> SemanticVersion.tryParse(version.version) } + .toList() + + mcVersionModel.removeAllElements() + mcVersionModel.addAll(mcVersions) + mcVersionProperty.set(mcVersions.firstOrNull() ?: emptyVersion) + } + + private fun updateYarnVersions() { + val fabricVersions = fabricVersions + ?: return + + val mcVersion = mcVersionProperty.get() + val mcVersionString = mcVersion.toString() + + val yarnVersions = if (yarnHasMatchingGameVersion.get()) { + fabricVersions.mappings.asSequence() + .filter { it.gameVersion == mcVersionString } + .map { it.version } + .toList() + } else { + fabricVersions.mappings.map { it.version } + } + yarnVersionModel.removeAllElements() + yarnVersionModel.addAll(yarnVersions) + yarnVersionProperty.set(yarnVersions.firstOrNull() ?: emptyValue.yarn) + } + + private fun updateFabricApiVersions() { + val fabricApiVersions = fabricApiVersions + ?: return + + val mcVersion = mcVersionProperty.get() + val mcVersionString = mcVersion.toString() + + val apiVersions = if (fabricApiHasMatchingGameVersion.get()) { + fabricApiVersions.versions.asSequence() + .filter { mcVersionString in it.gameVersions } + .map(FabricApiVersions.Version::version) + .toList() + } else { + fabricApiVersions.versions.map(FabricApiVersions.Version::version) + } + fabricApiVersionModel.removeAllElements() + fabricApiVersionModel.addAll(apiVersions) + fabricApiVersionProperty.set(apiVersions.firstOrNull() ?: emptyVersion) + } + + companion object { + private var hasDownloadedVersions = false + + private var fabricVersions: FabricVersions? = null + private var loomVersions: List? = null + private var fabricApiVersions: FabricApiVersions? = null + + private fun downloadVersion(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() + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = FabricVersionsCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt new file mode 100644 index 000000000..de3464fae --- /dev/null +++ b/src/main/kotlin/creator/custom/types/ForgeVersionsCreatorProperty.kt @@ -0,0 +1,219 @@ +/* + * 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.types + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.BuiltinValidations +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.withContext + +class ForgeVersionsCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, ForgeVersions::class.java) { + + private val emptyVersion = SemanticVersion.release() + + private val defaultValue = createDefaultValue(descriptor.default) + + private val loadingVersionsProperty = graph.property(true) + override val graphProperty: GraphProperty = graph.property(defaultValue) + var versions: ForgeVersions by graphProperty + + private var previousMcVersion: SemanticVersion? = null + + private val mcVersionProperty = graphProperty.transform({ it.minecraft }, { versions.copy(minecraft = it) }) + private val mcVersionsModel = DefaultComboBoxModel() + private val forgeVersionProperty = graphProperty.transform({ it.forge }, { versions.copy(forge = it) }) + private val forgeVersionsModel = DefaultComboBoxModel() + + private var mcVersionFilterParents: List? = null + + override fun createDefaultValue(raw: Any?): ForgeVersions { + if (raw is String) { + return deserialize(raw) + } + + return ForgeVersions(emptyVersion, emptyVersion) + } + + override fun serialize(value: ForgeVersions): String { + return "${value.minecraft} ${value.forge}" + } + + override fun deserialize(string: String): ForgeVersions { + val versions = string.split(' ') + .take(2) + .map { SemanticVersion.tryParse(it) ?: emptyVersion } + + return ForgeVersions( + versions.getOrNull(0) ?: emptyVersion, + versions.getOrNull(1) ?: emptyVersion, + ) + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row("") { + cell(AsyncProcessIcon("ForgeVersions download")) + label(MCDevBundle("creator.ui.versions_download.label")) + }.visibleIf(loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.mc_version.label")) { + comboBox(mcVersionsModel) + .bindItem(mcVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + label(MCDevBundle("creator.ui.forge_version.label")).gap(RightGap.SMALL) + comboBox(forgeVersionsModel) + .bindItem(forgeVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + mcVersionProperty.afterChange { mcVersion -> + if (mcVersion == previousMcVersion) { + return@afterChange + } + + previousMcVersion = mcVersion + val availableForgeVersions = forgeVersion!!.getForgeVersions(mcVersion) + .take(descriptor.limit ?: 50) + forgeVersionsModel.removeAllElements() + forgeVersionsModel.addAll(availableForgeVersions) + forgeVersionProperty.set(availableForgeVersions.firstOrNull() ?: emptyVersion) + } + + descriptor.parameters?.get("mcVersionFilterParents")?.let { parents -> + if (parents !is List<*> || parents.any { it !is String }) { + reporter.error("mcVersionFilterParents must be a list of strings") + } else { + @Suppress("UNCHECKED_CAST") + this.mcVersionFilterParents = parents as List + for (parent in parents) { + val parentProp = properties[parent] + if (parentProp == null) { + reporter.error("Unknown mcVersionFilter parent $parent") + continue + } + + parentProp.graphProperty.afterChange { + reloadMinecraftVersions() + } + } + } + } + + downloadVersions { + reloadMinecraftVersions() + + loadingVersionsProperty.set(false) + } + } + + private fun reloadMinecraftVersions() { + val forgeVersions = forgeVersion + ?: return + + val filterExpr = descriptor.parameters?.get("mcVersionFilter") as? String + val mcVersions = if (filterExpr != null) { + val conditionProps = collectPropertiesValues(mcVersionFilterParents) + forgeVersions.sortedMcVersions.filter { version -> + conditionProps["MC_VERSION"] = version + TemplateEvaluator.condition(conditionProps, filterExpr).getOrDefault(true) + } + } else { + forgeVersions.sortedMcVersions + } + + mcVersionsModel.removeAllElements() + mcVersionsModel.addAll(mcVersions) + + val selectedMcVersion = when { + mcVersionProperty.get() in mcVersions -> mcVersionProperty.get() + defaultValue.minecraft in mcVersions -> defaultValue.minecraft + else -> mcVersions.first() + } + mcVersionProperty.set(selectedMcVersion) + } + + companion object { + private var hasDownloadedVersions = false + + private var forgeVersion: ForgeVersion? = null + + private fun downloadVersions(uiCallback: () -> Unit) { + if (hasDownloadedVersions) { + uiCallback() + return + } + + application.executeOnPooledThread { + runBlocking { + forgeVersion = ForgeVersion.downloadData() + + hasDownloadedVersions = true + + withContext(Dispatchers.Swing) { + uiCallback() + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = ForgeVersionsCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/InlineStringListCreatorProperty.kt b/src/main/kotlin/creator/custom/types/InlineStringListCreatorProperty.kt new file mode 100644 index 000000000..67e931edf --- /dev/null +++ b/src/main/kotlin/creator/custom/types/InlineStringListCreatorProperty.kt @@ -0,0 +1,62 @@ +/* + * 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.types + +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 +import com.intellij.ui.dsl.builder.columns + +class InlineStringListCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, StringList::class.java) { + + override fun createDefaultValue(raw: Any?): StringList = deserialize(raw as? String ?: "") + + override fun serialize(value: StringList): String = value.values.joinToString(transform = String::trim) + + override fun deserialize(string: String): StringList = string.split(',') + .map(String::trim) + .filter(String::isNotBlank) + .run(::StringList) + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + this.textField().bindText(this@InlineStringListCreatorProperty.toStringProperty(graphProperty)) + .columns(COLUMNS_LARGE) + .enabled(descriptor.editable != false) + }.propertyVisibility() + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = InlineStringListCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt b/src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt new file mode 100644 index 000000000..bcd6edc6b --- /dev/null +++ b/src/main/kotlin/creator/custom/types/IntegerCreatorProperty.kt @@ -0,0 +1,82 @@ +/* + * 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.types + +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 +import com.intellij.ui.dsl.builder.columns + +class IntegerCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, Int::class.java) { + + override fun createDefaultValue(raw: Any?): Int = (raw as? Number)?.toInt() ?: 0 + + override fun serialize(value: Int): String = value.toString() + + override fun deserialize(string: String): Int = string.toIntOrNull() ?: 0 + + override fun convertSelectDerivationResult(original: Any?): Any? = (original as? Number)?.toInt() + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + this.intTextField().bindIntText(graphProperty) + .columns(COLUMNS_LARGE) + .enabled(descriptor.editable != false) + }.propertyVisibility() + } + + override fun setupDerivation( + reporter: TemplateValidationReporter, + derives: PropertyDerivation + ): PreparedDerivation? = when (derives.method) { + "recommendJavaVersionForMcVersion" -> { + val parents = collectDerivationParents(reporter) + RecommendJavaVersionForMcVersionPropertyDerivation.create(reporter, parents, derives) + } + + null -> { + // No need to collect parent values for this one because it is not used + SelectPropertyDerivation.create(reporter, emptyList(), derives) + } + + else -> null + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = IntegerCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/JdkCreatorProperty.kt b/src/main/kotlin/creator/custom/types/JdkCreatorProperty.kt new file mode 100644 index 000000000..02608ed5f --- /dev/null +++ b/src/main/kotlin/creator/custom/types/JdkCreatorProperty.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.creator.custom.types + +import com.demonwav.mcdev.creator.JdkComboBoxWithPreference +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 +import com.intellij.ui.dsl.builder.Panel + +class JdkCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, CreatorJdk::class.java) { + + private lateinit var jdkComboBox: JdkComboBoxWithPreference + + override fun createDefaultValue(raw: Any?): CreatorJdk = CreatorJdk(null) + + override fun serialize(value: CreatorJdk): String = value.sdk?.homePath ?: "" + + override fun deserialize(string: String): CreatorJdk = + CreatorJdk(ProjectJdkTable.getInstance().allJdks.find { it.homePath == string }) + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + val sdkProperty = graphProperty.transform(CreatorJdk::sdk, ::CreatorJdk) + jdkComboBox = this.jdkComboBoxWithPreference(context, sdkProperty, descriptor.name).component + + val minVersionPropName = descriptor.default as? String + if (minVersionPropName != null) { + val minVersionProperty = properties[minVersionPropName] + ?: throw RuntimeException( + "Could not find property $minVersionPropName referenced" + + " by default value of property ${descriptor.name}" + ) + + jdkComboBox.setPreferredJdk(JavaSdkVersion.entries[minVersionProperty.graphProperty.get() as Int]) + minVersionProperty.graphProperty.afterPropagation { + jdkComboBox.setPreferredJdk(JavaSdkVersion.entries[minVersionProperty.graphProperty.get() as Int]) + } + } + } + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = JdkCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/LicenseCreatorProperty.kt b/src/main/kotlin/creator/custom/types/LicenseCreatorProperty.kt new file mode 100644 index 000000000..fc6ae05df --- /dev/null +++ b/src/main/kotlin/creator/custom/types/LicenseCreatorProperty.kt @@ -0,0 +1,74 @@ +/* + * 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.types + +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 +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import java.time.ZonedDateTime + +class LicenseCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, LicenseData::class.java) { + + override val graphProperty: GraphProperty = + graph.property(createDefaultValue(descriptor.default)) + + override fun createDefaultValue(raw: Any?): LicenseData = + deserialize(raw as? String ?: License.ALL_RIGHTS_RESERVED.id) + + override fun serialize(value: LicenseData): String = value.id + + override fun deserialize(string: String): LicenseData = + LicenseData(string, License.byId(string)?.toString() ?: string, ZonedDateTime.now().year.toString()) + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + val model = EnumComboBoxModel(License::class.java) + val licenseEnumProperty = graphProperty.transform( + { License.byId(it.id) ?: License.entries.first() }, + { deserialize(it.id) } + ) + comboBox(model) + .bindItem(licenseEnumProperty) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + } + + class Factory : CreatorPropertyFactory { + + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = LicenseCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt b/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt new file mode 100644 index 000000000..733af37ae --- /dev/null +++ b/src/main/kotlin/creator/custom/types/MavenArtifactVersionCreatorProperty.kt @@ -0,0 +1,177 @@ +/* + * 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.types + +import com.demonwav.mcdev.creator.collectMavenVersions +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.withContext + +class MavenArtifactVersionCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SemanticVersionCreatorProperty(descriptor, graph, properties) { + + lateinit var sourceUrl: String + var rawVersionFilter: (String) -> Boolean = { true } + var versionFilter: (SemanticVersion) -> Boolean = { true } + + override val graphProperty: GraphProperty = graph.property(SemanticVersion(emptyList())) + private val versionsProperty = graph.property>(emptyList()) + private val loadingVersionsProperty = graph.property(true) + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + val combobox = comboBox(versionsProperty.get()) + .bindItem(graphProperty) + .enabled(descriptor.editable != false) + .also { ComboboxSpeedSearch.installOn(it.component) } + + cell(AsyncProcessIcon(makeStorageKey("progress"))) + .visibleIf(loadingVersionsProperty) + + versionsProperty.afterChange { versions -> + combobox.component.removeAllItems() + for (version in versions) { + combobox.component.addItem(version) + } + } + }.propertyVisibility() + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + val url = descriptor.parameters?.get("sourceUrl") as? String + if (url == null) { + reporter.error("Expected string parameter 'sourceUrl'") + return + } + + sourceUrl = url + + val rawVersionFilterCondition = descriptor.parameters?.get("rawVersionFilter") + if (rawVersionFilterCondition != null) { + if (rawVersionFilterCondition !is String) { + reporter.error("'rawVersionFilter' must be a string") + } else { + rawVersionFilter = { version -> + val props = mapOf("version" to version) + TemplateEvaluator.condition(props, rawVersionFilterCondition) + .getOrLogException(thisLogger()) == true + } + } + } + + val versionFilterCondition = descriptor.parameters?.get("versionFilter") + if (versionFilterCondition != null) { + if (versionFilterCondition !is String) { + reporter.error("'versionFilter' must be a string") + } else { + versionFilter = { version -> + val props = mapOf("version" to version) + TemplateEvaluator.condition(props, versionFilterCondition) + .getOrLogException(thisLogger()) == true + } + } + } + + downloadVersions( + // The key might be a bit too unique, but that'll do the job + descriptor.name + "@" + descriptor.hashCode(), + sourceUrl, + rawVersionFilter, + versionFilter, + descriptor.limit ?: 50 + ) { versions -> + versionsProperty.set(versions) + loadingVersionsProperty.set(false) + } + } + + companion object { + + private var versionsCache = ConcurrentHashMap>() + + private fun downloadVersions( + key: String, + url: String, + rawVersionFilter: (String) -> Boolean, + versionFilter: (SemanticVersion) -> Boolean, + limit: Int, + uiCallback: (List) -> Unit + ) { + // Let's not mix up cached versions if different properties + // point to the same URL, but have different filters or limits + val cacheKey = "$key-$url" + val cachedVersions = versionsCache[cacheKey] + if (cachedVersions != null) { + uiCallback(cachedVersions) + 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) + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = MavenArtifactVersionCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt b/src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt new file mode 100644 index 000000000..10925897d --- /dev/null +++ b/src/main/kotlin/creator/custom/types/NeoForgeVersionsCreatorProperty.kt @@ -0,0 +1,211 @@ +/* + * 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.types + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.BuiltinValidations +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.NeoForgeVersions +import com.demonwav.mcdev.platform.neoforge.version.NeoForgeVersion +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.withContext + +class NeoForgeVersionsCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, NeoForgeVersions::class.java) { + + private val emptyVersion = SemanticVersion.release() + + private val defaultValue = createDefaultValue(descriptor.default) + + private val loadingVersionsProperty = graph.property(true) + override val graphProperty: GraphProperty = graph.property(defaultValue) + var versions: NeoForgeVersions by graphProperty + + private var previousMcVersion: SemanticVersion? = null + + private val mcVersionProperty = graphProperty.transform({ it.minecraft }, { versions.copy(minecraft = it) }) + private val mcVersionsModel = DefaultComboBoxModel() + private val nfVersionProperty = graphProperty.transform({ it.neoforge }, { versions.copy(neoforge = it) }) + private val nfVersionsModel = DefaultComboBoxModel() + private val ngVersionProperty = graphProperty.transform({ it.neogradle }, { versions.copy(neogradle = it) }) + private val mdVersionProperty = graphProperty.transform({ it.moddev }, { versions.copy(moddev = it) }) + + override fun createDefaultValue(raw: Any?): NeoForgeVersions { + if (raw is String) { + return deserialize(raw) + } + + return NeoForgeVersions(emptyVersion, emptyVersion, emptyVersion, emptyVersion) + } + + override fun serialize(value: NeoForgeVersions): String { + return "${value.minecraft} ${value.neoforge} ${value.neogradle} ${value.moddev}" + } + + override fun deserialize(string: String): NeoForgeVersions { + val versions = string.split(' ') + .take(4) + .map { SemanticVersion.tryParse(it) ?: emptyVersion } + + return NeoForgeVersions( + versions.getOrNull(0) ?: emptyVersion, + versions.getOrNull(1) ?: emptyVersion, + versions.getOrNull(2) ?: emptyVersion, + versions.getOrNull(3) ?: emptyVersion, + ) + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row("") { + cell(AsyncProcessIcon("NeoForgeVersions download")) + label(MCDevBundle("creator.ui.versions_download.label")) + }.visibleIf(loadingVersionsProperty) + + panel.row(MCDevBundle("creator.ui.mc_version.label")) { + comboBox(mcVersionsModel) + .bindItem(mcVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + label(MCDevBundle("creator.ui.neoforge_version.label")).gap(RightGap.SMALL) + comboBox(nfVersionsModel) + .bindItem(nfVersionProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + .visibleIf(!loadingVersionsProperty) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + mcVersionProperty.afterChange { mcVersion -> + if (mcVersion == previousMcVersion) { + return@afterChange + } + + previousMcVersion = mcVersion + val availableNfVersions = nfVersion!!.getNeoForgeVersions(mcVersion) + .take(descriptor.limit ?: 50) + nfVersionsModel.removeAllElements() + nfVersionsModel.addAll(availableNfVersions) + nfVersionProperty.set(availableNfVersions.firstOrNull() ?: emptyVersion) + } + + val mcVersionFilter = descriptor.parameters?.get("mcVersionFilter") as? String + downloadVersion(mcVersionFilter) { + val mcVersions = mcVersions ?: return@downloadVersion + + mcVersionsModel.removeAllElements() + mcVersionsModel.addAll(mcVersions) + + val selectedMcVersion = when { + mcVersionProperty.get() in mcVersions -> mcVersionProperty.get() + defaultValue.minecraft in mcVersions -> defaultValue.minecraft + else -> mcVersions.first() + } + mcVersionProperty.set(selectedMcVersion) + + ngVersionProperty.set(ngVersion?.versions?.firstOrNull() ?: emptyVersion) + mdVersionProperty.set(mdVersion?.versions?.firstOrNull() ?: emptyVersion) + + loadingVersionsProperty.set(false) + } + } + + companion object { + + private var hasDownloadedVersions = false + + private var nfVersion: NeoForgeVersion? = null + private var ngVersion: NeoGradleVersion? = null + private var mdVersion: NeoModDevVersion? = null + private var mcVersions: List? = null + + private fun downloadVersion(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 + } + } + + hasDownloadedVersions = true + + withContext(Dispatchers.Swing) { + uiCallback() + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = NeoForgeVersionsCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt b/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt new file mode 100644 index 000000000..360c3d2f9 --- /dev/null +++ b/src/main/kotlin/creator/custom/types/ParchmentCreatorProperty.kt @@ -0,0 +1,281 @@ +/* + * 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.types + +import com.demonwav.mcdev.creator.ParchmentVersion +import com.demonwav.mcdev.creator.custom.BuiltinValidations +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.withContext + +class ParchmentCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : CreatorProperty(descriptor, graph, properties, ParchmentVersions::class.java) { + + private val emptyVersion = SemanticVersion.release() + + private val defaultValue = createDefaultValue(descriptor.default) + + override val graphProperty: GraphProperty = graph.property(defaultValue) + var versions: ParchmentVersions by graphProperty + + private var availableParchmentVersions: List = emptyList() + + private val useParchmentProperty = graphProperty.transform({ it.use }, { versions.copy(use = it) }) + private val versionProperty = graphProperty.transform({ it.version }, { versions.copy(version = it) }) + private val versionsModel = DefaultComboBoxModel() + private val mcVersionProperty = + graphProperty.transform({ it.minecraftVersion }, { versions.copy(minecraftVersion = it) }) + private val mcVersionsModel = DefaultComboBoxModel() + private val includeOlderMcVersionsProperty = + graphProperty.transform({ it.includeOlderMcVersions }, { versions.copy(includeOlderMcVersions = it) }) + private val includeSnapshotsProperty = + graphProperty.transform({ it.includeSnapshots }, { versions.copy(includeSnapshots = it) }) + + override fun createDefaultValue(raw: Any?): ParchmentVersions { + if (raw is String) { + return deserialize(raw) + } + + return ParchmentVersions(true, emptyVersion, emptyVersion, false, false) + } + + override fun serialize(value: ParchmentVersions): String { + return "${value.use} ${value.version} ${value.minecraftVersion}" + + " ${value.includeOlderMcVersions} ${value.includeSnapshots}" + } + + override fun deserialize(string: String): ParchmentVersions { + val segments = string.split(' ') + return ParchmentVersions( + segments.getOrNull(0)?.toBoolean() ?: true, + segments.getOrNull(1)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(2)?.let(SemanticVersion::tryParse) ?: emptyVersion, + segments.getOrNull(3).toBoolean(), + segments.getOrNull(4).toBoolean(), + ) + } + + override fun buildUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + checkBox("Use Parchment") + .bindSelected(useParchmentProperty) + + comboBox(mcVersionsModel) + .bindItem(mcVersionProperty) + .enabledIf(useParchmentProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + + comboBox(versionsModel) + .bindItem(versionProperty) + .enabledIf(useParchmentProperty) + .validationOnInput(BuiltinValidations.nonEmptyVersion) + .validationOnApply(BuiltinValidations.nonEmptyVersion) + .also { ComboboxSpeedSearch.installOn(it.component) } + }.enabled(descriptor.editable != false) + + panel.row("Include") { + checkBox("Older Minecraft versions") + .bindSelected(includeOlderMcVersionsProperty) + .enabledIf(useParchmentProperty) + + checkBox("Snapshots") + .bindSelected(includeSnapshotsProperty) + .enabledIf(useParchmentProperty) + }.enabled(descriptor.editable != false) + } + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + val platformMcVersionPropertyName = descriptor.parameters?.get("minecraftVersionProperty") as? String + val platformMcVersionProperty = properties[platformMcVersionPropertyName] + if (platformMcVersionProperty != null) { + graphProperty.dependsOn(platformMcVersionProperty.graphProperty, true) { + val minecraftVersion = getPlatformMinecraftVersion() + if (minecraftVersion != null) { + graphProperty.get().copy(minecraftVersion = minecraftVersion) + } else { + graphProperty.get() + } + } + } + + var previousMcVersion: SemanticVersion? = null + mcVersionProperty.afterChange { mcVersion -> + if (mcVersion == previousMcVersion) { + return@afterChange + } + + previousMcVersion = mcVersion + refreshVersionsLists(updateMcVersions = false) + } + + var previousOlderMcVersions: Boolean? = null + includeOlderMcVersionsProperty.afterChange { newValue -> + if (previousOlderMcVersions == newValue) { + return@afterChange + } + + previousOlderMcVersions = newValue + refreshVersionsLists() + } + + var previousIncludeSnapshots: Boolean? = null + includeSnapshotsProperty.afterChange { newValue -> + if (previousIncludeSnapshots == newValue) { + return@afterChange + } + + previousIncludeSnapshots = newValue + refreshVersionsLists() + } + + downloadVersions { + refreshVersionsLists() + + val minecraftVersion = getPlatformMinecraftVersion() + if (minecraftVersion != null) { + mcVersionProperty.set(minecraftVersion) + } + } + } + + private fun refreshVersionsLists(updateMcVersions: Boolean = true) { + val includeOlderMcVersions = includeOlderMcVersionsProperty.get() + val includeSnapshots = includeSnapshotsProperty.get() + + if (updateMcVersions) { + val platformMcVersion = getPlatformMinecraftVersion() + availableParchmentVersions = allParchmentVersions + ?.filter { version -> + if (!includeOlderMcVersions && platformMcVersion != null && version.mcVersion < platformMcVersion) { + return@filter false + } + + if (!includeSnapshots && version.parchmentVersion.contains("-SNAPSHOT")) { + return@filter false + } + + return@filter true + } + ?: return + + val mcVersions = availableParchmentVersions.mapTo(mutableSetOf(), ParchmentVersion::mcVersion) + mcVersionsModel.removeAllElements() + mcVersionsModel.addAll(mcVersions) + + val selectedMcVersion = when { + mcVersionProperty.get() in mcVersions -> mcVersionProperty.get() + defaultValue.minecraftVersion in mcVersions -> defaultValue.minecraftVersion + else -> getPlatformMinecraftVersion() ?: mcVersions.first() + } + + if (mcVersionProperty.get() != selectedMcVersion) { + mcVersionProperty.set(selectedMcVersion) + } + } + + val selectedMcVersion = mcVersionProperty.get() + val parchmentVersions = availableParchmentVersions.asSequence() + .filter { it.mcVersion == selectedMcVersion } + .mapNotNull { SemanticVersion.tryParse(it.parchmentVersion) } + .sortedDescending() + .toList() + versionsModel.removeAllElements() + versionsModel.addAll(parchmentVersions) + versionProperty.set(parchmentVersions.firstOrNull() ?: emptyVersion) + } + + private fun getPlatformMinecraftVersion(): SemanticVersion? { + val platformMcVersionPropertyName = descriptor.parameters?.get("minecraftVersionProperty") as? String + val platformMcVersionProperty = properties[platformMcVersionPropertyName] + + val version = when (val version = platformMcVersionProperty?.get()) { + is SemanticVersion -> version + is HasMinecraftVersion -> version.minecraftVersion + else -> return null + } + + // Ensures we get no trailing .0 for the first major version (1.21.0 -> 1.21) + // This is required because otherwise those versions won't be properly compared against Parchment's + val normalizedVersion = version.parts.dropLastWhile { part -> + part is SemanticVersion.Companion.VersionPart.ReleasePart && part.version == 0 + } + + return SemanticVersion(normalizedVersion) + } + + companion object { + + private var hasDownloadedVersions = false + + private var allParchmentVersions: List? = null + + private fun downloadVersions(uiCallback: () -> Unit) { + if (hasDownloadedVersions) { + uiCallback() + return + } + + application.executeOnPooledThread { + runBlocking { + allParchmentVersions = ParchmentVersion.downloadData() + .sortedByDescending(ParchmentVersion::parchmentVersion) + + hasDownloadedVersions = true + + withContext(Dispatchers.Swing) { + uiCallback() + } + } + } + } + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = ParchmentCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt b/src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt new file mode 100644 index 000000000..f500d03d0 --- /dev/null +++ b/src/main/kotlin/creator/custom/types/SemanticVersionCreatorProperty.kt @@ -0,0 +1,86 @@ +/* + * 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.types + +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.ExtractVersionMajorMinorPropertyDerivation +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 +import com.intellij.ui.dsl.builder.columns + +open class SemanticVersionCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, SemanticVersion::class.java) { + + override fun createDefaultValue(raw: Any?): SemanticVersion = + SemanticVersion.tryParse(raw as? String ?: "") ?: SemanticVersion(emptyList()) + + override fun serialize(value: SemanticVersion): String = value.toString() + + override fun deserialize(string: String): SemanticVersion = + SemanticVersion.tryParse(string) ?: SemanticVersion(emptyList()) + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + this.textField().bindText(this@SemanticVersionCreatorProperty.toStringProperty(graphProperty)) + .columns(COLUMNS_SHORT) + .enabled(descriptor.editable != false) + }.propertyVisibility() + } + + override fun setupDerivation( + reporter: TemplateValidationReporter, + derives: PropertyDerivation + ): PreparedDerivation? = when (derives.method) { + "extractVersionMajorMinor" -> { + val parents = collectDerivationParents(reporter) + ExtractVersionMajorMinorPropertyDerivation.create(reporter, parents, derives) + } + + null -> { + SelectPropertyDerivation.create(reporter, emptyList(), derives) + } + + else -> null + } + + override fun convertSelectDerivationResult(original: Any?): Any? { + return (original as? String)?.let(SemanticVersion::tryParse) + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = SemanticVersionCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt b/src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt new file mode 100644 index 000000000..eee7e0ee1 --- /dev/null +++ b/src/main/kotlin/creator/custom/types/SimpleCreatorProperty.kt @@ -0,0 +1,134 @@ +/* + * 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.types + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.creator.custom.BuiltinValidations +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 +import java.awt.Component +import javax.swing.DefaultListCellRenderer +import javax.swing.JList + +abstract class SimpleCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map>, + valueType: Class +) : CreatorProperty(descriptor, graph, properties, valueType) { + + private val options: Map? = makeOptionsList() + + private fun makeOptionsList(): Map? { + val map = when (val options = descriptor.options) { + is Map<*, *> -> options.mapValues { descriptor.translate(it.value.toString()) } + is Iterable<*> -> options.associateWithTo(linkedMapOf()) { + val optionKey = it.toString() + descriptor.translateOrNull("creator.ui.${descriptor.name.lowercase()}.option.${optionKey.lowercase()}") + ?: optionKey + } + + else -> null + } + + return map?.mapKeys { + @Suppress("UNCHECKED_CAST") + when (val key = it.key) { + is String -> deserialize(key) + else -> key + } as T + } + } + + private val isDropdown = !options.isNullOrEmpty() + private val defaultValue by lazy { + val raw = if (isDropdown) { + if (descriptor.default is Number && descriptor.options is List<*>) { + descriptor.options[descriptor.default.toInt()] + } else { + descriptor.default ?: options?.keys?.firstOrNull() + } + } else { + descriptor.default + } + + createDefaultValue(raw) + } + + override val graphProperty: GraphProperty by lazy { graph.property(defaultValue) } + + override fun buildUi(panel: Panel, context: WizardContext) { + if (isDropdown) { + if (graphProperty.get() !in options!!.keys) { + graphProperty.set(defaultValue) + } + + panel.row(descriptor.translatedLabel) { + if (descriptor.forceDropdown == true) { + comboBox(options.keys, DropdownAutoRenderer()) + .bindItem(graphProperty) + .enabled(descriptor.editable != false) + .also { + val component = it.component + ComboboxSpeedSearch.installOn(component) + val validation = + BuiltinValidations.isAnyOf(component::getSelectedItem, options.keys, component) + it.validationOnInput(validation) + it.validationOnApply(validation) + } + } else { + segmentedButton(options.keys) { text = options[it] ?: it.toString() } + .bind(graphProperty) + .enabled(descriptor.editable != false) + .maxButtonsCount(4) + .validation { + val message = MCDevBundle("creator.validation.invalid_option") + addInputRule(message) { it.selectedItem !in options.keys } + addApplyRule(message) { it.selectedItem !in options.keys } + } + } + }.propertyVisibility() + } else { + buildSimpleUi(panel, context) + } + } + + abstract fun buildSimpleUi(panel: Panel, context: WizardContext) + + private inner class DropdownAutoRenderer : DefaultListCellRenderer() { + + override fun getListCellRendererComponent( + list: JList?, + value: Any?, + index: Int, + isSelected: Boolean, + cellHasFocus: Boolean + ): Component { + val label = options!![value] ?: value.toString() + return super.getListCellRendererComponent(list, label, index, isSelected, cellHasFocus) + } + } +} diff --git a/src/main/kotlin/creator/custom/types/StringCreatorProperty.kt b/src/main/kotlin/creator/custom/types/StringCreatorProperty.kt new file mode 100644 index 000000000..31582bcc7 --- /dev/null +++ b/src/main/kotlin/creator/custom/types/StringCreatorProperty.kt @@ -0,0 +1,103 @@ +/* + * 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.types + +import com.demonwav.mcdev.creator.custom.BuiltinValidations +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 +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.textValidation + +class StringCreatorProperty( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> +) : SimpleCreatorProperty(descriptor, graph, properties, String::class.java) { + + private var validationRegex: Regex? = null + + override fun createDefaultValue(raw: Any?): String = raw as? String ?: "" + + override fun serialize(value: String): String = value + + override fun deserialize(string: String): String = string + + override fun toStringProperty(graphProperty: GraphProperty) = graphProperty + + override fun setupProperty(reporter: TemplateValidationReporter) { + super.setupProperty(reporter) + + val regexString = descriptor.validator as? String + if (regexString != null) { + try { + validationRegex = regexString.toRegex() + } catch (t: Throwable) { + reporter.error("Invalid validator regex: '$regexString': ${t.message}") + } + } + } + + override fun setupDerivation( + reporter: TemplateValidationReporter, + derives: PropertyDerivation + ): PreparedDerivation? = when (derives.method) { + "replace" -> { + val parents = collectDerivationParents(reporter) + ReplacePropertyDerivation.create(reporter, parents, derives) + } + + null -> { + // No need to collect parent values for this one because it is not used + SelectPropertyDerivation.create(reporter, emptyList(), derives) + } + + else -> null + } + + override fun buildSimpleUi(panel: Panel, context: WizardContext) { + panel.row(descriptor.translatedLabel) { + val textField = textField().bindText(this@StringCreatorProperty.toStringProperty(graphProperty)) + .columns(COLUMNS_LARGE) + .enabled(descriptor.editable != false) + if (validationRegex != null) { + textField.textValidation(BuiltinValidations.byRegex(validationRegex!!)) + } + }.propertyVisibility() + } + + class Factory : CreatorPropertyFactory { + override fun create( + descriptor: TemplatePropertyDescriptor, + graph: PropertyGraph, + properties: Map> + ): CreatorProperty<*> = StringCreatorProperty(descriptor, graph, properties) + } +} diff --git a/src/main/kotlin/creator/step/UseMixinsStep.kt b/src/main/kotlin/creator/step/UseMixinsStep.kt index e5b9b1c1b..c328712b5 100644 --- a/src/main/kotlin/creator/step/UseMixinsStep.kt +++ b/src/main/kotlin/creator/step/UseMixinsStep.kt @@ -36,7 +36,7 @@ class UseMixinsStep(parent: NewProjectWizardStep) : AbstractNewProjectWizardStep override fun setupUI(builder: Panel) { with(builder) { - row(MCDevBundle("creator.ui.mixins.label")) { + row(MCDevBundle("creator.ui.use_mixins.label")) { checkBox("") .bindSelected(useMixinsProperty) } diff --git a/src/main/kotlin/insight/generation/ui/EventListenerWizard.form b/src/main/kotlin/insight/generation/ui/EventListenerWizard.form deleted file mode 100644 index c30595322..000000000 --- a/src/main/kotlin/insight/generation/ui/EventListenerWizard.form +++ /dev/null @@ -1,64 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/kotlin/insight/generation/ui/EventListenerWizard.kt b/src/main/kotlin/insight/generation/ui/EventListenerWizard.kt index 1d78fad1b..81a153c39 100644 --- a/src/main/kotlin/insight/generation/ui/EventListenerWizard.kt +++ b/src/main/kotlin/insight/generation/ui/EventListenerWizard.kt @@ -22,60 +22,62 @@ package com.demonwav.mcdev.insight.generation.ui import com.intellij.ide.highlighter.JavaHighlightingColors import com.intellij.openapi.editor.ex.util.EditorUtil -import com.intellij.openapi.wm.ex.IdeFocusTraversalPolicy +import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.ui.JBColor -import com.intellij.uiDesigner.core.GridConstraints -import javax.swing.JLabel +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.text import javax.swing.JPanel -import javax.swing.JSeparator -import javax.swing.JTextField class EventListenerWizard(panel: JPanel?, className: String, defaultListenerName: String) { - lateinit var panel: JPanel - private lateinit var classNameTextField: JTextField - private lateinit var listenerNameTextField: JTextField - private lateinit var publicVoidLabel: JLabel - private lateinit var contentPanel: JPanel - private lateinit var separator: JSeparator - init { - classNameTextField.font = EditorUtil.getEditorFont() - listenerNameTextField.font = EditorUtil.getEditorFont() - publicVoidLabel.font = EditorUtil.getEditorFont() - if (!JBColor.isBright()) { - publicVoidLabel.foreground = JavaHighlightingColors.KEYWORD.defaultAttributes.foregroundColor - } else { - publicVoidLabel.foreground = - JavaHighlightingColors.KEYWORD.fallbackAttributeKey!!.defaultAttributes.foregroundColor - } + private val graph = PropertyGraph("EventListenerWizard graph") - if (panel != null) { - separator.isVisible = true - contentPanel.add(panel, innerContentPanelConstraints) - } + private val listenerNameProperty = graph.property(defaultListenerName) + val chosenClassName: String by listenerNameProperty - classNameTextField.text = className - listenerNameTextField.text = defaultListenerName + val panel: JPanel by lazy { + panel { + row { + textField() + .text(className) + .align(AlignX.FILL) + .apply { + component.font = EditorUtil.getEditorFont() + component.isEditable = false + } + } - IdeFocusTraversalPolicy.getPreferredFocusedComponent(listenerNameTextField).requestFocus() - listenerNameTextField.requestFocus() - } + row { + label("public void").apply { + component.font = EditorUtil.getEditorFont() + if (!JBColor.isBright()) { + component.foreground = JavaHighlightingColors.KEYWORD.defaultAttributes.foregroundColor + } else { + component.foreground = + JavaHighlightingColors.KEYWORD.fallbackAttributeKey!!.defaultAttributes.foregroundColor + } + } - val chosenClassName: String - get() = listenerNameTextField.text + textField() + .bindText(listenerNameProperty) + .columns(COLUMNS_LARGE) + .focused() + .apply { + component.font = EditorUtil.getEditorFont() + } + } - companion object { - private val innerContentPanelConstraints = GridConstraints() + if (panel != null) { + separator() - init { - innerContentPanelConstraints.row = 0 - innerContentPanelConstraints.column = 0 - innerContentPanelConstraints.rowSpan = 1 - innerContentPanelConstraints.colSpan = 1 - innerContentPanelConstraints.anchor = GridConstraints.ANCHOR_CENTER - innerContentPanelConstraints.fill = GridConstraints.FILL_BOTH - innerContentPanelConstraints.hSizePolicy = GridConstraints.SIZEPOLICY_FIXED - innerContentPanelConstraints.vSizePolicy = GridConstraints.SIZEPOLICY_FIXED + row { + cell(panel) + } + } } } } diff --git a/src/main/kotlin/nbt/lang/colors/NbttColorSettingsPage.kt b/src/main/kotlin/nbt/lang/colors/NbttColorSettingsPage.kt index 712fce2de..dca1c1108 100644 --- a/src/main/kotlin/nbt/lang/colors/NbttColorSettingsPage.kt +++ b/src/main/kotlin/nbt/lang/colors/NbttColorSettingsPage.kt @@ -88,21 +88,24 @@ class NbttColorSettingsPage : ColorSettingsPage { companion object { private val DESCRIPTORS = arrayOf( - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.keyword.display_name"), KEYWORD), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.string.display_name"), STRING), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.unquoted_string.display_name"), UNQUOTED_STRING), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.name.display_name"), STRING_NAME), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.keyword.display_name"), KEYWORD), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.string.display_name"), STRING), AttributesDescriptor( - MCDevBundle("nbt.lang.highlighting.unquoted_name.display_name"), + MCDevBundle.pointer("nbt.lang.highlighting.unquoted_string.display_name"), + UNQUOTED_STRING + ), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.name.display_name"), STRING_NAME), + AttributesDescriptor( + MCDevBundle.pointer("nbt.lang.highlighting.unquoted_name.display_name"), UNQUOTED_STRING_NAME ), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.byte.display_name"), BYTE), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.short.display_name"), SHORT), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.int.display_name"), INT), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.long.display_name"), LONG), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.float.display_name"), FLOAT), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.double.display_name"), DOUBLE), - AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.material.display_name"), MATERIAL), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.byte.display_name"), BYTE), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.short.display_name"), SHORT), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.int.display_name"), INT), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.long.display_name"), LONG), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.float.display_name"), FLOAT), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.double.display_name"), DOUBLE), + AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.material.display_name"), MATERIAL), ) private val map = mapOf( diff --git a/src/main/kotlin/platform/architectury/ArchitecturyVersion.kt b/src/main/kotlin/platform/architectury/ArchitecturyVersion.kt index dae07b3d7..e5df5ce13 100644 --- a/src/main/kotlin/platform/architectury/ArchitecturyVersion.kt +++ b/src/main/kotlin/platform/architectury/ArchitecturyVersion.kt @@ -29,21 +29,13 @@ import com.github.kittinunf.fuel.core.requests.suspendable import com.github.kittinunf.fuel.coroutines.awaitString import com.google.gson.Gson import com.google.gson.annotations.SerializedName -import java.io.IOException class ArchitecturyVersion private constructor( val versions: Map>, ) { fun getArchitecturyVersions(mcVersion: SemanticVersion): List { - return try { - val architecturyVersions = versions[mcVersion] - ?: throw IOException("Could not find any architectury versions for $mcVersion") - architecturyVersions.take(50) - } catch (e: IOException) { - e.printStackTrace() - emptyList() - } + return versions[mcVersion].orEmpty().take(50) } data class ModrinthVersionApi( diff --git a/src/main/kotlin/platform/bukkit/generation/BukkitEventGenerationPanel.form b/src/main/kotlin/platform/bukkit/generation/BukkitEventGenerationPanel.form deleted file mode 100644 index 9ae1aed1f..000000000 --- a/src/main/kotlin/platform/bukkit/generation/BukkitEventGenerationPanel.form +++ /dev/null @@ -1,48 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/main/kotlin/platform/bukkit/generation/BukkitEventGenerationPanel.kt b/src/main/kotlin/platform/bukkit/generation/BukkitEventGenerationPanel.kt index 771e6750f..8a2230377 100644 --- a/src/main/kotlin/platform/bukkit/generation/BukkitEventGenerationPanel.kt +++ b/src/main/kotlin/platform/bukkit/generation/BukkitEventGenerationPanel.kt @@ -20,40 +20,41 @@ package com.demonwav.mcdev.platform.bukkit.generation +import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.insight.generation.GenerationData import com.demonwav.mcdev.insight.generation.ui.EventGenerationPanel +import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.psi.PsiClass -import javax.swing.JCheckBox -import javax.swing.JComboBox +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.panel import javax.swing.JPanel class BukkitEventGenerationPanel(chosenClass: PsiClass) : EventGenerationPanel(chosenClass) { - private lateinit var ignoreCanceledCheckBox: JCheckBox - private lateinit var parentPanel: JPanel - private lateinit var eventPriorityComboBox: JComboBox + private val graph = PropertyGraph("BukkitEventGenerationPanel graph") - override val panel: JPanel - get() { - ignoreCanceledCheckBox.isSelected = true + private val ignoreCanceledProperty = graph.property(true) + private val eventPriorityProperty = graph.property("NORMAL") - // Not static because the form builder is not reliable - eventPriorityComboBox.addItem("MONITOR") - eventPriorityComboBox.addItem("HIGHEST") - eventPriorityComboBox.addItem("HIGH") - eventPriorityComboBox.addItem("NORMAL") - eventPriorityComboBox.addItem("LOW") - eventPriorityComboBox.addItem("LOWEST") + override val panel: JPanel by lazy { + panel { + row { + checkBox(MCDevBundle("generate.event_listener.ignore_if_canceled")) + .bindSelected(ignoreCanceledProperty) + } - eventPriorityComboBox.selectedIndex = 3 - - return parentPanel + row(MCDevBundle("generate.event_listener.event_priority")) { + comboBox(listOf("MONITOR", "HIGHEST", "HIGH", "NORMAL", "LOW", "LOWEST")) + .bindItem(eventPriorityProperty) + } } + } override fun gatherData(): GenerationData { return BukkitGenerationData( - ignoreCanceledCheckBox.isSelected, - eventPriorityComboBox.selectedItem?.toString() ?: error("No selected item") + ignoreCanceledProperty.get(), + eventPriorityProperty.get() ) } } diff --git a/src/main/kotlin/platform/bungeecord/generation/BungeeCordEventGenerationPanel.form b/src/main/kotlin/platform/bungeecord/generation/BungeeCordEventGenerationPanel.form deleted file mode 100644 index 45a1c7c42..000000000 --- a/src/main/kotlin/platform/bungeecord/generation/BungeeCordEventGenerationPanel.form +++ /dev/null @@ -1,40 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/main/kotlin/platform/bungeecord/generation/BungeeCordEventGenerationPanel.kt b/src/main/kotlin/platform/bungeecord/generation/BungeeCordEventGenerationPanel.kt index d950005e7..b04f92156 100644 --- a/src/main/kotlin/platform/bungeecord/generation/BungeeCordEventGenerationPanel.kt +++ b/src/main/kotlin/platform/bungeecord/generation/BungeeCordEventGenerationPanel.kt @@ -20,29 +20,28 @@ package com.demonwav.mcdev.platform.bungeecord.generation +import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.insight.generation.ui.EventGenerationPanel +import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.psi.PsiClass -import javax.swing.JComboBox +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.panel import javax.swing.JPanel class BungeeCordEventGenerationPanel(chosenClass: PsiClass) : EventGenerationPanel(chosenClass) { - private lateinit var eventPriorityComboBox: JComboBox - private lateinit var parentPanel: JPanel + private val graph = PropertyGraph("BungeeCordEventGenerationPanel graph") - override val panel: JPanel - get() { - // Not static because the form builder is not reliable - eventPriorityComboBox.addItem("HIGHEST") - eventPriorityComboBox.addItem("HIGH") - eventPriorityComboBox.addItem("NORMAL") - eventPriorityComboBox.addItem("LOW") - eventPriorityComboBox.addItem("LOWEST") + private val eventPriorityProperty = graph.property("NORMAL") - eventPriorityComboBox.selectedIndex = 2 - - return parentPanel + override val panel: JPanel by lazy { + panel { + row(MCDevBundle("generate.event_listener.event_priority")) { + comboBox(listOf("HIGHEST", "HIGH", "NORMAL", "LOW", "LOWEST")) + .bindItem(eventPriorityProperty) + } } + } - override fun gatherData() = BungeeCordGenerationData(eventPriorityComboBox.selectedItem.toString()) + override fun gatherData() = BungeeCordGenerationData(eventPriorityProperty.get()) } diff --git a/src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt b/src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt index 3ecde42dd..7092fca87 100644 --- a/src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt +++ b/src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt @@ -22,12 +22,15 @@ package com.demonwav.mcdev.platform.fabric.inspection import com.demonwav.mcdev.platform.fabric.reference.EntryPointReference import com.demonwav.mcdev.platform.fabric.util.FabricConstants +import com.demonwav.mcdev.util.equivalentTo import com.intellij.codeInspection.InspectionManager import com.intellij.codeInspection.LocalInspectionTool import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeInspection.ProblemHighlightType import com.intellij.codeInspection.ProblemsHolder +import com.intellij.json.psi.JsonArray import com.intellij.json.psi.JsonElementVisitor +import com.intellij.json.psi.JsonLiteral import com.intellij.json.psi.JsonProperty import com.intellij.json.psi.JsonStringLiteral import com.intellij.psi.JavaPsiFacade @@ -79,8 +82,7 @@ class FabricEntrypointsInspection : LocalInspectionTool() { val element = resolved.singleOrNull()?.element when { element is PsiClass && !literal.text.contains("::") -> { - val propertyKey = literal.parentOfType()?.name - val expectedType = propertyKey?.let { FabricConstants.ENTRYPOINT_BY_TYPE[it] } + val (propertyKey, expectedType) = findEntrypointKeyAndType(literal) if (propertyKey != null && expectedType != null && !isEntrypointOfCorrectType(element, propertyKey) ) { @@ -111,21 +113,43 @@ class FabricEntrypointsInspection : LocalInspectionTool() { reference.rangeInElement, ) } + + if (!element.hasModifierProperty(PsiModifier.PUBLIC)) { + holder.registerProblem( + literal, + "Entrypoint method must be public", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + reference.rangeInElement, + ) + } + + if (!element.hasModifierProperty(PsiModifier.STATIC)) { + val clazz = element.containingClass + if (clazz != null && clazz.constructors.isNotEmpty() && + clazz.constructors.find { !it.hasParameters() } == null + ) { + holder.registerProblem( + literal, + "Entrypoint instance method class must have an empty constructor", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + reference.rangeInElement, + ) + } + } } element is PsiField -> { - if (!element.hasModifierProperty(PsiModifier.STATIC)) { + if (!element.hasModifierProperty(PsiModifier.PUBLIC)) { holder.registerProblem( literal, - "Entrypoint field must be static", + "Entrypoint field must be public", ProblemHighlightType.GENERIC_ERROR_OR_WARNING, reference.rangeInElement, ) } - val propertyKey = literal.parentOfType()?.name + val (propertyKey, expectedType) = findEntrypointKeyAndType(literal) val fieldTypeClass = (element.type as? PsiClassType)?.resolve() - val expectedType = propertyKey?.let { FabricConstants.ENTRYPOINT_BY_TYPE[it] } if (propertyKey != null && fieldTypeClass != null && expectedType != null && !isEntrypointOfCorrectType(fieldTypeClass, propertyKey) ) { @@ -141,11 +165,21 @@ class FabricEntrypointsInspection : LocalInspectionTool() { } } + private fun findEntrypointKeyAndType(literal: JsonLiteral): Pair { + val propertyKey = when (val parent = literal.parent) { + is JsonArray -> (parent.parent as? JsonProperty)?.name + is JsonProperty -> parent.parentOfType()?.name + else -> null + } + val expectedType = propertyKey?.let { FabricConstants.ENTRYPOINT_BY_TYPE[it] } + return propertyKey to expectedType + } + private fun isEntrypointOfCorrectType(element: PsiClass, type: String): Boolean { val entrypointClass = FabricConstants.ENTRYPOINT_BY_TYPE[type] ?: return false val clazz = JavaPsiFacade.getInstance(element.project).findClass(entrypointClass, element.resolveScope) - return clazz != null && element.isInheritor(clazz, true) + return clazz != null && (element.equivalentTo(clazz) || element.isInheritor(clazz, true)) } } } diff --git a/src/main/kotlin/platform/fabric/reference/EntryPointReference.kt b/src/main/kotlin/platform/fabric/reference/EntryPointReference.kt index 796058d64..1b8041c8a 100644 --- a/src/main/kotlin/platform/fabric/reference/EntryPointReference.kt +++ b/src/main/kotlin/platform/fabric/reference/EntryPointReference.kt @@ -25,6 +25,8 @@ import com.demonwav.mcdev.util.fullQualifiedName import com.demonwav.mcdev.util.manipulator import com.demonwav.mcdev.util.reference.InspectionReference import com.intellij.codeInsight.completion.JavaLookupElementBuilder +import com.intellij.json.psi.JsonArray +import com.intellij.json.psi.JsonProperty import com.intellij.json.psi.JsonStringLiteral import com.intellij.openapi.util.TextRange import com.intellij.psi.JavaPsiFacade @@ -40,7 +42,9 @@ import com.intellij.psi.PsiReference import com.intellij.psi.PsiReferenceBase import com.intellij.psi.PsiReferenceProvider import com.intellij.psi.ResolveResult +import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.searches.ClassInheritorsSearch +import com.intellij.psi.util.parentOfType import com.intellij.util.ArrayUtil import com.intellij.util.IncorrectOperationException import com.intellij.util.ProcessingContext @@ -136,27 +140,17 @@ object EntryPointReference : PsiReferenceProvider() { fun isEntryPointReference(reference: PsiReference) = reference is Reference - fun isValidEntrypointClass(element: PsiClass): Boolean { - val psiFacade = JavaPsiFacade.getInstance(element.project) - var inheritsEntrypointInterface = false - for (entrypoint in FabricConstants.ENTRYPOINTS) { - val entrypointClass = psiFacade.findClass(entrypoint, element.resolveScope) - ?: continue - if (element.isInheritor(entrypointClass, true)) { - inheritsEntrypointInterface = true - break - } - } - return inheritsEntrypointInterface + fun isValidEntrypointClass(element: PsiClass, entrypointClass: PsiClass): Boolean { + return element.isInheritor(entrypointClass, true) } - fun isValidEntrypointField(field: PsiField): Boolean { + fun isValidEntrypointField(field: PsiField, entrypointClass: PsiClass): Boolean { if (!field.hasModifierProperty(PsiModifier.PUBLIC) || !field.hasModifierProperty(PsiModifier.STATIC)) { return false } val fieldTypeClass = (field.type as? PsiClassType)?.resolve() - return fieldTypeClass != null && isValidEntrypointClass(fieldTypeClass) + return fieldTypeClass != null && isValidEntrypointClass(fieldTypeClass, entrypointClass) } fun isValidEntrypointMethod(method: PsiMethod): Boolean { @@ -228,30 +222,36 @@ object EntryPointReference : PsiReferenceProvider() { val text = element.text.substring(range.startOffset, range.endOffset) val parts = text.split("::", limit = 2) + val psiFacade = JavaPsiFacade.getInstance(element.project) + val entrypointType = getEntrypointType()?.let(FabricConstants.ENTRYPOINT_BY_TYPE::get) + ?: return ArrayUtil.EMPTY_OBJECT_ARRAY + val entrypointClass = psiFacade.findClass(entrypointType, element.resolveScope) + ?: return ArrayUtil.EMPTY_OBJECT_ARRAY + val variants = mutableListOf() if (!isMemberReference) { - val psiFacade = JavaPsiFacade.getInstance(element.project) - for (entrypoint in FabricConstants.ENTRYPOINTS) { - val entrypointClass = psiFacade.findClass(entrypoint, element.resolveScope) - ?: continue - ClassInheritorsSearch.search(entrypointClass, true) - .mapNotNullTo(variants) { - val shortName = it.name ?: return@mapNotNullTo null - val fqName = it.fullQualifiedName ?: return@mapNotNullTo null - JavaLookupElementBuilder.forClass(it, fqName, true).withPresentableText(shortName) - } - } + val scope = element.resolveScope.intersectWith(GlobalSearchScope.projectScope(element.project)) + ClassInheritorsSearch.search(entrypointClass, scope, true) + .mapNotNullTo(variants) { + val shortName = it.name ?: return@mapNotNullTo null + val fqName = it.fullQualifiedName ?: return@mapNotNullTo null + JavaLookupElementBuilder.forClass(it, fqName, true).withPresentableText(shortName) + } } else if (parts.size >= 2) { - val psiFacade = JavaPsiFacade.getInstance(element.project) val className = parts[0].replace('$', '.') val clazz = psiFacade.findClass(className, element.resolveScope) if (clazz != null) { - clazz.fields.filterTo(variants, ::isValidEntrypointField) + clazz.fields.filterTo(variants) { isValidEntrypointField(it, entrypointClass) } clazz.methods.filterTo(variants, ::isValidEntrypointMethod) } } return variants.toTypedArray() } + + private fun getEntrypointType(): String? { + val entrypointsProperty = element.parentOfType()?.parent as? JsonProperty + return entrypointsProperty?.name + } } } diff --git a/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt b/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt index 896da285e..758e3e890 100644 --- a/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt +++ b/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt @@ -23,9 +23,11 @@ package com.demonwav.mcdev.platform.fabric.reference import com.demonwav.mcdev.platform.fabric.util.FabricConstants import com.demonwav.mcdev.util.isPropertyValue import com.intellij.json.psi.JsonArray +import com.intellij.json.psi.JsonElement import com.intellij.json.psi.JsonObject import com.intellij.json.psi.JsonStringLiteral import com.intellij.patterns.PlatformPatterns +import com.intellij.patterns.StandardPatterns import com.intellij.psi.PsiReferenceContributor import com.intellij.psi.PsiReferenceRegistrar @@ -34,19 +36,25 @@ class FabricReferenceContributor : PsiReferenceContributor() { val stringInModJson = PlatformPatterns.psiElement(JsonStringLiteral::class.java) .inVirtualFile(PlatformPatterns.virtualFile().withName(FabricConstants.FABRIC_MOD_JSON)) - val entryPointPattern = stringInModJson.withParent( - PlatformPatterns.psiElement(JsonArray::class.java) - .withSuperParent( - 2, - PlatformPatterns.psiElement(JsonObject::class.java).isPropertyValue("entrypoints"), - ), - ) + val entrypointsArray = PlatformPatterns.psiElement(JsonArray::class.java) + .withSuperParent(2, PlatformPatterns.psiElement(JsonObject::class.java).isPropertyValue("entrypoints")) + val entryPointSimplePattern = stringInModJson.withParent(entrypointsArray) + val entryPointObjectPattern = stringInModJson.isPropertyValue("value") + .withSuperParent(2, PlatformPatterns.psiElement(JsonObject::class.java).withParent(entrypointsArray)) + val entryPointPattern = StandardPatterns.or(entryPointSimplePattern, entryPointObjectPattern) registrar.registerReferenceProvider(entryPointPattern, EntryPointReference) - val mixinConfigPattern = stringInModJson.withParent( + val mixinConfigSimplePattern = stringInModJson.withParent( PlatformPatterns.psiElement(JsonArray::class.java).isPropertyValue("mixins"), ) - registrar.registerReferenceProvider(mixinConfigPattern, ResourceFileReference("mixin config '%s'")) + val mixinsConfigArray = PlatformPatterns.psiElement(JsonArray::class.java).isPropertyValue("mixins") + val mixinConfigObjectPattern = stringInModJson.isPropertyValue("config") + .withSuperParent(2, PlatformPatterns.psiElement(JsonElement::class.java).withParent(mixinsConfigArray)) + val mixinConfigPattern = StandardPatterns.or(mixinConfigSimplePattern, mixinConfigObjectPattern) + registrar.registerReferenceProvider( + mixinConfigPattern, + ResourceFileReference("mixin config '%s'", Regex("(.+)\\.mixins\\.json")) + ) registrar.registerReferenceProvider( stringInModJson.isPropertyValue("accessWidener"), diff --git a/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt b/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt index 834ae6e0c..1088b03e9 100644 --- a/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt +++ b/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt @@ -27,8 +27,12 @@ import com.demonwav.mcdev.util.manipulator import com.demonwav.mcdev.util.mapFirstNotNull import com.demonwav.mcdev.util.reference.InspectionReference 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.project.rootManager import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.vfs.findPsiFile import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager @@ -37,13 +41,17 @@ import com.intellij.psi.PsiReferenceBase import com.intellij.psi.PsiReferenceProvider import com.intellij.util.IncorrectOperationException import com.intellij.util.ProcessingContext +import org.jetbrains.jps.model.java.JavaResourceRootType -class ResourceFileReference(private val description: String) : PsiReferenceProvider() { +class ResourceFileReference( + private val description: String, + private val filenamePattern: Regex? = null +) : PsiReferenceProvider() { override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array { return arrayOf(Reference(description, element as JsonStringLiteral)) } - private class Reference(desc: String, element: JsonStringLiteral) : + private inner class Reference(desc: String, element: JsonStringLiteral) : PsiReferenceBase(element), InspectionReference { override val description = desc @@ -61,6 +69,9 @@ class ResourceFileReference(private val description: String) : PsiReferenceProvi ?: ModuleRootManager.getInstance(module) .getDependencies(false) .mapFirstNotNull(::findFileIn) + ?: ModuleManager.getInstance(element.project) + .getModuleDependentModules(module) + .mapFirstNotNull(::findFileIn) } override fun bindToElement(newTarget: PsiElement): PsiElement? { @@ -70,5 +81,30 @@ class ResourceFileReference(private val description: String) : PsiReferenceProvi val manipulator = element.manipulator ?: return null return manipulator.handleContentChange(element, manipulator.getRangeInElement(element), newTarget.name) } + + override fun getVariants(): Array { + if (filenamePattern == null) { + return emptyArray() + } + + val module = element.findModule() ?: return emptyArray() + val variants = mutableListOf() + val relevantModules = ModuleManager.getInstance(element.project).getModuleDependentModules(module) + module + runReadAction { + val relevantRoots = relevantModules.flatMap { + it.rootManager.getSourceRoots(JavaResourceRootType.RESOURCE) + } + for (roots in relevantRoots) { + for (child in roots.children) { + val relativePath = child.path.removePrefix(roots.path) + val testRelativePath = "/$relativePath" + if (testRelativePath.matches(filenamePattern)) { + variants.add(child.findPsiFile(element.project) ?: relativePath) + } + } + } + } + return variants.toTypedArray() + } } } diff --git a/src/main/kotlin/platform/fabric/util/FabricVersions.kt b/src/main/kotlin/platform/fabric/util/FabricVersions.kt index 9e1a03167..7b898ad78 100644 --- a/src/main/kotlin/platform/fabric/util/FabricVersions.kt +++ b/src/main/kotlin/platform/fabric/util/FabricVersions.kt @@ -20,6 +20,7 @@ package com.demonwav.mcdev.platform.fabric.util +import com.demonwav.mcdev.creator.custom.model.TemplateApi import com.demonwav.mcdev.creator.selectProxy import com.demonwav.mcdev.update.PluginUtil import com.demonwav.mcdev.util.SemanticVersion @@ -36,6 +37,7 @@ class FabricVersions(val game: List, val mappings: List, val loa class Game(val version: String, val stable: Boolean) class Mappings(val gameVersion: String, val version: YarnVersion) + @TemplateApi class YarnVersion(val name: String, val build: Int) : Comparable { override fun toString() = name override fun compareTo(other: YarnVersion) = build.compareTo(other.build) diff --git a/src/main/kotlin/platform/mcp/fabricloom/FabricLoomProjectResolverExtension.kt b/src/main/kotlin/platform/mcp/fabricloom/FabricLoomProjectResolverExtension.kt index 82ec68855..b89478dcc 100644 --- a/src/main/kotlin/platform/mcp/fabricloom/FabricLoomProjectResolverExtension.kt +++ b/src/main/kotlin/platform/mcp/fabricloom/FabricLoomProjectResolverExtension.kt @@ -20,10 +20,13 @@ package com.demonwav.mcdev.platform.mcp.fabricloom +import com.demonwav.mcdev.platform.mcp.McpModuleSettings +import com.demonwav.mcdev.platform.mcp.gradle.McpModelData import com.demonwav.mcdev.platform.mcp.gradle.tooling.fabricloom.FabricLoomModel import com.intellij.openapi.externalSystem.model.DataNode import com.intellij.openapi.externalSystem.model.project.ModuleData import org.gradle.tooling.model.idea.IdeaModule +import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData import org.jetbrains.plugins.gradle.service.project.AbstractProjectResolverExtension class FabricLoomProjectResolverExtension : AbstractProjectResolverExtension() { @@ -50,6 +53,23 @@ class FabricLoomProjectResolverExtension : AbstractProjectResolverExtension() { loomData.modSourceSets ) ideModule.createChild(FabricLoomData.KEY, data) + + val mcpData = McpModelData( + ideModule.data, + McpModuleSettings.State( + minecraftVersion = loomData.minecraftVersion, + ), + null, + null + ) + ideModule.createChild(McpModelData.KEY, mcpData) + + for (child in ideModule.children) { + val childData = child.data + if (childData is GradleSourceSetData) { + child.createChild(McpModelData.KEY, mcpData.copy(module = childData)) + } + } } super.populateModuleExtraModels(gradleModule, ideModule) diff --git a/src/main/kotlin/platform/mcp/gradle/McpProjectResolverExtension.kt b/src/main/kotlin/platform/mcp/gradle/McpProjectResolverExtension.kt index 53b90142a..5d45a3d79 100644 --- a/src/main/kotlin/platform/mcp/gradle/McpProjectResolverExtension.kt +++ b/src/main/kotlin/platform/mcp/gradle/McpProjectResolverExtension.kt @@ -23,9 +23,11 @@ package com.demonwav.mcdev.platform.mcp.gradle import com.demonwav.mcdev.platform.mcp.gradle.datahandler.McpModelFG2Handler import com.demonwav.mcdev.platform.mcp.gradle.datahandler.McpModelFG3Handler import com.demonwav.mcdev.platform.mcp.gradle.datahandler.McpModelNG7Handler +import com.demonwav.mcdev.platform.mcp.gradle.datahandler.McpModelNMDHandler import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelFG2 import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelFG3 import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelNG7 +import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelNMD import com.demonwav.mcdev.util.runGradleTask import com.intellij.openapi.externalSystem.model.DataNode import com.intellij.openapi.externalSystem.model.project.ModuleData @@ -38,7 +40,7 @@ class McpProjectResolverExtension : AbstractProjectResolverExtension() { // Register our custom Gradle tooling API model in IntelliJ's project resolver override fun getExtraProjectModelClasses(): Set> = - setOf(McpModelFG2::class.java, McpModelFG3::class.java, McpModelNG7::class.java) + setOf(McpModelFG2::class.java, McpModelFG3::class.java, McpModelNG7::class.java, McpModelNMD::class.java) override fun getToolingExtensionsClasses() = extraProjectModelClasses @@ -91,6 +93,6 @@ class McpProjectResolverExtension : AbstractProjectResolverExtension() { } companion object { - private val handlers = listOf(McpModelFG2Handler, McpModelFG3Handler, McpModelNG7Handler) + private val handlers = listOf(McpModelFG2Handler, McpModelFG3Handler, McpModelNG7Handler, McpModelNMDHandler) } } diff --git a/src/main/kotlin/platform/mcp/gradle/datahandler/McpModelNMDHandler.kt b/src/main/kotlin/platform/mcp/gradle/datahandler/McpModelNMDHandler.kt new file mode 100644 index 000000000..ca496ad49 --- /dev/null +++ b/src/main/kotlin/platform/mcp/gradle/datahandler/McpModelNMDHandler.kt @@ -0,0 +1,76 @@ +/* + * 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.gradle.datahandler + +import com.demonwav.mcdev.platform.mcp.McpModuleSettings +import com.demonwav.mcdev.platform.mcp.at.AtFileType +import com.demonwav.mcdev.platform.mcp.gradle.McpModelData +import com.demonwav.mcdev.platform.mcp.gradle.tooling.McpModelNMD +import com.demonwav.mcdev.platform.mcp.srg.SrgType +import com.demonwav.mcdev.util.runWriteTaskLater +import com.intellij.openapi.externalSystem.model.DataNode +import com.intellij.openapi.externalSystem.model.project.ModuleData +import com.intellij.openapi.fileTypes.ExactFileNameMatcher +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.openapi.vfs.LocalFileSystem +import org.gradle.tooling.model.idea.IdeaModule +import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData +import org.jetbrains.plugins.gradle.service.project.ProjectResolverContext + +object McpModelNMDHandler : McpModelDataHandler { + + override fun build( + gradleModule: IdeaModule, + node: DataNode, + resolverCtx: ProjectResolverContext, + ) { + val data = resolverCtx.getExtraProject(gradleModule, McpModelNMD::class.java) ?: return + + val state = McpModuleSettings.State( + "1." + data.neoForgeVersion.substringBefore('.'), + null, + data.mappingsFile?.absolutePath, + SrgType.TSRG, + data.neoForgeVersion, + ) + + val ats = data.accessTransformers + if (ats != null && ats.isNotEmpty()) { + runWriteTaskLater { + for (at in ats) { + val fileTypeManager = FileTypeManager.getInstance() + val atFile = LocalFileSystem.getInstance().findFileByIoFile(at) ?: continue + fileTypeManager.associate(AtFileType, ExactFileNameMatcher(atFile.name)) + } + } + } + + val modelData = McpModelData(node.data, state, null, data.accessTransformers) + node.createChild(McpModelData.KEY, modelData) + + for (child in node.children) { + val childData = child.data + if (childData is GradleSourceSetData) { + child.createChild(McpModelData.KEY, modelData.copy(module = childData)) + } + } + } +} diff --git a/src/main/kotlin/platform/mcp/mappings/HardcodedYarnToMojmap.kt b/src/main/kotlin/platform/mcp/mappings/HardcodedYarnToMojmap.kt index 5f3182872..e2919e8e0 100644 --- a/src/main/kotlin/platform/mcp/mappings/HardcodedYarnToMojmap.kt +++ b/src/main/kotlin/platform/mcp/mappings/HardcodedYarnToMojmap.kt @@ -43,6 +43,15 @@ object HardcodedYarnToMojmap { owner = "net.minecraft.network.chat.Component", name = "translatableEscape", descriptor = "(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/network/chat/MutableComponent;" + ), + MemberReference( + owner = "net.minecraft.client.resource.language.I18n", + name = "translate", + descriptor = "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;" + ) mapTo MemberReference( + owner = "net.minecraft.client.resources.language.I18n", + name = "get", + descriptor = "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;" ) ), hashMapOf(), diff --git a/src/main/kotlin/platform/mcp/mappings/Mappings.kt b/src/main/kotlin/platform/mcp/mappings/Mappings.kt index 9ddf8e6a6..1135d40e9 100644 --- a/src/main/kotlin/platform/mcp/mappings/Mappings.kt +++ b/src/main/kotlin/platform/mcp/mappings/Mappings.kt @@ -119,6 +119,13 @@ fun Module.getMappedMethod(mojangClass: String, mojangMethod: String, mojangDesc return getMappedMethod(MemberReference(mojangMethod, mojangDescriptor, mojangClass)) } +fun Module.getMappedMethodCall(mojangClass: String, mojangMethod: String, mojangDescriptor: String, p: String): String { + val mappedMethodRef = namedToMojang?.tryGetMappedMethod( + MemberReference(mojangMethod, mojangDescriptor, mojangClass) + ) ?: return "$mojangClass.$mojangMethod($p)" + return "${mappedMethodRef.owner}.${mappedMethodRef.name}($p)" +} + fun Module.getMojangMethod(mappedMethod: MemberReference): String { return namedToMojang?.getIntermediaryMethod(mappedMethod)?.name ?: return mappedMethod.name } diff --git a/src/main/kotlin/platform/mixin/action/FindMixinsComponent.form b/src/main/kotlin/platform/mixin/action/FindMixinsComponent.form deleted file mode 100644 index 8b9f602e7..000000000 --- a/src/main/kotlin/platform/mixin/action/FindMixinsComponent.form +++ /dev/null @@ -1,26 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/main/kotlin/platform/mixin/action/FindMixinsComponent.kt b/src/main/kotlin/platform/mixin/action/FindMixinsComponent.kt index bf06d7f6e..b0504e6e8 100644 --- a/src/main/kotlin/platform/mixin/action/FindMixinsComponent.kt +++ b/src/main/kotlin/platform/mixin/action/FindMixinsComponent.kt @@ -20,27 +20,26 @@ package com.demonwav.mcdev.platform.mixin.action -import com.demonwav.mcdev.util.toArray import com.intellij.ide.util.PsiClassListCellRenderer import com.intellij.psi.PsiClass import com.intellij.ui.components.JBList +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.panel import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.JPanel -import javax.swing.ListModel class FindMixinsComponent(classes: List) : MouseAdapter() { - private lateinit var classList: JBList - lateinit var panel: JPanel - private set - - init { - @Suppress("UNCHECKED_CAST") - classList.model = JBList.createDefaultListModel(*classes.toArray()) as ListModel - classList.cellRenderer = PsiClassListCellRenderer() + private val classList = JBList(classes).apply { + cellRenderer = PsiClassListCellRenderer() + addMouseListener(this@FindMixinsComponent) + } - classList.addMouseListener(this) + val panel: JPanel = panel { + row { + cell(classList).align(Align.FILL) + } } override fun mouseClicked(e: MouseEvent) { diff --git a/src/main/kotlin/platform/mixin/action/GenerateAccessorAction.kt b/src/main/kotlin/platform/mixin/action/GenerateAccessorAction.kt index 865eb6296..921b260ce 100644 --- a/src/main/kotlin/platform/mixin/action/GenerateAccessorAction.kt +++ b/src/main/kotlin/platform/mixin/action/GenerateAccessorAction.kt @@ -20,6 +20,7 @@ package com.demonwav.mcdev.platform.mixin.action +import com.demonwav.mcdev.platform.mixin.MixinModuleType import com.intellij.codeInsight.FileModificationService import com.intellij.codeInsight.generation.actions.BaseGenerateAction import com.intellij.openapi.application.ApplicationManager @@ -75,7 +76,7 @@ class GenerateAccessorAction : BaseGenerateAction(GenerateAccessorHandler()) { } override fun isValidForFile(project: Project, editor: Editor, file: PsiFile): Boolean { - if (file !is PsiJavaFile) { + if (file !is PsiJavaFile || !MixinModuleType.isInModule(file)) { return false } diff --git a/src/main/kotlin/platform/mixin/action/GenerateShadowAction.kt b/src/main/kotlin/platform/mixin/action/GenerateShadowAction.kt index f53938550..293543c47 100644 --- a/src/main/kotlin/platform/mixin/action/GenerateShadowAction.kt +++ b/src/main/kotlin/platform/mixin/action/GenerateShadowAction.kt @@ -20,7 +20,7 @@ package com.demonwav.mcdev.platform.mixin.action -import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.MinecraftProjectSettings import com.demonwav.mcdev.platform.mixin.util.MixinConstants import com.demonwav.mcdev.platform.mixin.util.findFields import com.demonwav.mcdev.platform.mixin.util.findMethods @@ -237,7 +237,7 @@ private fun copyAnnotation(modifiers: PsiModifierList, newModifiers: PsiModifier } inline fun disableAnnotationWrapping(project: Project, func: () -> Unit) { - if (!MinecraftSettings.instance.isShadowAnnotationsSameLine) { + if (!MinecraftProjectSettings.getInstance(project).isShadowAnnotationsSameLine) { func() return } diff --git a/src/main/kotlin/platform/mixin/completion/MixinCompletionConfidence.kt b/src/main/kotlin/platform/mixin/completion/MixinCompletionConfidence.kt index d8d5535a8..d3a5c35f0 100644 --- a/src/main/kotlin/platform/mixin/completion/MixinCompletionConfidence.kt +++ b/src/main/kotlin/platform/mixin/completion/MixinCompletionConfidence.kt @@ -39,6 +39,7 @@ class MixinCompletionConfidence : CompletionConfidence() { PsiJavaPatterns.psiAnnotation().qName( StandardPatterns.or( StandardPatterns.string().startsWith(MixinConstants.PACKAGE), + StandardPatterns.string().startsWith(MixinConstants.MixinExtras.PACKAGE), StandardPatterns.string() .oneOf(MixinAnnotationHandler.getBuiltinHandlers().map { it.first }.toList()), ) diff --git a/src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.kt b/src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.kt new file mode 100644 index 000000000..147b34023 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.kt @@ -0,0 +1,117 @@ +/* + * 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.expression + +import com.demonwav.mcdev.platform.mixin.MixinModuleType +import com.demonwav.mcdev.platform.mixin.folding.MixinFoldingSettings +import com.demonwav.mcdev.platform.mixin.reference.target.FieldDefinitionReference +import com.demonwav.mcdev.platform.mixin.reference.target.MethodDefinitionReference +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.MemberReference +import com.intellij.lang.ASTNode +import com.intellij.lang.folding.CustomFoldingBuilder +import com.intellij.lang.folding.FoldingDescriptor +import com.intellij.openapi.editor.Document +import com.intellij.openapi.util.TextRange +import com.intellij.psi.JavaRecursiveElementWalkingVisitor +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiJavaFile +import com.intellij.psi.PsiLiteralExpression +import com.intellij.psi.PsiModifierList +import com.intellij.psi.util.PsiTreeUtil + +class MEDefinitionFoldingBuilder : CustomFoldingBuilder() { + override fun isDumbAware() = false + + override fun isRegionCollapsedByDefault(node: ASTNode): Boolean = + MixinFoldingSettings.instance.state.foldDefinitions + + override fun getLanguagePlaceholderText(node: ASTNode, range: TextRange): String { + val psi = node.psi + if (psi is PsiLiteralExpression) { + val value = psi.value as? String ?: return "..." + val memberReference = MemberReference.parse(value) ?: return "..." + return memberReference.presentableText + } + return "..." + } + + override fun buildLanguageFoldRegions( + descriptors: MutableList, + root: PsiElement, + document: Document, + quick: Boolean + ) { + if (root !is PsiJavaFile || !MixinModuleType.isInModule(root)) { + return + } + + root.accept(Visitor(descriptors)) + } + + private class Visitor(private val descriptors: MutableList) : + JavaRecursiveElementWalkingVisitor() { + override fun visitModifierList(list: PsiModifierList) { + val currentDefinitionList = mutableListOf() + val definitionLists = mutableListOf>() + + for (annotation in list.annotations) { + if (annotation.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION)) { + currentDefinitionList += annotation + } else if (currentDefinitionList.isNotEmpty()) { + definitionLists += currentDefinitionList.toList() + currentDefinitionList.clear() + } + } + + if (currentDefinitionList.isNotEmpty()) { + definitionLists += currentDefinitionList + } + + if (definitionLists.isEmpty()) { + return + } + + for (definitionList in definitionLists) { + val range = TextRange( + definitionList.first().parameterList.firstChild.nextSibling.textRange.startOffset, + PsiTreeUtil.getDeepestVisibleLast(definitionList.last())!!.textRange.startOffset, + ) + if (!range.isEmpty) { + descriptors.add(FoldingDescriptor(list.node, range)) + } + } + + super.visitModifierList(list) + } + + override fun visitLiteralExpression(expression: PsiLiteralExpression) { + if (FieldDefinitionReference.ELEMENT_PATTERN.accepts(expression) || + MethodDefinitionReference.ELEMENT_PATTERN.accepts(expression) + ) { + if (MemberReference.parse(expression.value as String) != null) { + descriptors.add(FoldingDescriptor(expression.node, expression.textRange)) + } + } + } + } +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionAnnotator.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionAnnotator.kt new file mode 100644 index 000000000..638bf6f13 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionAnnotator.kt @@ -0,0 +1,368 @@ +/* + * 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.expression + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArrayAccessExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEBinaryExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEBoundMethodReferenceExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEDeclaration +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEDeclarationItem +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEFreeMethodReferenceExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MELitExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEMemberAccessExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEMethodCallExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENameExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENewExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStaticMethodCallExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MESuperCallExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype +import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.findMultiInjectionHost +import com.intellij.codeInsight.AutoPopupController +import com.intellij.codeInspection.InspectionManager +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement +import com.intellij.codeInspection.ProblemHighlightType +import com.intellij.codeInspection.RemoveAnnotationQuickFix +import com.intellij.lang.annotation.AnnotationBuilder +import com.intellij.lang.annotation.AnnotationHolder +import com.intellij.lang.annotation.Annotator +import com.intellij.lang.annotation.HighlightSeverity +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiModifierListOwner +import com.intellij.psi.impl.source.tree.injected.InjectedLanguageEditorUtil +import com.intellij.psi.search.searches.ReferencesSearch +import com.intellij.psi.util.TypeConversionUtil +import com.intellij.psi.util.parentOfType + +class MEExpressionAnnotator : Annotator { + override fun annotate(element: PsiElement, holder: AnnotationHolder) { + when (element) { + is MEDeclaration -> { + val parent = element.parent as? MEDeclarationItem ?: return + if (parent.isType) { + highlightDeclaration(holder, element, MEExpressionSyntaxHighlighter.IDENTIFIER_TYPE_DECLARATION) + } else { + highlightDeclaration(holder, element, MEExpressionSyntaxHighlighter.IDENTIFIER_DECLARATION) + } + } + is MEName -> { + if (!element.isWildcard) { + when (val parent = element.parent) { + is METype, + is MENewExpression -> highlightType(holder, element) + is MEMemberAccessExpression -> highlightVariable( + holder, + element, + MEExpressionSyntaxHighlighter.IDENTIFIER_MEMBER_NAME, + true, + ) + is MESuperCallExpression, + is MEMethodCallExpression, + is MEStaticMethodCallExpression, + is MEBoundMethodReferenceExpression, + is MEFreeMethodReferenceExpression -> highlightVariable( + holder, + element, + MEExpressionSyntaxHighlighter.IDENTIFIER_CALL, + false, + ) + is MENameExpression -> { + if (METypeUtil.isExpressionDirectlyInTypePosition(parent)) { + highlightType(holder, element) + } else { + highlightVariable( + holder, + element, + MEExpressionSyntaxHighlighter.IDENTIFIER_VARIABLE, + false, + ) + } + } + else -> highlightType(holder, element) + } + } + } + is MELitExpression -> { + val minusToken = element.minusToken + if (minusToken != null) { + holder.newSilentAnnotation(HighlightSeverity.TEXT_ATTRIBUTES) + .range(minusToken) + .textAttributes(MEExpressionSyntaxHighlighter.NUMBER) + .create() + } + + if (!element.isNull && !element.isString && element.value == null) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.invalid_number") + ) + .range(element) + .create() + } + } + is MEBinaryExpression -> { + val rightExpr = element.rightExpr + if (element.operator == MEExpressionTypes.TOKEN_INSTANCEOF && + rightExpr !is MENameExpression && + rightExpr !is MEArrayAccessExpression && + rightExpr != null + ) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.instanceof_non_type") + ) + .range(rightExpr) + .create() + } + } + is MEArrayAccessExpression -> { + if (METypeUtil.isExpressionDirectlyInTypePosition(element)) { + val indexExpr = element.indexExpr + if (indexExpr != null) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.index_not_expected_in_type"), + ) + .range(indexExpr) + .create() + } + val arrayExpr = element.arrayExpr + if (arrayExpr !is MEArrayAccessExpression && arrayExpr !is MENameExpression) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.instanceof_non_type"), + ) + .range(arrayExpr) + .create() + } + } else if (element.indexExpr == null) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.array_access_missing_index"), + ) + .range(element.leftBracketToken) + .create() + } + } + is MENewExpression -> { + if (element.isArrayCreation) { + val initializer = element.arrayInitializer + if (initializer != null) { + if (element.dimExprs.isNotEmpty()) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.new_array_dim_expr_with_initializer"), + ) + .range(initializer) + .create() + } else if (initializer.expressionList.isEmpty()) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.empty_array_initializer"), + ) + .range(initializer) + .create() + } + } else { + if (element.dimExprs.isEmpty()) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.missing_array_length") + ) + .range(element.dimExprTokens[0].leftBracket) + .create() + } else { + element.dimExprTokens.asSequence().dropWhile { it.expr != null }.forEach { + if (it.expr != null) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.array_length_after_empty") + ) + .range(it.expr) + .create() + } + } + } + } + } else if (!element.hasConstructorArguments) { + val type = element.type + if (type != null) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.new_no_constructor_args_or_array"), + ) + .range(type) + .create() + } + } + } + } + } + + private fun highlightDeclaration( + holder: AnnotationHolder, + declaration: MEDeclaration, + defaultColor: TextAttributesKey, + ) { + val isUnused = ReferencesSearch.search(declaration).findFirst() == null + + if (isUnused) { + val message = MCDevBundle("mixinextras.expression.lang.errors.unused_definition") + val annotation = holder.newAnnotation(HighlightSeverity.WARNING, message) + .range(declaration) + .highlightType(ProblemHighlightType.LIKE_UNUSED_SYMBOL) + + val containingAnnotation = declaration.findMultiInjectionHost()?.parentOfType()?.takeIf { + it.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION) + } + if (containingAnnotation != null) { + val inspectionManager = InspectionManager.getInstance(containingAnnotation.project) + @Suppress("StatefulEp") // IntelliJ is wrong here + val fix = object : RemoveAnnotationQuickFix( + containingAnnotation, + containingAnnotation.parentOfType() + ) { + override fun getFamilyName() = MCDevBundle("mixinextras.expression.lang.errors.unused_symbol.fix") + } + val problemDescriptor = inspectionManager.createProblemDescriptor( + declaration, + message, + fix, + ProblemHighlightType.LIKE_UNKNOWN_SYMBOL, + true + ) + annotation.newLocalQuickFix(fix, problemDescriptor).registerFix() + } + + annotation.create() + } else { + holder.newSilentAnnotation(HighlightSeverity.TEXT_ATTRIBUTES) + .range(declaration) + .textAttributes(defaultColor) + .create() + } + } + + private fun highlightType(holder: AnnotationHolder, type: MEName) { + val typeName = type.text + val isPrimitive = typeName != "void" && TypeConversionUtil.isPrimitive(typeName) + val isUnresolved = !isPrimitive && type.reference?.resolve() == null + + if (isUnresolved) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.unresolved_symbol") + ) + .range(type) + .highlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL) + .withDefinitionFix(type) + .create() + } else { + holder.newSilentAnnotation(HighlightSeverity.TEXT_ATTRIBUTES) + .range(type) + .textAttributes( + if (isPrimitive) { + MEExpressionSyntaxHighlighter.IDENTIFIER_PRIMITIVE_TYPE + } else { + MEExpressionSyntaxHighlighter.IDENTIFIER_CLASS_NAME + } + ) + .create() + } + } + + private fun highlightVariable( + holder: AnnotationHolder, + variable: MEName, + defaultColor: TextAttributesKey, + isMember: Boolean, + ) { + val variableName = variable.text + val isUnresolved = (variableName != "length" || !isMember) && variable.reference?.resolve() == null + + if (isUnresolved) { + holder.newAnnotation( + HighlightSeverity.ERROR, + MCDevBundle("mixinextras.expression.lang.errors.unresolved_symbol") + ) + .range(variable) + .highlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL) + .withDefinitionFix(variable) + .create() + } else { + holder.newSilentAnnotation(HighlightSeverity.TEXT_ATTRIBUTES) + .range(variable) + .textAttributes(defaultColor) + .create() + } + } + + private fun AnnotationBuilder.withDefinitionFix(name: MEName) = + withFix(AddDefinitionInspection(name)) + + private class AddDefinitionInspection(name: MEName) : LocalQuickFixAndIntentionActionOnPsiElement(name) { + private val id = name.text + + override fun getFamilyName(): String = "Add @Definition" + + override fun getText(): String = "$familyName(id = \"$id\")" + + override fun invoke( + project: Project, + file: PsiFile, + editor: Editor?, + startElement: PsiElement, + endElement: PsiElement + ) { + if (editor == null) { + MEExpressionCompletionUtil.addDefinition( + project, + startElement, + id, + "" + ) + return + } + val annotation = MEExpressionCompletionUtil.addDefinition( + project, + startElement, + id, + "dummy" + ) ?: return + val dummy = annotation.findAttribute("dummy") as? PsiElement ?: return + val hostEditor = InjectedLanguageEditorUtil.getTopLevelEditor(editor) + hostEditor.caretModel.moveToOffset(dummy.textOffset) + PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(hostEditor.document) + hostEditor.document.replaceString(dummy.textRange.startOffset, dummy.textRange.endOffset, "") + AutoPopupController.getInstance(project).autoPopupMemberLookup(hostEditor, null) + } + } +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionBraceMatcher.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionBraceMatcher.kt new file mode 100644 index 000000000..323856006 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionBraceMatcher.kt @@ -0,0 +1,41 @@ +/* + * 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.expression + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.intellij.lang.BracePair +import com.intellij.lang.PairedBraceMatcher +import com.intellij.psi.PsiFile +import com.intellij.psi.tree.IElementType + +class MEExpressionBraceMatcher : PairedBraceMatcher { + companion object { + private val PAIRS = arrayOf( + BracePair(MEExpressionTypes.TOKEN_LEFT_PAREN, MEExpressionTypes.TOKEN_RIGHT_PAREN, false), + BracePair(MEExpressionTypes.TOKEN_LEFT_BRACKET, MEExpressionTypes.TOKEN_RIGHT_BRACKET, false), + BracePair(MEExpressionTypes.TOKEN_LEFT_BRACE, MEExpressionTypes.TOKEN_RIGHT_BRACE, false), + ) + } + + override fun getPairs() = PAIRS + override fun isPairedBracesAllowedBeforeType(lbraceType: IElementType, contextType: IElementType?) = true + override fun getCodeConstructStart(file: PsiFile?, openingBraceOffset: Int) = openingBraceOffset +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionColorSettingsPage.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionColorSettingsPage.kt new file mode 100644 index 000000000..964bd4a67 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionColorSettingsPage.kt @@ -0,0 +1,153 @@ +/* + * 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.expression + +import com.demonwav.mcdev.asset.MCDevBundle +import com.demonwav.mcdev.asset.PlatformAssets +import com.intellij.openapi.options.colors.AttributesDescriptor +import com.intellij.openapi.options.colors.ColorDescriptor +import com.intellij.openapi.options.colors.ColorSettingsPage + +class MEExpressionColorSettingsPage : ColorSettingsPage { + companion object { + private val DESCRIPTORS = arrayOf( + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.string.display_name"), + MEExpressionSyntaxHighlighter.STRING + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.string_escape.display_name"), + MEExpressionSyntaxHighlighter.STRING_ESCAPE + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.number.display_name"), + MEExpressionSyntaxHighlighter.NUMBER + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.keyword.display_name"), + MEExpressionSyntaxHighlighter.KEYWORD + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.operator.display_name"), + MEExpressionSyntaxHighlighter.OPERATOR + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.parens.display_name"), + MEExpressionSyntaxHighlighter.PARENS + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.brackets.display_name"), + MEExpressionSyntaxHighlighter.BRACKETS + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.braces.display_name"), + MEExpressionSyntaxHighlighter.BRACES + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.dot.display_name"), + MEExpressionSyntaxHighlighter.DOT + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.method_reference.display_name"), + MEExpressionSyntaxHighlighter.METHOD_REFERENCE + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.comma.display_name"), + MEExpressionSyntaxHighlighter.COMMA + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.capture.display_name"), + MEExpressionSyntaxHighlighter.CAPTURE + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.wildcard.display_name"), + MEExpressionSyntaxHighlighter.WILDCARD + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.identifier.display_name"), + MEExpressionSyntaxHighlighter.IDENTIFIER + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.call_identifier.display_name"), + MEExpressionSyntaxHighlighter.IDENTIFIER_CALL + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.class_name_identifier.display_name"), + MEExpressionSyntaxHighlighter.IDENTIFIER_CLASS_NAME + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.primitive_type_identifier.display_name"), + MEExpressionSyntaxHighlighter.IDENTIFIER_PRIMITIVE_TYPE + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.member_name_identifier.display_name"), + MEExpressionSyntaxHighlighter.IDENTIFIER_MEMBER_NAME + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.variable_identifier.display_name"), + MEExpressionSyntaxHighlighter.IDENTIFIER_VARIABLE + ), + AttributesDescriptor( + MCDevBundle.pointer( + "mixinextras.expression.lang.highlighting.type_declaration_identifier.display_name" + ), + MEExpressionSyntaxHighlighter.IDENTIFIER_TYPE_DECLARATION + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.declaration_identifier.display_name"), + MEExpressionSyntaxHighlighter.IDENTIFIER_DECLARATION + ), + AttributesDescriptor( + MCDevBundle.pointer("mixinextras.expression.lang.highlighting.bad_char.display_name"), + MEExpressionSyntaxHighlighter.BAD_CHAR + ), + ) + + private val TAGS = mapOf( + "call" to MEExpressionSyntaxHighlighter.IDENTIFIER_CALL, + "class_name" to MEExpressionSyntaxHighlighter.IDENTIFIER_CLASS_NAME, + "member_name" to MEExpressionSyntaxHighlighter.IDENTIFIER_MEMBER_NAME, + "primitive_type" to MEExpressionSyntaxHighlighter.IDENTIFIER_PRIMITIVE_TYPE, + "variable" to MEExpressionSyntaxHighlighter.IDENTIFIER_VARIABLE, + ) + } + + override fun getIcon() = PlatformAssets.MIXIN_ICON + override fun getHighlighter() = MEExpressionSyntaxHighlighter() + + override fun getDemoText() = """ + variable.function( + 'a string with \\ escapes', + 123 + @(45), + ?, + ClassName.class, + foo.bar, + new int[] { 1, 2, 3 }, + method::reference, + 'a bad character: ' # other_identifier + )[0] + """.trimIndent() + + override fun getAdditionalHighlightingTagToDescriptorMap() = TAGS + override fun getAttributeDescriptors() = DESCRIPTORS + override fun getColorDescriptors(): Array = ColorDescriptor.EMPTY_ARRAY + override fun getDisplayName() = MCDevBundle("mixinextras.expression.lang.display_name") +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionContributor.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionContributor.kt new file mode 100644 index 000000000..762a7c22c --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionContributor.kt @@ -0,0 +1,139 @@ +/* + * 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.expression + +import com.intellij.codeInsight.TailType +import com.intellij.codeInsight.TailTypes +import com.intellij.codeInsight.completion.BasicExpressionCompletionContributor +import com.intellij.codeInsight.completion.CompletionContributor +import com.intellij.codeInsight.completion.CompletionParameters +import com.intellij.codeInsight.completion.CompletionProvider +import com.intellij.codeInsight.completion.CompletionResultSet +import com.intellij.codeInsight.completion.CompletionType +import com.intellij.codeInsight.completion.InsertionContext +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.TailTypeDecorator +import com.intellij.util.ProcessingContext + +class MEExpressionCompletionContributor : CompletionContributor() { + init { + extend( + CompletionType.BASIC, + MEExpressionCompletionUtil.STATEMENT_KEYWORD_PLACE, + KeywordCompletionProvider( + Keyword("return", TailTypes.insertSpaceType()), + Keyword("throw", TailTypes.insertSpaceType()), + ) + ) + extend( + CompletionType.BASIC, + MEExpressionCompletionUtil.VALUE_KEYWORD_PLACE, + KeywordCompletionProvider( + Keyword("this"), + Keyword("super"), + Keyword("true"), + Keyword("false"), + Keyword("null"), + Keyword("new", TailTypes.insertSpaceType()), + ) + ) + extend( + CompletionType.BASIC, + MEExpressionCompletionUtil.CLASS_PLACE, + KeywordCompletionProvider( + Keyword("class") + ) + ) + extend( + CompletionType.BASIC, + MEExpressionCompletionUtil.INSTANCEOF_PLACE, + KeywordCompletionProvider( + Keyword("instanceof", TailTypes.insertSpaceType()) + ) + ) + extend( + CompletionType.BASIC, + MEExpressionCompletionUtil.METHOD_REFERENCE_PLACE, + KeywordCompletionProvider( + Keyword("new") + ) + ) + extend( + CompletionType.BASIC, + MEExpressionCompletionUtil.STRING_LITERAL_PLACE, + object : CompletionProvider() { + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) { + result.addAllElements( + MEExpressionCompletionUtil.getStringCompletions( + parameters.originalFile.project, + parameters.position + ) + ) + } + } + ) + extend( + CompletionType.BASIC, + MEExpressionCompletionUtil.FROM_BYTECODE_PLACE, + object : CompletionProvider() { + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) { + val project = parameters.originalFile.project + result.addAllElements( + MEExpressionCompletionUtil.getCompletionVariantsFromBytecode(project, parameters.position) + ) + } + } + ) + } + + private class KeywordCompletionProvider( + private vararg val keywords: Keyword, + ) : CompletionProvider() { + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet + ) { + result.addAllElements( + keywords.map { keyword -> + var lookupItem = + BasicExpressionCompletionContributor.createKeywordLookupItem(parameters.position, keyword.name) + if (keyword.tailType != TailTypes.noneType()) { + lookupItem = object : TailTypeDecorator(lookupItem) { + override fun computeTailType(context: InsertionContext?) = keyword.tailType + } + } + lookupItem + } + ) + } + } + + private class Keyword(val name: String, val tailType: TailType = TailTypes.noneType()) +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionUtil.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionUtil.kt new file mode 100644 index 000000000..c172b57d6 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionUtil.kt @@ -0,0 +1,1339 @@ +/* + * 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.expression + +import com.demonwav.mcdev.MinecraftProjectSettings +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil.virtualInsn +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil.virtualInsnOrNull +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArrayAccessExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEAssignStatement +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEBoundMethodReferenceExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MECapturingExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MECastExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEClassConstantExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionStatement +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEFreeMethodReferenceExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MELitExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEMemberAccessExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEMethodCallExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENameExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENewExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEParenthesizedExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatement +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatementItem +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStaticMethodCallExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MESuperCallExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype +import com.demonwav.mcdev.platform.mixin.expression.psi.MEMatchableElement +import com.demonwav.mcdev.platform.mixin.expression.psi.MEPsiUtil +import com.demonwav.mcdev.platform.mixin.expression.psi.MERecursiveWalkingVisitor +import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil +import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil.notInTypePosition +import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil.validType +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.util.AsmDfaUtil +import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.platform.mixin.util.SignatureToPsi +import com.demonwav.mcdev.platform.mixin.util.canonicalName +import com.demonwav.mcdev.platform.mixin.util.hasAccess +import com.demonwav.mcdev.platform.mixin.util.isPrimitive +import com.demonwav.mcdev.platform.mixin.util.mixinTargets +import com.demonwav.mcdev.platform.mixin.util.textify +import com.demonwav.mcdev.platform.mixin.util.toPsiType +import com.demonwav.mcdev.util.BeforeOrAfter +import com.demonwav.mcdev.util.constantStringValue +import com.demonwav.mcdev.util.findContainingClass +import com.demonwav.mcdev.util.findContainingModifierList +import com.demonwav.mcdev.util.findContainingNameValuePair +import com.demonwav.mcdev.util.findModule +import com.demonwav.mcdev.util.findMultiInjectionHost +import com.demonwav.mcdev.util.invokeLater +import com.demonwav.mcdev.util.mapFirstNotNull +import com.demonwav.mcdev.util.packageName +import com.intellij.codeInsight.TailType +import com.intellij.codeInsight.completion.InsertionContext +import com.intellij.codeInsight.folding.CodeFoldingManager +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.codeInsight.lookup.TailTypeDecorator +import com.intellij.codeInsight.template.Expression +import com.intellij.codeInsight.template.Template +import com.intellij.codeInsight.template.TemplateBuilderImpl +import com.intellij.codeInsight.template.TemplateEditingAdapter +import com.intellij.codeInsight.template.TemplateManager +import com.intellij.codeInsight.template.TextResult +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.command.CommandProcessor +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.FoldRegion +import com.intellij.openapi.project.Project +import com.intellij.patterns.PlatformPatterns +import com.intellij.patterns.StandardPatterns +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiAnonymousClass +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiModifierList +import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.psi.codeStyle.JavaCodeStyleManager +import com.intellij.psi.impl.source.tree.injected.InjectedLanguageEditorUtil +import com.intellij.psi.tree.TokenSet +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.PsiUtil +import com.intellij.psi.util.createSmartPointer +import com.intellij.psi.util.parentOfType +import com.intellij.psi.util.parents +import com.intellij.util.PlatformIcons +import com.intellij.util.text.CharArrayUtil +import com.llamalad7.mixinextras.expression.impl.flow.ComplexFlowValue +import com.llamalad7.mixinextras.expression.impl.flow.DummyFlowValue +import com.llamalad7.mixinextras.expression.impl.flow.FlowValue +import com.llamalad7.mixinextras.expression.impl.flow.expansion.InsnExpander +import com.llamalad7.mixinextras.expression.impl.flow.postprocessing.InstantiationInfo +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext +import com.llamalad7.mixinextras.expression.impl.pool.IdentifierPool +import com.llamalad7.mixinextras.expression.impl.utils.FlowDecorations +import org.apache.commons.lang3.mutable.MutableInt +import org.objectweb.asm.Handle +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type +import org.objectweb.asm.signature.SignatureReader +import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.FieldInsnNode +import org.objectweb.asm.tree.IincInsnNode +import org.objectweb.asm.tree.InsnNode +import org.objectweb.asm.tree.IntInsnNode +import org.objectweb.asm.tree.InvokeDynamicInsnNode +import org.objectweb.asm.tree.LdcInsnNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MethodNode +import org.objectweb.asm.tree.MultiANewArrayInsnNode +import org.objectweb.asm.tree.TypeInsnNode +import org.objectweb.asm.tree.VarInsnNode + +private typealias TemplateExpressionContext = com.intellij.codeInsight.template.ExpressionContext + +object MEExpressionCompletionUtil { + private const val DEBUG_COMPLETION = false + + private val NORMAL_ELEMENT = PlatformPatterns.psiElement() + .inside(MEStatement::class.java) + .andNot(PlatformPatterns.psiElement().inside(MELitExpression::class.java)) + .notInTypePosition() + private val TYPE_PATTERN = PlatformPatterns.psiElement() + .inside(MEStatement::class.java) + .validType() + private val AFTER_END_EXPRESSION_PATTERN = StandardPatterns.or( + PlatformPatterns.psiElement().afterLeaf( + PlatformPatterns.psiElement().withElementType( + TokenSet.create( + MEExpressionTypes.TOKEN_IDENTIFIER, + MEExpressionTypes.TOKEN_WILDCARD, + MEExpressionTypes.TOKEN_RIGHT_PAREN, + MEExpressionTypes.TOKEN_RIGHT_BRACKET, + MEExpressionTypes.TOKEN_RIGHT_BRACE, + MEExpressionTypes.TOKEN_BOOL_LIT, + MEExpressionTypes.TOKEN_CLASS, + MEExpressionTypes.TOKEN_INT_LIT, + MEExpressionTypes.TOKEN_DEC_LIT, + MEExpressionTypes.TOKEN_NULL_LIT, + MEExpressionTypes.TOKEN_STRING_TERMINATOR, + ) + ) + ), + PlatformPatterns.psiElement().afterLeaf(PlatformPatterns.psiElement().withText("new").afterLeaf("::")), + ) + + val STATEMENT_KEYWORD_PLACE = PlatformPatterns.psiElement().afterLeaf( + PlatformPatterns.psiElement().withText("{").withParent(MEStatementItem::class.java) + ) + val VALUE_KEYWORD_PLACE = StandardPatterns.and( + NORMAL_ELEMENT, + StandardPatterns.not(AFTER_END_EXPRESSION_PATTERN), + StandardPatterns.not(PlatformPatterns.psiElement().afterLeaf(".")), + StandardPatterns.not(PlatformPatterns.psiElement().afterLeaf("::")), + ) + val CLASS_PLACE = StandardPatterns.and( + NORMAL_ELEMENT, + PlatformPatterns.psiElement() + .afterLeaf( + PlatformPatterns.psiElement().withText(".") + .withParent(PlatformPatterns.psiElement().withFirstChild(TYPE_PATTERN)) + ), + ) + val INSTANCEOF_PLACE = StandardPatterns.and( + NORMAL_ELEMENT, + AFTER_END_EXPRESSION_PATTERN, + ) + val METHOD_REFERENCE_PLACE = StandardPatterns.and( + NORMAL_ELEMENT, + PlatformPatterns.psiElement().afterLeaf("::"), + ) + val STRING_LITERAL_PLACE = PlatformPatterns.psiElement().withElementType( + TokenSet.create(MEExpressionTypes.TOKEN_STRING, MEExpressionTypes.TOKEN_STRING_TERMINATOR) + ) + val FROM_BYTECODE_PLACE = PlatformPatterns.psiElement() + .inside(MEStatement::class.java) + .andNot(PlatformPatterns.psiElement().inside(MELitExpression::class.java)) + + private val DOT_CLASS_TAIL = object : TailType() { + override fun processTail(editor: Editor, tailOffset: Int): Int { + editor.document.insertString(tailOffset, ".class") + return moveCaret(editor, tailOffset, 6) + } + + override fun isApplicable(context: InsertionContext): Boolean { + val chars = context.document.charsSequence + val dotOffset = CharArrayUtil.shiftForward(chars, context.tailOffset, " \n\t") + if (!CharArrayUtil.regionMatches(chars, dotOffset, ".")) { + return true + } + val classOffset = CharArrayUtil.shiftForward(chars, dotOffset + 1, " \n\t") + return !CharArrayUtil.regionMatches(chars, classOffset, "class") + } + } + + private val COLON_COLON_NEW_TAIL = object : TailType() { + override fun processTail(editor: Editor, tailOffset: Int): Int { + editor.document.insertString(tailOffset, "::new") + return moveCaret(editor, tailOffset, 5) + } + + override fun isApplicable(context: InsertionContext): Boolean { + val chars = context.document.charsSequence + val colonColonOffset = CharArrayUtil.shiftForward(chars, context.tailOffset, " \n\t") + if (!CharArrayUtil.regionMatches(chars, colonColonOffset, "::")) { + return true + } + val newOffset = CharArrayUtil.shiftForward(chars, colonColonOffset + 2, " \n\t") + return !CharArrayUtil.regionMatches(chars, newOffset, "new") + } + } + + fun getStringCompletions(project: Project, contextElement: PsiElement): List { + val expressionAnnotation = contextElement.findMultiInjectionHost()?.parentOfType() + ?: return emptyList() + if (!expressionAnnotation.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION)) { + return emptyList() + } + + val modifierList = expressionAnnotation.findContainingModifierList() ?: return emptyList() + + val (handler, handlerAnnotation) = modifierList.annotations.mapFirstNotNull { annotation -> + val qName = annotation.qualifiedName ?: return@mapFirstNotNull null + val handler = MixinAnnotationHandler.forMixinAnnotation(qName, project) ?: return@mapFirstNotNull null + handler to annotation + } ?: return emptyList() + + return handler.resolveTarget(handlerAnnotation).flatMap { + (it as? MethodTargetMember)?.classAndMethod?.method?.instructions?.mapNotNull { insn -> + if (insn is LdcInsnNode && insn.cst is String) { + LookupElementBuilder.create(insn.cst) + } else { + null + } + } ?: emptyList() + } + } + + fun getCompletionVariantsFromBytecode(project: Project, contextElement: PsiElement): List { + val statement = contextElement.parentOfType() ?: return emptyList() + + val expressionAnnotation = contextElement.findMultiInjectionHost()?.parentOfType() + ?: return emptyList() + if (!expressionAnnotation.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION)) { + return emptyList() + } + + val modifierList = expressionAnnotation.findContainingModifierList() ?: return emptyList() + val module = modifierList.findModule() ?: return emptyList() + + val mixinClass = modifierList.findContainingClass() ?: return emptyList() + + val (handler, handlerAnnotation) = modifierList.annotations.mapFirstNotNull { annotation -> + val qName = annotation.qualifiedName ?: return@mapFirstNotNull null + val handler = MixinAnnotationHandler.forMixinAnnotation(qName, project) ?: return@mapFirstNotNull null + handler to annotation + } ?: return emptyList() + + val cursorOffset = contextElement.textRange.startOffset - statement.textRange.startOffset + + return mixinClass.mixinTargets.flatMap { targetClass -> + val poolFactory = MEExpressionMatchUtil.createIdentifierPoolFactory(module, targetClass, modifierList) + handler.resolveTarget(handlerAnnotation, targetClass) + .filterIsInstance() + .flatMap { methodTarget -> + getCompletionVariantsFromBytecode( + project, + mixinClass, + cursorOffset, + statement.copy() as MEStatement, + targetClass, + methodTarget.classAndMethod.method, + poolFactory, + ) + } + } + } + + private fun getCompletionVariantsFromBytecode( + project: Project, + mixinClass: PsiClass, + cursorOffsetIn: Int, + statement: MEStatement, + targetClass: ClassNode, + targetMethod: MethodNode, + poolFactory: IdentifierPoolFactory, + ): List { + /* + * MixinExtras isn't designed to match against incomplete expressions, which is what we need to do to produce + * completion options. The only support there is, is to match incomplete parameter lists and so on + * ("list inputs" to expressions). What follows is a kind of DIY match where we figure out different options + * for what the user might be trying to complete and hand it to MixinExtras to do the actual matching. Note that + * IntelliJ already inserts an identifier at the caret position to make auto-completion easier. + * + * We have four classes of problems to solve here: + * 1. There may already be a capture in the expression causing MixinExtras to return the wrong instructions. + * 2. There may be unresolved identifiers in the expression, causing MixinExtras to match nothing, which isn't + * ideal. + * 3. "this." expands to a field access, but the user may be trying to complete a method call (and other + * similar situations). + * 4. What the user is typing may form only a subexpression of a larger expression. For example, with + * "foo()", the user may actually be trying to type the expression "foo(x + y) + z". That is, "x", + * which is where the caret is, may not be a direct subexpression to the "foo" call expression, which itself + * may not be a direct subexpression of its parent. + * + * Throughout this process, we have to keep careful track of where the caret is, because: + * 1. As we make changes to the expression to the left of the caret, the caret may shift. + * 2. As we make copies of the element, or entirely new elements, that new element's textOffset may be different + * from the original one. + */ + + if (DEBUG_COMPLETION) { + println("======") + println(targetMethod.textify()) + println("======") + } + + if (targetMethod.instructions == null) { + return emptyList() + } + + val cursorOffset = MutableInt(cursorOffsetIn) + val pool = poolFactory(targetMethod) + val flows = MEExpressionMatchUtil.getFlowMap(project, targetClass, targetMethod) ?: return emptyList() + + // Removing all explicit captures from the expression solves problem 1 (see comment above). + removeExplicitCaptures(statement, cursorOffset) + // Replacing unresolved names with wildcards solves problem 2 (see comment above). + replaceUnresolvedNamesWithWildcards(project, statement, cursorOffset, pool) + + val elementAtCursor = statement.findElementAt(cursorOffset.toInt()) ?: return emptyList() + + /* + * To solve problem 4 (see comment above), we first find matches for the top level statement, ignoring the + * subexpression that the caret is on. Then we iterate down into the subexpression that contains the caret and + * match that against all the statement's input flows in the same way as we matched the statement against all + * the instructions in the target method. Then we keep iterating until we reach the identifier the caret is on. + */ + + // Replace the subexpression the caret is on with a wildcard expression, so MixinExtras ignores it. + val wildcardReplacedStatement = statement.copy() as MEStatement + var cursorOffsetInCopyFile = + cursorOffset.toInt() - statement.textRange.startOffset + wildcardReplacedStatement.textRange.startOffset + replaceCursorInputWithWildcard(project, wildcardReplacedStatement, cursorOffsetInCopyFile) + + // Iterate through possible "variants" of the statement that the user may be trying to complete; it doesn't + // matter if they don't parse, then we just skip them. This solves problem 3 (see comment above). + var matchingFlows = mutableListOf() + for (statementToMatch in getStatementVariants(project.meExpressionElementFactory, wildcardReplacedStatement)) { + if (DEBUG_COMPLETION) { + println("Matching against statement ${statementToMatch.text}") + } + + val meStatement = MEExpressionMatchUtil.createExpression(statementToMatch.text) ?: continue + MEExpressionMatchUtil.findMatchingInstructions( + targetClass, + targetMethod, + pool, + flows, + meStatement, + flows.keys, + ExpressionContext.Type.MODIFY_EXPRESSION_VALUE, // use most permissive type for completion + true, + ) { match -> + matchingFlows += match.flow + if (DEBUG_COMPLETION) { + println("Matched ${match.flow.virtualInsnOrNull?.insn?.textify()}") + } + } + } + if (matchingFlows.isEmpty()) { + return emptyList() + } + + // Iterate through subexpressions until we reach the identifier the caret is on + var roundNumber = 0 + var subExpr: MEMatchableElement = statement + while (true) { + // Replace the subexpression the caret is on with a wildcard expression, so MixinExtras ignores it. + val inputExprOnCursor = subExpr.getInputExprs().firstOrNull { it.textRange.contains(cursorOffset.toInt()) } + ?: break + val wildcardReplacedExpr = inputExprOnCursor.copy() as MEExpression + cursorOffsetInCopyFile = cursorOffset.toInt() - + inputExprOnCursor.textRange.startOffset + wildcardReplacedExpr.textRange.startOffset + + if (DEBUG_COMPLETION) { + val exprText = wildcardReplacedExpr.text + val cursorOffsetInExpr = cursorOffsetInCopyFile - wildcardReplacedExpr.textRange.startOffset + val exprWithCaretMarker = when { + cursorOffsetInExpr < 0 -> "$exprText" + cursorOffsetInExpr > exprText.length -> "$exprText" + else -> exprText.replaceRange(cursorOffsetInExpr, cursorOffsetInExpr, "") + } + println("=== Round ${++roundNumber}: handling $exprWithCaretMarker") + } + + replaceCursorInputWithWildcard(project, wildcardReplacedExpr, cursorOffsetInCopyFile) + + // Iterate through the possible "varaints" of the expression in the same way as we did for the statement + // above. This solves problem 3 (see comment above). + val newMatchingFlows = mutableSetOf() + for (exprToMatch in getExpressionVariants(project.meExpressionElementFactory, wildcardReplacedExpr)) { + if (DEBUG_COMPLETION) { + println("Matching against expression ${exprToMatch.text}") + } + + val meExpression = MEExpressionMatchUtil.createExpression(exprToMatch.text) ?: continue + + val flattenedInstructions = mutableSetOf() + for (flow in matchingFlows) { + getInstructionsInFlowTree( + flow, + flattenedInstructions, + subExpr !is MEExpressionStatement && subExpr !is MEParenthesizedExpression + ) + } + + MEExpressionMatchUtil.findMatchingInstructions( + targetClass, + targetMethod, + pool, + flows, + meExpression, + flattenedInstructions.map { it.insn }, + ExpressionContext.Type.MODIFY_EXPRESSION_VALUE, // use most permissive type for completion + true, + ) { match -> + newMatchingFlows += match.flow + if (DEBUG_COMPLETION) { + println("Matched ${match.flow.virtualInsnOrNull?.insn?.textify()}") + } + } + } + + if (newMatchingFlows.isEmpty()) { + return emptyList() + } + matchingFlows = newMatchingFlows.toMutableList() + + subExpr = inputExprOnCursor + } + + val cursorInstructions = mutableSetOf() + for (flow in matchingFlows) { + getInstructionsInFlowTree(flow, cursorInstructions, false) + } + + if (DEBUG_COMPLETION) { + println("Found ${cursorInstructions.size} matching instructions:") + for (insn in cursorInstructions) { + println("- ${insn.insn.insn.textify()}") + } + } + + // Try to decide if we should be completing types or normal expressions. + // Not as easy as it sounds (think incomplete casts looking like parenthesized expressions). + // Note that it's possible to complete types and expressions at the same time. + val isInsideMeType = PsiTreeUtil.getParentOfType( + elementAtCursor, + METype::class.java, + false, + MEExpression::class.java + ) != null + val isInsideNewExpr = PsiTreeUtil.getParentOfType( + elementAtCursor, + MENewExpression::class.java, + false, + MEExpression::class.java + ) != null + val cursorExprInTypePosition = !isInsideMeType && + elementAtCursor.parentOfType()?.let(METypeUtil::isExpressionInTypePosition) == true + val inTypePosition = isInsideMeType || isInsideNewExpr || cursorExprInTypePosition + val isPossiblyIncompleteCast = !inTypePosition && + elementAtCursor.parentOfType() + ?.parents(false) + ?.dropWhile { it is MEArrayAccessExpression && it.indexExpr == null } + ?.firstOrNull() is MEParenthesizedExpression + val canCompleteExprs = !inTypePosition + val canCompleteTypes = inTypePosition || isPossiblyIncompleteCast + + if (DEBUG_COMPLETION) { + println("canCompleteExprs = $canCompleteExprs") + println("canCompleteTypes = $canCompleteTypes") + } + + val eliminableResults = cursorInstructions.flatMap { insn -> + getCompletionsForInstruction( + project, + targetClass, + targetMethod, + insn.insn, + insn.originalInsn, + flows, + mixinClass, + canCompleteExprs, + canCompleteTypes + ) + } + + // In the case of multiple instructions producing the same lookup, attempt to show only the "best" lookup. + // For example, if a local variable is only sometimes able to be targeted using implicit ordinals in this + // expression, prefer specifying the ordinal. + return eliminableResults.groupBy { it.uniquenessKey }.values.map { it.max().lookupElement } + } + + private fun replaceUnresolvedNamesWithWildcards( + project: Project, + statement: MEStatement, + cursorOffset: MutableInt, + pool: IdentifierPool, + ) { + val unresolvedNames = mutableListOf() + statement.accept(object : MERecursiveWalkingVisitor() { + override fun visitType(o: METype) { + val name = o.meName + if (!name.isWildcard && !pool.typeExists(name.text)) { + unresolvedNames += name + } + } + + override fun visitNameExpression(o: MENameExpression) { + val name = o.meName + if (!name.isWildcard) { + if (METypeUtil.isExpressionDirectlyInTypePosition(o)) { + if (!pool.typeExists(name.text)) { + unresolvedNames += name + } + } else { + if (!pool.memberExists(name.text)) { + unresolvedNames += name + } + } + } + } + + override fun visitSuperCallExpression(o: MESuperCallExpression) { + val name = o.memberName + if (name != null && !name.isWildcard && !pool.memberExists(name.text)) { + unresolvedNames += name + } + super.visitSuperCallExpression(o) + } + + override fun visitMethodCallExpression(o: MEMethodCallExpression) { + val name = o.memberName + if (!name.isWildcard && !pool.memberExists(name.text)) { + unresolvedNames += name + } + super.visitMethodCallExpression(o) + } + + override fun visitStaticMethodCallExpression(o: MEStaticMethodCallExpression) { + val name = o.memberName + if (!name.isWildcard && !pool.memberExists(name.text)) { + unresolvedNames += name + } + super.visitStaticMethodCallExpression(o) + } + + override fun visitBoundMethodReferenceExpression(o: MEBoundMethodReferenceExpression) { + val name = o.memberName + if (name != null && !name.isWildcard && !pool.memberExists(name.text)) { + unresolvedNames += name + } + super.visitBoundMethodReferenceExpression(o) + } + + override fun visitFreeMethodReferenceExpression(o: MEFreeMethodReferenceExpression) { + val name = o.memberName + if (name != null && !name.isWildcard && !pool.memberExists(name.text)) { + unresolvedNames += name + } + super.visitFreeMethodReferenceExpression(o) + } + + override fun visitMemberAccessExpression(o: MEMemberAccessExpression) { + val name = o.memberName + if (!name.isWildcard && !pool.memberExists(name.text)) { + unresolvedNames += name + } + super.visitMemberAccessExpression(o) + } + + override fun visitNewExpression(o: MENewExpression) { + val name = o.type + if (name != null && !name.isWildcard && !pool.typeExists(name.text)) { + unresolvedNames += name + } + super.visitNewExpression(o) + } + }) + + for (unresolvedName in unresolvedNames) { + val startOffset = unresolvedName.textRange.startOffset + if (cursorOffset.toInt() > startOffset) { + cursorOffset.setValue(cursorOffset.toInt() - unresolvedName.textLength + 1) + } + + unresolvedName.replace(project.meExpressionElementFactory.createName("?")) + } + } + + private fun removeExplicitCaptures(statement: MEStatement, cursorOffset: MutableInt) { + val captures = mutableListOf() + + statement.accept(object : MERecursiveWalkingVisitor() { + override fun elementFinished(element: PsiElement) { + // do this on elementFinished to ensure that inner captures are replaced before outer captures + if (element is MECapturingExpression) { + captures += element + } + } + }) + + for (capture in captures) { + val innerExpr = capture.expression ?: continue + val textRange = capture.textRange + + if (cursorOffset.toInt() > textRange.startOffset) { + cursorOffset.setValue(cursorOffset.toInt() - if (cursorOffset.toInt() >= textRange.endOffset) 3 else 2) + } + + capture.replace(innerExpr) + } + } + + private fun replaceCursorInputWithWildcard(project: Project, element: MEMatchableElement, cursorOffset: Int) { + for (input in element.getInputExprs()) { + if (input.textRange.contains(cursorOffset)) { + input.replace(project.meExpressionElementFactory.createExpression("?")) + return + } + } + } + + private fun getInstructionsInFlowTree( + flow: FlowValue, + outInstructions: MutableSet, + strict: Boolean, + ) { + if (flow is DummyFlowValue || flow is ComplexFlowValue) { + return + } + + if (!strict) { + val originalInsn = InsnExpander.getRepresentative(flow) ?: flow.insn + if (!outInstructions.add(ExpandedInstruction(flow.virtualInsn, originalInsn))) { + return + } + } + for (i in 0 until flow.inputCount()) { + getInstructionsInFlowTree(flow.getInput(i), outInstructions, false) + } + } + + private fun getCompletionsForInstruction( + project: Project, + targetClass: ClassNode, + targetMethod: MethodNode, + insn: VirtualInsn, + originalInsn: AbstractInsnNode, + flows: FlowMap, + mixinClass: PsiClass, + canCompleteExprs: Boolean, + canCompleteTypes: Boolean + ): List { + val flow = flows[insn] + when (insn.insn) { + is LdcInsnNode -> { + when (val cst = insn.insn.cst) { + is Type -> { + if (canCompleteExprs && cst.isAccessibleFrom(mixinClass)) { + return listOf( + createTypeLookup(cst) + .withTailText(".class") + .withTail(DOT_CLASS_TAIL) + .createEliminable("class ${insn.insn.cst}") + ) + } + } + } + } + is VarInsnNode -> return createLocalVariableLookups( + project, + targetClass, + targetMethod, + originalInsn, + insn.insn.`var`, + insn.insn.opcode in Opcodes.ISTORE..Opcodes.ASTORE, + mixinClass + ) + is IincInsnNode -> return createLocalVariableLookups( + project, + targetClass, + targetMethod, + originalInsn, + insn.insn.`var`, + false, + mixinClass + ) + is FieldInsnNode -> { + if (canCompleteExprs) { + val definitionValue = "field = \"L${insn.insn.owner};${insn.insn.name}:${insn.insn.desc}\"" + var lookup = createUniqueLookup(insn.insn.name.toValidIdentifier()) + .withIcon(PlatformIcons.FIELD_ICON) + .withPresentableText(insn.insn.owner.substringAfterLast('/') + "." + insn.insn.name) + .withTypeText(Type.getType(insn.insn.desc).presentableName()) + .withDefinitionAndFold(insn.insn.name.toValidIdentifier(), "field", definitionValue) + if (insn.insn.opcode == Opcodes.GETSTATIC || insn.insn.opcode == Opcodes.PUTSTATIC) { + lookup = lookup.withLookupString(insn.insn.owner.substringAfterLast('/') + "." + insn.insn.name) + } + return listOf( + lookup.createEliminable("field ${insn.insn.owner}.${insn.insn.name}:${insn.insn.desc}") + ) + } + } + is MethodInsnNode -> { + if (canCompleteExprs) { + val definitionValue = "method = \"L${insn.insn.owner};${insn.insn.name}${insn.insn.desc}\"" + var lookup = createUniqueLookup(insn.insn.name.toValidIdentifier()) + .withIcon(PlatformIcons.METHOD_ICON) + .withPresentableText(insn.insn.owner.substringAfterLast('/') + "." + insn.insn.name) + .withDescTailText(insn.insn.desc) + .withTypeText(Type.getReturnType(insn.insn.desc).presentableName()) + .withDefinitionAndFold(insn.insn.name.toValidIdentifier(), "method", definitionValue) + if (insn.insn.opcode == Opcodes.INVOKESTATIC) { + lookup = lookup.withLookupString(insn.insn.owner.substringAfterLast('/') + "." + insn.insn.name) + } + return listOf( + lookup.withTail(ParenthesesTailType(!insn.insn.desc.startsWith("()"))) + .createEliminable("invoke ${insn.insn.owner}.${insn.insn.name}${insn.insn.desc}") + ) + } + } + is TypeInsnNode -> { + val type = Type.getObjectType(insn.insn.desc) + if (canCompleteTypes && type.isAccessibleFrom(mixinClass)) { + val lookup = createTypeLookup(type) + when (insn.insn.opcode) { + Opcodes.ANEWARRAY -> { + val arrayType = Type.getType('[' + Type.getObjectType(insn.insn.desc).descriptor) + return createNewArrayCompletion(flow, arrayType) + } + Opcodes.NEW -> { + val initCall = flow + ?.getDecoration(FlowDecorations.INSTANTIATION_INFO) + ?.initCall + ?.virtualInsnOrNull + ?.insn as? MethodInsnNode + ?: return emptyList() + return listOf( + lookup + .withDescTailText(initCall.desc) + .withTail(ParenthesesTailType(!initCall.desc.startsWith("()"))) + .createEliminable("new ${insn.insn.desc}${initCall.desc}") + ) + } + else -> return listOf(lookup.createEliminable("type ${insn.insn.desc}")) + } + } + } + is IntInsnNode -> { + if (insn.insn.opcode == Opcodes.NEWARRAY) { + if (canCompleteTypes) { + val arrayType = Type.getType( + when (insn.insn.operand) { + Opcodes.T_BOOLEAN -> "[B" + Opcodes.T_CHAR -> "[C" + Opcodes.T_FLOAT -> "[F" + Opcodes.T_DOUBLE -> "[D" + Opcodes.T_BYTE -> "[B" + Opcodes.T_SHORT -> "[S" + Opcodes.T_INT -> "[I" + Opcodes.T_LONG -> "[J" + else -> "[Lnull;" // wtf? + } + ) + return createNewArrayCompletion(flow, arrayType) + } + } + } + is MultiANewArrayInsnNode -> { + if (canCompleteTypes) { + val arrayType = Type.getType(insn.insn.desc) + return createNewArrayCompletion(flow, arrayType) + } + } + is InsnNode -> { + when (insn.insn.opcode) { + Opcodes.ARRAYLENGTH -> { + if (canCompleteExprs) { + return listOf( + createUniqueLookup("length") + .withIcon(PlatformIcons.FIELD_ICON) + .withTypeText("int") + .createEliminable("arraylength") + ) + } + } + } + } + is InvokeDynamicInsnNode -> { + if (insn.insn.bsm.owner == "java/lang/invoke/LambdaMetafactory") { + if (!canCompleteExprs) { + return emptyList() + } + + val handle = insn.insn.bsmArgs.getOrNull(1) as? Handle ?: return emptyList() + val definitionValue = "method = \"L${handle.owner};${handle.name}${handle.desc}\"" + if (handle.tag !in Opcodes.H_INVOKEVIRTUAL..Opcodes.H_INVOKEINTERFACE) { + return emptyList() + } + if (handle.tag == Opcodes.H_NEWINVOKESPECIAL) { + return listOf( + createTypeLookup(Type.getObjectType(handle.owner)) + .withTailText("::new") + .withTail(COLON_COLON_NEW_TAIL) + .createEliminable("constructorRef ${handle.owner}") + ) + } else { + return listOf( + createUniqueLookup(handle.name.toValidIdentifier()) + .withIcon(PlatformIcons.METHOD_ICON) + .withPresentableText(handle.owner.substringAfterLast('/') + "." + handle.name) + .withTypeText(Type.getReturnType(handle.desc).presentableName()) + .withDefinitionAndFold(handle.name.toValidIdentifier(), "method", definitionValue) + .createEliminable("methodRef ${handle.owner}.${handle.name}${handle.desc}") + ) + } + } + } + } + + return emptyList() + } + + private fun Type.typeNameToInsert(): String { + if (sort == Type.ARRAY) { + return elementType.typeNameToInsert() + "[]".repeat(dimensions) + } + if (sort != Type.OBJECT) { + return className + } + + val simpleName = internalName.substringAfterLast('/') + val lastValidCharIndex = (simpleName.length - 1 downTo 0).firstOrNull { + MEPsiUtil.isIdentifierStart(simpleName[it]) + } ?: return "_" + simpleName.filterInvalidIdentifierChars() + + return simpleName.substring(simpleName.lastIndexOf('$', lastValidCharIndex) + 1).toValidIdentifier() + } + + private fun String.toValidIdentifier(): String { + return when { + isEmpty() -> "_" + !MEPsiUtil.isIdentifierStart(this[0]) -> "_" + filterInvalidIdentifierChars() + else -> this[0] + substring(1).filterInvalidIdentifierChars() + } + } + + private fun String.filterInvalidIdentifierChars(): String { + return asSequence().joinToString("") { + if (MEPsiUtil.isIdentifierPart(it)) it.toString() else "_" + } + } + + private fun Type.presentableName(): String = when (sort) { + Type.ARRAY -> elementType.presentableName() + "[]".repeat(dimensions) + Type.OBJECT -> internalName.substringAfterLast('/') + else -> className + } + + private fun Type.isAccessibleFrom(fromClass: PsiClass): Boolean { + return when (sort) { + Type.ARRAY -> elementType.isAccessibleFrom(fromClass) + Type.OBJECT -> { + val facade = JavaPsiFacade.getInstance(fromClass.project) + val clazz = facade.findClass(canonicalName, fromClass.resolveScope) ?: return false + val pkg = fromClass.packageName?.let(facade::findPackage) ?: return false + clazz !is PsiAnonymousClass && PsiUtil.isAccessibleFromPackage(clazz, pkg) + } + else -> true + } + } + + private fun createTypeLookup(type: Type): LookupElementBuilder { + val definitionId = type.typeNameToInsert() + + val lookupElement = createUniqueLookup(definitionId) + .withIcon(PlatformIcons.CLASS_ICON) + .withPresentableText(type.presentableName()) + + return if (type.isPrimitive) { + lookupElement + } else { + lookupElement.withDefinition(definitionId, "type = ${type.canonicalName}.class") + } + } + + private fun createNewArrayCompletion(flow: FlowValue?, arrayType: Type): List { + val hasInitializer = flow?.hasDecoration(FlowDecorations.ARRAY_CREATION_INFO) == true + val initializerText = if (hasInitializer) "{}" else "" + return listOf( + createTypeLookup(arrayType.elementType) + .withTailText("[]".repeat(arrayType.dimensions) + initializerText) + .withTail( + BracketsTailType( + arrayType.dimensions, + hasInitializer, + ) + ) + .createEliminable("new ${arrayType.descriptor}$initializerText") + ) + } + + private fun createLocalVariableLookups( + project: Project, + targetClass: ClassNode, + targetMethod: MethodNode, + originalInsn: AbstractInsnNode, + index: Int, + isStore: Boolean, + mixinClass: PsiClass, + ): List { + // ignore "this" + if (!targetMethod.hasAccess(Opcodes.ACC_STATIC) && index == 0) { + return emptyList() + } + + var argumentsSize = Type.getArgumentsAndReturnSizes(targetMethod.desc) shr 2 + if (targetMethod.hasAccess(Opcodes.ACC_STATIC)) { + argumentsSize-- + } + val isArgsOnly = index < argumentsSize + + if (targetMethod.localVariables != null) { + val localsHere = targetMethod.localVariables.filter { localVariable -> + val firstValidInstruction = if (isStore) { + generateSequence(localVariable.start) { it.previous } + .firstOrNull { it.opcode >= 0 } + } else { + localVariable.start.next + } + if (firstValidInstruction == null) { + return@filter false + } + val validRange = targetMethod.instructions.indexOf(firstValidInstruction) until + targetMethod.instructions.indexOf(localVariable.end) + targetMethod.instructions.indexOf(originalInsn) in validRange + } + val locals = localsHere.filter { it.index == index } + + val elementFactory = JavaPsiFacade.getElementFactory(project) + + return locals.map { localVariable -> + val localPsiType = if (localVariable.signature != null) { + val sigToPsi = SignatureToPsi(elementFactory, mixinClass) + SignatureReader(localVariable.signature).acceptType(sigToPsi) + sigToPsi.type + } else { + Type.getType(localVariable.desc).toPsiType(elementFactory, mixinClass) + } + val localsOfMyType = localsHere.filter { it.desc == localVariable.desc } + val ordinal = localsOfMyType.indexOf(localVariable) + val isImplicit = localsOfMyType.size == 1 + val localName = localVariable.name.toValidIdentifier() + createUniqueLookup(localName) + .withIcon(PlatformIcons.VARIABLE_ICON) + .withTypeText(localPsiType.presentableText) + .withLocalDefinition( + localName, + Type.getType(localVariable.desc), + ordinal, + isArgsOnly, + isImplicit, + mixinClass, + ) + .createEliminable("local $localName", if (isImplicit) -1 else 0) + } + } + + // fallback to ASM dataflow + val localTypes = AsmDfaUtil.getLocalVariableTypes(project, targetClass, targetMethod, originalInsn) + ?: return emptyList() + val localType = localTypes.getOrNull(index) ?: return emptyList() + val ordinal = localTypes.asSequence().take(index).filter { it == localType }.count() + val localName = localType.typeNameToInsert().replace("[]", "Array") + (ordinal + 1) + val isImplicit = localTypes.count { it == localType } == 1 + return listOf( + createUniqueLookup(localName) + .withIcon(PlatformIcons.VARIABLE_ICON) + .withTypeText(localType.presentableName()) + .withLocalDefinition(localName, localType, ordinal, isArgsOnly, isImplicit, mixinClass) + .createEliminable("local $localName", if (isImplicit) -1 else 0) + ) + } + + private fun LookupElementBuilder.withDescTailText(desc: String) = + withTailText( + Type.getArgumentTypes(desc).joinToString(prefix = "(", postfix = ")") { it.presentableName() } + ) + + private fun LookupElement.withTail(tailType: TailType?) = object : TailTypeDecorator(this) { + override fun computeTailType(context: InsertionContext?) = tailType + } + + private fun LookupElementBuilder.withDefinition(id: String, definitionValue: String) = + withDefinition(id, definitionValue) { _, _ -> } + + private fun LookupElementBuilder.withDefinitionAndFold(id: String, foldAttribute: String, definitionValue: String) = + withDefinition(id, definitionValue) { context, annotation -> + val hostEditor = InjectedLanguageEditorUtil.getTopLevelEditor(context.editor) + CodeFoldingManager.getInstance(context.project).updateFoldRegions(hostEditor) + val foldingModel = hostEditor.foldingModel + val regionsToFold = mutableListOf() + val annotationRange = annotation.textRange + for (foldRegion in foldingModel.allFoldRegions) { + if (!annotationRange.contains(foldRegion.textRange)) { + continue + } + val nameValuePair = annotation.findElementAt(foldRegion.startOffset - annotationRange.startOffset) + ?.findContainingNameValuePair() ?: continue + if (nameValuePair.name == foldAttribute && + nameValuePair.parentOfType()?.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION) + == true + ) { + regionsToFold += foldRegion + } + } + + foldingModel.runBatchFoldingOperation { + for (foldRegion in regionsToFold) { + foldRegion.isExpanded = false + } + } + } + + private fun LookupElementBuilder.withLocalDefinition( + name: String, + type: Type, + ordinal: Int, + isArgsOnly: Boolean, + canBeImplicit: Boolean, + mixinClass: PsiClass, + ): LookupElementBuilder { + val isTypeAccessible = type.isAccessibleFrom(mixinClass) + val isImplicit = canBeImplicit && isTypeAccessible + + val definitionValue = buildString { + append("local = @${MixinConstants.MixinExtras.LOCAL}(") + if (isTypeAccessible) { + append("type = ${type.className}.class, ") + } + if (!isImplicit) { + append("ordinal = ") + append(ordinal) + append(", ") + } + if (isArgsOnly) { + append("argsOnly = true, ") + } + + if (endsWith(", ")) { + setLength(length - 2) + } + + append(")") + } + return withDefinition(name, definitionValue) { context, annotation -> + if (isImplicit) { + return@withDefinition + } + + invokeLater { + WriteCommandAction.runWriteCommandAction( + context.project, + "Choose How to Target Local Variable", + null, + { runLocalTemplate(context.project, context.editor, context.file, annotation, ordinal, name) }, + annotation.containingFile, + ) + } + } + } + + private fun runLocalTemplate( + project: Project, + editor: Editor, + file: PsiFile, + annotation: PsiAnnotation, + ordinal: Int, + name: String + ) { + val elementToReplace = + (annotation.findDeclaredAttributeValue("local") as? PsiAnnotation) + ?.findDeclaredAttributeValue("ordinal") + ?.findContainingNameValuePair() ?: return + + val hostEditor = InjectedLanguageEditorUtil.getTopLevelEditor(editor) + val hostElement = file.findElementAt(editor.caretModel.offset)?.findMultiInjectionHost() ?: return + + val template = TemplateBuilderImpl(annotation) + val lookupItems = arrayOf( + LookupElementBuilder.create("ordinal = $ordinal"), + LookupElementBuilder.create("name = \"$name\"") + ) + template.replaceElement( + elementToReplace, + object : Expression() { + override fun calculateLookupItems(context: TemplateExpressionContext?) = lookupItems + override fun calculateQuickResult(context: TemplateExpressionContext?) = calculateResult(context) + override fun calculateResult(context: TemplateExpressionContext?) = + TextResult("ordinal = $ordinal") + }, + true, + ) + + val prevCursorPosInLiteral = hostEditor.caretModel.offset - hostElement.textRange.startOffset + val hostElementPtr = hostElement.createSmartPointer(project) + hostEditor.caretModel.moveToOffset(annotation.textRange.startOffset) + TemplateManager.getInstance(project).startTemplate( + hostEditor, + template.buildInlineTemplate(), + object : TemplateEditingAdapter() { + override fun templateFinished(template: Template, brokenOff: Boolean) { + PsiDocumentManager.getInstance(project).commitDocument(hostEditor.document) + val newHostElement = hostElementPtr.element ?: return + hostEditor.caretModel.moveToOffset(newHostElement.textRange.startOffset + prevCursorPosInLiteral) + } + } + ) + } + + private inline fun LookupElementBuilder.withDefinition( + id: String, + definitionValue: String, + crossinline andThen: (InsertionContext, PsiAnnotation) -> Unit + ) = withInsertHandler { context, _ -> + context.laterRunnable = Runnable { + context.commitDocument() + CommandProcessor.getInstance().runUndoTransparentAction { + runWriteAction { + val annotation = addDefinition(context, id, definitionValue) + if (annotation != null) { + andThen(context, annotation) + } + } + } + } + } + + private fun addDefinition(context: InsertionContext, id: String, definitionValue: String): PsiAnnotation? { + val contextElement = context.file.findElementAt(context.startOffset) ?: return null + return addDefinition(context.project, contextElement, id, definitionValue) + } + + fun addDefinition( + project: Project, + contextElement: PsiElement, + id: String, + definitionValue: String + ): PsiAnnotation? { + val injectionHost = contextElement.findMultiInjectionHost() ?: return null + val expressionAnnotation = injectionHost.parentOfType() ?: return null + if (!expressionAnnotation.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION)) { + return null + } + val modifierList = expressionAnnotation.findContainingModifierList() ?: return null + + // look for an existing definition with this id, skip if it exists + for (annotation in modifierList.annotations) { + if (annotation.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION) && + annotation.findDeclaredAttributeValue("id")?.constantStringValue == id + ) { + return null + } + } + + // create and add the new @Definition annotation + var newAnnotation = JavaPsiFacade.getElementFactory(project).createAnnotationFromText( + "@${MixinConstants.MixinExtras.DEFINITION}(id = \"$id\", $definitionValue)", + modifierList, + ) + var anchor = modifierList.annotations.lastOrNull { it.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION) } + if (anchor == null) { + val definitionPosRelativeToExpression = + MinecraftProjectSettings.getInstance(project).definitionPosRelativeToExpression + if (definitionPosRelativeToExpression == BeforeOrAfter.AFTER) { + anchor = expressionAnnotation + } + } + newAnnotation = modifierList.addAfter(newAnnotation, anchor) as PsiAnnotation + + // add imports and reformat + newAnnotation = + JavaCodeStyleManager.getInstance(project).shortenClassReferences(newAnnotation) as PsiAnnotation + JavaCodeStyleManager.getInstance(project).optimizeImports(modifierList.containingFile) + val annotationIndex = modifierList.annotations.indexOf(newAnnotation) + val formattedModifierList = + CodeStyleManager.getInstance(project).reformat(modifierList) as PsiModifierList + return formattedModifierList.annotations.getOrNull(annotationIndex) + } + + private fun getStatementVariants( + factory: MEExpressionElementFactory, + statement: MEStatement + ): List { + return if (statement is MEExpressionStatement) { + getExpressionVariants(factory, statement.expression) + } else { + listOf(statement) + } + } + + private fun getExpressionVariants( + factory: MEExpressionElementFactory, + expression: MEExpression + ): List { + val variants = mutableListOf(expression) + + val assignmentStatement = factory.createStatement("? = ?") as MEAssignStatement + assignmentStatement.targetExpr.replace(expression.copy()) + variants += assignmentStatement + + when (expression) { + is MEParenthesizedExpression -> { + val castExpr = factory.createExpression("(?) ?") as MECastExpression + castExpr.castTypeExpr!!.replace(expression.copy()) + variants += castExpr + } + is MENameExpression -> { + val callExpr = factory.createExpression("?()") as MEStaticMethodCallExpression + callExpr.memberName.replace(expression.meName) + variants += callExpr + + val classExpr = factory.createExpression("${expression.text}.class") as MEClassConstantExpression + variants += classExpr + } + is MEMemberAccessExpression -> { + val callExpr = factory.createExpression("?.?()") as MEMethodCallExpression + callExpr.receiverExpr.replace(expression.receiverExpr) + callExpr.memberName.replace(expression.memberName) + variants += callExpr + } + is MENewExpression -> { + val type = expression.type + if (type != null && !expression.hasConstructorArguments && !expression.isArrayCreation) { + val fixedNewExpr = factory.createExpression("new ?()") as MENewExpression + fixedNewExpr.type!!.replace(type) + variants += fixedNewExpr + + val fixedNewArrayExpr = factory.createExpression("new ?[?]") as MENewExpression + fixedNewArrayExpr.type!!.replace(type) + variants += fixedNewArrayExpr + + val arrayLitExpr = factory.createExpression("new ?[]{?}") as MENewExpression + arrayLitExpr.type!!.replace(type) + variants += arrayLitExpr + } + } + is MESuperCallExpression -> { + // Might be missing its parentheses + val callExpr = factory.createExpression("super.?()") as MESuperCallExpression + expression.memberName?.let { callExpr.memberName!!.replace(it) } + variants += callExpr + } + } + + return variants + } + + private fun createUniqueLookup(text: String) = LookupElementBuilder.create(Any(), text) + + private fun LookupElement.createEliminable(uniquenessKey: String, priority: Int = 0) = + EliminableLookup(uniquenessKey, this, priority) + + private class EliminableLookup( + val uniquenessKey: String, + val lookupElement: LookupElement, + private val priority: Int + ) : Comparable { + override fun compareTo(other: EliminableLookup) = priority.compareTo(other.priority) + } + + private data class ExpandedInstruction(val insn: VirtualInsn, val originalInsn: AbstractInsnNode) + + private class ParenthesesTailType(private val hasParameters: Boolean) : TailType() { + override fun processTail(editor: Editor, tailOffset: Int): Int { + editor.document.insertString(tailOffset, "()") + return moveCaret(editor, tailOffset, if (hasParameters) 1 else 2) + } + + override fun isApplicable(context: InsertionContext): Boolean { + val chars = context.document.charsSequence + val offset = CharArrayUtil.shiftForward(chars, context.tailOffset, " \n\t") + return !CharArrayUtil.regionMatches(chars, offset, "(") + } + } + + private class BracketsTailType(private val dimensions: Int, private val hasInitializer: Boolean) : TailType() { + override fun processTail(editor: Editor, tailOffset: Int): Int { + editor.document.insertString(tailOffset, "[]".repeat(dimensions) + if (hasInitializer) "{}" else "") + return moveCaret(editor, tailOffset, if (hasInitializer) 2 * dimensions + 1 else 1) + } + + override fun isApplicable(context: InsertionContext): Boolean { + val chars = context.document.charsSequence + val offset = CharArrayUtil.shiftForward(chars, context.tailOffset, " \n\t") + return !CharArrayUtil.regionMatches(chars, offset, "[") + } + } +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionElementFactory.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionElementFactory.kt new file mode 100644 index 000000000..1f93df64b --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionElementFactory.kt @@ -0,0 +1,73 @@ +/* + * 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.expression + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEClassConstantExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionStatement +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENameExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatement +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype +import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionFile +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFileFactory +import com.intellij.util.IncorrectOperationException + +class MEExpressionElementFactory(private val project: Project) { + fun createFile(text: String): MEExpressionFile { + return PsiFileFactory.getInstance(project).createFileFromText( + "dummy.mixinextrasexpression", + MEExpressionFileType, + text + ) as MEExpressionFile + } + + fun createStatement(text: String): MEStatement { + return createFile("do {$text}").statements.singleOrNull() + ?: throw IncorrectOperationException("'$text' is not a statement") + } + + fun createExpression(text: String): MEExpression { + return (createStatement(text) as? MEExpressionStatement)?.expression + ?: throw IncorrectOperationException("'$text' is not an expression") + } + + fun createName(text: String): MEName { + return (createExpression(text) as? MENameExpression)?.meName + ?: throw IncorrectOperationException("'$text' is not a name") + } + + fun createIdentifier(text: String): PsiElement { + return createName(text).identifierElement + ?: throw IncorrectOperationException("'$text' is not an identifier") + } + + fun createType(text: String): METype { + return (createExpression("$text.class") as? MEClassConstantExpression)?.type + ?: throw IncorrectOperationException("'$text' is not a type") + } + + fun createType(name: MEName) = createType(name.text) +} + +val Project.meExpressionElementFactory get() = MEExpressionElementFactory(this) diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionFileType.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionFileType.kt new file mode 100644 index 000000000..87d4bd31c --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionFileType.kt @@ -0,0 +1,31 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.expression + +import com.demonwav.mcdev.asset.PlatformAssets +import com.intellij.openapi.fileTypes.LanguageFileType + +object MEExpressionFileType : LanguageFileType(MEExpressionLanguage) { + override fun getName() = "MixinExtras Expression File" + override fun getDescription() = "MixinExtras expression file" + override fun getDefaultExtension() = "mixinextrasexpression" + override fun getIcon() = PlatformAssets.MIXIN_ICON +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionInjector.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionInjector.kt new file mode 100644 index 000000000..22de47c71 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionInjector.kt @@ -0,0 +1,207 @@ +/* + * 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.expression + +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.findContainingModifierList +import com.demonwav.mcdev.util.findContainingNameValuePair +import com.intellij.lang.injection.InjectedLanguageManager +import com.intellij.lang.injection.MultiHostInjector +import com.intellij.lang.injection.MultiHostRegistrar +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.TextRange +import com.intellij.openapi.util.component1 +import com.intellij.openapi.util.component2 +import com.intellij.psi.ElementManipulators +import com.intellij.psi.JavaTokenType +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiLanguageInjectionHost +import com.intellij.psi.PsiLiteralExpression +import com.intellij.psi.PsiParenthesizedExpression +import com.intellij.psi.PsiPolyadicExpression +import com.intellij.psi.impl.source.tree.injected.JavaConcatenationToInjectorAdapter +import com.intellij.psi.util.PsiLiteralUtil +import com.intellij.psi.util.PsiModificationTracker +import com.intellij.psi.util.PsiUtil +import com.intellij.psi.util.parentOfType +import com.intellij.util.SmartList + +class MEExpressionInjector : MultiHostInjector { + companion object { + private val ELEMENTS = listOf(PsiLiteralExpression::class.java) + private val ME_EXPRESSION_INJECTION = Key.create("mcdev.meExpressionInjection") + + private val CLASS_INJECTION_RESULT = + Class.forName("com.intellij.psi.impl.source.tree.injected.InjectionResult") + private val CLASS_INJECTION_REGISTRAR_IMPL = + Class.forName("com.intellij.psi.impl.source.tree.injected.InjectionRegistrarImpl") + @JvmStatic + private val METHOD_ADD_TO_RESULTS = + CLASS_INJECTION_REGISTRAR_IMPL.getDeclaredMethod("addToResults", CLASS_INJECTION_RESULT) + .also { it.isAccessible = true } + @JvmStatic + private val METHOD_GET_INJECTED_RESULT = + CLASS_INJECTION_REGISTRAR_IMPL.getDeclaredMethod("getInjectedResult") + .also { it.isAccessible = true } + } + + private data class MEExpressionInjection(val modCount: Long, val injectionResult: Any) + + private fun shouldInjectIn(anchor: PsiElement): Boolean { + val nameValuePair = anchor.findContainingNameValuePair() ?: return false + return when (nameValuePair.name) { + "value", null -> nameValuePair.parentOfType() + ?.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION) == true + "id" -> nameValuePair.parentOfType() + ?.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION) == true + else -> false + } + } + + override fun getLanguagesToInject(registrar: MultiHostRegistrar, context: PsiElement) { + val project = context.project + val (anchor, _) = JavaConcatenationToInjectorAdapter(project).computeAnchorAndOperands(context) + + if (!shouldInjectIn(anchor)) { + return + } + + val modifierList = anchor.findContainingModifierList() ?: return + + val modCount = PsiModificationTracker.getInstance(project).modificationCount + val primaryElement = modifierList.getUserData(ME_EXPRESSION_INJECTION) + if (primaryElement != null && primaryElement.modCount == modCount) { + METHOD_ADD_TO_RESULTS.invoke(registrar, primaryElement.injectionResult) + return + } + + // A Frankenstein injection is an injection where we don't know the entire contents, and therefore errors should + // not be reported. + var isFrankenstein = false + registrar.startInjecting(MEExpressionLanguage) + + for (annotation in modifierList.annotations) { + if (annotation.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION)) { + val idExpr = annotation.findDeclaredAttributeValue("id") ?: continue + val isType = annotation.findDeclaredAttributeValue("type") != null + var needsPrefix = true + iterateConcatenation(idExpr) { op -> + if (op is PsiLanguageInjectionHost) { + for (textRange in getTextRanges(op)) { + val prefix = "\nclass $isType ".takeIf { needsPrefix } + needsPrefix = false + registrar.addPlace(prefix, null, op, textRange) + } + } else { + isFrankenstein = true + } + } + } else if (annotation.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION)) { + val valueExpr = annotation.findDeclaredAttributeValue("value") ?: continue + val places = mutableListOf>() + iterateConcatenation(valueExpr) { op -> + if (op is PsiLanguageInjectionHost) { + for (textRange in getTextRanges(op)) { + places += op to textRange + } + } else { + isFrankenstein = true + } + } + if (places.isNotEmpty()) { + for ((i, place) in places.withIndex()) { + val (host, range) = place + val prefix = "\ndo { ".takeIf { i == 0 } + val suffix = " }".takeIf { i == places.size - 1 } + registrar.addPlace(prefix, suffix, host, range) + } + } + } + } + + registrar.doneInjecting() + + modifierList.putUserData( + ME_EXPRESSION_INJECTION, + MEExpressionInjection(modCount, METHOD_GET_INJECTED_RESULT.invoke(registrar)) + ) + + if (isFrankenstein) { + @Suppress("DEPRECATION") // no replacement for this method + com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil.putInjectedFileUserData( + context, + MEExpressionLanguage, + InjectedLanguageManager.FRANKENSTEIN_INJECTION, + true + ) + } + } + + private fun iterateConcatenation(element: PsiElement, consumer: (PsiElement) -> Unit) { + when (element) { + is PsiParenthesizedExpression -> { + val inner = PsiUtil.skipParenthesizedExprDown(element) ?: return + iterateConcatenation(inner, consumer) + } + is PsiPolyadicExpression -> { + if (element.operationTokenType == JavaTokenType.PLUS) { + for (operand in element.operands) { + iterateConcatenation(operand, consumer) + } + } else { + consumer(element) + } + } + else -> consumer(element) + } + } + + private fun getTextRanges(host: PsiLanguageInjectionHost): List { + if (host is PsiLiteralExpression && host.isTextBlock) { + val textRange = ElementManipulators.getValueTextRange(host) + val indent = PsiLiteralUtil.getTextBlockIndent(host) + if (indent <= 0) { + return listOf(textRange) + } + + val text = (host as PsiElement).text + var startOffset = textRange.startOffset + indent + var endOffset = text.indexOf('\n', startOffset) + val result = SmartList() + while (endOffset > 0) { + endOffset++ + result.add(TextRange(startOffset, endOffset)) + startOffset = endOffset + indent + endOffset = text.indexOf('\n', startOffset) + } + endOffset = textRange.endOffset + if (startOffset < endOffset) { + result.add(TextRange(startOffset, endOffset)) + } + return result + } else { + return listOf(ElementManipulators.getValueTextRange(host)) + } + } + + override fun elementsToInjectIn() = ELEMENTS +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionLanguage.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionLanguage.kt new file mode 100644 index 000000000..af5496462 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionLanguage.kt @@ -0,0 +1,25 @@ +/* + * 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.expression + +import com.intellij.lang.Language + +object MEExpressionLanguage : Language("MEExpression") diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionLexerAdapter.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionLexerAdapter.kt new file mode 100644 index 000000000..8eb499188 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionLexerAdapter.kt @@ -0,0 +1,25 @@ +/* + * 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.expression + +import com.intellij.lexer.FlexAdapter + +class MEExpressionLexerAdapter : FlexAdapter(MEExpressionLexer(null)) diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt new file mode 100644 index 000000000..541cdd455 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt @@ -0,0 +1,317 @@ +/* + * 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.expression + +import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler +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 +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.intellij.openapi.diagnostic.logger +import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.ProcessCanceledException +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiModifierList +import com.llamalad7.mixinextras.expression.impl.ExpressionParserFacade +import com.llamalad7.mixinextras.expression.impl.ExpressionService +import com.llamalad7.mixinextras.expression.impl.ast.expressions.Expression +import com.llamalad7.mixinextras.expression.impl.flow.ComplexDataException +import com.llamalad7.mixinextras.expression.impl.flow.FlowInterpreter +import com.llamalad7.mixinextras.expression.impl.flow.FlowValue +import com.llamalad7.mixinextras.expression.impl.flow.expansion.InsnExpander +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext +import com.llamalad7.mixinextras.expression.impl.pool.IdentifierPool +import com.llamalad7.mixinextras.expression.impl.pool.SimpleMemberDefinition +import org.objectweb.asm.Handle +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type +import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.FieldInsnNode +import org.objectweb.asm.tree.MethodInsnNode +import org.objectweb.asm.tree.MethodNode +import org.objectweb.asm.tree.VarInsnNode +import org.objectweb.asm.tree.analysis.Analyzer + +typealias IdentifierPoolFactory = (MethodNode) -> IdentifierPool +typealias FlowMap = Map + +/** + * An instruction that MixinExtras generates (via instruction expansion), as opposed to an instruction in the original + * method. One type of instruction cannot be directly assigned to another, to avoid a method instruction being used when + * a virtual instruction is expected and vice versa. + */ +@JvmInline +value class VirtualInsn(val insn: AbstractInsnNode) + +object MEExpressionMatchUtil { + private val LOGGER = logger() + + init { + ExpressionService.offerInstance(MEExpressionService) + } + + fun getFlowMap(project: Project, classIn: ClassNode, methodIn: MethodNode): FlowMap? { + if (methodIn.instructions == null) { + return null + } + + return methodIn.cached(classIn, project) { classNode, methodNode -> + val interpreter = object : FlowInterpreter(classNode, methodNode, MEFlowContext(project)) { + override fun newValue(type: Type?): FlowValue? { + ProgressManager.checkCanceled() + return super.newValue(type) + } + + override fun newOperation(insn: AbstractInsnNode?): FlowValue? { + ProgressManager.checkCanceled() + return super.newOperation(insn) + } + + override fun copyOperation(insn: AbstractInsnNode?, value: FlowValue?): FlowValue? { + ProgressManager.checkCanceled() + return super.copyOperation(insn, value) + } + + override fun unaryOperation(insn: AbstractInsnNode?, value: FlowValue?): FlowValue? { + ProgressManager.checkCanceled() + return super.unaryOperation(insn, value) + } + + override fun binaryOperation( + insn: AbstractInsnNode?, + value1: FlowValue?, + value2: FlowValue? + ): FlowValue? { + ProgressManager.checkCanceled() + return super.binaryOperation(insn, value1, value2) + } + + override fun ternaryOperation( + insn: AbstractInsnNode?, + value1: FlowValue?, + value2: FlowValue?, + value3: FlowValue? + ): FlowValue? { + ProgressManager.checkCanceled() + return super.ternaryOperation(insn, value1, value2, value3) + } + + override fun naryOperation(insn: AbstractInsnNode?, values: MutableList?): FlowValue? { + ProgressManager.checkCanceled() + return super.naryOperation(insn, values) + } + + override fun returnOperation(insn: AbstractInsnNode?, value: FlowValue?, expected: FlowValue?) { + ProgressManager.checkCanceled() + super.returnOperation(insn, value, expected) + } + + override fun merge(value1: FlowValue?, value2: FlowValue?): FlowValue? { + ProgressManager.checkCanceled() + return super.merge(value1, value2) + } + } + + try { + Analyzer(interpreter).analyze(classNode.name, methodNode) + } catch (e: RuntimeException) { + if (e is ProcessCanceledException) { + throw e + } + LOGGER.warn("MEExpressionMatchUtil.getFlowMap failed", e) + return@cached null + } + + interpreter.finish().asSequence().mapNotNull { flow -> flow.virtualInsnOrNull?.let { it to flow } }.toMap() + } + } + + fun createIdentifierPoolFactory( + module: Module, + targetClass: ClassNode, + modifierList: PsiModifierList, + ): IdentifierPoolFactory = { targetMethod -> + val pool = IdentifierPool() + + for (annotation in modifierList.annotations) { + if (!annotation.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION)) { + continue + } + + val definitionId = annotation.findDeclaredAttributeValue("id")?.constantStringValue ?: "" + + val fields = annotation.findDeclaredAttributeValue("field")?.computeStringArray() ?: emptyList() + for (field in fields) { + val fieldRef = MemberReference.parse(field) ?: continue + pool.addMember( + definitionId, + SimpleMemberDefinition { + it is FieldInsnNode && fieldRef.matchField(it.owner, it.name, it.desc) + } + ) + } + + val methods = annotation.findDeclaredAttributeValue("method")?.computeStringArray() ?: emptyList() + for (method in methods) { + val methodRef = MemberReference.parse(method) ?: continue + pool.addMember( + definitionId, + object : SimpleMemberDefinition { + override fun matches(insn: AbstractInsnNode) = + insn is MethodInsnNode && methodRef.matchMethod(insn.owner, insn.name, insn.desc) + + override fun matches(handle: Handle) = + handle.tag in Opcodes.H_INVOKEVIRTUAL..Opcodes.H_INVOKEINTERFACE && + methodRef.matchMethod(handle.owner, handle.name, handle.desc) + } + ) + } + + val types = annotation.findDeclaredAttributeValue("type")?.resolveTypeArray() ?: emptyList() + for (type in types) { + val asmType = Type.getType(type.descriptor) + pool.addType(definitionId) { it == asmType } + } + + val locals = annotation.findDeclaredAttributeValue("local")?.findAnnotations() ?: emptyList() + for (localAnnotation in locals) { + val localType = localAnnotation.findDeclaredAttributeValue("type")?.resolveType() + val localInfo = LocalInfo.fromAnnotation(localType, localAnnotation) + pool.addMember(definitionId) { node -> + val virtualInsn = node.insn + if (virtualInsn !is VarInsnNode) { + return@addMember false + } + val physicalInsn = InsnExpander.getRepresentative(node) + val actualInsn = if (virtualInsn.opcode >= Opcodes.ISTORE && virtualInsn.opcode <= Opcodes.ASTORE) { + physicalInsn.next ?: return@addMember false + } else { + physicalInsn + } + + val unfilteredLocals = localInfo.getLocals(module, targetClass, targetMethod, actualInsn) + ?: return@addMember false + val filteredLocals = localInfo.matchLocals(unfilteredLocals, CollectVisitor.Mode.MATCH_ALL) + filteredLocals.any { it.index == virtualInsn.`var` } + } + } + } + + pool + } + + fun createExpression(text: String): Expression? { + return try { + ExpressionParserFacade.parse(text) + } catch (e: Exception) { + null + } catch (e: StackOverflowError) { + null + } + } + + fun getContextType(project: Project, annotationName: String?): ExpressionContext.Type { + if (annotationName == null) { + return ExpressionContext.Type.CUSTOM + } + if (annotationName == MixinConstants.Annotations.SLICE) { + return ExpressionContext.Type.SLICE + } + + val handler = MixinAnnotationHandler.forMixinAnnotation(annotationName, project) as? InjectorAnnotationHandler + ?: return ExpressionContext.Type.CUSTOM + return handler.mixinExtrasExpressionContextType + } + + inline fun findMatchingInstructions( + targetClass: ClassNode, + targetMethod: MethodNode, + pool: IdentifierPool, + flows: FlowMap, + expr: Expression, + insns: Iterable, + contextType: ExpressionContext.Type, + forCompletion: Boolean, + callback: (ExpressionMatch) -> Unit + ) { + for (insn in insns) { + val decorations = mutableMapOf>() + val captured = mutableListOf>() + + val sink = object : Expression.OutputSink { + override fun capture(node: FlowValue, expr: Expression?, ctx: ExpressionContext?) { + captured += node to (expr?.src?.startIndex ?: 0) + decorations.getOrPut(insn, ::mutableMapOf).putAll(node.decorations) + } + + override fun decorate(insn: AbstractInsnNode, key: String, value: Any?) { + decorations.getOrPut(VirtualInsn(insn), ::mutableMapOf)[key] = value + } + + override fun decorateInjectorSpecific(insn: AbstractInsnNode, key: String, value: Any?) { + // Our maps are per-injector anyway, so this is just a normal decoration. + decorations.getOrPut(VirtualInsn(insn), ::mutableMapOf)[key] = value + } + } + + val flow = flows[insn] ?: continue + try { + val context = ExpressionContext(pool, sink, targetClass, targetMethod, contextType, forCompletion) + if (expr.matches(flow, context)) { + for ((capturedFlow, startOffset) in captured) { + val capturedInsn = capturedFlow.virtualInsnOrNull ?: continue + val originalInsn = InsnExpander.getRepresentative(capturedFlow) ?: capturedInsn.insn + callback(ExpressionMatch(flow, originalInsn, startOffset, decorations[capturedInsn].orEmpty())) + } + } + } catch (e: ProcessCanceledException) { + throw e + } catch (ignored: Exception) { + // MixinExtras throws lots of different exceptions + } + } + } + + val FlowValue.virtualInsn: VirtualInsn get() = VirtualInsn(insn) + + val FlowValue.virtualInsnOrNull: VirtualInsn? get() = try { + VirtualInsn(insn) + } catch (e: ComplexDataException) { + null + } + + class ExpressionMatch @PublishedApi internal constructor( + val flow: FlowValue, + val originalInsn: AbstractInsnNode, + val startOffset: Int, + val decorations: Map, + ) +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionParserDefinition.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionParserDefinition.kt new file mode 100644 index 000000000..7d16e8816 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionParserDefinition.kt @@ -0,0 +1,46 @@ +/* + * 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.expression + +import com.demonwav.mcdev.platform.mixin.expression.gen.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 +import com.intellij.lang.ASTNode +import com.intellij.lang.ParserDefinition +import com.intellij.openapi.project.Project +import com.intellij.psi.FileViewProvider +import com.intellij.psi.PsiElement +import com.intellij.psi.tree.IFileElementType +import com.intellij.psi.tree.TokenSet + +class MEExpressionParserDefinition : ParserDefinition { + + override fun createLexer(project: Project) = MEExpressionLexerAdapter() + override fun getCommentTokens(): TokenSet = TokenSet.EMPTY + override fun getStringLiteralElements() = MEExpressionTokenSets.STRINGS + override fun createParser(project: Project) = MEExpressionParser() + override fun getFileNodeType() = FILE + override fun createFile(viewProvider: FileViewProvider) = MEExpressionFile(viewProvider) + override fun createElement(node: ASTNode): PsiElement = MEExpressionTypes.Factory.createElement(node) +} + +val FILE = IFileElementType(MEExpressionLanguage) diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionQuoteHandler.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionQuoteHandler.kt new file mode 100644 index 000000000..dc1a1796d --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionQuoteHandler.kt @@ -0,0 +1,26 @@ +/* + * 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.expression + +import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionTokenSets +import com.intellij.codeInsight.editorActions.SimpleTokenSetQuoteHandler + +class MEExpressionQuoteHandler : SimpleTokenSetQuoteHandler(MEExpressionTokenSets.STRINGS) diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionRefactoringSupport.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionRefactoringSupport.kt new file mode 100644 index 000000000..2294b4bdd --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionRefactoringSupport.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.platform.mixin.expression + +import com.intellij.lang.refactoring.RefactoringSupportProvider +import com.intellij.psi.PsiElement + +class MEExpressionRefactoringSupport : RefactoringSupportProvider() { + // Inplace renaming doesn't work due to IDEA-348784 + override fun isInplaceRenameAvailable(element: PsiElement, context: PsiElement?) = false +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionService.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionService.kt new file mode 100644 index 000000000..473717217 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionService.kt @@ -0,0 +1,76 @@ +/* + * 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.expression + +import com.demonwav.mcdev.platform.mixin.util.toPsiType +import com.demonwav.mcdev.util.descriptor +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiElementFactory +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiType +import com.llamalad7.mixinextras.expression.impl.ExpressionService +import com.llamalad7.mixinextras.expression.impl.flow.FlowContext +import org.objectweb.asm.Type + +object MEExpressionService : ExpressionService() { + override fun getCommonSuperClass(ctx: FlowContext, type1: Type, type2: Type): Type { + ctx as MEFlowContext + val elementFactory = JavaPsiFacade.getElementFactory(ctx.project) + return Type.getType( + getCommonSuperClass( + ctx.project, + type1.toPsiType(elementFactory) as PsiClassType, + type2.toPsiType(elementFactory) as PsiClassType + )?.descriptor ?: error("Could not intersect types $type1 and $type2!") + ) + } + + // Copied from ClassInfo + private fun getCommonSuperClass( + project: Project, + type1: PsiType, + type2: PsiType + ): PsiClassType? { + val left = (type1 as? PsiClassType)?.resolve() ?: return null + val right = (type2 as? PsiClassType)?.resolve() ?: return null + + fun objectType() = PsiType.getJavaLangObject(PsiManager.getInstance(project), left.resolveScope) + fun PsiClass.type() = PsiElementFactory.getInstance(project).createType(this) + + if (left.isInheritor(right, true)) { + return right.type() + } + if (right.isInheritor(left, true)) { + return left.type() + } + if (left.isInterface || right.isInterface) { + return objectType() + } + + return generateSequence(left) { it.superClass } + .firstOrNull { right.isInheritor(it, true) } + ?.type() + ?: objectType() + } +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionSyntaxHighlighter.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionSyntaxHighlighter.kt new file mode 100644 index 000000000..a44fdc28c --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionSyntaxHighlighter.kt @@ -0,0 +1,198 @@ +/* + * 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.expression + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionTokenSets +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors +import com.intellij.openapi.editor.HighlighterColors +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.editor.colors.TextAttributesKey.createTextAttributesKey +import com.intellij.openapi.fileTypes.SyntaxHighlighterBase +import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.TokenType +import com.intellij.psi.tree.IElementType + +class MEExpressionSyntaxHighlighter : SyntaxHighlighterBase() { + companion object { + val STRING = createTextAttributesKey( + "MEEXPRESSION_STRING", + DefaultLanguageHighlighterColors.STRING + ) + val STRING_ESCAPE = createTextAttributesKey( + "MEEXPRESSION_STRING_ESCAPE", + DefaultLanguageHighlighterColors.VALID_STRING_ESCAPE + ) + val NUMBER = createTextAttributesKey( + "MEEXPRESSION_NUMBER", + DefaultLanguageHighlighterColors.NUMBER + ) + val KEYWORD = createTextAttributesKey( + "MEEXPRESSION_KEYWORD", + DefaultLanguageHighlighterColors.KEYWORD, + ) + val OPERATOR = createTextAttributesKey( + "MEEXPRESSION_OPERATOR", + DefaultLanguageHighlighterColors.OPERATION_SIGN + ) + val PARENS = createTextAttributesKey( + "MEEXPRESSION_PARENS", + DefaultLanguageHighlighterColors.PARENTHESES + ) + val BRACKETS = createTextAttributesKey( + "MEEXPRESSION_BRACKETS", + DefaultLanguageHighlighterColors.BRACKETS + ) + val BRACES = createTextAttributesKey( + "MEEXPRESSION_BRACES", + DefaultLanguageHighlighterColors.BRACES + ) + val DOT = createTextAttributesKey( + "MEEXPRESSION_DOT", + DefaultLanguageHighlighterColors.DOT + ) + val METHOD_REFERENCE = createTextAttributesKey( + "MEEXPRESSION_METHOD_REFERENCE", + DefaultLanguageHighlighterColors.DOT + ) + val COMMA = createTextAttributesKey( + "MEEXPRESSION_COMMA", + DefaultLanguageHighlighterColors.COMMA + ) + val CAPTURE = createTextAttributesKey( + "MEEXPRESSION_CAPTURE", + DefaultLanguageHighlighterColors.OPERATION_SIGN + ) + val WILDCARD = createTextAttributesKey( + "MEEXPRESSION_WILDCARD", + DefaultLanguageHighlighterColors.OPERATION_SIGN + ) + val IDENTIFIER = createTextAttributesKey( + "MEEXPRESSION_IDENTIFIER", + DefaultLanguageHighlighterColors.IDENTIFIER + ) + val IDENTIFIER_CALL = createTextAttributesKey( + "MEEXPRESSION_IDENTIFIER_CALL", + DefaultLanguageHighlighterColors.FUNCTION_CALL + ) + val IDENTIFIER_CLASS_NAME = createTextAttributesKey( + "MEEXPRESSION_IDENTIFIER_CLASS_NAME", + DefaultLanguageHighlighterColors.CLASS_REFERENCE + ) + val IDENTIFIER_PRIMITIVE_TYPE = createTextAttributesKey( + "MEEXPRESSION_IDENTIFIER_PRIMITIVE_TYPE", + DefaultLanguageHighlighterColors.KEYWORD + ) + val IDENTIFIER_MEMBER_NAME = createTextAttributesKey( + "MEEXPRESSION_IDENTIFIER_MEMBER_NAME", + DefaultLanguageHighlighterColors.INSTANCE_FIELD + ) + val IDENTIFIER_VARIABLE = createTextAttributesKey( + "MEEXPRESSION_IDENTIFIER_VARIABLE", + DefaultLanguageHighlighterColors.LOCAL_VARIABLE + ) + val IDENTIFIER_TYPE_DECLARATION = createTextAttributesKey( + "MEEXPRESSION_IDENTIFIER_TYPE_DECLARATION", + DefaultLanguageHighlighterColors.CLASS_NAME + ) + val IDENTIFIER_DECLARATION = createTextAttributesKey( + "MEEXPRESSION_IDENTIFIER_DECLARATION", + DefaultLanguageHighlighterColors.FUNCTION_DECLARATION + ) + val BAD_CHAR = createTextAttributesKey( + "MEEXPRESSION_BAD_CHARACTER", + HighlighterColors.BAD_CHARACTER + ) + + val STRING_KEYS = arrayOf(STRING) + val STRING_ESCAPE_KEYS = arrayOf(STRING_ESCAPE) + val NUMBER_KEYS = arrayOf(NUMBER) + val KEYWORD_KEYS = arrayOf(KEYWORD) + val OPERATOR_KEYS = arrayOf(OPERATOR) + val PARENS_KEYS = arrayOf(PARENS) + val BRACKETS_KEYS = arrayOf(BRACKETS) + val BRACES_KEYS = arrayOf(BRACES) + val DOT_KEYS = arrayOf(DOT) + val METHOD_REFERENCE_KEYS = arrayOf(METHOD_REFERENCE) + val COMMA_KEYS = arrayOf(COMMA) + val CAPTURE_KEYS = arrayOf(CAPTURE) + val WILDCARD_KEYS = arrayOf(WILDCARD) + val IDENTIFIER_KEYS = arrayOf(IDENTIFIER) + val BAD_CHAR_KEYS = arrayOf(BAD_CHAR) + } + + override fun getHighlightingLexer() = MEExpressionLexerAdapter() + override fun getTokenHighlights(tokenType: IElementType): Array { + if (tokenType == MEExpressionTypes.TOKEN_STRING_ESCAPE) { + return STRING_ESCAPE_KEYS + } + if (MEExpressionTokenSets.STRINGS.contains(tokenType)) { + return STRING_KEYS + } + if (tokenType == MEExpressionTypes.TOKEN_IDENTIFIER) { + return IDENTIFIER_KEYS + } + if (MEExpressionTokenSets.NUMBERS.contains(tokenType)) { + return NUMBER_KEYS + } + if (MEExpressionTokenSets.KEYWORDS.contains(tokenType)) { + return KEYWORD_KEYS + } + if (MEExpressionTokenSets.OPERATORS.contains(tokenType)) { + return OPERATOR_KEYS + } + if (MEExpressionTokenSets.PARENS.contains(tokenType)) { + return PARENS_KEYS + } + if (MEExpressionTokenSets.BRACKETS.contains(tokenType)) { + return BRACKETS_KEYS + } + if (MEExpressionTokenSets.BRACES.contains(tokenType)) { + return BRACES_KEYS + } + if (tokenType == MEExpressionTypes.TOKEN_DOT) { + return DOT_KEYS + } + if (tokenType == MEExpressionTypes.TOKEN_METHOD_REF) { + return METHOD_REFERENCE_KEYS + } + if (tokenType == MEExpressionTypes.TOKEN_COMMA) { + return COMMA_KEYS + } + if (tokenType == MEExpressionTypes.TOKEN_AT) { + return CAPTURE_KEYS + } + if (tokenType == MEExpressionTypes.TOKEN_WILDCARD) { + return WILDCARD_KEYS + } + if (tokenType == TokenType.BAD_CHARACTER) { + return BAD_CHAR_KEYS + } + + return TextAttributesKey.EMPTY_ARRAY + } +} + +class MEExpressionSyntaxHighlighterFactory : SyntaxHighlighterFactory() { + override fun getSyntaxHighlighter(project: Project?, virtualFile: VirtualFile?) = MEExpressionSyntaxHighlighter() +} diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionTypedHandlerDelegate.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionTypedHandlerDelegate.kt new file mode 100644 index 000000000..6d714b43b --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEExpressionTypedHandlerDelegate.kt @@ -0,0 +1,42 @@ +/* + * 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.expression + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.intellij.codeInsight.AutoPopupController +import com.intellij.codeInsight.editorActions.TypedHandlerDelegate +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiFile +import com.intellij.psi.util.elementType + +class MEExpressionTypedHandlerDelegate : TypedHandlerDelegate() { + override fun checkAutoPopup(charTyped: Char, project: Project, editor: Editor, file: PsiFile): Result { + if (charTyped == ':' && file.language == MEExpressionLanguage) { + AutoPopupController.getInstance(project).autoPopupMemberLookup(editor) { + val offset = editor.caretModel.offset + it.findElementAt(offset - 1).elementType == MEExpressionTypes.TOKEN_METHOD_REF + } + return Result.STOP + } + return Result.CONTINUE + } +} diff --git a/src/main/kotlin/platform/mixin/expression/MEFlowContext.kt b/src/main/kotlin/platform/mixin/expression/MEFlowContext.kt new file mode 100644 index 000000000..e7d22f578 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MEFlowContext.kt @@ -0,0 +1,26 @@ +/* + * 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.expression + +import com.intellij.openapi.project.Project +import com.llamalad7.mixinextras.expression.impl.flow.FlowContext + +class MEFlowContext(val project: Project) : FlowContext diff --git a/src/main/kotlin/platform/mixin/expression/MESourceMatchContext.kt b/src/main/kotlin/platform/mixin/expression/MESourceMatchContext.kt new file mode 100644 index 000000000..8a6312d24 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/MESourceMatchContext.kt @@ -0,0 +1,98 @@ +/* + * 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.expression + +import com.demonwav.mcdev.platform.mixin.util.LocalInfo +import com.demonwav.mcdev.util.MemberReference +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement + +class MESourceMatchContext(val project: Project) { + @PublishedApi + internal var realElement: PsiElement? = null + private val capturesInternal = mutableListOf() + val captures: List get() = capturesInternal + + private val types = mutableMapOf>() + private val fields = mutableMapOf>() + private val methods = mutableMapOf>() + private val localInfos = mutableMapOf>() + + init { + addType("byte", "B") + addType("char", "C") + addType("double", "D") + addType("float", "F") + addType("int", "I") + addType("long", "J") + addType("short", "S") + } + + fun addCapture(capturedElement: PsiElement) { + val element = realElement ?: capturedElement + capturesInternal += element + } + + fun getTypes(key: String): List = types[key] ?: emptyList() + + fun addType(key: String, desc: String) { + types.getOrPut(key, ::mutableListOf) += desc + } + + fun getFields(key: String): List = fields[key] ?: emptyList() + + fun addField(key: String, field: MemberReference) { + fields.getOrPut(key, ::mutableListOf) += field + } + + fun getMethods(key: String): List = methods[key] ?: emptyList() + + fun addMethod(key: String, method: MemberReference) { + methods.getOrPut(key, ::mutableListOf) += method + } + + fun getLocalInfos(key: String): List = localInfos[key] ?: emptyList() + + fun addLocalInfo(key: String, localInfo: LocalInfo) { + localInfos.getOrPut(key, ::mutableListOf) += localInfo + } + + fun reset() { + capturesInternal.clear() + } + + inline fun fakeElementScope( + isFake: Boolean, + realElement: PsiElement, + action: () -> T + ): T { + if (this.realElement != null || !isFake) { + return action() + } + + this.realElement = realElement + try { + return action() + } finally { + this.realElement = null + } + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEExpressionElementType.kt b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionElementType.kt new file mode 100644 index 000000000..697faee8b --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionElementType.kt @@ -0,0 +1,27 @@ +/* + * 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.expression.psi + +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionLanguage +import com.intellij.psi.tree.IElementType +import org.jetbrains.annotations.NonNls + +class MEExpressionElementType(@NonNls debugName: String) : IElementType(debugName, MEExpressionLanguage) diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEExpressionFile.kt b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionFile.kt new file mode 100644 index 000000000..1f9002488 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionFile.kt @@ -0,0 +1,41 @@ +/* + * 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.expression.psi + +import com.demonwav.mcdev.asset.PlatformAssets +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionFileType +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionLanguage +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEDeclarationItem +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEItem +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatement +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatementItem +import com.intellij.extapi.psi.PsiFileBase +import com.intellij.psi.FileViewProvider + +class MEExpressionFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, MEExpressionLanguage) { + override fun getFileType() = MEExpressionFileType + override fun toString() = "MixinExtras Expression File" + override fun getIcon(flags: Int) = PlatformAssets.MIXIN_ICON + + val items: Array get() = findChildrenByClass(MEItem::class.java) + val declarations: List get() = items.filterIsInstance() + val statements: List get() = items.mapNotNull { (it as? MEStatementItem)?.statement } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEExpressionParserUtil.kt b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionParserUtil.kt new file mode 100644 index 000000000..2b6c41702 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionParserUtil.kt @@ -0,0 +1,45 @@ +/* + * 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 . + */ + +@file:JvmName("MEExpressionParserUtil") + +package com.demonwav.mcdev.platform.mixin.expression.psi + +import com.intellij.lang.PsiBuilder +import com.intellij.lang.parser.GeneratedParserUtilBase.* // ktlint-disable no-wildcard-imports + +fun parseToRightBracket( + builder: PsiBuilder, + level: Int, + recoverParser: Parser, + rightBracketParser: Parser +): Boolean { + recursion_guard_(builder, level, "parseToRightBracket") + + // continue over any stuff inside the brackets as error elements. We need to find our precious right bracket. + var marker = enter_section_(builder, level, _NONE_) + exit_section_(builder, level, marker, false, false, recoverParser) + + // consume our right bracket. + marker = enter_section_(builder) + val result = rightBracketParser.parse(builder, level) + exit_section_(builder, marker, null, result) + return result +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenSets.kt b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenSets.kt new file mode 100644 index 000000000..cd4a1842e --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenSets.kt @@ -0,0 +1,73 @@ +/* + * 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.expression.psi + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.intellij.psi.tree.TokenSet + +object MEExpressionTokenSets { + val STRINGS = TokenSet.create( + MEExpressionTypes.TOKEN_STRING, + MEExpressionTypes.TOKEN_STRING_ESCAPE, + MEExpressionTypes.TOKEN_STRING_TERMINATOR, + ) + val NUMBERS = TokenSet.create( + MEExpressionTypes.TOKEN_INT_LIT, + MEExpressionTypes.TOKEN_DEC_LIT, + ) + val KEYWORDS = TokenSet.create( + MEExpressionTypes.TOKEN_BOOL_LIT, + MEExpressionTypes.TOKEN_NULL_LIT, + MEExpressionTypes.TOKEN_DO, + MEExpressionTypes.TOKEN_INSTANCEOF, + MEExpressionTypes.TOKEN_NEW, + MEExpressionTypes.TOKEN_RETURN, + MEExpressionTypes.TOKEN_THROW, + MEExpressionTypes.TOKEN_THIS, + MEExpressionTypes.TOKEN_SUPER, + MEExpressionTypes.TOKEN_CLASS, + MEExpressionTypes.TOKEN_RESERVED, + ) + val OPERATORS = TokenSet.create( + MEExpressionTypes.TOKEN_BITWISE_NOT, + MEExpressionTypes.TOKEN_MULT, + MEExpressionTypes.TOKEN_DIV, + MEExpressionTypes.TOKEN_MOD, + MEExpressionTypes.TOKEN_PLUS, + MEExpressionTypes.TOKEN_MINUS, + MEExpressionTypes.TOKEN_SHL, + MEExpressionTypes.TOKEN_SHR, + MEExpressionTypes.TOKEN_USHR, + MEExpressionTypes.TOKEN_LT, + MEExpressionTypes.TOKEN_LE, + MEExpressionTypes.TOKEN_GT, + MEExpressionTypes.TOKEN_GE, + MEExpressionTypes.TOKEN_EQ, + MEExpressionTypes.TOKEN_NE, + MEExpressionTypes.TOKEN_BITWISE_AND, + MEExpressionTypes.TOKEN_BITWISE_XOR, + MEExpressionTypes.TOKEN_BITWISE_OR, + MEExpressionTypes.TOKEN_ASSIGN, + ) + val PARENS = TokenSet.create(MEExpressionTypes.TOKEN_LEFT_PAREN, MEExpressionTypes.TOKEN_RIGHT_PAREN) + val BRACKETS = TokenSet.create(MEExpressionTypes.TOKEN_LEFT_BRACKET, MEExpressionTypes.TOKEN_RIGHT_BRACKET) + val BRACES = TokenSet.create(MEExpressionTypes.TOKEN_LEFT_BRACE, MEExpressionTypes.TOKEN_RIGHT_BRACE) +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenType.kt b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenType.kt new file mode 100644 index 000000000..54a746b88 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenType.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.platform.mixin.expression.psi + +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionLanguage +import com.intellij.psi.tree.IElementType +import org.jetbrains.annotations.NonNls + +class MEExpressionTokenType(@NonNls debugName: String) : IElementType(debugName, MEExpressionLanguage) { + override fun toString() = "MEExpressionTokenType.${super.toString()}" +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEMatchableElement.kt b/src/main/kotlin/platform/mixin/expression/psi/MEMatchableElement.kt new file mode 100644 index 000000000..3474a3067 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MEMatchableElement.kt @@ -0,0 +1,31 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.expression.psi + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.intellij.psi.PsiElement + +interface MEMatchableElement : PsiElement { + fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean + + fun getInputExprs(): List +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/MENameElementManipulator.kt b/src/main/kotlin/platform/mixin/expression/psi/MENameElementManipulator.kt new file mode 100644 index 000000000..ba971b0e2 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MENameElementManipulator.kt @@ -0,0 +1,34 @@ +/* + * 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.expression.psi + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory +import com.intellij.openapi.util.TextRange +import com.intellij.psi.AbstractElementManipulator + +class MENameElementManipulator : AbstractElementManipulator() { + override fun handleContentChange(element: MEName, range: TextRange, newContent: String): MEName { + val text = element.text + val newText = text.substring(0, range.startOffset) + newContent + text.substring(range.endOffset) + return element.project.meExpressionElementFactory.createName(newText) + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEPsiUtil.kt b/src/main/kotlin/platform/mixin/expression/psi/MEPsiUtil.kt new file mode 100644 index 000000000..b9856a13e --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MEPsiUtil.kt @@ -0,0 +1,58 @@ +/* + * 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.expression.psi + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEAssignStatement +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENameExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEParenthesizedExpression + +object MEPsiUtil { + fun isAccessedForReading(expr: MEExpression): Boolean { + return !isAccessedForWriting(expr) + } + + fun isAccessedForWriting(expr: MEExpression): Boolean { + val parent = expr.parent + return parent is MEAssignStatement && expr == parent.targetExpr + } + + fun skipParenthesizedExprDown(expr: MEExpression): MEExpression? { + var e: MEExpression? = expr + while (e is MEParenthesizedExpression) { + e = e.expression + } + return e + } + + fun isWildcardExpression(expr: MEExpression): Boolean { + val actualExpr = skipParenthesizedExprDown(expr) ?: return false + return actualExpr is MENameExpression && actualExpr.meName.isWildcard + } + + fun isIdentifierStart(char: Char): Boolean { + return char in 'a'..'z' || char in 'A'..'Z' || char == '_' + } + + fun isIdentifierPart(char: Char): Boolean { + return isIdentifierStart(char) || char in '0'..'9' + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/MERecursiveWalkingVisitor.kt b/src/main/kotlin/platform/mixin/expression/psi/MERecursiveWalkingVisitor.kt new file mode 100644 index 000000000..40f6beab5 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/MERecursiveWalkingVisitor.kt @@ -0,0 +1,45 @@ +/* + * 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.expression.psi + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEVisitor +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiRecursiveVisitor +import com.intellij.psi.PsiWalkingState + +abstract class MERecursiveWalkingVisitor : MEVisitor(), PsiRecursiveVisitor { + private val walkingState = object : PsiWalkingState(this) { + override fun elementFinished(element: PsiElement) { + this@MERecursiveWalkingVisitor.elementFinished(element) + } + } + + override fun visitElement(element: PsiElement) { + walkingState.elementStarted(element) + } + + open fun elementFinished(element: PsiElement) { + } + + fun stopWalking() { + walkingState.stopWalking() + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/METypeUtil.kt b/src/main/kotlin/platform/mixin/expression/psi/METypeUtil.kt new file mode 100644 index 000000000..95c047703 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/METypeUtil.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.expression.psi + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArrayAccessExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEBinaryExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MECastExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENameExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEParenthesizedExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatement +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype +import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory +import com.intellij.patterns.ObjectPattern +import com.intellij.patterns.PatternCondition +import com.intellij.psi.PsiElement +import com.intellij.psi.util.parentOfType +import com.intellij.util.ProcessingContext + +object METypeUtil { + fun convertExpressionToType(expr: MEExpression): METype? { + return if (isExpressionValidType(expr)) { + expr.project.meExpressionElementFactory.createType(expr.text) + } else { + null + } + } + + private fun isExpressionValidType(expr: MEExpression): Boolean { + var e = expr + while (true) { + when (e) { + is MEArrayAccessExpression -> { + if (e.indexExpr != null || e.rightBracketToken == null) { + return false + } + e = e.arrayExpr + } + is MENameExpression -> return true + else -> return false + } + } + } + + fun isExpressionDirectlyInTypePosition(expr: MEExpression): Boolean { + var e: PsiElement? = expr + while (e != null) { + val parent = e.parent + when (parent) { + is MEArrayAccessExpression -> {} + is MEParenthesizedExpression -> { + val grandparent = parent.parent + return grandparent is MECastExpression && e == grandparent.castTypeExpr + } + is MEBinaryExpression -> { + return parent.operator == MEExpressionTypes.TOKEN_INSTANCEOF && e == parent.rightExpr + } + else -> return false + } + e = parent + } + + return false + } + + fun isExpressionInTypePosition(expr: MEExpression): Boolean { + var e: PsiElement? = expr + while (e != null) { + val parent = e.parent + when (parent) { + is MEParenthesizedExpression -> { + val grandparent = parent.parent + if (grandparent is MECastExpression && e == grandparent.castTypeExpr) { + return true + } + } + is MEBinaryExpression -> { + if (parent.operator == MEExpressionTypes.TOKEN_INSTANCEOF && e == parent.rightExpr) { + return true + } + } + is MEStatement -> return false + } + e = parent + } + + return false + } + + fun > ObjectPattern.inTypePosition(): Self = + with(InTypePositionCondition) + fun > ObjectPattern.notInTypePosition(): Self = + without(InTypePositionCondition) + fun > ObjectPattern.validType(): Self = + with(ValidTypeCondition) + + private object InTypePositionCondition : PatternCondition("inTypePosition") { + override fun accepts(t: PsiElement, context: ProcessingContext?) = + t.parentOfType()?.let(::isExpressionInTypePosition) == true + } + + private object ValidTypeCondition : PatternCondition("validType") { + override fun accepts(t: PsiElement, context: ProcessingContext?) = + t.parentOfType(withSelf = true)?.let(::isExpressionValidType) == true + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArgumentsMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArgumentsMixin.kt new file mode 100644 index 000000000..2ef6ed9f3 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArgumentsMixin.kt @@ -0,0 +1,34 @@ +/* + * 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.expression.psi.mixins + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiExpression +import com.intellij.psi.PsiExpressionList + +interface MEArgumentsMixin : PsiElement { + fun matchesJava(java: PsiExpressionList, context: MESourceMatchContext): Boolean { + return matchesJava(java.expressions, context) + } + + fun matchesJava(java: Array, context: MESourceMatchContext): Boolean +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArrayAccessExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArrayAccessExpressionMixin.kt new file mode 100644 index 000000000..c026499db --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArrayAccessExpressionMixin.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.platform.mixin.expression.psi.mixins + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.intellij.psi.PsiElement + +interface MEArrayAccessExpressionMixin : MEExpression { + val leftBracketToken: PsiElement + val rightBracketToken: PsiElement? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MEBinaryExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEBinaryExpressionMixin.kt new file mode 100644 index 000000000..1631852dd --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEBinaryExpressionMixin.kt @@ -0,0 +1,30 @@ +/* + * 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.expression.psi.mixins + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype +import com.intellij.psi.tree.IElementType + +interface MEBinaryExpressionMixin : MEExpression { + val operator: IElementType + val castType: METype? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MECastExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MECastExpressionMixin.kt new file mode 100644 index 000000000..3e61e8c13 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MECastExpressionMixin.kt @@ -0,0 +1,30 @@ +/* + * 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.expression.psi.mixins + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype + +interface MECastExpressionMixin : MEExpression { + val castType: METype? + val castTypeExpr: MEExpression? + val castedExpr: MEExpression? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MEDeclarationItemMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEDeclarationItemMixin.kt new file mode 100644 index 000000000..cb8d52136 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEDeclarationItemMixin.kt @@ -0,0 +1,27 @@ +/* + * 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.expression.psi.mixins + +import com.intellij.psi.PsiElement + +interface MEDeclarationItemMixin : PsiElement { + val isType: Boolean +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MELitExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MELitExpressionMixin.kt new file mode 100644 index 000000000..e1a376343 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MELitExpressionMixin.kt @@ -0,0 +1,31 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.expression.psi.mixins + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.intellij.lang.ASTNode + +interface MELitExpressionMixin : MEExpression { + val value: Any? + val isNull: Boolean + val isString: Boolean + val minusToken: ASTNode? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MENameMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MENameMixin.kt new file mode 100644 index 000000000..5e344d9d2 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MENameMixin.kt @@ -0,0 +1,28 @@ +/* + * 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.expression.psi.mixins + +import com.intellij.psi.PsiElement + +interface MENameMixin : PsiElement { + val isWildcard: Boolean + val identifierElement: PsiElement? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MENewExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MENewExpressionMixin.kt new file mode 100644 index 000000000..31b29b4a9 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MENewExpressionMixin.kt @@ -0,0 +1,35 @@ +/* + * 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.expression.psi.mixins + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArguments +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.intellij.psi.PsiElement + +interface MENewExpressionMixin : PsiElement { + val isArrayCreation: Boolean + val hasConstructorArguments: Boolean + val dimensions: Int + val dimExprTokens: List + val arrayInitializer: MEArguments? + + class DimExprTokens(val leftBracket: PsiElement, val expr: MEExpression?, val rightBracket: PsiElement?) +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/METypeMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/METypeMixin.kt new file mode 100644 index 000000000..30d404f6c --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/METypeMixin.kt @@ -0,0 +1,32 @@ +/* + * 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.expression.psi.mixins + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiType + +interface METypeMixin : PsiElement { + val isArray: Boolean + val dimensions: Int + + fun matchesJava(java: PsiType, context: MESourceMatchContext): Boolean +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MEUnaryExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEUnaryExpressionMixin.kt new file mode 100644 index 000000000..21cb6abf6 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEUnaryExpressionMixin.kt @@ -0,0 +1,28 @@ +/* + * 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.expression.psi.mixins + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.intellij.psi.tree.IElementType + +interface MEUnaryExpressionMixin : MEExpression { + val operator: IElementType +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArgumentsImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArgumentsImplMixin.kt new file mode 100644 index 000000000..a83224344 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArgumentsImplMixin.kt @@ -0,0 +1,44 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEArgumentsMixin +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiExpression +import com.intellij.psi.util.PsiUtil + +abstract class MEArgumentsImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), MEArgumentsMixin { + override fun matchesJava(java: Array, context: MESourceMatchContext): Boolean { + val exprs = expressionList + if (exprs.size != java.size) { + return false + } + return exprs.asSequence().zip(java.asSequence()).all { (expr, javaExpr) -> + val actualJavaExpr = PsiUtil.skipParenthesizedExprDown(javaExpr) ?: return@all false + expr.matchesJava(actualJavaExpr, context) + } + } + + protected abstract val expressionList: List +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArrayAccessExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArrayAccessExpressionImplMixin.kt new file mode 100644 index 000000000..dcc6f6f9f --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArrayAccessExpressionImplMixin.kt @@ -0,0 +1,58 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.expression.psi.MEPsiUtil +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEArrayAccessExpressionMixin +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiArrayAccessExpression +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiUtil + +abstract class MEArrayAccessExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MEArrayAccessExpressionMixin { + override val leftBracketToken get() = findNotNullChildByType(MEExpressionTypes.TOKEN_LEFT_BRACKET) + override val rightBracketToken get() = findChildByType(MEExpressionTypes.TOKEN_RIGHT_BRACKET) + + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiArrayAccessExpression) { + return false + } + + val readMatch = MEPsiUtil.isAccessedForReading(this) && PsiUtil.isAccessedForReading(java) + val writeMatch = MEPsiUtil.isAccessedForWriting(this) && PsiUtil.isAccessedForWriting(java) + if (!readMatch && !writeMatch) { + return false + } + + val javaArray = PsiUtil.skipParenthesizedExprDown(java.arrayExpression) ?: return false + val javaIndex = PsiUtil.skipParenthesizedExprDown(java.indexExpression) ?: return false + return arrayExpr.matchesJava(javaArray, context) && indexExpr?.matchesJava(javaIndex, context) == true + } + + override fun getInputExprs() = listOfNotNull(arrayExpr, indexExpr) + + protected abstract val arrayExpr: MEExpression + protected abstract val indexExpr: MEExpression? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEAssignStatementImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEAssignStatementImplMixin.kt new file mode 100644 index 000000000..f3fbfb396 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEAssignStatementImplMixin.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.platform.mixin.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEStatementImpl +import com.intellij.lang.ASTNode +import com.intellij.psi.JavaTokenType +import com.intellij.psi.PsiAssignmentExpression +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiUtil +import com.siyeh.ig.PsiReplacementUtil + +abstract class MEAssignStatementImplMixin(node: ASTNode) : MEStatementImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiAssignmentExpression) { + return false + } + val isOperatorAssignment = java.operationTokenType != JavaTokenType.EQ + val expandedJava = if (isOperatorAssignment) { + PsiReplacementUtil.replaceOperatorAssignmentWithAssignmentExpression(java.copy() as PsiAssignmentExpression) + as PsiAssignmentExpression + } else { + java + } + + val leftJava = PsiUtil.skipParenthesizedExprDown(expandedJava.lExpression) ?: return false + val rightJava = PsiUtil.skipParenthesizedExprDown(expandedJava.rExpression) ?: return false + context.fakeElementScope(isOperatorAssignment, java) { + return targetExpr.matchesJava(leftJava, context) && rightExpr?.matchesJava(rightJava, context) == true + } + } + + override fun getInputExprs() = targetExpr.getInputExprs() + listOfNotNull(rightExpr) + + protected abstract val targetExpr: MEExpression + protected abstract val rightExpr: MEExpression? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBinaryExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBinaryExpressionImplMixin.kt new file mode 100644 index 000000000..96c6ae245 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBinaryExpressionImplMixin.kt @@ -0,0 +1,124 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEBinaryExpressionMixin +import com.intellij.lang.ASTNode +import com.intellij.psi.JavaTokenType +import com.intellij.psi.PsiBinaryExpression +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiInstanceOfExpression +import com.intellij.psi.PsiTypeTestPattern +import com.intellij.psi.tree.TokenSet +import com.intellij.psi.util.JavaPsiPatternUtil +import com.intellij.psi.util.PsiUtil + +abstract class MEBinaryExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MEBinaryExpressionMixin { + override val operator get() = node.findChildByType(operatorTokens)!!.elementType + override val castType get() = rightExpr + ?.takeIf { operator == MEExpressionTypes.TOKEN_INSTANCEOF } + ?.let(METypeUtil::convertExpressionToType) + + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (operator == MEExpressionTypes.TOKEN_INSTANCEOF) { + if (java !is PsiInstanceOfExpression) { + return false + } + if (!leftExpr.matchesJava(java.operand, context)) { + return false + } + val javaType = java.checkType?.type + ?: (JavaPsiPatternUtil.skipParenthesizedPatternDown(java.pattern) as? PsiTypeTestPattern) + ?.checkType?.type + ?: return false + return castType?.matchesJava(javaType, context) == true + } else { + if (java !is PsiBinaryExpression) { + return false + } + + val operatorMatches = when (java.operationTokenType) { + JavaTokenType.ASTERISK -> operator == MEExpressionTypes.TOKEN_MULT + JavaTokenType.DIV -> operator == MEExpressionTypes.TOKEN_DIV + JavaTokenType.PERC -> operator == MEExpressionTypes.TOKEN_MOD + JavaTokenType.PLUS -> operator == MEExpressionTypes.TOKEN_PLUS + JavaTokenType.MINUS -> operator == MEExpressionTypes.TOKEN_MINUS + JavaTokenType.LTLT -> operator == MEExpressionTypes.TOKEN_SHL + JavaTokenType.GTGT -> operator == MEExpressionTypes.TOKEN_SHR + JavaTokenType.GTGTGT -> operator == MEExpressionTypes.TOKEN_USHR + JavaTokenType.LT -> operator == MEExpressionTypes.TOKEN_LT + JavaTokenType.LE -> operator == MEExpressionTypes.TOKEN_LE + JavaTokenType.GT -> operator == MEExpressionTypes.TOKEN_GT + JavaTokenType.GE -> operator == MEExpressionTypes.TOKEN_GE + JavaTokenType.EQEQ -> operator == MEExpressionTypes.TOKEN_EQ + JavaTokenType.NE -> operator == MEExpressionTypes.TOKEN_NE + JavaTokenType.AND -> operator == MEExpressionTypes.TOKEN_BITWISE_AND + JavaTokenType.XOR -> operator == MEExpressionTypes.TOKEN_BITWISE_XOR + JavaTokenType.OR -> operator == MEExpressionTypes.TOKEN_BITWISE_OR + else -> false + } + if (!operatorMatches) { + return false + } + + val javaLeft = PsiUtil.skipParenthesizedExprDown(java.lOperand) ?: return false + val javaRight = PsiUtil.skipParenthesizedExprDown(java.rOperand) ?: return false + return leftExpr.matchesJava(javaLeft, context) && rightExpr?.matchesJava(javaRight, context) == true + } + } + + override fun getInputExprs() = if (operator == MEExpressionTypes.TOKEN_INSTANCEOF) { + listOf(leftExpr) + } else { + listOfNotNull(leftExpr, rightExpr) + } + + protected abstract val leftExpr: MEExpression + protected abstract val rightExpr: MEExpression? + + companion object { + private val operatorTokens = TokenSet.create( + MEExpressionTypes.TOKEN_MULT, + MEExpressionTypes.TOKEN_DIV, + MEExpressionTypes.TOKEN_MOD, + MEExpressionTypes.TOKEN_PLUS, + MEExpressionTypes.TOKEN_MINUS, + MEExpressionTypes.TOKEN_SHL, + MEExpressionTypes.TOKEN_SHR, + MEExpressionTypes.TOKEN_USHR, + MEExpressionTypes.TOKEN_LT, + MEExpressionTypes.TOKEN_LE, + MEExpressionTypes.TOKEN_GT, + MEExpressionTypes.TOKEN_GE, + MEExpressionTypes.TOKEN_EQ, + MEExpressionTypes.TOKEN_NE, + MEExpressionTypes.TOKEN_BITWISE_AND, + MEExpressionTypes.TOKEN_BITWISE_XOR, + MEExpressionTypes.TOKEN_BITWISE_OR, + MEExpressionTypes.TOKEN_INSTANCEOF, + ) + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBoundReferenceExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBoundReferenceExpressionImplMixin.kt new file mode 100644 index 000000000..08e035b61 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBoundReferenceExpressionImplMixin.kt @@ -0,0 +1,64 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiMethodReferenceExpression +import com.intellij.psi.util.PsiUtil + +abstract class MEBoundReferenceExpressionImplMixin(node: ASTNode) : MEExpressionImplMixin(node), MEExpression { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiMethodReferenceExpression) { + return false + } + + if (java.isConstructor) { + return false + } + + val qualifier = PsiUtil.skipParenthesizedExprDown(java.qualifierExpression) ?: return false + if (!receiverExpr.matchesJava(qualifier, context)) { + return false + } + + val memberName = this.memberName ?: return false + if (memberName.isWildcard) { + return true + } + + val method = java.resolve() as? PsiMethod ?: return false + val qualifierClass = QualifiedMember.resolveQualifier(java) ?: method.containingClass ?: return false + return context.getMethods(memberName.text).any { reference -> + reference.matchMethod(method, qualifierClass) + } + } + + override fun getInputExprs() = listOf(receiverExpr) + + abstract val receiverExpr: MEExpression + abstract val memberName: MEName? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECapturingExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECapturingExpressionImplMixin.kt new file mode 100644 index 000000000..3401aab82 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECapturingExpressionImplMixin.kt @@ -0,0 +1,38 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement + +abstract class MECapturingExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + context.addCapture(java) + return expression?.matchesJava(java, context) == true + } + + override fun getInputExprs() = listOfNotNull(expression) + + protected abstract val expression: MEExpression? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECastExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECastExpressionImplMixin.kt new file mode 100644 index 000000000..866999357 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECastExpressionImplMixin.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.mixin.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEParenthesizedExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.expression.psi.MEPsiUtil +import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MECastExpressionMixin +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiInstanceOfExpression +import com.intellij.psi.PsiTypeCastExpression +import com.intellij.psi.PsiTypeTestPattern +import com.intellij.psi.util.JavaPsiPatternUtil +import com.intellij.psi.util.PsiUtil + +abstract class MECastExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MECastExpressionMixin { + override val castType get() = castTypeExpr?.let(METypeUtil::convertExpressionToType) + override val castTypeExpr get() = + (expressionList.let { it.getOrNull(it.size - 2) } as? MEParenthesizedExpression)?.expression + override val castedExpr get() = expressionList.lastOrNull() + + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + return when (java) { + is PsiTypeCastExpression -> { + val javaType = java.castType?.type ?: return false + val javaOperand = PsiUtil.skipParenthesizedExprDown(java.operand) ?: return false + castType?.matchesJava(javaType, context) == true && + castedExpr?.matchesJava(javaOperand, context) == true + } + is PsiInstanceOfExpression -> { + val pattern = JavaPsiPatternUtil.skipParenthesizedPatternDown(java.pattern) as? PsiTypeTestPattern + ?: return false + val javaType = pattern.checkType?.type ?: return false + val castedExpr = this.castedExpr ?: return false + return MEPsiUtil.isWildcardExpression(castedExpr) && castType?.matchesJava(javaType, context) == true + } + else -> false + } + } + + override fun getInputExprs() = listOfNotNull(castedExpr) + + protected abstract val expressionList: List +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEClassConstantExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEClassConstantExpressionImplMixin.kt new file mode 100644 index 000000000..8415f7cd0 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEClassConstantExpressionImplMixin.kt @@ -0,0 +1,60 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.intellij.lang.ASTNode +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiClassObjectAccessExpression +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiField +import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.util.PsiTypesUtil + +abstract class MEClassConstantExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + return when (java) { + is PsiClassObjectAccessExpression -> type.matchesJava(java.operand.type, context) + is PsiReferenceExpression -> { + if (java.referenceName != "TYPE") { + return false + } + val field = java.resolve() as? PsiField ?: return false + val containingClass = field.containingClass?.qualifiedName ?: return false + val unboxedType = PsiTypesUtil.unboxIfPossible(containingClass) + if (unboxedType == null || unboxedType == containingClass) { + return false + } + val javaType = JavaPsiFacade.getElementFactory(context.project).createPrimitiveTypeFromText(unboxedType) + type.matchesJava(javaType, context) + } + else -> false + } + } + + override fun getInputExprs() = emptyList() + + protected abstract val type: METype +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEConstructorReferenceExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEConstructorReferenceExpressionImplMixin.kt new file mode 100644 index 000000000..ecaecca53 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEConstructorReferenceExpressionImplMixin.kt @@ -0,0 +1,47 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethodReferenceExpression + +abstract class MEConstructorReferenceExpressionImplMixin(node: ASTNode) : MEExpressionImplMixin(node), MEExpression { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiMethodReferenceExpression) { + return false + } + + if (!java.isConstructor) { + return false + } + + val qualifierType = java.qualifierType?.type ?: return false + return className.matchesJava(qualifierType, context) + } + + override fun getInputExprs() = emptyList() + + abstract val className: METype +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationImplMixin.kt new file mode 100644 index 000000000..6201fbb9c --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationImplMixin.kt @@ -0,0 +1,63 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.asset.PlatformAssets +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEDeclarationItem +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEItemImpl +import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory +import com.intellij.lang.ASTNode +import com.intellij.navigation.ItemPresentation +import com.intellij.openapi.util.Iconable +import com.intellij.psi.NavigatablePsiElement +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiNameIdentifierOwner +import com.intellij.psi.PsiNamedElement +import com.intellij.psi.search.LocalSearchScope +import com.intellij.util.PlatformIcons +import javax.swing.Icon + +abstract class MEDeclarationImplMixin( + node: ASTNode +) : MEItemImpl(node), PsiNamedElement, PsiNameIdentifierOwner, NavigatablePsiElement { + override fun getName(): String = nameIdentifier.text + + override fun setName(name: String): PsiElement { + this.nameIdentifier.replace(project.meExpressionElementFactory.createIdentifier(name)) + return this + } + + override fun getNameIdentifier(): PsiElement = firstChild + + override fun getUseScope() = containingFile?.let(::LocalSearchScope) ?: super.getUseScope() + + override fun getPresentation() = object : ItemPresentation { + override fun getPresentableText() = name + + override fun getIcon(unused: Boolean) = this@MEDeclarationImplMixin.getIcon(Iconable.ICON_FLAG_VISIBILITY) + } + + override fun getIcon(flags: Int): Icon = if ((parent as? MEDeclarationItem)?.isType == true) { + PlatformIcons.CLASS_ICON + } else { + PlatformAssets.MIXIN_ICON + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationItemImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationItemImplMixin.kt new file mode 100644 index 000000000..fbd21db66 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationItemImplMixin.kt @@ -0,0 +1,32 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEDeclarationItemMixin +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement + +abstract class MEDeclarationItemImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), MEDeclarationItemMixin { + override val isType: Boolean + get() = findChildByType(MEExpressionTypes.TOKEN_BOOL_LIT)?.text == "true" +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionImplMixin.kt new file mode 100644 index 000000000..9cc9b64a6 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionImplMixin.kt @@ -0,0 +1,38 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.psi.MEMatchableElement +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement + +abstract class MEExpressionImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), MEMatchableElement { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + throw UnsupportedOperationException("Please implement matchesJava for your expression type") + } + + override fun getInputExprs(): List { + throw UnsupportedOperationException("Please implement getInputExprs for your expression type") + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionStatementImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionStatementImplMixin.kt new file mode 100644 index 000000000..e895d32ba --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionStatementImplMixin.kt @@ -0,0 +1,37 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEStatementImpl +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement + +abstract class MEExpressionStatementImplMixin(node: ASTNode) : MEStatementImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + return expression.matchesJava(java, context) + } + + override fun getInputExprs() = listOf(expression) + + protected abstract val expression: MEExpression +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEFreeMethodReferenceExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEFreeMethodReferenceExpressionImplMixin.kt new file mode 100644 index 000000000..d264eb4ce --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEFreeMethodReferenceExpressionImplMixin.kt @@ -0,0 +1,60 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiMethodReferenceExpression + +abstract class MEFreeMethodReferenceExpressionImplMixin(node: ASTNode) : MEExpressionImplMixin(node), MEExpression { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiMethodReferenceExpression) { + return false + } + + if (java.isConstructor) { + return false + } + + val qualifierClass = (java.qualifierType?.type as? PsiClassType)?.resolve() ?: return false + + // check wildcard after checking for the qualifier class, otherwise the reference could have been qualified by + // an expression. + val memberName = this.memberName ?: return false + if (memberName.isWildcard) { + return true + } + + val method = java.resolve() as? PsiMethod ?: return false + return context.getMethods(memberName.text).any { reference -> + reference.matchMethod(method, qualifierClass) + } + } + + override fun getInputExprs() = emptyList() + + abstract val memberName: MEName? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MELitExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MELitExpressionImplMixin.kt new file mode 100644 index 000000000..733f689f8 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MELitExpressionImplMixin.kt @@ -0,0 +1,124 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MELitExpressionMixin +import com.intellij.lang.ASTNode +import com.intellij.psi.JavaTokenType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiLiteral +import com.intellij.psi.PsiUnaryExpression +import com.intellij.psi.util.PsiUtil +import com.intellij.util.IncorrectOperationException + +abstract class MELitExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MELitExpressionMixin { + override val value get() = when (node.firstChildNode.elementType) { + MEExpressionTypes.TOKEN_NULL_LIT -> null + MEExpressionTypes.TOKEN_MINUS -> { + when (node.lastChildNode.elementType) { + MEExpressionTypes.TOKEN_INT_LIT -> { + val text = node.lastChildNode.text + if (text.startsWith("0x")) { + "-${text.substring(2)}".toLongOrNull(16) + } else { + "-$text".toLongOrNull() + } + } + MEExpressionTypes.TOKEN_DEC_LIT -> { + "-${node.lastChildNode.text}".toDoubleOrNull() + } + else -> throw IncorrectOperationException("Invalid number literal format") + } + } + MEExpressionTypes.TOKEN_BOOL_LIT -> node.chars[0] == 't' + MEExpressionTypes.TOKEN_INT_LIT -> { + val text = this.text + if (text.startsWith("0x")) { + text.substring(2).toLongOrNull(16) + } else { + text.toLongOrNull() + } + } + MEExpressionTypes.TOKEN_DEC_LIT -> text.toDoubleOrNull() + else -> { + val text = this.text + if (text.length >= 2) { + text.substring(1, text.length - 1).replace("\\'", "'").replace("\\\\", "\\") + } else { + null + } + } + } + + override val isNull get() = node.firstChildNode.elementType == MEExpressionTypes.TOKEN_NULL_LIT + override val isString get() = node.firstChildNode.elementType == MEExpressionTypes.TOKEN_STRING_TERMINATOR + + override val minusToken get() = node.firstChildNode.takeIf { it.elementType == MEExpressionTypes.TOKEN_MINUS } + + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + return when (java) { + is PsiLiteral -> { + val value = this.value + val javaValue = java.value.widened + // MixinExtras compares floats as strings + when (value) { + is Double -> javaValue is Double && value.toString() == javaValue.toString() + is String -> { + val matchesChar = + value.length == 1 && javaValue is Long && value.firstOrNull()?.code?.toLong() == javaValue + matchesChar || value == javaValue + } + else -> value == javaValue + } + } + is PsiUnaryExpression -> { + if (java.operationTokenType != JavaTokenType.MINUS) { + return false + } + val javaOperand = PsiUtil.skipParenthesizedExprDown(java.operand) ?: return false + if (javaOperand !is PsiLiteral) { + return false + } + val value = this.value + val javaValue = javaOperand.value.widened + when (value) { + is Long -> javaValue == -value + is Double -> javaValue is Double && javaValue.toString() == (-value).toString() + else -> false + } + } + else -> false + } + } + + override fun getInputExprs() = emptyList() + + private val Any?.widened: Any? get() = when (this) { + is Int -> toLong() + is Float -> toDouble() + is Char -> code.toLong() + else -> this + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMemberAccessExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMemberAccessExpressionImplMixin.kt new file mode 100644 index 000000000..4c4c4a11e --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMemberAccessExpressionImplMixin.kt @@ -0,0 +1,71 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember +import com.intellij.lang.ASTNode +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiField +import com.intellij.psi.PsiModifier +import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.util.PsiUtil +import com.siyeh.ig.psiutils.ExpressionUtils + +abstract class MEMemberAccessExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiReferenceExpression) { + return false + } + + val arrayFromLength = ExpressionUtils.getArrayFromLengthExpression(java) + if (arrayFromLength != null) { + if (memberName.isWildcard || memberName.text == "length") { + return true + } + } + + val resolved = java.resolve() as? PsiField ?: return false + if (resolved.hasModifierProperty(PsiModifier.STATIC)) { + return false + } + + val javaReceiver = PsiUtil.skipParenthesizedExprDown(java.qualifierExpression) + ?: JavaPsiFacade.getElementFactory(context.project).createExpressionFromText("this", null) + context.fakeElementScope(java.qualifierExpression == null, java) { + if (!receiverExpr.matchesJava(javaReceiver, context)) { + return false + } + } + + val qualifier = QualifiedMember.resolveQualifier(java) ?: resolved.containingClass ?: return false + return context.getFields(memberName.text).any { it.matchField(resolved, qualifier) } + } + + override fun getInputExprs() = listOf(receiverExpr) + + protected abstract val receiverExpr: MEExpression + protected abstract val memberName: MEName +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMethodCallExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMethodCallExpressionImplMixin.kt new file mode 100644 index 000000000..2f397998e --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMethodCallExpressionImplMixin.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.mixin.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArguments +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember +import com.intellij.lang.ASTNode +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethodCallExpression +import com.intellij.psi.PsiModifier +import com.intellij.psi.util.PsiUtil +import com.siyeh.ig.psiutils.MethodCallUtils + +abstract class MEMethodCallExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiMethodCallExpression) { + return false + } + + if (MethodCallUtils.hasSuperQualifier(java)) { + return false + } + + val method = java.resolveMethod() ?: return false + if (method.hasModifierProperty(PsiModifier.STATIC)) { + return false + } + + if (!memberName.isWildcard) { + val methodId = memberName.text + val qualifier = + QualifiedMember.resolveQualifier(java.methodExpression) ?: method.containingClass ?: return false + if (context.getMethods(methodId).none { it.matchMethod(method, qualifier) }) { + return false + } + } + + val javaReceiver = PsiUtil.skipParenthesizedExprDown(java.methodExpression.qualifierExpression) + ?: JavaPsiFacade.getElementFactory(context.project).createExpressionFromText("this", null) + context.fakeElementScope(java.methodExpression.qualifierExpression == null, java.methodExpression) { + if (!receiverExpr.matchesJava(javaReceiver, context)) { + return false + } + } + + return arguments?.matchesJava(java.argumentList, context) == true + } + + override fun getInputExprs() = listOf(receiverExpr) + (arguments?.expressionList ?: emptyList()) + + protected abstract val receiverExpr: MEExpression + protected abstract val memberName: MEName + protected abstract val arguments: MEArguments? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameExpressionImplMixin.kt new file mode 100644 index 000000000..decc485d4 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameExpressionImplMixin.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.mixin.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember +import com.demonwav.mcdev.platform.mixin.util.LocalVariables +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiField +import com.intellij.psi.PsiReferenceExpression +import com.intellij.psi.PsiVariable +import com.intellij.psi.util.PsiUtil + +abstract class MENameExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (MEName.isWildcard) { + return true + } + + if (java !is PsiReferenceExpression) { + return false + } + val variable = java.resolve() as? PsiVariable ?: return false + + val name = MEName.text + + // match against fields + if (variable is PsiField) { + val qualifier = QualifiedMember.resolveQualifier(java) ?: variable.containingClass ?: return false + return context.getFields(name).any { it.matchField(variable, qualifier) } + } + + // match against local variables + val sourceArgs by lazy { + LocalVariables.guessLocalsAt(java, true, !PsiUtil.isAccessedForWriting(java)) + } + val sourceVariables by lazy { + LocalVariables.guessLocalsAt(java, false, !PsiUtil.isAccessedForWriting(java)) + } + for (localInfo in context.getLocalInfos(name)) { + val sourceLocals = if (localInfo.argsOnly) sourceArgs else sourceVariables + for (local in localInfo.matchSourceLocals(sourceLocals)) { + if (local.variable == variable) { + return true + } + } + } + + return false + } + + override fun getInputExprs() = emptyList() + + @Suppress("PropertyName") + protected abstract val MEName: MEName +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameImplMixin.kt new file mode 100644 index 000000000..50b02d3c7 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameImplMixin.kt @@ -0,0 +1,41 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MENameMixin +import com.demonwav.mcdev.platform.mixin.expression.reference.MEDefinitionReference +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiReference + +abstract class MENameImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), MENameMixin { + override val isWildcard get() = node.firstChildNode.elementType == MEExpressionTypes.TOKEN_WILDCARD + override val identifierElement get() = if (isWildcard) null else firstChild + + override fun getReference(): PsiReference? { + if (isWildcard) { + return null + } + return MEDefinitionReference(this as MEName) + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENewExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENewExpressionImplMixin.kt new file mode 100644 index 000000000..1f72ada47 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENewExpressionImplMixin.kt @@ -0,0 +1,138 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArguments +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MENewExpressionMixin +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiArrayType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiNewExpression +import com.intellij.psi.util.PsiUtil +import com.intellij.psi.util.siblings + +abstract class MENewExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MENewExpressionMixin { + override val isArrayCreation get() = findChildByType(MEExpressionTypes.TOKEN_LEFT_BRACKET) != null + + override val hasConstructorArguments get() = findChildByType(MEExpressionTypes.TOKEN_LEFT_PAREN) != null + + override val dimensions get() = findChildrenByType(MEExpressionTypes.TOKEN_LEFT_BRACKET).size + + override val dimExprTokens: List get() { + val result = mutableListOf() + + var leftBracket: PsiElement? = findNotNullChildByType(MEExpressionTypes.TOKEN_LEFT_BRACKET) + while (leftBracket != null) { + var expr: MEExpression? = null + var rightBracket: PsiElement? = null + var nextLeftBracket: PsiElement? = null + for (child in leftBracket.siblings(withSelf = false)) { + if (child is MEExpression) { + expr = child + } else { + when (child.node.elementType) { + MEExpressionTypes.TOKEN_RIGHT_BRACKET -> rightBracket = child + MEExpressionTypes.TOKEN_LEFT_BRACKET -> { + nextLeftBracket = child + break + } + } + } + } + result += MENewExpressionMixin.DimExprTokens(leftBracket, expr, rightBracket) + leftBracket = nextLeftBracket + } + + return result + } + + override val arrayInitializer get() = if (isArrayCreation) { + arguments + } else { + null + } + + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiNewExpression) { + return false + } + + if (isArrayCreation) { + if (!java.isArrayCreation) { + return false + } + + val javaArrayType = java.type as? PsiArrayType ?: return false + if (javaArrayType.arrayDimensions != dimensions) { + return false + } + + val matchesType = context.project.meExpressionElementFactory.createType(type) + .matchesJava(javaArrayType.deepComponentType, context) + if (!matchesType) { + return false + } + + val javaArrayDims = java.arrayDimensions + val arrayDims = dimExprs + if (javaArrayDims.size != arrayDims.size) { + return false + } + if (!javaArrayDims.asSequence().zip(arrayDims.asSequence()).all { (javaArrayDim, arrayDim) -> + val actualJavaDim = PsiUtil.skipParenthesizedExprDown(javaArrayDim) ?: return@all false + arrayDim.matchesJava(actualJavaDim, context) + } + ) { + return false + } + + val javaArrayInitializer = java.arrayInitializer + val arrayInitializer = this.arrayInitializer + return if (javaArrayInitializer == null) { + arrayInitializer == null + } else { + arrayInitializer?.matchesJava(javaArrayInitializer.initializers, context) == true + } + } else { // !isArrayCreation + if (java.isArrayCreation) { + return false + } + + val javaType = java.type ?: return false + val javaArgs = java.argumentList ?: return false + + return context.project.meExpressionElementFactory.createType(type).matchesJava(javaType, context) && + arguments?.matchesJava(javaArgs, context) == true + } + } + + override fun getInputExprs() = dimExprs + (arguments?.expressionList ?: emptyList()) + + protected abstract val type: MEName + protected abstract val dimExprs: List + protected abstract val arguments: MEArguments? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEParenthesizedExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEParenthesizedExpressionImplMixin.kt new file mode 100644 index 000000000..4061c6c6a --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEParenthesizedExpressionImplMixin.kt @@ -0,0 +1,37 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement + +abstract class MEParenthesizedExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + return expression?.matchesJava(java, context) == true + } + + override fun getInputExprs() = listOfNotNull(expression) + + protected abstract val expression: MEExpression? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEReturnStatementImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEReturnStatementImplMixin.kt new file mode 100644 index 000000000..0113a7b86 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEReturnStatementImplMixin.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.platform.mixin.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEStatementImpl +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiReturnStatement +import com.intellij.psi.util.PsiUtil + +abstract class MEReturnStatementImplMixin(node: ASTNode) : MEStatementImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiReturnStatement) { + return false + } + val javaReturnValue = PsiUtil.skipParenthesizedExprDown(java.returnValue) ?: return false + return valueExpr?.matchesJava(javaReturnValue, context) == true + } + + override fun getInputExprs() = listOfNotNull(valueExpr) + + protected abstract val valueExpr: MEExpression? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStatementImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStatementImplMixin.kt new file mode 100644 index 000000000..39f8ad153 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStatementImplMixin.kt @@ -0,0 +1,38 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.psi.MEMatchableElement +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement + +abstract class MEStatementImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), MEMatchableElement { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + throw UnsupportedOperationException("Please implement matchesJava for your statement type") + } + + override fun getInputExprs(): List { + throw UnsupportedOperationException("Please implement getInputExprs for your statement type") + } +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStaticMethodCallExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStaticMethodCallExpressionImplMixin.kt new file mode 100644 index 000000000..7a578b242 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStaticMethodCallExpressionImplMixin.kt @@ -0,0 +1,60 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArguments +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethodCallExpression +import com.intellij.psi.PsiModifier + +abstract class MEStaticMethodCallExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiMethodCallExpression) { + return false + } + + val method = java.resolveMethod() ?: return false + if (!method.hasModifierProperty(PsiModifier.STATIC)) { + return false + } + + if (!memberName.isWildcard) { + val methodId = memberName.text + val qualifier = + QualifiedMember.resolveQualifier(java.methodExpression) ?: method.containingClass ?: return false + if (context.getMethods(methodId).none { it.matchMethod(method, qualifier) }) { + return false + } + } + + return arguments?.matchesJava(java.argumentList, context) == true + } + + override fun getInputExprs() = arguments?.expressionList ?: emptyList() + + protected abstract val memberName: MEName + protected abstract val arguments: MEArguments? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MESuperCallExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MESuperCallExpressionImplMixin.kt new file mode 100644 index 000000000..2dee0b9c7 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MESuperCallExpressionImplMixin.kt @@ -0,0 +1,60 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArguments +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethodCallExpression +import com.siyeh.ig.psiutils.MethodCallUtils + +abstract class MESuperCallExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiMethodCallExpression) { + return false + } + if (!MethodCallUtils.hasSuperQualifier(java)) { + return false + } + + val memberName = this.memberName ?: return false + if (!memberName.isWildcard) { + val method = java.resolveMethod() ?: return false + val methodId = memberName.text + val qualifier = + QualifiedMember.resolveQualifier(java.methodExpression) ?: method.containingClass ?: return false + if (context.getMethods(methodId).none { it.matchMethod(method, qualifier) }) { + return false + } + } + + return arguments?.matchesJava(java.argumentList, context) == true + } + + override fun getInputExprs() = arguments?.expressionList ?: emptyList() + + protected abstract val memberName: MEName? + protected abstract val arguments: MEArguments? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhisExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhisExpressionImplMixin.kt new file mode 100644 index 000000000..6a4d25339 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhisExpressionImplMixin.kt @@ -0,0 +1,36 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiThisExpression + +abstract class METhisExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + return java is PsiThisExpression && java.qualifier == null + } + + override fun getInputExprs() = emptyList() +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhrowStatementImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhrowStatementImplMixin.kt new file mode 100644 index 000000000..4226d24d3 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhrowStatementImplMixin.kt @@ -0,0 +1,44 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEStatementImpl +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiThrowStatement +import com.intellij.psi.util.PsiUtil + +abstract class METhrowStatementImplMixin(node: ASTNode) : MEStatementImpl(node) { + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiThrowStatement) { + return false + } + + val javaException = PsiUtil.skipParenthesizedExprDown(java.exception) ?: return false + return valueExpr?.matchesJava(javaException, context) == true + } + + override fun getInputExprs() = listOfNotNull(valueExpr) + + protected abstract val valueExpr: MEExpression? +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METypeImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METypeImplMixin.kt new file mode 100644 index 000000000..f41550915 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METypeImplMixin.kt @@ -0,0 +1,53 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.METypeMixin +import com.demonwav.mcdev.util.descriptor +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode +import com.intellij.psi.PsiArrayType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiType + +abstract class METypeImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), METypeMixin { + override val isArray get() = findChildByType(MEExpressionTypes.TOKEN_LEFT_BRACKET) != null + override val dimensions get() = findChildrenByType(MEExpressionTypes.TOKEN_LEFT_BRACKET).size + + override fun matchesJava(java: PsiType, context: MESourceMatchContext): Boolean { + if (MEName.isWildcard) { + return java.arrayDimensions >= dimensions + } else { + var unwrappedElementType = java + repeat(dimensions) { + unwrappedElementType = (unwrappedElementType as? PsiArrayType)?.componentType ?: return false + } + val descriptor = unwrappedElementType.descriptor + return context.getTypes(MEName.text).any { it == descriptor } + } + } + + @Suppress("PropertyName") + protected abstract val MEName: MEName +} diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEUnaryExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEUnaryExpressionImplMixin.kt new file mode 100644 index 000000000..8238e13d8 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEUnaryExpressionImplMixin.kt @@ -0,0 +1,65 @@ +/* + * 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.expression.psi.mixins.impl + +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl +import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEUnaryExpressionMixin +import com.intellij.lang.ASTNode +import com.intellij.psi.JavaTokenType +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiLiteral +import com.intellij.psi.PsiUnaryExpression +import com.intellij.psi.util.PsiUtil + +abstract class MEUnaryExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MEUnaryExpressionMixin { + override val operator get() = node.firstChildNode.elementType + + override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean { + if (java !is PsiUnaryExpression) { + return false + } + + val operatorMatches = when (java.operationTokenType) { + JavaTokenType.MINUS -> operator == MEExpressionTypes.TOKEN_MINUS + JavaTokenType.TILDE -> operator == MEExpressionTypes.TOKEN_BITWISE_NOT + else -> false + } + if (!operatorMatches) { + return false + } + + val javaOperand = PsiUtil.skipParenthesizedExprDown(java.operand) ?: return false + + if (operator == MEExpressionTypes.TOKEN_MINUS && javaOperand is PsiLiteral) { + // avoid matching "-1" etc + return false + } + + return expression?.matchesJava(javaOperand, context) == true + } + + override fun getInputExprs() = listOfNotNull(expression) + + protected abstract val expression: MEExpression? +} diff --git a/src/main/kotlin/platform/mixin/expression/reference/MEDefinitionReference.kt b/src/main/kotlin/platform/mixin/expression/reference/MEDefinitionReference.kt new file mode 100644 index 000000000..2b281f093 --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/reference/MEDefinitionReference.kt @@ -0,0 +1,70 @@ +/* + * 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.expression.reference + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName +import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory +import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionFile +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiReference +import com.intellij.psi.util.parentOfType +import com.intellij.util.ArrayUtilRt +import com.intellij.util.IncorrectOperationException + +class MEDefinitionReference(private var name: MEName) : PsiReference { + override fun getElement() = name + + override fun getRangeInElement() = TextRange(0, name.textLength) + + override fun resolve(): PsiElement? { + val file = element.parentOfType() ?: return null + val name = element.text + for (declItem in file.declarations) { + val declaration = declItem.declaration + if (declaration?.name == name) { + return declaration + } + } + + return null + } + + override fun getCanonicalText(): String = name.text + + override fun handleElementRename(newElementName: String): PsiElement { + name = name.replace(name.project.meExpressionElementFactory.createName(newElementName)) as MEName + return name + } + + override fun bindToElement(element: PsiElement): PsiElement { + throw IncorrectOperationException() + } + + override fun isReferenceTo(element: PsiElement) = element.manager.areElementsEquivalent(element, resolve()) + + override fun isSoft() = false + + override fun getVariants(): Array { + return (name.containingFile as? MEExpressionFile)?.declarations?.mapNotNull { it.declaration }?.toTypedArray() + ?: ArrayUtilRt.EMPTY_OBJECT_ARRAY + } +} diff --git a/src/main/kotlin/platform/mixin/expression/reference/MEExpressionFindUsagesProvider.kt b/src/main/kotlin/platform/mixin/expression/reference/MEExpressionFindUsagesProvider.kt new file mode 100644 index 000000000..c20296b7c --- /dev/null +++ b/src/main/kotlin/platform/mixin/expression/reference/MEExpressionFindUsagesProvider.kt @@ -0,0 +1,38 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin.expression.reference + +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEDeclaration +import com.intellij.lang.findUsages.FindUsagesProvider +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiNamedElement + +class MEExpressionFindUsagesProvider : FindUsagesProvider { + override fun canFindUsagesFor(psiElement: PsiElement) = psiElement is MEDeclaration + + override fun getHelpId(psiElement: PsiElement) = null + + override fun getType(element: PsiElement) = "Definition" + + override fun getDescriptiveName(element: PsiElement) = (element as? PsiNamedElement)?.name ?: "null" + + override fun getNodeText(element: PsiElement, useFullName: Boolean) = getDescriptiveName(element) +} diff --git a/src/main/kotlin/platform/mixin/folding/MixinFoldingOptionsProvider.kt b/src/main/kotlin/platform/mixin/folding/MixinFoldingOptionsProvider.kt index 7f359fd58..44ccb2c9a 100644 --- a/src/main/kotlin/platform/mixin/folding/MixinFoldingOptionsProvider.kt +++ b/src/main/kotlin/platform/mixin/folding/MixinFoldingOptionsProvider.kt @@ -56,5 +56,15 @@ class MixinFoldingOptionsProvider : { settings.state.foldAccessorMethodCalls }, { b -> settings.state.foldAccessorMethodCalls = b }, ) + checkBox( + "Fold MixinExtras expression definitions", + { settings.state.foldDefinitions }, + { b -> settings.state.foldDefinitions = b }, + ) + checkBox( + "Fold MixinExtras expression definition fields and methods", + { settings.state.foldDefinitionFieldsAndMethods }, + { b -> settings.state.foldDefinitionFieldsAndMethods = b }, + ) } } diff --git a/src/main/kotlin/platform/mixin/folding/MixinFoldingSettings.kt b/src/main/kotlin/platform/mixin/folding/MixinFoldingSettings.kt index 081cc03aa..61fee79af 100644 --- a/src/main/kotlin/platform/mixin/folding/MixinFoldingSettings.kt +++ b/src/main/kotlin/platform/mixin/folding/MixinFoldingSettings.kt @@ -35,6 +35,8 @@ class MixinFoldingSettings : PersistentStateComponent) companion object { @@ -217,4 +220,6 @@ object DefaultInjectorAnnotationHandler : InjectorAnnotationHandler() { ) = null override val isSoft = true + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.CUSTOM } diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt index d34c4aad2..69a4197e6 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt @@ -30,6 +30,7 @@ import com.demonwav.mcdev.util.descriptor import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiMethod +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.Type import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode @@ -135,4 +136,6 @@ class ModifyArgHandler : InjectorAnnotationHandler() { } } } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_ARG } diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt index 446e9a20f..0d3b5476e 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt @@ -27,6 +27,7 @@ import com.demonwav.mcdev.util.Parameter import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiTypes +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.MethodInsnNode @@ -58,4 +59,6 @@ class ModifyArgsHandler : InjectorAnnotationHandler() { ), ) } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_ARGS } diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt index eebd528de..839d9f832 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt @@ -31,6 +31,7 @@ import com.intellij.psi.PsiMethod import com.intellij.psi.PsiType import com.intellij.psi.PsiTypes import com.intellij.psi.util.parentOfType +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.Opcodes import org.objectweb.asm.Type import org.objectweb.asm.tree.AbstractInsnNode @@ -131,4 +132,6 @@ class ModifyConstantHandler : InjectorAnnotationHandler() { override fun isInsnAllowed(insn: AbstractInsnNode): Boolean { return insn.opcode in allowedOpcodes } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_CONSTANT } diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt index d9e4eeae9..42398c23d 100644 --- a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt @@ -32,6 +32,7 @@ import com.demonwav.mcdev.util.findContainingMethod import com.demonwav.mcdev.util.findModule import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.Type import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.MethodNode @@ -85,4 +86,6 @@ class ModifyVariableHandler : InjectorAnnotationHandler() { return result } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_VARIABLE } diff --git a/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt b/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt index 81e494291..70d2d7fd2 100644 --- a/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt @@ -39,6 +39,7 @@ import com.intellij.psi.PsiElementFactory import com.intellij.psi.PsiManager import com.intellij.psi.PsiType import com.intellij.psi.PsiTypes +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.Opcodes import org.objectweb.asm.Type import org.objectweb.asm.tree.AbstractInsnNode @@ -105,6 +106,8 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() { override val allowCoerce = true + override val mixinExtrasExpressionContextType = ExpressionContext.Type.REDIRECT + private interface RedirectType { fun isInsnAllowed(node: AbstractInsnNode) = true diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt index c1b1df6ac..ec61f4a4c 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt @@ -210,6 +210,7 @@ class AtResolver( val targetPsiClass = targetElement.parentOfType() ?: return emptyList() val navigationVisitor = injectionPoint.createNavigationVisitor(at, target, targetPsiClass) ?: return emptyList() + navigationVisitor.configureBytecodeTarget(targetClass, targetMethod) targetElement.accept(navigationVisitor) return bytecodeResults.mapNotNull { bytecodeResult -> diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt index 8939f36ae..17a917eef 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt @@ -321,6 +321,9 @@ abstract class NavigationVisitor : JavaRecursiveElementVisitor() { result += element } + open fun configureBytecodeTarget(classNode: ClassNode, methodNode: MethodNode) { + } + open fun visitStart(executableElement: PsiElement) { } @@ -407,6 +410,7 @@ abstract class CollectVisitor(protected val mode: Mode) { insn: AbstractInsnNode, element: T, qualifier: String? = null, + decorations: Map = emptyMap(), ) { // apply shift. // being able to break out of the shift loops is important to prevent IDE freezes in case of large shift bys. @@ -427,7 +431,14 @@ abstract class CollectVisitor(protected val mode: Mode) { } } - val result = Result(nextIndex++, insn, shiftedInsn ?: return, element, qualifier) + val result = Result( + nextIndex++, + insn, + shiftedInsn ?: return, + element, + qualifier, + if (insn === shiftedInsn) decorations else emptyMap() + ) var isFiltered = false for ((name, filter) in resultFilters) { if (!filter(result, method)) { @@ -463,6 +474,7 @@ abstract class CollectVisitor(protected val mode: Mode) { val insn: AbstractInsnNode, val target: T, val qualifier: String? = null, + val decorations: Map ) enum class Mode { MATCH_ALL, MATCH_FIRST, COMPLETION } diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt index 4e7378045..7ad828179 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt @@ -28,7 +28,6 @@ import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.MODIFY_ import com.demonwav.mcdev.util.constantValue import com.demonwav.mcdev.util.findContainingMethod import com.demonwav.mcdev.util.findModule -import com.demonwav.mcdev.util.isErasureEquivalentTo import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.openapi.module.Module import com.intellij.psi.JavaPsiFacade @@ -166,13 +165,13 @@ abstract class AbstractLoadInjectionPoint(private val store: Boolean) : Injectio val parentExpr = PsiUtil.skipParenthesizedExprUp(expression.parent) val isIincUnary = parentExpr is PsiUnaryExpression && ( - parentExpr.operationSign.tokenType == JavaTokenType.PLUSPLUS || - parentExpr.operationSign.tokenType == JavaTokenType.MINUSMINUS + parentExpr.operationTokenType == JavaTokenType.PLUSPLUS || + parentExpr.operationTokenType == JavaTokenType.MINUSMINUS ) val isIincAssignment = parentExpr is PsiAssignmentExpression && ( - parentExpr.operationSign.tokenType == JavaTokenType.PLUSEQ || - parentExpr.operationSign.tokenType == JavaTokenType.MINUSEQ + parentExpr.operationTokenType == JavaTokenType.PLUSEQ || + parentExpr.operationTokenType == JavaTokenType.MINUSEQ ) && PsiUtil.isConstantExpression(parentExpr.rExpression) && (parentExpr.rExpression?.constantValue as? Number)?.toInt() @@ -239,42 +238,10 @@ abstract class AbstractLoadInjectionPoint(private val store: Boolean) : Injectio name: String, localsHere: List, ) { - if (info.ordinal != null) { - val local = localsHere.asSequence().filter { - it.type.isErasureEquivalentTo(info.type) - }.drop(info.ordinal).firstOrNull() - if (name == local?.name) { + for (local in info.matchSourceLocals(localsHere)) { + if (name == local.name) { addResult(location) } - return - } - - if (info.index != null) { - val local = localsHere.getOrNull(info.index) - if (name == local?.name) { - addResult(location) - } - return - } - - if (info.names.isNotEmpty()) { - val matchingLocals = localsHere.filter { - info.names.contains(it.mixinName) - } - for (local in matchingLocals) { - if (local.name == name) { - addResult(location) - } - } - return - } - - // implicit mode - val local = localsHere.singleOrNull { - it.type.isErasureEquivalentTo(info.type) - } - if (local != null && local.name == name) { - addResult(location) } } } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ExpressionInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ExpressionInjectionPoint.kt new file mode 100644 index 000000000..680d8ea19 --- /dev/null +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ExpressionInjectionPoint.kt @@ -0,0 +1,284 @@ +/* + * 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.mixinextras + +import com.demonwav.mcdev.platform.mixin.expression.IdentifierPoolFactory +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil +import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MECapturingExpression +import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatement +import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory +import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionFile +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AtResolver +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InjectionPoint +import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.NavigationVisitor +import com.demonwav.mcdev.platform.mixin.reference.MixinSelector +import com.demonwav.mcdev.platform.mixin.util.LocalInfo +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.MemberReference +import com.demonwav.mcdev.util.computeStringArray +import com.demonwav.mcdev.util.constantStringValue +import com.demonwav.mcdev.util.descriptor +import com.demonwav.mcdev.util.findAnnotations +import com.demonwav.mcdev.util.findContainingModifierList +import com.demonwav.mcdev.util.findModule +import com.demonwav.mcdev.util.findMultiInjectionHost +import com.demonwav.mcdev.util.ifEmpty +import com.demonwav.mcdev.util.parseArray +import com.demonwav.mcdev.util.resolveType +import com.demonwav.mcdev.util.resolveTypeArray +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.lang.injection.InjectedLanguageManager +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiLiteral +import com.intellij.psi.PsiModifierList +import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.psi.codeStyle.JavaCodeStyleManager +import com.intellij.psi.util.parentOfType +import com.llamalad7.mixinextras.expression.impl.ast.expressions.Expression +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext +import java.util.IdentityHashMap +import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.ClassNode +import org.objectweb.asm.tree.MethodNode + +class ExpressionInjectionPoint : InjectionPoint() { + override fun onCompleted(editor: Editor, reference: PsiLiteral) { + val modifierList = reference.findContainingModifierList() ?: return + if (modifierList.hasAnnotation(MixinConstants.MixinExtras.EXPRESSION)) { + return + } + + val project = reference.project + + val exprAnnotation = modifierList.addAfter( + JavaPsiFacade.getElementFactory(project) + .createAnnotationFromText("@${MixinConstants.MixinExtras.EXPRESSION}(\"\")", reference), + null + ) + + // add imports and reformat + JavaCodeStyleManager.getInstance(project).shortenClassReferences(exprAnnotation) + JavaCodeStyleManager.getInstance(project).optimizeImports(modifierList.containingFile) + val formattedModifierList = CodeStyleManager.getInstance(project).reformat(modifierList) as PsiModifierList + + // move the caret to @Expression("") + val formattedExprAnnotation = formattedModifierList.findAnnotation(MixinConstants.MixinExtras.EXPRESSION) + ?: return + val exprLiteral = formattedExprAnnotation.findDeclaredAttributeValue(null) ?: return + editor.caretModel.moveToOffset(exprLiteral.textRange.startOffset + 1) + } + + override fun createNavigationVisitor( + at: PsiAnnotation, + target: MixinSelector?, + targetClass: PsiClass + ): NavigationVisitor? { + val project = at.project + + val atId = at.findDeclaredAttributeValue("id")?.constantStringValue ?: "" + + val injectorAnnotation = AtResolver.findInjectorAnnotation(at) ?: return null + val modifierList = injectorAnnotation.parent as? PsiModifierList ?: return null + val parsedExprs = parseExpressions(project, modifierList, atId) + parsedExprs.ifEmpty { return null } + + val sourceMatchContext = createSourceMatchContext(project, modifierList) + + return MyNavigationVisitor(parsedExprs.map { it.second }, sourceMatchContext) + } + + private fun createSourceMatchContext( + project: Project, + modifierList: PsiModifierList + ): MESourceMatchContext { + val matchContext = MESourceMatchContext(project) + + for (annotation in modifierList.annotations) { + if (!annotation.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION)) { + continue + } + + val definitionId = annotation.findDeclaredAttributeValue("id")?.constantStringValue ?: "" + + val fields = annotation.findDeclaredAttributeValue("field")?.computeStringArray() ?: emptyList() + for (field in fields) { + val fieldRef = MemberReference.parse(field) ?: continue + matchContext.addField(definitionId, fieldRef) + } + + val methods = annotation.findDeclaredAttributeValue("method")?.computeStringArray() ?: emptyList() + for (method in methods) { + val methodRef = MemberReference.parse(method) ?: continue + matchContext.addMethod(definitionId, methodRef) + } + + val types = annotation.findDeclaredAttributeValue("type")?.resolveTypeArray() ?: emptyList() + for (type in types) { + matchContext.addType(definitionId, type.descriptor) + } + + val locals = annotation.findDeclaredAttributeValue("local")?.findAnnotations() ?: emptyList() + for (localAnnotation in locals) { + val localType = localAnnotation.findDeclaredAttributeValue("type")?.resolveType() + val localInfo = LocalInfo.fromAnnotation(localType, localAnnotation) + matchContext.addLocalInfo(definitionId, localInfo) + } + } + + return matchContext + } + + override fun doCreateCollectVisitor( + at: PsiAnnotation, + target: MixinSelector?, + targetClass: ClassNode, + mode: CollectVisitor.Mode + ): CollectVisitor? { + val project = at.project + + val atId = at.findDeclaredAttributeValue("id")?.constantStringValue ?: "" + + val contextType = MEExpressionMatchUtil.getContextType(project, at.parentOfType()?.qualifiedName) + + val injectorAnnotation = AtResolver.findInjectorAnnotation(at) ?: return null + val modifierList = injectorAnnotation.parent as? PsiModifierList ?: return null + val parsedExprs = parseExpressions(project, modifierList, atId) + parsedExprs.ifEmpty { return null } + + val module = at.findModule() ?: return null + + val poolFactory = MEExpressionMatchUtil.createIdentifierPoolFactory(module, targetClass, modifierList) + + return MyCollectVisitor(mode, project, targetClass, parsedExprs, poolFactory, contextType) + } + + private fun parseExpressions( + project: Project, + modifierList: PsiModifierList, + atId: String + ): List> { + return modifierList.annotations.asSequence() + .filter { exprAnnotation -> + exprAnnotation.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION) && + (exprAnnotation.findDeclaredAttributeValue("id")?.constantStringValue ?: "") == atId + } + .flatMap { exprAnnotation -> + val expressionElements = exprAnnotation.findDeclaredAttributeValue("value")?.parseArray { it } + ?: return@flatMap emptySequence>() + expressionElements.asSequence().mapNotNull { expressionElement -> + val text = expressionElement.constantStringValue ?: return@mapNotNull null + val rootStatementPsi = InjectedLanguageManager.getInstance(project) + .getInjectedPsiFiles(expressionElement)?.firstOrNull() + ?.let { + (it.first as? MEExpressionFile)?.statements?.firstOrNull { stmt -> + stmt.findMultiInjectionHost()?.parentOfType() == exprAnnotation + } + } + ?: project.meExpressionElementFactory.createFile("do {$text}").statements.singleOrNull() + ?: project.meExpressionElementFactory.createStatement("empty") + MEExpressionMatchUtil.createExpression(text)?.let { it to rootStatementPsi } + } + } + .toList() + } + + override fun createLookup( + targetClass: ClassNode, + result: CollectVisitor.Result + ): LookupElementBuilder? { + return null + } + + private class MyCollectVisitor( + mode: Mode, + private val project: Project, + private val targetClass: ClassNode, + private val expressions: List>, + private val poolFactory: IdentifierPoolFactory, + private val contextType: ExpressionContext.Type, + ) : CollectVisitor(mode) { + override fun accept(methodNode: MethodNode) { + val insns = methodNode.instructions ?: return + + val pool = poolFactory(methodNode) + val flows = MEExpressionMatchUtil.getFlowMap(project, targetClass, methodNode) ?: return + + val result = IdentityHashMap>>() + + for ((expr, psiExpr) in expressions) { + MEExpressionMatchUtil.findMatchingInstructions( + targetClass, + methodNode, + pool, + flows, + expr, + flows.keys, + contextType, + false + ) { match -> + val capturedExpr = psiExpr.findElementAt(match.startOffset) + ?.parentOfType(withSelf = true) + ?.expression + ?: psiExpr + result.putIfAbsent(match.originalInsn, capturedExpr to match.decorations) + } + } + + if (result.isEmpty()) { + return + } + + for (insn in insns) { + val (element, decorations) = result[insn] ?: continue + addResult(insn, element, decorations = decorations) + } + } + } + + private class MyNavigationVisitor( + private val statements: List, + private val matchContext: MESourceMatchContext + ) : NavigationVisitor() { + override fun visitElement(element: PsiElement) { + for (statement in statements) { + if (statement.matchesJava(element, matchContext)) { + if (matchContext.captures.isNotEmpty()) { + for (capture in matchContext.captures) { + addResult(capture) + } + } else { + addResult(element) + } + } + matchContext.reset() + } + + super.visitElement(element) + } + } +} diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt index a0cb1ba40..2e76bcb9b 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt @@ -35,8 +35,10 @@ import com.demonwav.mcdev.util.Parameter import com.demonwav.mcdev.util.toJavaIdentifier import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation +import com.intellij.psi.PsiElement import com.intellij.psi.PsiType import com.intellij.psi.PsiTypes +import com.llamalad7.mixinextras.expression.impl.utils.ExpressionDecorations import org.objectweb.asm.Opcodes import org.objectweb.asm.Type import org.objectweb.asm.tree.AbstractInsnNode @@ -52,30 +54,43 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler( enum class InstructionType { METHOD_CALL { - override fun matches(insn: AbstractInsnNode) = insn is MethodInsnNode && insn.name != "" + override fun matches(target: TargetInsn) = target.insn is MethodInsnNode && target.insn.name != "" }, FIELD_GET { - override fun matches(insn: AbstractInsnNode) = - insn.opcode == Opcodes.GETFIELD || insn.opcode == Opcodes.GETSTATIC + override fun matches(target: TargetInsn) = + target.insn.opcode == Opcodes.GETFIELD || target.insn.opcode == Opcodes.GETSTATIC }, FIELD_SET { - override fun matches(insn: AbstractInsnNode) = - insn.opcode == Opcodes.PUTFIELD || insn.opcode == Opcodes.PUTSTATIC + override fun matches(target: TargetInsn) = + target.insn.opcode == Opcodes.PUTFIELD || target.insn.opcode == Opcodes.PUTSTATIC }, INSTANTIATION { - override fun matches(insn: AbstractInsnNode) = insn.opcode == Opcodes.NEW + override fun matches(target: TargetInsn) = target.insn.opcode == Opcodes.NEW }, INSTANCEOF { - override fun matches(insn: AbstractInsnNode) = insn.opcode == Opcodes.INSTANCEOF + override fun matches(target: TargetInsn) = target.insn.opcode == Opcodes.INSTANCEOF }, CONSTANT { - override fun matches(insn: AbstractInsnNode) = isConstant(insn) + override fun matches(target: TargetInsn) = isConstant(target.insn) }, RETURN { - override fun matches(insn: AbstractInsnNode) = insn.opcode in Opcodes.IRETURN..Opcodes.ARETURN + override fun matches(target: TargetInsn) = target.insn.opcode in Opcodes.IRETURN..Opcodes.ARETURN + }, + SIMPLE_OPERATION { + override fun matches(target: TargetInsn) = + target.hasDecoration(ExpressionDecorations.SIMPLE_OPERATION_ARGS) && + target.hasDecoration(ExpressionDecorations.SIMPLE_OPERATION_RETURN_TYPE) + }, + SIMPLE_EXPRESSION { + override fun matches(target: TargetInsn) = + target.hasDecoration(ExpressionDecorations.SIMPLE_EXPRESSION_TYPE) + }, + STRING_CONCAT_EXPRESSION { + override fun matches(target: TargetInsn) = + target.hasDecoration(ExpressionDecorations.IS_STRING_CONCAT_EXPRESSION) }; - abstract fun matches(insn: AbstractInsnNode): Boolean + abstract fun matches(target: TargetInsn): Boolean } abstract val supportedInstructionTypes: Collection @@ -86,9 +101,13 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler( annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insn: AbstractInsnNode + target: TargetInsn, ): Pair? + open fun intLikeTypePositions( + target: TargetInsn + ): List = emptyList() + override val allowCoerce = true override fun expectedMethodSignature( @@ -98,26 +117,82 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler( ): List? { val insns = resolveInstructions(annotation, targetClass, targetMethod) .ifEmpty { return emptyList() } - .map { it.insn } + .map { TargetInsn(it.insn, it.decorations) } if (insns.any { insn -> supportedInstructionTypes.none { it.matches(insn) } }) return emptyList() - val signatures = insns.map { expectedMethodSignature(annotation, targetClass, targetMethod, it) } + val signatures = insns.map { insn -> + expectedMethodSignature(annotation, targetClass, targetMethod, insn) + } val firstMatch = signatures[0] ?: return emptyList() if (signatures.drop(1).any { it != firstMatch }) return emptyList() - return listOf( - MethodSignature( - listOf( - firstMatch.first, - ParameterGroup( - collectTargetMethodParameters(annotation.project, targetClass, targetMethod), - required = ParameterGroup.RequiredLevel.OPTIONAL, - isVarargs = true, - ), - ), - firstMatch.second - ) + val intLikeTypePositions = insns.map { intLikeTypePositions(it) }.distinct().singleOrNull().orEmpty() + return allPossibleSignatures( + annotation, + targetClass, + targetMethod, + firstMatch.first, + firstMatch.second, + intLikeTypePositions ) } + private fun allPossibleSignatures( + annotation: PsiAnnotation, + targetClass: ClassNode, + targetMethod: MethodNode, + params: ParameterGroup, + returnType: PsiType, + intLikeTypePositions: List + ): List { + if (intLikeTypePositions.isEmpty()) { + return listOf( + makeSignature(annotation, targetClass, targetMethod, params, returnType, intLikeTypePositions) + ) + } + return buildList { + for (actualType in intLikePsiTypes) { + val newParams = params.parameters.toMutableList() + var newReturnType = returnType + for (pos in intLikeTypePositions) { + when (pos) { + is MethodSignature.TypePosition.Return -> newReturnType = actualType + is MethodSignature.TypePosition.Param -> + newParams[pos.index] = newParams[pos.index].copy(type = actualType) + } + } + add( + makeSignature( + annotation, + targetClass, + targetMethod, + ParameterGroup(newParams), + newReturnType, + intLikeTypePositions + ) + ) + } + } + } + + private fun makeSignature( + annotation: PsiAnnotation, + targetClass: ClassNode, + targetMethod: MethodNode, + params: ParameterGroup, + returnType: PsiType, + intLikeTypePositions: List + ) = MethodSignature( + listOf( + params, + ParameterGroup( + collectTargetMethodParameters(annotation.project, targetClass, targetMethod), + required = ParameterGroup.RequiredLevel.OPTIONAL, + isVarargs = true, + ), + ), + returnType, + intLikeTypePositions + ) + protected fun getInsnReturnType(insn: AbstractInsnNode): Type? { return when { insn is MethodInsnNode -> Type.getReturnType(insn.desc) @@ -287,7 +362,12 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler( } else -> null - } ?: getInsnArgTypes(insn, targetClass)?.map { Parameter(null, it.toPsiType(elementFactory)) } + } ?: getInsnArgTypes(insn, targetClass)?.toParameters(annotation) + } + + protected fun List.toParameters(context: PsiElement, names: Array? = null): List { + val elementFactory = JavaPsiFacade.getElementFactory(context.project) + return mapIndexed { i, it -> Parameter(names?.getOrNull(i), it.toPsiType(elementFactory)) } } } @@ -348,3 +428,7 @@ private fun getConstantType(insn: AbstractInsnNode?): Type? { } } } + +private val intLikePsiTypes = listOf( + PsiTypes.intType(), PsiTypes.booleanType(), PsiTypes.charType(), PsiTypes.byteType(), PsiTypes.shortType() +) diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt index 791423584..f16bf4924 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt @@ -20,10 +20,16 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras +import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup +import com.demonwav.mcdev.platform.mixin.util.toPsiType import com.demonwav.mcdev.util.Parameter +import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiType +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext +import com.llamalad7.mixinextras.expression.impl.utils.ExpressionASMUtils +import com.llamalad7.mixinextras.expression.impl.utils.ExpressionDecorations import org.objectweb.asm.Type import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode @@ -31,7 +37,8 @@ import org.objectweb.asm.tree.MethodNode class ModifyExpressionValueHandler : MixinExtrasInjectorAnnotationHandler() { override val supportedInstructionTypes = listOf( - InstructionType.METHOD_CALL, InstructionType.FIELD_GET, InstructionType.INSTANTIATION, InstructionType.CONSTANT + InstructionType.METHOD_CALL, InstructionType.FIELD_GET, InstructionType.INSTANTIATION, InstructionType.CONSTANT, + InstructionType.SIMPLE_EXPRESSION, InstructionType.STRING_CONCAT_EXPRESSION ) override fun extraTargetRestrictions(insn: AbstractInsnNode): Boolean { @@ -43,9 +50,36 @@ class ModifyExpressionValueHandler : MixinExtrasInjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insn: AbstractInsnNode + target: TargetInsn ): Pair? { - val psiType = getPsiReturnType(insn, annotation) ?: return null + val psiType = getReturnType(target, annotation) ?: return null return ParameterGroup(listOf(Parameter("original", psiType))) to psiType } + + override fun intLikeTypePositions(target: TargetInsn): List { + val expressionType = target.getDecoration(ExpressionDecorations.SIMPLE_EXPRESSION_TYPE) + if (expressionType == ExpressionASMUtils.INTLIKE_TYPE) { + return listOf(MethodSignature.TypePosition.Return, MethodSignature.TypePosition.Param(0)) + } + return emptyList() + } + + private fun getReturnType( + target: TargetInsn, + annotation: PsiAnnotation + ): PsiType? { + if (target.hasDecoration(ExpressionDecorations.IS_STRING_CONCAT_EXPRESSION)) { + return PsiType.getJavaLangString(annotation.manager, annotation.resolveScope) + } + val psiReturnType = getPsiReturnType(target.insn, annotation) + val rawReturnType = getInsnReturnType(target.insn) + val exprType = target.getDecoration(ExpressionDecorations.SIMPLE_EXPRESSION_TYPE) + if (exprType != null && rawReturnType != exprType) { + // The expression knows more than the standard logic does. + return exprType.toPsiType(JavaPsiFacade.getElementFactory(annotation.project)) + } + return psiReturnType + } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_EXPRESSION_VALUE } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt index 0c3c3c564..38ec7fc8a 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt @@ -23,6 +23,7 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiType +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.Opcodes import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode @@ -44,9 +45,11 @@ class ModifyReceiverHandler : MixinExtrasInjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insn: AbstractInsnNode + target: TargetInsn ): Pair? { - val params = getPsiParameters(insn, targetClass, annotation) ?: return null + val params = getPsiParameters(target.insn, targetClass, annotation) ?: return null return ParameterGroup(params) to params[0].type } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_RECEIVER } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt index 8c2706c33..df9157186 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt @@ -25,7 +25,7 @@ import com.demonwav.mcdev.platform.mixin.util.getGenericReturnType import com.demonwav.mcdev.util.Parameter import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiType -import org.objectweb.asm.tree.AbstractInsnNode +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.MethodNode @@ -36,9 +36,11 @@ class ModifyReturnValueHandler : MixinExtrasInjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insn: AbstractInsnNode - ): Pair? { + target: TargetInsn + ): Pair { val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project) return ParameterGroup(listOf(Parameter("original", returnType))) to returnType } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_RETURN_VALUE } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/TargetInsn.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/TargetInsn.kt new file mode 100644 index 000000000..1a03ff012 --- /dev/null +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/TargetInsn.kt @@ -0,0 +1,30 @@ +/* + * 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.mixinextras + +import org.objectweb.asm.tree.AbstractInsnNode + +class TargetInsn(val insn: AbstractInsnNode, private val decorations: Map) { + fun hasDecoration(key: String) = key in decorations + + @Suppress("UNCHECKED_CAST") + fun getDecoration(key: String): T? = decorations[key] as T +} diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt index cd2011c19..938f0103e 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt @@ -31,6 +31,7 @@ import com.demonwav.mcdev.util.Parameter import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiElement import com.intellij.psi.search.GlobalSearchScope +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.MethodNode @@ -80,4 +81,6 @@ class WrapMethodHandler : InjectorAnnotationHandler() { canDecompile = true )?.let(::listOf).orEmpty() } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.CUSTOM } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt index a5e2242e8..ef1726cc8 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt @@ -20,19 +20,25 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras +import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup import com.demonwav.mcdev.platform.mixin.util.mixinExtrasOperationType +import com.demonwav.mcdev.platform.mixin.util.toPsiType import com.demonwav.mcdev.util.Parameter +import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiType -import org.objectweb.asm.tree.AbstractInsnNode +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext +import com.llamalad7.mixinextras.expression.impl.utils.ExpressionASMUtils +import com.llamalad7.mixinextras.expression.impl.utils.ExpressionDecorations +import org.objectweb.asm.Type import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.MethodNode class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() { override val supportedInstructionTypes = listOf( InstructionType.METHOD_CALL, InstructionType.FIELD_GET, InstructionType.FIELD_SET, InstructionType.INSTANCEOF, - InstructionType.INSTANTIATION + InstructionType.INSTANTIATION, InstructionType.SIMPLE_OPERATION ) override fun getAtKey(annotation: PsiAnnotation): String { @@ -43,13 +49,51 @@ class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insn: AbstractInsnNode + target: TargetInsn ): Pair? { - val params = getPsiParameters(insn, targetClass, annotation) ?: return null - val returnType = getPsiReturnType(insn, annotation) ?: return null + val params = getParameterTypes(target, targetClass, annotation) ?: return null + val returnType = getReturnType(target, annotation) ?: return null val operationType = mixinExtrasOperationType(annotation, returnType) ?: return null return ParameterGroup( params + Parameter("original", operationType) ) to returnType } + + override fun intLikeTypePositions(target: TargetInsn) = buildList { + if ( + target.getDecoration(ExpressionDecorations.SIMPLE_OPERATION_RETURN_TYPE) + == ExpressionASMUtils.INTLIKE_TYPE + ) { + add(MethodSignature.TypePosition.Return) + } + target.getDecoration>(ExpressionDecorations.SIMPLE_OPERATION_ARGS)?.forEachIndexed { i, it -> + if (it == ExpressionASMUtils.INTLIKE_TYPE) { + add(MethodSignature.TypePosition.Param(i)) + } + } + } + + private fun getParameterTypes( + target: TargetInsn, + targetClass: ClassNode, + annotation: PsiAnnotation + ): List? { + getPsiParameters(target.insn, targetClass, annotation)?.let { return it } + val args = target.getDecoration>(ExpressionDecorations.SIMPLE_OPERATION_ARGS) ?: return null + return args.toList().toParameters( + annotation, + target.getDecoration(ExpressionDecorations.SIMPLE_OPERATION_PARAM_NAMES) + ) + } + + private fun getReturnType( + target: TargetInsn, + annotation: PsiAnnotation + ): PsiType? { + getPsiReturnType(target.insn, annotation)?.let { return it } + val type = target.getDecoration(ExpressionDecorations.SIMPLE_OPERATION_RETURN_TYPE) ?: return null + return type.toPsiType(JavaPsiFacade.getElementFactory(annotation.project)) + } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.WRAP_OPERATION } diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt index 8a92d6bc6..df64324b7 100644 --- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt +++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt @@ -24,6 +24,7 @@ import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiType import com.intellij.psi.PsiTypes +import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext import org.objectweb.asm.Type import org.objectweb.asm.tree.AbstractInsnNode import org.objectweb.asm.tree.ClassNode @@ -42,9 +43,11 @@ class WrapWithConditionHandler : MixinExtrasInjectorAnnotationHandler() { annotation: PsiAnnotation, targetClass: ClassNode, targetMethod: MethodNode, - insn: AbstractInsnNode + target: TargetInsn ): Pair? { - val params = getPsiParameters(insn, targetClass, annotation) ?: return null + val params = getPsiParameters(target.insn, targetClass, annotation) ?: return null return ParameterGroup(params) to PsiTypes.booleanType() } + + override val mixinExtrasExpressionContextType = ExpressionContext.Type.WRAP_WITH_CONDITION } diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt index 5d636de7f..bc168040a 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt @@ -20,7 +20,6 @@ package com.demonwav.mcdev.platform.mixin.inspection.injector -import com.demonwav.mcdev.platform.mixin.handlers.InjectAnnotationHandler import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection @@ -34,18 +33,35 @@ import com.demonwav.mcdev.platform.mixin.util.isConstructor import com.demonwav.mcdev.platform.mixin.util.isMixinExtrasSugar import com.demonwav.mcdev.util.Parameter import com.demonwav.mcdev.util.fullQualifiedName +import com.demonwav.mcdev.util.invokeLater import com.demonwav.mcdev.util.synchronize +import com.intellij.codeInsight.FileModificationService import com.intellij.codeInsight.intention.FileModifier.SafeFieldForPreview import com.intellij.codeInsight.intention.QuickFixFactory -import com.intellij.codeInspection.LocalQuickFix -import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.codeInsight.template.Expression +import com.intellij.codeInsight.template.ExpressionContext +import com.intellij.codeInsight.template.Template +import com.intellij.codeInsight.template.TemplateBuilderImpl +import com.intellij.codeInsight.template.TemplateManager +import com.intellij.codeInsight.template.TextResult +import com.intellij.codeInsight.template.impl.VariableNode +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement import com.intellij.codeInspection.ProblemHighlightType import com.intellij.codeInspection.ProblemsHolder +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange import com.intellij.psi.JavaElementVisitor import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiEllipsisType +import com.intellij.psi.PsiFile import com.intellij.psi.PsiMethod import com.intellij.psi.PsiModifier import com.intellij.psi.PsiNameHelper @@ -56,6 +72,8 @@ import com.intellij.psi.codeStyle.JavaCodeStyleManager import com.intellij.psi.codeStyle.VariableKind import com.intellij.psi.util.PsiUtil import com.intellij.psi.util.TypeConversionUtil +import com.intellij.psi.util.parentOfType +import com.intellij.refactoring.suggested.startOffset import org.objectweb.asm.Opcodes class InvalidInjectorMethodSignatureInspection : MixinInspection() { @@ -165,45 +183,42 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { } if (!isValid) { - val (expectedParameters, expectedReturnType) = possibleSignatures[0] - - val checkResult = checkParameters(parameters, expectedParameters, handler.allowCoerce) - if (checkResult != CheckResult.OK) { - reportedSignature = true - - val description = - "Method parameters do not match expected parameters for $annotationName" - val quickFix = ParametersQuickFix( - expectedParameters, - handler is InjectAnnotationHandler, - ) - if (checkResult == CheckResult.ERROR) { - holder.registerProblem(parameters, description, quickFix) - } else { - holder.registerProblem( - parameters, - description, - ProblemHighlightType.WARNING, - quickFix, - ) - } + val (expectedParameters, expectedReturnType, intLikeTypePositions) = possibleSignatures[0] + val normalizedReturnType = when (expectedReturnType) { + is PsiEllipsisType -> expectedReturnType.toArrayType() + else -> expectedReturnType } + val paramsCheck = checkParameters(parameters, expectedParameters, handler.allowCoerce) + val isWarning = paramsCheck == CheckResult.WARNING val methodReturnType = method.returnType - if (methodReturnType == null || - !checkReturnType(expectedReturnType, methodReturnType, method, handler.allowCoerce) - ) { + val returnTypeOk = methodReturnType != null && + checkReturnType(normalizedReturnType, methodReturnType, method, handler.allowCoerce) + val isError = paramsCheck == CheckResult.ERROR || !returnTypeOk + if (isWarning || isError) { reportedSignature = true + val description = + "Method signature does not match expected signature for $annotationName" + val quickFix = SignatureQuickFix( + method, + expectedParameters.takeUnless { paramsCheck == CheckResult.OK }, + normalizedReturnType.takeUnless { returnTypeOk }, + intLikeTypePositions + ) + val highlightType = + if (isError) + ProblemHighlightType.GENERIC_ERROR_OR_WARNING + else + ProblemHighlightType.WARNING + val declarationStart = (method.returnTypeElement ?: identifier).startOffsetInParent + val declarationEnd = method.parameterList.textRangeInParent.endOffset holder.registerProblem( - method.returnTypeElement ?: identifier, - "Expected return type '${expectedReturnType.presentableText}' " + - "for $annotationName method", - QuickFixFactory.getInstance().createMethodReturnFix( - method, - expectedReturnType, - false, - ), + method, + description, + highlightType, + TextRange.create(declarationStart, declarationEnd), + quickFix ) } } @@ -283,22 +298,43 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { OK, WARNING, ERROR } - private class ParametersQuickFix( + private class SignatureQuickFix( + method: PsiMethod, @SafeFieldForPreview - private val expected: List, - isInject: Boolean, - ) : LocalQuickFix { - - private val fixName = if (isInject) { - "Fix method parameters" - } else { - "Fix method parameters (won't keep captured locals)" - } + private val expectedParams: List?, + @SafeFieldForPreview + private val expectedReturnType: PsiType?, + private val intLikeTypePositions: List + ) : LocalQuickFixAndIntentionActionOnPsiElement(method) { + + private val fixName = "Fix method signature" override fun getFamilyName() = fixName - override fun applyFix(project: Project, descriptor: ProblemDescriptor) { - val parameters = descriptor.psiElement as PsiParameterList + override fun getText() = familyName + + override fun startInWriteAction() = false + + override fun invoke( + project: Project, + file: PsiFile, + editor: Editor?, + startElement: PsiElement, + endElement: PsiElement, + ) { + if (!FileModificationService.getInstance().preparePsiElementForWrite(startElement)) { + return + } + val method = startElement as PsiMethod + fixParameters(project, method.parameterList) + fixReturnType(method) + fixIntLikeTypes(method, editor ?: return) + } + + private fun fixParameters(project: Project, parameters: PsiParameterList) { + if (expectedParams == null) { + return + } // We want to preserve captured locals val locals = parameters.parameters.dropWhile { val fqname = (it.type as? PsiClassType)?.fullQualifiedName ?: return@dropWhile true @@ -310,7 +346,7 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { // We want to preserve sugars, and while we're at it, we might as well move them all to the end val sugars = parameters.parameters.filter { it.isMixinExtrasSugar } - val newParams = expected.flatMapTo(mutableListOf()) { + val newParams = expectedParams.flatMapTo(mutableListOf()) { if (it.default) { val nameHelper = PsiNameHelper.getInstance(project) val languageLevel = PsiUtil.getLanguageLevel(parameters) @@ -329,7 +365,81 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() { // Restore the captured locals and sugars before applying the fix newParams.addAll(locals) newParams.addAll(sugars) - parameters.synchronize(newParams) + runWriteAction { + parameters.synchronize(newParams) + } } + + private fun fixReturnType(method: PsiMethod) { + if (expectedReturnType == null) { + return + } + QuickFixFactory.getInstance() + .createMethodReturnFix(method, expectedReturnType, false) + .applyFix() + } + + private fun fixIntLikeTypes(method: PsiMethod, editor: Editor) { + if (intLikeTypePositions.isEmpty()) { + return + } + invokeLater { + WriteCommandAction.runWriteCommandAction( + method.project, + "Choose Int-Like Type", + null, + { + val template = makeIntLikeTypeTemplate(method, intLikeTypePositions) + if (template != null) { + editor.caretModel.moveToOffset(method.startOffset) + TemplateManager.getInstance(method.project) + .startTemplate(editor, template) + } + }, + method.parentOfType()!! + ) + } + } + + private fun makeIntLikeTypeTemplate( + method: PsiMethod, + positions: List + ): Template? { + val builder = TemplateBuilderImpl(method) + builder.replaceElement( + positions.first().getElement(method) ?: return null, + "intliketype", + ChooseIntLikeTypeExpression(), + true + ) + for (pos in positions.drop(1)) { + builder.replaceElement( + pos.getElement(method) ?: return null, + VariableNode("intliketype", null), + false + ) + } + return builder.buildInlineTemplate() + } + } +} + +private class ChooseIntLikeTypeExpression : Expression() { + private val lookupItems: Array = intLikeTypes.map(LookupElementBuilder::create).toTypedArray() + + override fun calculateLookupItems(context: ExpressionContext) = if (lookupItems.size > 1) lookupItems else null + + override fun calculateQuickResult(context: ExpressionContext) = calculateResult(context) + + override fun calculateResult(context: ExpressionContext) = TextResult("int") + + private companion object { + private val intLikeTypes = listOf( + "int", + "char", + "boolean", + "byte", + "short" + ) } } diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt index 167782cbe..631e1acf9 100644 --- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt +++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt @@ -20,6 +20,24 @@ package com.demonwav.mcdev.platform.mixin.inspection.injector +import com.intellij.psi.PsiMethod import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypeElement -data class MethodSignature(val parameters: List, val returnType: PsiType) +data class MethodSignature( + val parameters: List, + val returnType: PsiType, + val intLikeTypes: List = emptyList() +) { + sealed interface TypePosition { + fun getElement(method: PsiMethod): PsiTypeElement? + + data object Return : TypePosition { + override fun getElement(method: PsiMethod) = method.returnTypeElement + } + + data class Param(val index: Int) : TypePosition { + override fun getElement(method: PsiMethod) = method.parameterList.parameters[index].typeElement + } + } +} diff --git a/src/main/kotlin/platform/mixin/inspection/suppress/MixinClassCastInspectionSuppressor.kt b/src/main/kotlin/platform/mixin/inspection/suppress/MixinClassCastInspectionSuppressor.kt index 720a4084a..79ea61bfb 100644 --- a/src/main/kotlin/platform/mixin/inspection/suppress/MixinClassCastInspectionSuppressor.kt +++ b/src/main/kotlin/platform/mixin/inspection/suppress/MixinClassCastInspectionSuppressor.kt @@ -38,6 +38,7 @@ import com.intellij.psi.PsiInstanceOfExpression import com.intellij.psi.PsiType import com.intellij.psi.PsiTypeCastExpression import com.intellij.psi.PsiTypeTestPattern +import com.intellij.psi.util.JavaPsiPatternUtil import com.intellij.psi.util.PsiUtil /** @@ -54,7 +55,8 @@ class MixinClassCastInspectionSuppressor : InspectionSuppressor { // check instanceof if (element is PsiInstanceOfExpression) { val castType = element.checkType?.type - ?: (element.pattern as? PsiTypeTestPattern)?.checkType?.type + ?: (JavaPsiPatternUtil.skipParenthesizedPatternDown(element.pattern) as? PsiTypeTestPattern) + ?.checkType?.type ?: return false var operand = PsiUtil.skipParenthesizedExprDown(element.operand) ?: return false while (operand is PsiTypeCastExpression) { diff --git a/src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt b/src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt index 416eacdf7..58d68e12e 100644 --- a/src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt +++ b/src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt @@ -20,6 +20,8 @@ package com.demonwav.mcdev.platform.mixin.reference +import com.demonwav.mcdev.platform.mixin.reference.target.FieldDefinitionReference +import com.demonwav.mcdev.platform.mixin.reference.target.MethodDefinitionReference import com.demonwav.mcdev.platform.mixin.reference.target.TargetReference import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.AT import com.demonwav.mcdev.util.insideAnnotationAttribute @@ -65,5 +67,15 @@ class MixinReferenceContributor : PsiReferenceContributor() { InvokerReference.ELEMENT_PATTERN, InvokerReference, ) + + // Definition references + registrar.registerReferenceProvider( + FieldDefinitionReference.ELEMENT_PATTERN, + FieldDefinitionReference, + ) + registrar.registerReferenceProvider( + MethodDefinitionReference.ELEMENT_PATTERN, + MethodDefinitionReference, + ) } } diff --git a/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt b/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt index 4e8d34f4f..88e912674 100644 --- a/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt +++ b/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt @@ -48,7 +48,6 @@ import com.demonwav.mcdev.util.resolveTypeArray import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project import com.intellij.openapi.util.RecursionManager -import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.CommonClassNames import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotation @@ -228,71 +227,7 @@ fun MemberReference.toMixinString(): String { } class MixinMemberParser : MixinSelectorParser { - override fun parse(value: String, context: PsiElement): MixinSelector? { - val reference = value.replace(" ", "") - val owner: String? - - var pos = reference.lastIndexOf('.') - if (pos != -1) { - // Everything before the dot is the qualifier/owner - owner = reference.substring(0, pos).replace('/', '.') - } else { - pos = reference.indexOf(';') - if (pos != -1 && reference.startsWith('L')) { - val internalOwner = reference.substring(1, pos) - if (!StringUtil.isJavaIdentifier(internalOwner.replace('/', '_'))) { - // Invalid: Qualifier should only contain slashes - return null - } - - owner = internalOwner.replace('/', '.') - - // if owner is all there is to the selector, match anything with the owner - if (pos == reference.length - 1) { - return MemberReference("", null, owner, matchAllNames = true, matchAllDescs = true) - } - } else { - // No owner/qualifier specified - pos = -1 - owner = null - } - } - - val descriptor: String? - val name: String - val matchAllNames = reference.getOrNull(pos + 1) == '*' - val matchAllDescs: Boolean - - // Find descriptor separator - val methodDescPos = reference.indexOf('(', pos + 1) - if (methodDescPos != -1) { - // Method descriptor - descriptor = reference.substring(methodDescPos) - name = reference.substring(pos + 1, methodDescPos) - matchAllDescs = false - } else { - val fieldDescPos = reference.indexOf(':', pos + 1) - if (fieldDescPos != -1) { - descriptor = reference.substring(fieldDescPos + 1) - name = reference.substring(pos + 1, fieldDescPos) - matchAllDescs = false - } else { - descriptor = null - matchAllDescs = reference.endsWith('*') - name = if (matchAllDescs) { - reference.substring(pos + 1, reference.lastIndex) - } else { - reference.substring(pos + 1) - } - } - } - - if (!matchAllNames && !StringUtil.isJavaIdentifier(name) && name != "" && name != "") { - return null - } - - return MemberReference(if (matchAllNames) "*" else name, descriptor, owner, matchAllNames, matchAllDescs) - } + override fun parse(value: String, context: PsiElement) = MemberReference.parse(value) } // Regex reference diff --git a/src/main/kotlin/platform/mixin/reference/target/DefinitionReferenceGTDHandler.kt b/src/main/kotlin/platform/mixin/reference/target/DefinitionReferenceGTDHandler.kt new file mode 100644 index 000000000..4489eff4e --- /dev/null +++ b/src/main/kotlin/platform/mixin/reference/target/DefinitionReferenceGTDHandler.kt @@ -0,0 +1,45 @@ +/* + * 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.reference.target + +import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiLiteral +import com.intellij.psi.util.parentOfType + +class DefinitionReferenceGTDHandler : GotoDeclarationHandler { + override fun getGotoDeclarationTargets( + sourceElement: PsiElement?, + offset: Int, + editor: Editor? + ): Array? { + if (sourceElement == null) return null + val stringLiteral = sourceElement.parentOfType() ?: return null + if (FieldDefinitionReference.ELEMENT_PATTERN.accepts(stringLiteral)) { + return FieldDefinitionReference.resolveForNavigation(stringLiteral) + } + if (MethodDefinitionReference.ELEMENT_PATTERN.accepts(stringLiteral)) { + return MethodDefinitionReference.resolveForNavigation(stringLiteral) + } + return null + } +} diff --git a/src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt b/src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt new file mode 100644 index 000000000..a6927b8a8 --- /dev/null +++ b/src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt @@ -0,0 +1,182 @@ +/* + * 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.reference.target + +import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.reference.MixinReference +import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.util.MemberReference +import com.demonwav.mcdev.util.constantStringValue +import com.demonwav.mcdev.util.findContainingModifierList +import com.demonwav.mcdev.util.findField +import com.demonwav.mcdev.util.findMethods +import com.demonwav.mcdev.util.insideAnnotationAttribute +import com.demonwav.mcdev.util.mapFirstNotNull +import com.demonwav.mcdev.util.mapToArray +import com.demonwav.mcdev.util.reference.PolyReferenceResolver +import com.demonwav.mcdev.util.toTypedArray +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.project.Project +import com.intellij.patterns.PsiJavaPatterns +import com.intellij.patterns.StandardPatterns +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementResolveResult +import com.intellij.psi.PsiMember +import com.intellij.psi.ResolveResult +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.util.containers.sequenceOfNotNull +import com.llamalad7.mixinextras.expression.impl.flow.FlowValue +import com.llamalad7.mixinextras.expression.impl.flow.postprocessing.LMFInfo +import com.llamalad7.mixinextras.expression.impl.utils.FlowDecorations +import org.objectweb.asm.tree.FieldInsnNode +import org.objectweb.asm.tree.MethodInsnNode + +abstract class AbstractDefinitionReference : PolyReferenceResolver(), MixinReference { + abstract fun getFullReferenceIfMatches(memberReference: MemberReference, node: FlowValue): MemberReference? + abstract fun getMatchesInClass(memberReference: MemberReference, clazz: PsiClass): Sequence + abstract fun referenceToString(memberReference: MemberReference): String + + override fun isUnresolved(context: PsiElement) = resolveInBytecode(context).isNotEmpty() + + override fun isValidAnnotation(name: String, project: Project) = name == MixinConstants.MixinExtras.DEFINITION + + override fun resolveReference(context: PsiElement): Array { + return resolveForNavigation(context).mapToArray(::PsiElementResolveResult) + } + + fun resolveForNavigation(context: PsiElement): Array { + val project = context.project + val facade = JavaPsiFacade.getInstance(project) + return resolveInBytecode(context).asSequence().flatMap { memberReference -> + val ownerClass = facade.findClass( + memberReference.owner!!.replace('$', '.'), + GlobalSearchScope.allScope(project) + ) ?: return@flatMap emptySequence() + getMatchesInClass(memberReference.withoutOwner, ownerClass) + }.toTypedArray() + } + + override fun collectVariants(context: PsiElement) = + resolveInBytecode( + context, + MemberReference("*", null, null, matchAllNames = true, matchAllDescs = true) + ).mapToArray { + LookupElementBuilder.create(referenceToString(it)) + .withPresentableText(it.presentableText) + .withLookupString(it.name) + } + + fun resolveInBytecode(context: PsiElement): List { + val memberReference = context.constantStringValue?.let(MemberReference::parse) ?: return emptyList() + return resolveInBytecode(context, memberReference) + } + + private fun resolveInBytecode(context: PsiElement, memberReference: MemberReference): List { + val project = context.project + val modifierList = context.findContainingModifierList() ?: return emptyList() + val (annotation, handler) = modifierList.annotations.mapFirstNotNull { annotation -> + val qName = annotation.qualifiedName ?: return@mapFirstNotNull null + val handler = MixinAnnotationHandler.forMixinAnnotation(qName, project) ?: return@mapFirstNotNull null + annotation to handler + } ?: return emptyList() + + val result = mutableListOf() + + for (target in handler.resolveTarget(annotation)) { + if (target !is MethodTargetMember) { + continue + } + + if (target.classAndMethod.method.instructions == null) { + continue + } + + val flow = MEExpressionMatchUtil.getFlowMap( + project, + target.classAndMethod.clazz, + target.classAndMethod.method + ) ?: continue + + for (node in flow.values) { + val fullReference = getFullReferenceIfMatches(memberReference, node) ?: continue + result += fullReference + } + } + + return result + } +} + +object FieldDefinitionReference : AbstractDefinitionReference() { + val ELEMENT_PATTERN = PsiJavaPatterns.psiLiteral(StandardPatterns.string()) + .insideAnnotationAttribute(MixinConstants.MixinExtras.DEFINITION, "field") + + override fun getFullReferenceIfMatches(memberReference: MemberReference, node: FlowValue): MemberReference? { + val insn = node.insn + if (insn !is FieldInsnNode || !memberReference.matchField(insn.owner, insn.name, insn.desc)) { + return null + } + + return MemberReference(insn.name, insn.desc, insn.owner.replace('/', '.')) + } + + override fun getMatchesInClass(memberReference: MemberReference, clazz: PsiClass) = + sequenceOfNotNull(clazz.findField(memberReference, checkBases = true)) + + override fun referenceToString(memberReference: MemberReference) = + "L${memberReference.owner?.replace('.', '/')};${memberReference.name}:${memberReference.descriptor}" + + override val description = "defined field '%s'" +} + +object MethodDefinitionReference : AbstractDefinitionReference() { + val ELEMENT_PATTERN = PsiJavaPatterns.psiLiteral(StandardPatterns.string()) + .insideAnnotationAttribute(MixinConstants.MixinExtras.DEFINITION, "method") + + override fun getFullReferenceIfMatches(memberReference: MemberReference, node: FlowValue): MemberReference? { + val info = node.getDecoration(FlowDecorations.LMF_INFO) + val insn = node.insn + val (owner, name, desc) = when { + info != null && (info.type == LMFInfo.Type.FREE_METHOD || info.type == LMFInfo.Type.BOUND_METHOD) -> + Triple(info.impl.owner, info.impl.name, info.impl.desc) + + insn is MethodInsnNode -> Triple(insn.owner, insn.name, insn.desc) + else -> return null + } + if (!memberReference.matchMethod(owner, name, desc)) { + return null + } + + return MemberReference(name, desc, owner.replace('/', '.')) + } + + override fun getMatchesInClass(memberReference: MemberReference, clazz: PsiClass) = + clazz.findMethods(memberReference, checkBases = true) + + override fun referenceToString(memberReference: MemberReference) = + "L${memberReference.owner?.replace('.', '/')};${memberReference.name}${memberReference.descriptor}" + + override val description = "defined method '%s'" +} diff --git a/src/main/kotlin/platform/mixin/util/AsmDfaUtil.kt b/src/main/kotlin/platform/mixin/util/AsmDfaUtil.kt index 3e800c643..842be4863 100644 --- a/src/main/kotlin/platform/mixin/util/AsmDfaUtil.kt +++ b/src/main/kotlin/platform/mixin/util/AsmDfaUtil.kt @@ -41,8 +41,8 @@ import org.objectweb.asm.tree.analysis.SimpleVerifier object AsmDfaUtil { private val LOGGER = thisLogger() - fun analyzeMethod(project: Project, clazz: ClassNode, method: MethodNode): Array?>? { - return method.cached(clazz, project) { + fun analyzeMethod(project: Project, classIn: ClassNode, methodIn: MethodNode): Array?>? { + return methodIn.cached(classIn, project) { clazz, method -> try { Analyzer( PsiBytecodeInterpreter( diff --git a/src/main/kotlin/platform/mixin/util/AsmUtil.kt b/src/main/kotlin/platform/mixin/util/AsmUtil.kt index da1de2c4a..981be7320 100644 --- a/src/main/kotlin/platform/mixin/util/AsmUtil.kt +++ b/src/main/kotlin/platform/mixin/util/AsmUtil.kt @@ -32,6 +32,7 @@ import com.demonwav.mcdev.util.findQualifiedClass import com.demonwav.mcdev.util.fullQualifiedName import com.demonwav.mcdev.util.hasSyntheticMethod import com.demonwav.mcdev.util.isErasureEquivalentTo +import com.demonwav.mcdev.util.lockedCached import com.demonwav.mcdev.util.loggerForTopLevel import com.demonwav.mcdev.util.mapToArray import com.demonwav.mcdev.util.realName @@ -42,6 +43,7 @@ import com.intellij.openapi.module.Module import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.project.Project import com.intellij.openapi.roots.CompilerModuleExtension +import com.intellij.openapi.util.Key import com.intellij.openapi.util.RecursionManager import com.intellij.psi.JavaPsiFacade import com.intellij.psi.JavaRecursiveElementWalkingVisitor @@ -67,19 +69,27 @@ import com.intellij.psi.PsiModifierList import com.intellij.psi.PsiParameter import com.intellij.psi.PsiParameterList import com.intellij.psi.PsiType +import com.intellij.psi.PsiTypes import com.intellij.psi.impl.compiled.ClsElementImpl import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.util.CachedValue import com.intellij.psi.util.PsiUtil import com.intellij.refactoring.util.LambdaRefactoringUtil import com.intellij.util.CommonJavaRefactoringUtil +import com.llamalad7.mixinextras.expression.impl.utils.ExpressionASMUtils +import java.io.PrintWriter +import java.io.StringWriter import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap import org.objectweb.asm.ClassReader import org.objectweb.asm.Handle import org.objectweb.asm.Opcodes import org.objectweb.asm.Type import org.objectweb.asm.signature.SignatureReader import org.objectweb.asm.tree.AbstractInsnNode +import org.objectweb.asm.tree.AnnotationNode import org.objectweb.asm.tree.ClassNode import org.objectweb.asm.tree.FieldInsnNode import org.objectweb.asm.tree.FieldNode @@ -89,6 +99,10 @@ import org.objectweb.asm.tree.InvokeDynamicInsnNode import org.objectweb.asm.tree.MethodInsnNode import org.objectweb.asm.tree.MethodNode import org.objectweb.asm.tree.VarInsnNode +import org.objectweb.asm.util.Textifier +import org.objectweb.asm.util.TraceAnnotationVisitor +import org.objectweb.asm.util.TraceClassVisitor +import org.objectweb.asm.util.TraceMethodVisitor private val LOGGER = loggerForTopLevel() @@ -129,10 +143,25 @@ private fun hasModifier(access: Int, @PsiModifier.ModifierConstant modifier: Str } fun Type.toPsiType(elementFactory: PsiElementFactory, context: PsiElement? = null): PsiType { + if (this == ExpressionASMUtils.INTLIKE_TYPE) { + return PsiTypes.intType() + } val javaClassName = className.replace("(\\$)(\\D)".toRegex()) { "." + it.groupValues[2] } return elementFactory.createTypeFromText(javaClassName, context) } +val Type.canonicalName get() = computeCanonicalName(this) + +private fun computeCanonicalName(type: Type): String { + return when (type.sort) { + Type.ARRAY -> computeCanonicalName(type.elementType) + "[]".repeat(type.dimensions) + Type.OBJECT -> type.className.replace('$', '.') + else -> type.className + } +} + +val Type.isPrimitive get() = sort != Type.ARRAY && sort != Type.OBJECT && sort != Type.METHOD + private fun hasAccess(access: Int, flag: Int) = (access and flag) != 0 // ClassNode @@ -152,13 +181,10 @@ private val LOAD_CLASS_FILE_BYTES: Method? = runCatching { .let { it.isAccessible = true; it } }.getOrNull() +private val INNER_CLASS_NODES_KEY = Key.create>>("mcdev.innerClassNodes") + /** * Tries to find the bytecode for the class for the given qualified name. - * - * ### Implementation note: - * First attempts to resolve the class using [findQualifiedClass]. This may fail in the case of anonymous classes, which - * don't exist inside `PsiCompiledElement`s, so it then creates a fake `PsiClass` based on the qualified name and - * attempts to resolve it from that. */ fun findClassNodeByQualifiedName(project: Project, module: Module?, fqn: String): ClassNode? { val psiClass = findQualifiedClass(project, fqn) @@ -166,52 +192,70 @@ fun findClassNodeByQualifiedName(project: Project, module: Module?, fqn: String) return findClassNodeByPsiClass(psiClass, module) } - // try to find it by a fake one - val fakeClassNode = ClassNode() - fakeClassNode.name = fqn.replace('.', '/') - val fakePsiClass = fakeClassNode.constructClass(project, "") ?: return null - return findClassNodeByPsiClass(fakePsiClass, module) + fun resolveViaFakeClass(): ClassNode? { + val fakeClassNode = ClassNode() + fakeClassNode.name = fqn.replace('.', '/') + val fakePsiClass = fakeClassNode.constructClass(project, "") ?: return null + return findClassNodeByPsiClass(fakePsiClass, module) + } + + val outerClass = findQualifiedClass(project, fqn.substringBefore('$')) + if (outerClass != null) { + val innerClasses = outerClass.lockedCached( + INNER_CLASS_NODES_KEY, + compute = ::ConcurrentHashMap + ) + return innerClasses.computeIfAbsent(fqn) { resolveViaFakeClass() } + } + + return resolveViaFakeClass() } +private val NODE_BY_PSI_CLASS_KEY = Key.create>("mcdev.nodeByPsiClass") + fun findClassNodeByPsiClass(psiClass: PsiClass, module: Module? = psiClass.findModule()): ClassNode? { - return try { - val bytes = LOAD_CLASS_FILE_BYTES?.invoke(null, psiClass) as? ByteArray - if (bytes == null) { - // find compiler output - if (module == null) return null - val fqn = psiClass.fullQualifiedName ?: return null - var parentDir = CompilerModuleExtension.getInstance(module)?.compilerOutputPath ?: return null - val packageName = fqn.substringBeforeLast('.', "") - if (packageName.isNotEmpty()) { - for (dir in packageName.split('.')) { - parentDir = parentDir.findChild(dir) ?: return null + return psiClass.lockedCached(NODE_BY_PSI_CLASS_KEY) { + try { + val bytes = LOAD_CLASS_FILE_BYTES?.invoke(null, psiClass) as? ByteArray + if (bytes == null) { + // find compiler output + if (module == null) return@lockedCached null + val fqn = psiClass.fullQualifiedName ?: return@lockedCached null + var parentDir = CompilerModuleExtension.getInstance(module)?.compilerOutputPath + ?: return@lockedCached null + val packageName = fqn.substringBeforeLast('.', "") + if (packageName.isNotEmpty()) { + for (dir in packageName.split('.')) { + parentDir = parentDir.findChild(dir) ?: return@lockedCached null + } } + val classFile = parentDir.findChild("${fqn.substringAfterLast('.')}.class") + ?: return@lockedCached null + val node = ClassNode() + classFile.inputStream.use { ClassReader(it).accept(node, 0) } + node + } else { + val node = ClassNode() + ClassReader(bytes).accept(node, 0) + node + } + } catch (e: Throwable) { + val actualThrowable = if (e is InvocationTargetException) e.cause ?: e else e + if (actualThrowable is ProcessCanceledException) { + throw actualThrowable } - val classFile = parentDir.findChild("${fqn.substringAfterLast('.')}.class") ?: return null - val node = ClassNode() - classFile.inputStream.use { ClassReader(it).accept(node, 0) } - node - } else { - val node = ClassNode() - ClassReader(bytes).accept(node, 0) - node - } - } catch (e: Throwable) { - val actualThrowable = if (e is InvocationTargetException) e.cause ?: e else e - if (actualThrowable is ProcessCanceledException) { - throw actualThrowable - } - if (actualThrowable is NoSuchFileException) { - return null - } + if (actualThrowable is NoSuchFileException) { + return@lockedCached null + } - val message = actualThrowable.message - // TODO: display an error to the user? - if (message == null || !message.contains("Unsupported class file major version")) { - LOGGER.error(actualThrowable) + val message = actualThrowable.message + // TODO: display an error to the user? + if (message == null || !message.contains("Unsupported class file major version")) { + LOGGER.error(actualThrowable) + } + null } - null } } @@ -325,8 +369,11 @@ private fun ClassNode.constructClass(project: Project, body: String): PsiClass? return clazz } -inline fun ClassNode.cached(project: Project, vararg dependencies: Any, crossinline compute: () -> T): T { - return findStubClass(project)?.cached(*dependencies, compute = compute) ?: compute() +fun ClassNode.cached(project: Project, vararg dependencies: Any, compute: (ClassNode) -> T): T { + val unsafeClass = UnsafeCachedValueCapture(this) + return findStubClass(project)?.cached(*dependencies) { + compute(unsafeClass.value) + } ?: compute(this) } /** @@ -452,13 +499,17 @@ fun FieldNode.getGenericType( return Type.getType(this.desc).toPsiType(elementFactory) } -inline fun FieldNode.cached( +fun FieldNode.cached( clazz: ClassNode, project: Project, vararg dependencies: Any, - crossinline compute: () -> T, + compute: (ClassNode, FieldNode) -> T, ): T { - return findStubField(clazz, project)?.cached(*dependencies, compute = compute) ?: compute() + val unsafeClass = UnsafeCachedValueCapture(clazz) + val unsafeField = UnsafeCachedValueCapture(this) + return findStubField(clazz, project)?.cached(*dependencies) { + compute(unsafeClass.value, unsafeField.value) + } ?: compute(clazz, this) } fun FieldNode.findStubField(clazz: ClassNode, project: Project): PsiField? { @@ -693,13 +744,17 @@ private fun findAssociatedLambda(psiClass: PsiClass, clazz: ClassNode, lambdaMet } } -inline fun MethodNode.cached( +fun MethodNode.cached( clazz: ClassNode, project: Project, vararg dependencies: Array, - crossinline compute: () -> T, + compute: (ClassNode, MethodNode) -> T, ): T { - return findStubMethod(clazz, project)?.cached(*dependencies, compute = compute) ?: compute() + val unsafeClass = UnsafeCachedValueCapture(clazz) + val unsafeMethod = UnsafeCachedValueCapture(this) + return findStubMethod(clazz, project)?.cached(*dependencies) { + compute(unsafeClass.value, unsafeMethod.value) + } ?: compute(clazz, this) } fun MethodNode.findStubMethod(clazz: ClassNode, project: Project): PsiMethod? { @@ -932,3 +987,43 @@ fun MethodInsnNode.fakeResolve(): ClassAndMethodNode { addConstructorToFakeClass(clazz) return ClassAndMethodNode(clazz, method) } + +// Textifier + +fun ClassNode.textify(): String { + val sw = StringWriter() + accept(TraceClassVisitor(PrintWriter(sw))) + return sw.toString().replaceIndent().trimEnd() +} + +fun FieldNode.textify(): String { + val cv = TraceClassVisitor(null) + accept(cv) + val sw = StringWriter() + cv.p.print(PrintWriter(sw)) + return sw.toString().replaceIndent().trimEnd() +} + +fun MethodNode.textify(): String { + val cv = TraceClassVisitor(null) + accept(cv) + val sw = StringWriter() + cv.p.print(PrintWriter(sw)) + return sw.toString().replaceIndent().trimEnd() +} + +fun AnnotationNode.textify(): String { + val textifier = Textifier() + accept(TraceAnnotationVisitor(textifier)) + val sw = StringWriter() + textifier.print(PrintWriter(sw)) + return sw.toString().replaceIndent().trimEnd() +} + +fun AbstractInsnNode.textify(): String { + val mv = TraceMethodVisitor(Textifier()) + accept(mv) + val sw = StringWriter() + mv.p.print(PrintWriter(sw)) + return sw.toString().replaceIndent().trimEnd() +} diff --git a/src/main/kotlin/platform/mixin/util/LocalInfo.kt b/src/main/kotlin/platform/mixin/util/LocalInfo.kt index 710e7834e..fc684799d 100644 --- a/src/main/kotlin/platform/mixin/util/LocalInfo.kt +++ b/src/main/kotlin/platform/mixin/util/LocalInfo.kt @@ -24,9 +24,11 @@ import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor import com.demonwav.mcdev.util.computeStringArray import com.demonwav.mcdev.util.constantValue import com.demonwav.mcdev.util.descriptor +import com.demonwav.mcdev.util.isErasureEquivalentTo import com.intellij.openapi.module.Module import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiType +import com.intellij.util.containers.sequenceOfNotNull import org.objectweb.asm.Opcodes import org.objectweb.asm.Type import org.objectweb.asm.tree.AbstractInsnNode @@ -128,6 +130,27 @@ class LocalInfo( } } + fun matchSourceLocals( + sourceLocals: List + ): Sequence { + if (ordinal != null) { + return sequenceOfNotNull( + sourceLocals.asSequence().filter { it.type.isErasureEquivalentTo(type) }.drop(ordinal).firstOrNull() + ) + } + if (index != null) { + return sequenceOfNotNull(sourceLocals.getOrNull(index)) + } + if (names.isNotEmpty()) { + return sourceLocals.asSequence().filter { it.mixinName in names } + } + + // implicit mode + return sequenceOfNotNull( + sourceLocals.singleOrNull { it.type.isErasureEquivalentTo(type) } + ) + } + companion object { /** * Gets a [LocalInfo] from an annotation which declares the following attributes: diff --git a/src/main/kotlin/platform/mixin/util/LocalVariables.kt b/src/main/kotlin/platform/mixin/util/LocalVariables.kt index 59ee23686..6c5b15441 100644 --- a/src/main/kotlin/platform/mixin/util/LocalVariables.kt +++ b/src/main/kotlin/platform/mixin/util/LocalVariables.kt @@ -119,7 +119,13 @@ object LocalVariables { for (parameter in method.parameterList.parameters) { val mixinName = if (argsOnly) "var$argsIndex" else parameter.name - args += SourceLocalVariable(parameter.name, parameter.type, argsIndex, mixinName = mixinName) + args += SourceLocalVariable( + parameter.name, + parameter.type, + argsIndex, + mixinName = mixinName, + variable = parameter + ) argsIndex++ if (parameter.isDoubleSlot) { argsIndex++ @@ -207,7 +213,12 @@ object LocalVariables { localsHere = localsHere.copyOf(localIndex + 1) } val name = instruction.variable.name ?: return - localsHere[localIndex] = SourceLocalVariable(name, instruction.variable.type, localIndex) + localsHere[localIndex] = SourceLocalVariable( + name, + instruction.variable.type, + localIndex, + variable = instruction.variable + ) if (instruction.variable.isDoubleSlot && localIndex + 1 < localsHere.size) { localsHere[localIndex + 1] = null } @@ -850,11 +861,16 @@ object LocalVariables { } } + /** + * Represents a local variable in source code and its probable relationship to the bytecode. Don't store instances + * of this class. + */ data class SourceLocalVariable( val name: String, val type: PsiType, val index: Int, val mixinName: String = name, + val variable: PsiVariable? = null, val implicitLoadCountBefore: Int = 0, val implicitLoadCountAfter: Int = 0, val implicitStoreCountBefore: Int = 0, diff --git a/src/main/kotlin/platform/mixin/util/MixinConstants.kt b/src/main/kotlin/platform/mixin/util/MixinConstants.kt index 93ae8408d..173fc2050 100644 --- a/src/main/kotlin/platform/mixin/util/MixinConstants.kt +++ b/src/main/kotlin/platform/mixin/util/MixinConstants.kt @@ -84,11 +84,14 @@ object MixinConstants { } object MixinExtras { + const val PACKAGE = "com.llamalad7.mixinextras." const val OPERATION = "com.llamalad7.mixinextras.injector.wrapoperation.Operation" const val WRAP_OPERATION = "com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation" const val WRAP_METHOD = "com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod" const val LOCAL = "com.llamalad7.mixinextras.sugar.Local" const val LOCAL_REF_PACKAGE = "com.llamalad7.mixinextras.sugar.ref." + const val EXPRESSION = "com.llamalad7.mixinextras.expression.Expression" + const val DEFINITION = "com.llamalad7.mixinextras.expression.Definition" fun PsiType.unwrapLocalRef(): PsiType { if (this !is PsiClassType) { diff --git a/src/main/kotlin/platform/mixin/util/UnsafeCachedValueCapture.kt b/src/main/kotlin/platform/mixin/util/UnsafeCachedValueCapture.kt new file mode 100644 index 000000000..bcc1c15d4 --- /dev/null +++ b/src/main/kotlin/platform/mixin/util/UnsafeCachedValueCapture.kt @@ -0,0 +1,28 @@ +/* + * 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.util + +// See CachedValueStabilityChecker +class UnsafeCachedValueCapture(val value: T) { + override fun hashCode() = 0 + override fun equals(other: Any?) = other is UnsafeCachedValueCapture<*> + override fun toString() = value.toString() +} diff --git a/src/main/kotlin/platform/neoforge/version/platform/neoforge/version/NeoModDevVersion.kt b/src/main/kotlin/platform/neoforge/version/platform/neoforge/version/NeoModDevVersion.kt new file mode 100644 index 000000000..c6d8ac4d0 --- /dev/null +++ b/src/main/kotlin/platform/neoforge/version/platform/neoforge/version/NeoModDevVersion.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.neoforge.version.platform.neoforge.version + +import com.demonwav.mcdev.creator.collectMavenVersions +import com.demonwav.mcdev.util.SemanticVersion +import com.intellij.openapi.diagnostic.logger +import java.io.IOException + +class NeoModDevVersion private constructor(val versions: List) { + + companion object { + private val LOGGER = logger() + + suspend fun downloadData(): NeoModDevVersion? { + try { + val url = "https://maven.neoforged.net/releases/net/neoforged/moddev" + + "/net.neoforged.moddev.gradle.plugin/maven-metadata.xml" + val versions = collectMavenVersions(url) + .asSequence() + .mapNotNull(SemanticVersion.Companion::tryParse) + .sortedDescending() + .take(50) + .toList() + return NeoModDevVersion(versions) + } catch (e: IOException) { + LOGGER.error("Failed to retrieve NeoForged ModDev version data", e) + } + return null + } + } +} diff --git a/src/main/kotlin/platform/sponge/generation/SpongeEventGenerationPanel.form b/src/main/kotlin/platform/sponge/generation/SpongeEventGenerationPanel.form deleted file mode 100644 index b49fc8dbd..000000000 --- a/src/main/kotlin/platform/sponge/generation/SpongeEventGenerationPanel.form +++ /dev/null @@ -1,48 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/main/kotlin/platform/sponge/generation/SpongeEventGenerationPanel.kt b/src/main/kotlin/platform/sponge/generation/SpongeEventGenerationPanel.kt index d02630f9a..b3796684c 100644 --- a/src/main/kotlin/platform/sponge/generation/SpongeEventGenerationPanel.kt +++ b/src/main/kotlin/platform/sponge/generation/SpongeEventGenerationPanel.kt @@ -20,40 +20,38 @@ package com.demonwav.mcdev.platform.sponge.generation +import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.insight.generation.GenerationData import com.demonwav.mcdev.insight.generation.ui.EventGenerationPanel +import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.psi.PsiClass -import javax.swing.JCheckBox -import javax.swing.JComboBox +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.panel import javax.swing.JPanel class SpongeEventGenerationPanel(chosenClass: PsiClass) : EventGenerationPanel(chosenClass) { - private lateinit var parentPanel: JPanel - private lateinit var eventOrderComboBox: JComboBox - private lateinit var ignoreCanceledCheckBox: JCheckBox + private val graph = PropertyGraph("SpongeEventGenerationPanel graph") - override val panel: JPanel - get() { - ignoreCanceledCheckBox.isSelected = true + private val ignoreCanceledProperty = graph.property(true) + private val eventOrderProperty = graph.property("DEFAULT") - // Not static because the form builder is not reliable - eventOrderComboBox.addItem("PRE") - eventOrderComboBox.addItem("AFTER_PRE") - eventOrderComboBox.addItem("FIRST") - eventOrderComboBox.addItem("EARLY") - eventOrderComboBox.addItem("DEFAULT") - eventOrderComboBox.addItem("LATE") - eventOrderComboBox.addItem("LAST") - eventOrderComboBox.addItem("BEFORE_POST") - eventOrderComboBox.addItem("POST") + override val panel: JPanel by lazy { + panel { + row { + checkBox(MCDevBundle("generate.event_listener.ignore_if_canceled")) + .bindSelected(ignoreCanceledProperty) + } - eventOrderComboBox.selectedIndex = 4 - - return parentPanel + row(MCDevBundle("generate.event_listener.event_order")) { + comboBox(listOf("PRE", "AFTER_PRE", "FIRST", "EARLY", "DEFAULT", "LATE", "LAST", "BEFORE_POST", "POST")) + .bindItem(eventOrderProperty) + } } + } override fun gatherData(): GenerationData { - return SpongeGenerationData(ignoreCanceledCheckBox.isSelected, eventOrderComboBox.selectedItem as String) + return SpongeGenerationData(ignoreCanceledProperty.get(), eventOrderProperty.get()) } } diff --git a/src/main/kotlin/platform/sponge/inspection/SpongeInjectionInspection.kt b/src/main/kotlin/platform/sponge/inspection/SpongeInjectionInspection.kt index 7387d5d5a..76c14eb02 100644 --- a/src/main/kotlin/platform/sponge/inspection/SpongeInjectionInspection.kt +++ b/src/main/kotlin/platform/sponge/inspection/SpongeInjectionInspection.kt @@ -244,11 +244,13 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() { ) } } - "ninja.leaping.configurate.loader.ConfigurationLoader" -> { + "ninja.leaping.configurate.loader.ConfigurationLoader", + "org.spongepowered.configurate.reference.ConfigurationReference", + "org.spongepowered.configurate.loader.ConfigurationLoader" -> { if (defaultConfig == null) { holder.registerProblem( variable.nameIdentifier ?: variable, - "Injected ConfigurationLoader must be annotated with @DefaultConfig.", + "Injected ${classType.name} must be annotated with @DefaultConfig.", ProblemHighlightType.GENERIC_ERROR, AddAnnotationFix(SpongeConstants.DEFAULT_CONFIG_ANNOTATION, annotationsOwner), ) @@ -257,7 +259,7 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() { if (configDir != null) { holder.registerProblem( configDir, - "Injected ConfigurationLoader cannot be annotated with @ConfigDir.", + "Injected ${classType.name} cannot be annotated with @ConfigDir.", ProblemHighlightType.GENERIC_ERROR, QuickFixFactory.getInstance().createDeleteFix(configDir, "Remove @ConfigDir"), ) @@ -267,7 +269,7 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() { val ref = classType.reference holder.registerProblem( ref, - "Injected ConfigurationLoader must have a generic parameter.", + "Injected ${classType.name} must have a generic parameter.", ProblemHighlightType.GENERIC_ERROR, MissingConfLoaderTypeParamFix(ref), ) @@ -275,14 +277,17 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() { classType.parameters.firstOrNull()?.let { param -> val paramType = param as? PsiClassReferenceType ?: return@let val paramTypeFQName = paramType.fullQualifiedName ?: return@let - if (paramTypeFQName != "ninja.leaping.configurate.commented.CommentedConfigurationNode") { + if ( + paramTypeFQName != "ninja.leaping.configurate.commented.CommentedConfigurationNode" && + paramTypeFQName != "org.spongepowered.configurate.CommentedConfigurationNode" + ) { val ref = param.reference holder.registerProblem( ref, "Injected ConfigurationLoader generic parameter must be " + "CommentedConfigurationNode.", ProblemHighlightType.GENERIC_ERROR, - WrongConfLoaderTypeParamFix(ref), + WrongConfLoaderTypeParamFix(classType.className, ref), ) } } @@ -371,7 +376,8 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() { } } - class WrongConfLoaderTypeParamFix(ref: PsiJavaCodeReferenceElement) : LocalQuickFixOnPsiElement(ref) { + class WrongConfLoaderTypeParamFix(private val clazzName: String, param: PsiJavaCodeReferenceElement) : + LocalQuickFixOnPsiElement(param) { override fun getFamilyName(): String = name @@ -379,7 +385,11 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() { override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { val newRef = JavaPsiFacade.getElementFactory(project).createReferenceFromText( - "ninja.leaping.configurate.commented.CommentedConfigurationNode", + when (clazzName) { + "ninja.leaping.configurate.loader.ConfigurationLoader" -> + "ninja.leaping.configurate.commented.CommentedConfigurationNode" + else -> { "org.spongepowered.configurate.CommentedConfigurationNode" } + }, startElement, ) startElement.replace(newRef) @@ -393,11 +403,23 @@ class SpongeInjectionInspection : AbstractBaseJavaLocalInspectionTool() { override fun getText(): String = "Insert generic parameter" override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { - val newRef = JavaPsiFacade.getElementFactory(project).createReferenceFromText( - "ninja.leaping.configurate.loader.ConfigurationLoader" + - "", - startElement, - ) + val newRef: PsiElement = if ( + JavaPsiFacade.getInstance(project) + .findPackage("ninja.leaping.configurate") != null + ) { + JavaPsiFacade.getElementFactory(project).createReferenceFromText( + "ninja.leaping.configurate.loader.ConfigurationLoader" + + "", + startElement + ) + } else { + JavaPsiFacade.getElementFactory(project).createReferenceFromText( + "org.spongepowered.configurate.loader.ConfigurationLoader" + + "", + startElement + ) + } + startElement.replace(newRef) } } diff --git a/src/main/kotlin/platform/velocity/generation/VelocityEventGenerationPanel.form b/src/main/kotlin/platform/velocity/generation/VelocityEventGenerationPanel.form deleted file mode 100644 index d6653508a..000000000 --- a/src/main/kotlin/platform/velocity/generation/VelocityEventGenerationPanel.form +++ /dev/null @@ -1,40 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/main/kotlin/platform/velocity/generation/VelocityEventGenerationPanel.kt b/src/main/kotlin/platform/velocity/generation/VelocityEventGenerationPanel.kt index 503cd8ff5..f49225945 100644 --- a/src/main/kotlin/platform/velocity/generation/VelocityEventGenerationPanel.kt +++ b/src/main/kotlin/platform/velocity/generation/VelocityEventGenerationPanel.kt @@ -20,32 +20,31 @@ package com.demonwav.mcdev.platform.velocity.generation +import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.insight.generation.GenerationData import com.demonwav.mcdev.insight.generation.ui.EventGenerationPanel +import com.intellij.openapi.observable.properties.PropertyGraph import com.intellij.psi.PsiClass -import javax.swing.JComboBox +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.panel import javax.swing.JPanel class VelocityEventGenerationPanel(chosenClass: PsiClass) : EventGenerationPanel(chosenClass) { - private lateinit var parentPanel: JPanel - private lateinit var eventOrderComboBox: JComboBox + private val graph = PropertyGraph("VelocityEventGenerationPanel graph") - override val panel: JPanel - get() { - // Not static because the form builder is not reliable - eventOrderComboBox.addItem("FIRST") - eventOrderComboBox.addItem("EARLY") - eventOrderComboBox.addItem("NORMAL") - eventOrderComboBox.addItem("LATE") - eventOrderComboBox.addItem("LAST") + private val eventOrderProperty = graph.property("NORMAL") - eventOrderComboBox.selectedIndex = 2 - - return parentPanel + override val panel: JPanel by lazy { + panel { + row(MCDevBundle("generate.event_listener.event_order")) { + comboBox(listOf("FIRST", "EARLY", "NORMAL", "LATE", "LAST")) + .bindItem(eventOrderProperty) + } } + } override fun gatherData(): GenerationData { - return VelocityGenerationData(eventOrderComboBox.selectedItem as String) + return VelocityGenerationData(eventOrderProperty.get()) } } diff --git a/src/main/kotlin/translations/TranslationFiles.kt b/src/main/kotlin/translations/TranslationFiles.kt index b2fd47f71..8b8f74027 100644 --- a/src/main/kotlin/translations/TranslationFiles.kt +++ b/src/main/kotlin/translations/TranslationFiles.kt @@ -20,6 +20,7 @@ package com.demonwav.mcdev.translations +import com.demonwav.mcdev.TranslationSettings import com.demonwav.mcdev.translations.index.TranslationIndex import com.demonwav.mcdev.translations.index.TranslationInverseIndex import com.demonwav.mcdev.translations.lang.LangFile @@ -110,12 +111,43 @@ object TranslationFiles { element.delete() } + fun findTranslationKeyForText(context: PsiElement, text: String): String? { + val module = context.findModule() + ?: throw IllegalArgumentException("Cannot add translation for element outside of module") + var jsonVersion = true + if (!TranslationSettings.getInstance(context.project).isForceJsonTranslationFile) { + val version = + context.mcVersion ?: throw IllegalArgumentException("Cannot determine MC version for element $context") + jsonVersion = version > MC_1_12_2 + } + + if (!jsonVersion) { + // This feature only supports JSON translation files + return null + } + + val files = FileTypeIndex.getFiles( + JsonFileType.INSTANCE, + GlobalSearchScope.moduleScope(module), + ).filter { getLocale(it) == TranslationConstants.DEFAULT_LOCALE } + + for (file in files) { + val psiFile = PsiManager.getInstance(context.project).findFile(file) ?: continue + psiFile.findKeyForTextAsJson(text)?.let { return it } + } + + return null + } + fun add(context: PsiElement, key: String, text: String) { val module = context.findModule() ?: throw IllegalArgumentException("Cannot add translation for element outside of module") - val version = - context.mcVersion ?: throw IllegalArgumentException("Cannot determine MC version for element $context") - val jsonVersion = version > MC_1_12_2 + var jsonVersion = true + if (!TranslationSettings.getInstance(context.project).isForceJsonTranslationFile) { + val version = + context.mcVersion ?: throw IllegalArgumentException("Cannot determine MC version for element $context") + jsonVersion = version > MC_1_12_2 + } fun write(files: Iterable) { for (file in files) { @@ -223,6 +255,13 @@ object TranslationFiles { doc.insertString(rootObject.lastChild.prevSibling.textOffset, content) } + private fun PsiFile.findKeyForTextAsJson(text: String): String? { + val rootObject = this.firstChild as? JsonObject ?: return null + return rootObject.propertyList.firstOrNull { + (it.value as? JsonStringLiteral)?.value == text + }?.name + } + private fun generateJsonFile( leadingComma: Boolean, indent: CharSequence, @@ -292,9 +331,12 @@ object TranslationFiles { fun buildSortingTemplateFromDefault(context: PsiElement, domain: String? = null): Template? { val module = context.findModule() ?: throw IllegalArgumentException("Cannot add translation for element outside of module") - val version = - context.mcVersion ?: throw IllegalArgumentException("Cannot determine MC version for element $context") - val jsonVersion = version > MC_1_12_2 + var jsonVersion = true + if (!TranslationSettings.getInstance(context.project).isForceJsonTranslationFile) { + val version = + context.mcVersion ?: throw IllegalArgumentException("Cannot determine MC version for element $context") + jsonVersion = version > MC_1_12_2 + } val defaultTranslationFile = FileBasedIndex.getInstance() .getContainingFiles( diff --git a/src/main/kotlin/translations/actions/ConvertToTranslationAction.kt b/src/main/kotlin/translations/actions/ConvertToTranslationAction.kt deleted file mode 100644 index 9dc71689f..000000000 --- a/src/main/kotlin/translations/actions/ConvertToTranslationAction.kt +++ /dev/null @@ -1,51 +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.translations.actions - -import com.demonwav.mcdev.translations.intentions.ConvertToTranslationIntention -import com.intellij.openapi.actionSystem.ActionUpdateThread -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.LangDataKeys -import com.intellij.openapi.actionSystem.PlatformDataKeys -import com.intellij.psi.PsiLiteral - -class ConvertToTranslationAction : AnAction() { - override fun actionPerformed(e: AnActionEvent) { - val file = e.getData(LangDataKeys.PSI_FILE) ?: return - val editor = e.getData(PlatformDataKeys.EDITOR) ?: return - val element = file.findElementAt(editor.caretModel.offset) ?: return - ConvertToTranslationIntention().invoke(editor.project ?: return, editor, element) - } - - override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT - - override fun update(e: AnActionEvent) { - val file = e.getData(LangDataKeys.PSI_FILE) - val editor = e.getData(PlatformDataKeys.EDITOR) - if (file == null || editor == null) { - e.presentation.isEnabledAndVisible = false - return - } - val element = file.findElementAt(editor.caretModel.offset) - e.presentation.isEnabledAndVisible = (element?.parent as? PsiLiteral)?.value is String - } -} diff --git a/src/main/kotlin/translations/actions/TranslationSortOrderDialog.form b/src/main/kotlin/translations/actions/TranslationSortOrderDialog.form deleted file mode 100644 index 477020b84..000000000 --- a/src/main/kotlin/translations/actions/TranslationSortOrderDialog.form +++ /dev/null @@ -1,89 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/main/kotlin/translations/actions/TranslationSortOrderDialog.kt b/src/main/kotlin/translations/actions/TranslationSortOrderDialog.kt index 2c284a5f5..c72b6817f 100644 --- a/src/main/kotlin/translations/actions/TranslationSortOrderDialog.kt +++ b/src/main/kotlin/translations/actions/TranslationSortOrderDialog.kt @@ -20,44 +20,58 @@ package com.demonwav.mcdev.translations.actions +import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.translations.sorting.Ordering +import com.intellij.CommonBundle +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.bindIntValue +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.panel import java.awt.Component import java.awt.event.KeyEvent import java.awt.event.WindowAdapter import java.awt.event.WindowEvent -import javax.swing.DefaultComboBoxModel import javax.swing.DefaultListCellRenderer -import javax.swing.JButton -import javax.swing.JComboBox import javax.swing.JComponent import javax.swing.JDialog import javax.swing.JList -import javax.swing.JPanel -import javax.swing.JSpinner import javax.swing.KeyStroke -import javax.swing.SpinnerNumberModel import javax.swing.WindowConstants class TranslationSortOrderDialog(excludeDefaultOption: Boolean, defaultSelection: Ordering) : JDialog() { - private lateinit var contentPane: JPanel - private lateinit var buttonOK: JButton - private lateinit var buttonCancel: JButton - private lateinit var comboSelection: JComboBox - private lateinit var spinnerComments: JSpinner - init { - setContentPane(contentPane) - isModal = true - title = "Select Sort Order" - getRootPane().defaultButton = buttonOK + private val graph = PropertyGraph("TranslationSortOrderDialog graph") + + private val orderProperty = graph.property(defaultSelection) + private val keepCommentsProperty = graph.property(0) - buttonOK.addActionListener { onOK() } - buttonCancel.addActionListener { onCancel() } - spinnerComments.model = SpinnerNumberModel(0, 0, Int.MAX_VALUE, 1) + private var canceled = false + + init { val availableOrderings = if (excludeDefaultOption) NON_DEFAULT_ORDERINGS else ALL_ORDERINGS - comboSelection.model = DefaultComboBoxModel(availableOrderings) - comboSelection.renderer = CellRenderer - comboSelection.selectedItem = defaultSelection + val panel = panel { + row(MCDevBundle("translation_sort.order")) { + comboBox(availableOrderings, CellRenderer) + .bindItem(orderProperty) + } + + row(MCDevBundle("translation_sort.keep_comment")) { + spinner(0..Int.MAX_VALUE) + .bindIntValue(keepCommentsProperty::get, keepCommentsProperty::set) + } + + row { + button(CommonBundle.message("button.ok")) { onOK() }.align(AlignX.RIGHT).also { + getRootPane().defaultButton = it.component + } + button(CommonBundle.message("button.cancel")) { onCancel() }.align(AlignX.RIGHT) + } + } + contentPane = panel + + isModal = true + title = MCDevBundle("translation_sort.title") // call onCancel() when cross is clicked defaultCloseOperation = WindowConstants.DO_NOTHING_ON_CLOSE @@ -70,7 +84,7 @@ class TranslationSortOrderDialog(excludeDefaultOption: Boolean, defaultSelection ) // call onCancel() on ESCAPE - contentPane.registerKeyboardAction( + panel.registerKeyboardAction( { onCancel() }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, @@ -82,7 +96,7 @@ class TranslationSortOrderDialog(excludeDefaultOption: Boolean, defaultSelection } private fun onCancel() { - comboSelection.selectedIndex = -1 + canceled = true dispose() } @@ -100,17 +114,17 @@ class TranslationSortOrderDialog(excludeDefaultOption: Boolean, defaultSelection } companion object { - private val ALL_ORDERINGS = Ordering.values() - private val NON_DEFAULT_ORDERINGS = Ordering.values() - .filterNot { it == Ordering.LIKE_DEFAULT }.toTypedArray() + private val ALL_ORDERINGS = Ordering.entries + private val NON_DEFAULT_ORDERINGS = Ordering.entries + .filterNot { it == Ordering.LIKE_DEFAULT } fun show(excludeDefaultOption: Boolean, defaultSelection: Ordering): Pair { val dialog = TranslationSortOrderDialog(excludeDefaultOption, defaultSelection) dialog.pack() dialog.setLocationRelativeTo(dialog.owner) dialog.isVisible = true - val order = dialog.comboSelection.selectedItem as? Ordering - val comments = dialog.spinnerComments.value as Int + val order = if (dialog.canceled) null else dialog.orderProperty.get() + val comments = dialog.keepCommentsProperty.get() return (order to comments) } } diff --git a/src/main/kotlin/translations/folding.kt b/src/main/kotlin/translations/folding.kt index b58fe7b41..f71f95651 100644 --- a/src/main/kotlin/translations/folding.kt +++ b/src/main/kotlin/translations/folding.kt @@ -34,8 +34,11 @@ import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.FoldingGroup import com.intellij.openapi.options.BeanConfigurable import com.intellij.psi.PsiElement -import com.intellij.psi.PsiExpressionList -import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.textRange +import org.jetbrains.uast.toUElement +import org.jetbrains.uast.visitor.AbstractUastVisitor class TranslationCodeFoldingOptionsProvider : BeanConfigurable(TranslationFoldingSettings.instance), CodeFoldingOptionsProvider { @@ -88,23 +91,35 @@ class TranslationFoldingBuilder : FoldingBuilderEx() { val descriptors = mutableListOf() for (identifier in TranslationIdentifier.INSTANCES) { - val elements = PsiTreeUtil.findChildrenOfType(root, identifier.elementClass()) - for (element in elements) { + val uElement = root.toUElement() ?: continue + val children = mutableListOf() + uElement.accept(object : AbstractUastVisitor() { + override fun visitElement(node: UElement): Boolean { + if (identifier.elementClass().isAssignableFrom(node.javaClass)) { + children.add(node) + } + + return super.visitElement(node) + } + }) + for (element in children) { val translation = identifier.identifyUnsafe(element) val foldingElement = translation?.foldingElement ?: continue val range = - if (foldingElement is PsiExpressionList) { - val args = foldingElement.expressions.drop(translation.foldStart) - args.first().textRange.union(args.last().textRange) + if (foldingElement is UCallExpression && translation.foldStart != 0) { + val args = foldingElement.valueArguments.drop(translation.foldStart) + val startRange = args.first().textRange ?: continue + val endRange = args.last().textRange ?: continue + startRange.union(endRange) } else { - foldingElement.textRange + foldingElement.textRange ?: continue } if (!translation.required && translation.formattingError != null) { continue } descriptors.add( FoldingDescriptor( - translation.foldingElement.node, + translation.foldingElement.sourcePsi?.node!!, range, FoldingGroup.newGroup("mc.translation." + translation.key), if (translation.formattingError == TranslationInstance.Companion.FormattingError.MISSING) { diff --git a/src/main/kotlin/translations/identification/LiteralTranslationIdentifier.kt b/src/main/kotlin/translations/identification/LiteralTranslationIdentifier.kt index a8806ab42..aa096590a 100644 --- a/src/main/kotlin/translations/identification/LiteralTranslationIdentifier.kt +++ b/src/main/kotlin/translations/identification/LiteralTranslationIdentifier.kt @@ -21,24 +21,20 @@ package com.demonwav.mcdev.translations.identification import com.intellij.codeInsight.completion.CompletionUtilCore -import com.intellij.psi.PsiLiteralExpression +import org.jetbrains.uast.ULiteralExpression -class LiteralTranslationIdentifier : TranslationIdentifier() { - override fun identify(element: PsiLiteralExpression): TranslationInstance? { - val statement = element.parent - if (element.value is String) { - val result = identify(element.project, element, statement, element) - return result?.copy( - key = result.key.copy( - infix = result.key.infix.replace( - CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED, - "", - ), - ), - ) +class LiteralTranslationIdentifier : TranslationIdentifier() { + override fun identify(element: ULiteralExpression): TranslationInstance? { + val statement = element.uastParent ?: return null + if (element.value !is String) { + return null } - return null + + val project = element.sourcePsi?.project ?: return null + val result = identify(project, element, statement, element) ?: return null + val infix = result.key.infix.replace(CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED, "") + return result.copy(key = result.key.copy(infix = infix)) } - override fun elementClass(): Class = PsiLiteralExpression::class.java + override fun elementClass(): Class = ULiteralExpression::class.java } diff --git a/src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt b/src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt index b6085615b..95ee709fe 100644 --- a/src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt +++ b/src/main/kotlin/translations/identification/ReferenceTranslationIdentifier.kt @@ -21,46 +21,34 @@ package com.demonwav.mcdev.translations.identification import com.intellij.codeInsight.completion.CompletionUtilCore -import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.PsiField -import com.intellij.psi.PsiLiteral -import com.intellij.psi.PsiModifier -import com.intellij.psi.PsiReferenceExpression -import com.intellij.psi.impl.source.PsiClassReferenceType -import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiType +import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.UVariable +import org.jetbrains.uast.resolveToUElement -class ReferenceTranslationIdentifier : TranslationIdentifier() { - override fun identify(element: PsiReferenceExpression): TranslationInstance? { - val reference = element.resolve() - val statement = element.parent - - if (reference is PsiField) { - val scope = GlobalSearchScope.allScope(element.project) - val stringClass = - JavaPsiFacade.getInstance(element.project).findClass("java.lang.String", scope) ?: return null - val isConstant = - reference.hasModifierProperty(PsiModifier.STATIC) && reference.hasModifierProperty(PsiModifier.FINAL) - val type = reference.type as? PsiClassReferenceType ?: return null - val resolved = type.resolve() ?: return null - if (isConstant && (resolved.isEquivalentTo(stringClass) || resolved.isInheritor(stringClass, true))) { - val referenceElement = reference.initializer as? PsiLiteral ?: return null - val result = identify(element.project, element, statement, referenceElement) +class ReferenceTranslationIdentifier : TranslationIdentifier() { + override fun identify(element: UReferenceExpression): TranslationInstance? { + val statement = element.uastParent ?: return null + val project = element.sourcePsi?.project ?: return null + val reference = element.resolveToUElement() as? UVariable ?: return null + if (!reference.isFinal) { + return null + } - return result?.copy( - key = result.key.copy( - infix = result.key.infix.replace( - CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED, - "", - ), - ), - ) - } + val resolveScope = element.sourcePsi?.resolveScope ?: return null + val psiManager = PsiManager.getInstance(project) + val stringType = PsiType.getJavaLangString(psiManager, resolveScope) + if (!stringType.isAssignableFrom(reference.type)) { + return null } - return null - } + val referenceElement = reference.uastInitializer ?: return null + val result = identify(project, element, statement, referenceElement) ?: return null - override fun elementClass(): Class { - return PsiReferenceExpression::class.java + val infix = result.key.infix.replace(CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED, "") + return result.copy(key = result.key.copy(infix = infix)) } + + override fun elementClass(): Class = UReferenceExpression::class.java } diff --git a/src/main/kotlin/translations/identification/TranslationIdentifier.kt b/src/main/kotlin/translations/identification/TranslationIdentifier.kt index 2b74bfe1d..68c2babfa 100644 --- a/src/main/kotlin/translations/identification/TranslationIdentifier.kt +++ b/src/main/kotlin/translations/identification/TranslationIdentifier.kt @@ -37,19 +37,23 @@ import com.intellij.codeInspection.dataFlow.CommonDataflow import com.intellij.openapi.project.Project import com.intellij.psi.CommonClassNames import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.PsiCall -import com.intellij.psi.PsiCallExpression import com.intellij.psi.PsiElement import com.intellij.psi.PsiEllipsisType import com.intellij.psi.PsiExpression -import com.intellij.psi.PsiExpressionList -import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiParameter import java.util.IllegalFormatException import java.util.MissingFormatArgumentException - -abstract class TranslationIdentifier { +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UQualifiedReferenceExpression +import org.jetbrains.uast.evaluateString +import org.jetbrains.uast.getContainingUClass + +abstract class TranslationIdentifier { @Suppress("UNCHECKED_CAST") - fun identifyUnsafe(element: PsiElement): TranslationInstance? { + fun identifyUnsafe(element: UElement): TranslationInstance? { return identify(element as T) } @@ -62,20 +66,19 @@ abstract class TranslationIdentifier { fun identify( project: Project, - element: PsiExpression, - container: PsiElement, - referenceElement: PsiElement, + element: UExpression, + container: UElement, + referenceElement: UElement, ): TranslationInstance? { - if (container !is PsiExpressionList) { - return null - } - val call = container.parent as? PsiCallExpression ?: return null - val index = container.expressions.indexOf(element) + val call = container as? UCallExpression ?: return null + val index = container.valueArguments.indexOf(element) val method = call.referencedMethod ?: return null - val parameter = method.parameterList.getParameter(index) ?: return null - val translatableAnnotation = - AnnotationUtil.findAnnotation(parameter, TranslationConstants.TRANSLATABLE_ANNOTATION) ?: return null + val parameter = method.uastParameters.getOrNull(index) ?: return null + val translatableAnnotation = AnnotationUtil.findAnnotation( + parameter.javaPsi as PsiParameter, + TranslationConstants.TRANSLATABLE_ANNOTATION + ) ?: return null val prefix = translatableAnnotation.findAttributeValue(TranslationConstants.PREFIX)?.constantStringValue ?: "" @@ -85,13 +88,16 @@ abstract class TranslationIdentifier { translatableAnnotation.findAttributeValue(TranslationConstants.REQUIRED)?.constantValue as? Boolean ?: true val isPreEscapeException = - method.containingClass?.qualifiedName?.startsWith("net.minecraft.") == true && - isPreEscapeMcVersion(project, element) + method.getContainingUClass()?.qualifiedName?.startsWith("net.minecraft.") == true && + isPreEscapeMcVersion(project, element.sourcePsi!!) val allowArbitraryArgs = isPreEscapeException || translatableAnnotation.findAttributeValue( TranslationConstants.ALLOW_ARBITRARY_ARGS )?.constantValue as? Boolean ?: false - val translationKey = CommonDataflow.computeValue(element) as? String ?: return null + val translationKey = when (val javaPsi = element.javaPsi) { + is PsiExpression -> CommonDataflow.computeValue(javaPsi) as? String + else -> element.evaluateString() + } ?: return null val entries = TranslationIndex.getAllDefaultEntries(project).merge("") val translation = entries[prefix + translationKey + suffix]?.text @@ -110,15 +116,16 @@ abstract class TranslationIdentifier { ?: false val formatting = - (method.parameterList.parameters.last().type as? PsiEllipsisType) + (method.uastParameters.last().type as? PsiEllipsisType) ?.componentType?.equalsToText(CommonClassNames.JAVA_LANG_OBJECT) == true val foldingElement = if (foldMethod) { - call + // Make sure qualifiers, like I18n in 'I18n.translate()' is also folded + call.uastParent as? UQualifiedReferenceExpression ?: call } else if ( index == 0 && - container.expressionCount > 1 && - method.parameterList.parametersCount == 2 && + container.valueArgumentCount > 1 && + method.uastParameters.size == 2 && formatting ) { container @@ -156,14 +163,15 @@ abstract class TranslationIdentifier { } } - private fun format(method: PsiMethod, translation: String, call: PsiCall): Pair? { + private fun format(method: UMethod, translation: String, call: UCallExpression): Pair? { val format = NUMBER_FORMATTING_PATTERN.replace(translation, "%$1s") val paramCount = STRING_FORMATTING_PATTERN.findAll(format).count() - val varargs = call.extractVarArgs(method.parameterList.parametersCount - 1, true, true) + val parametersCount = method.uastParameters.size + val varargs = call.extractVarArgs(parametersCount - 1, true, true) ?: return null val varargStart = if (varargs.size > paramCount) { - method.parameterList.parametersCount - 1 + paramCount + parametersCount - 1 + paramCount } else { -1 } diff --git a/src/main/kotlin/translations/identification/TranslationInstance.kt b/src/main/kotlin/translations/identification/TranslationInstance.kt index 9976e9c85..393836cbe 100644 --- a/src/main/kotlin/translations/identification/TranslationInstance.kt +++ b/src/main/kotlin/translations/identification/TranslationInstance.kt @@ -20,12 +20,12 @@ package com.demonwav.mcdev.translations.identification -import com.intellij.psi.PsiElement +import org.jetbrains.uast.UElement data class TranslationInstance( - val foldingElement: PsiElement?, + val foldingElement: UElement?, val foldStart: Int, - val referenceElement: PsiElement?, + val referenceElement: UElement?, val key: Key, val text: String?, val required: Boolean, @@ -44,7 +44,7 @@ data class TranslationInstance( MISSING, SUPERFLUOUS } - fun find(element: PsiElement): TranslationInstance? = + fun find(element: UElement): TranslationInstance? = TranslationIdentifier.INSTANCES .firstOrNull { it.elementClass().isAssignableFrom(element.javaClass) } ?.identifyUnsafe(element) diff --git a/src/main/kotlin/translations/inspections/ChangeTranslationQuickFix.kt b/src/main/kotlin/translations/inspections/ChangeTranslationQuickFix.kt index 69891966d..b05ac801b 100644 --- a/src/main/kotlin/translations/inspections/ChangeTranslationQuickFix.kt +++ b/src/main/kotlin/translations/inspections/ChangeTranslationQuickFix.kt @@ -29,17 +29,19 @@ import com.intellij.ide.util.gotoByName.ChooseByNamePopup import com.intellij.ide.util.gotoByName.ChooseByNamePopupComponent import com.intellij.openapi.application.ModalityState import com.intellij.openapi.project.Project -import com.intellij.psi.JavaPsiFacade -import com.intellij.psi.PsiLiteralExpression import com.intellij.psi.PsiNamedElement import com.intellij.util.IncorrectOperationException +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.generate.getUastElementFactory +import org.jetbrains.uast.generate.replace +import org.jetbrains.uast.toUElementOfType class ChangeTranslationQuickFix(private val name: String) : LocalQuickFix { override fun getName() = name override fun applyFix(project: Project, descriptor: ProblemDescriptor) { try { - val literal = descriptor.psiElement as PsiLiteralExpression + val literal = descriptor.psiElement.toUElementOfType() ?: return val key = LiteralTranslationIdentifier().identify(literal)?.key ?: return val popup = ChooseByNamePopup.createPopup( project, @@ -50,17 +52,15 @@ class ChangeTranslationQuickFix(private val name: String) : LocalQuickFix { object : ChooseByNamePopupComponent.Callback() { override fun elementChosen(element: Any) { val selectedKey = (element as PsiNamedElement).name ?: return - literal.containingFile.runWriteAction { - val insertion = selectedKey.substring( - key.prefix.length, - selectedKey.length - key.suffix.length, - ) - literal.replace( - JavaPsiFacade.getInstance(project).elementFactory.createExpressionFromText( - "\"$insertion\"", - literal.context, - ), - ) + val insertion = selectedKey.substring( + key.prefix.length, + selectedKey.length - key.suffix.length, + ) + val elementFactory = literal.getUastElementFactory(project) ?: return + val replacement = elementFactory.createStringLiteralExpression(insertion, element) + ?: return + descriptor.psiElement.containingFile.runWriteAction { + literal.replace(replacement) } } }, diff --git a/src/main/kotlin/translations/inspections/MissingFormatInspection.kt b/src/main/kotlin/translations/inspections/MissingFormatInspection.kt index 89e66b265..45f5d6655 100644 --- a/src/main/kotlin/translations/inspections/MissingFormatInspection.kt +++ b/src/main/kotlin/translations/inspections/MissingFormatInspection.kt @@ -24,31 +24,38 @@ import com.demonwav.mcdev.translations.identification.TranslationInstance import com.demonwav.mcdev.translations.identification.TranslationInstance.Companion.FormattingError import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.ProblemsHolder -import com.intellij.psi.JavaElementVisitor import com.intellij.psi.PsiElementVisitor -import com.intellij.psi.PsiExpression -import com.intellij.psi.PsiLiteralExpression -import com.intellij.psi.PsiReferenceExpression +import com.intellij.uast.UastHintedVisitorAdapter +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor class MissingFormatInspection : TranslationInspection() { override fun getStaticDescription() = "Detects missing format arguments for translations" - override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder) + private val typesHint: Array> = arrayOf(UExpression::class.java) - private class Visitor(private val holder: ProblemsHolder) : JavaElementVisitor() { - override fun visitReferenceExpression(expression: PsiReferenceExpression) { - visit(expression) + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = + UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), typesHint) + + private class Visitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() { + + override fun visitExpression(node: UExpression): Boolean { + visit(node) + return super.visitElement(node) } - override fun visitLiteralExpression(expression: PsiLiteralExpression) { - visit(expression, ChangeTranslationQuickFix("Use a different translation")) + override fun visitLiteralExpression(node: ULiteralExpression): Boolean { + visit(node, ChangeTranslationQuickFix("Use a different translation")) + return true } - private fun visit(expression: PsiExpression, vararg quickFixes: LocalQuickFix) { + private fun visit(expression: UExpression, vararg quickFixes: LocalQuickFix) { val result = TranslationInstance.find(expression) if (result != null && result.required && result.formattingError == FormattingError.MISSING) { holder.registerProblem( - expression, + expression.sourcePsi!!, "There are missing formatting arguments to satisfy '${result.text}'", *quickFixes, ) diff --git a/src/main/kotlin/translations/inspections/NoTranslationInspection.kt b/src/main/kotlin/translations/inspections/NoTranslationInspection.kt index e588155f4..4ac1c2295 100644 --- a/src/main/kotlin/translations/inspections/NoTranslationInspection.kt +++ b/src/main/kotlin/translations/inspections/NoTranslationInspection.kt @@ -29,29 +29,38 @@ import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages -import com.intellij.psi.JavaElementVisitor import com.intellij.psi.PsiElementVisitor -import com.intellij.psi.PsiLiteralExpression +import com.intellij.uast.UastHintedVisitorAdapter import com.intellij.util.IncorrectOperationException +import org.jetbrains.uast.UElement +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.toUElementOfType +import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor class NoTranslationInspection : TranslationInspection() { override fun getStaticDescription() = "Checks whether a translation key used in calls to StatCollector.translateToLocal(), " + "StatCollector.translateToLocalFormatted() or I18n.format() exists." - override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder) + private val typesHint: Array> = arrayOf(ULiteralExpression::class.java) - private class Visitor(private val holder: ProblemsHolder) : JavaElementVisitor() { - override fun visitLiteralExpression(expression: PsiLiteralExpression) { - val result = LiteralTranslationIdentifier().identify(expression) + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = + UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), typesHint) + + private class Visitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() { + + override fun visitLiteralExpression(node: ULiteralExpression): Boolean { + val result = LiteralTranslationIdentifier().identify(node) if (result != null && result.required && result.text == null) { holder.registerProblem( - expression, + node.sourcePsi!!, "The given translation key does not exist", CreateTranslationQuickFix, ChangeTranslationQuickFix("Use existing translation"), ) } + + return true } } @@ -60,7 +69,7 @@ class NoTranslationInspection : TranslationInspection() { override fun applyFix(project: Project, descriptor: ProblemDescriptor) { try { - val literal = descriptor.psiElement as PsiLiteralExpression + val literal = descriptor.psiElement.toUElementOfType() ?: return val translation = LiteralTranslationIdentifier().identify(literal) val literalValue = literal.value as String val key = translation?.key?.copy(infix = literalValue)?.full ?: literalValue @@ -71,7 +80,7 @@ class NoTranslationInspection : TranslationInspection() { Messages.getQuestionIcon(), ) if (result != null) { - TranslationFiles.add(literal, key, result) + TranslationFiles.add(literal.sourcePsi!!, key, result) } } catch (ignored: IncorrectOperationException) { } catch (e: Exception) { diff --git a/src/main/kotlin/translations/inspections/SuperfluousFormatInspection.kt b/src/main/kotlin/translations/inspections/SuperfluousFormatInspection.kt index b6b9496c3..a3448dcc0 100644 --- a/src/main/kotlin/translations/inspections/SuperfluousFormatInspection.kt +++ b/src/main/kotlin/translations/inspections/SuperfluousFormatInspection.kt @@ -27,58 +27,68 @@ import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeInspection.ProblemsHolder import com.intellij.openapi.project.Project -import com.intellij.psi.JavaElementVisitor -import com.intellij.psi.PsiCall import com.intellij.psi.PsiElementVisitor -import com.intellij.psi.PsiExpression -import com.intellij.psi.PsiLiteralExpression -import com.intellij.psi.PsiReferenceExpression -import com.intellij.psi.SmartPointerManager -import com.intellij.psi.SmartPsiElementPointer +import com.intellij.uast.UastHintedVisitorAdapter +import com.intellij.uast.UastSmartPointer +import com.intellij.uast.createUastSmartPointer import com.intellij.util.IncorrectOperationException +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor class SuperfluousFormatInspection : TranslationInspection() { override fun getStaticDescription() = "Detect superfluous format arguments for translations" - override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder) + private val typesHint: Array> = + arrayOf(UReferenceExpression::class.java, ULiteralExpression::class.java) - private class Visitor(private val holder: ProblemsHolder) : JavaElementVisitor() { - override fun visitReferenceExpression(expression: PsiReferenceExpression) { - val result = TranslationInstance.find(expression) + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = + UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), typesHint) + + private class Visitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() { + + override fun visitExpression(node: UExpression): Boolean { + val result = TranslationInstance.find(node) if ( - result != null && result.foldingElement is PsiCall && + result != null && result.foldingElement is UCallExpression && result.formattingError == FormattingError.SUPERFLUOUS ) { - registerProblem(expression, result) + registerProblem(node, result) } + + return super.visitExpression(node) } - override fun visitLiteralExpression(expression: PsiLiteralExpression) { - val result = TranslationInstance.find(expression) + override fun visitLiteralExpression(node: ULiteralExpression): Boolean { + val result = TranslationInstance.find(node) if ( - result != null && result.required && result.foldingElement is PsiCall && + result != null && result.required && result.foldingElement is UCallExpression && result.formattingError == FormattingError.SUPERFLUOUS ) { registerProblem( - expression, + node, result, RemoveArgumentsQuickFix( - SmartPointerManager.getInstance(holder.project) - .createSmartPsiElementPointer(result.foldingElement), + result.foldingElement.createUastSmartPointer(), result.superfluousVarargStart, ), ChangeTranslationQuickFix("Use a different translation"), ) } + + return super.visitLiteralExpression(node) } private fun registerProblem( - expression: PsiExpression, + expression: UExpression, result: TranslationInstance, vararg quickFixes: LocalQuickFix, ) { holder.registerProblem( - expression, + expression.sourcePsi!!, "There are missing formatting arguments to satisfy '${result.text}'", *quickFixes, ) @@ -86,7 +96,7 @@ class SuperfluousFormatInspection : TranslationInspection() { } private class RemoveArgumentsQuickFix( - private val call: SmartPsiElementPointer, + private val call: UastSmartPointer, private val position: Int, ) : LocalQuickFix { override fun getName() = "Remove superfluous arguments" @@ -94,7 +104,7 @@ class SuperfluousFormatInspection : TranslationInspection() { override fun applyFix(project: Project, descriptor: ProblemDescriptor) { try { descriptor.psiElement.containingFile.runWriteAction { - call.element?.argumentList?.expressions?.drop(position)?.forEach { it.delete() } + call.element?.valueArguments?.drop(position)?.forEach { it.sourcePsi?.delete() } } } catch (ignored: IncorrectOperationException) { } diff --git a/src/main/kotlin/translations/inspections/TranslationInspection.kt b/src/main/kotlin/translations/inspections/TranslationInspection.kt index bfb3c0664..1c3852d46 100644 --- a/src/main/kotlin/translations/inspections/TranslationInspection.kt +++ b/src/main/kotlin/translations/inspections/TranslationInspection.kt @@ -21,14 +21,14 @@ package com.demonwav.mcdev.translations.inspections import com.demonwav.mcdev.platform.mcp.McpModuleType -import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool import com.intellij.codeInspection.InspectionManager +import com.intellij.codeInspection.LocalInspectionTool import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeInspection.ProblemsHolder import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiFile -abstract class TranslationInspection : AbstractBaseJavaLocalInspectionTool() { +abstract class TranslationInspection : LocalInspectionTool() { protected abstract fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor final override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { diff --git a/src/main/kotlin/translations/inspections/WrongTypeInTranslationArgsInspection.kt b/src/main/kotlin/translations/inspections/WrongTypeInTranslationArgsInspection.kt index 40b30620a..da210a3d6 100644 --- a/src/main/kotlin/translations/inspections/WrongTypeInTranslationArgsInspection.kt +++ b/src/main/kotlin/translations/inspections/WrongTypeInTranslationArgsInspection.kt @@ -24,59 +24,83 @@ import com.demonwav.mcdev.platform.mcp.mappings.getMappedClass import com.demonwav.mcdev.platform.mcp.mappings.getMappedMethod import com.demonwav.mcdev.translations.identification.TranslationInstance import com.demonwav.mcdev.util.findModule +import com.intellij.codeInsight.intention.FileModifier import com.intellij.codeInspection.LocalQuickFix import com.intellij.codeInspection.LocalQuickFixOnPsiElement +import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeInspection.ProblemsHolder import com.intellij.openapi.project.Project import com.intellij.psi.CommonClassNames -import com.intellij.psi.JavaElementVisitor import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiCall import com.intellij.psi.PsiElement import com.intellij.psi.PsiElementVisitor import com.intellij.psi.PsiEllipsisType import com.intellij.psi.PsiFile -import com.intellij.psi.PsiLiteralExpression import com.intellij.psi.PsiManager -import com.intellij.psi.PsiMethodCallExpression -import com.intellij.psi.PsiReferenceExpression import com.intellij.psi.PsiType +import com.intellij.uast.UastHintedVisitorAdapter +import com.intellij.uast.createUastSmartPointer import com.siyeh.ig.psiutils.CommentTracker import com.siyeh.ig.psiutils.MethodCallUtils +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UElement +import org.jetbrains.uast.UIdentifier +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.generate.replace +import org.jetbrains.uast.getContainingUClass +import org.jetbrains.uast.resolveToUElement +import org.jetbrains.uast.util.isMethodCall +import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor class WrongTypeInTranslationArgsInspection : TranslationInspection() { override fun getStaticDescription() = "Detect wrong argument types in translation arguments" - override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder) + private val typesHint: Array> = + arrayOf(UReferenceExpression::class.java, ULiteralExpression::class.java) - private class Visitor(private val holder: ProblemsHolder) : JavaElementVisitor() { - override fun visitReferenceExpression(expression: PsiReferenceExpression) { - doCheck(expression) + override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = + UastHintedVisitorAdapter.create(holder.file.language, Visitor(holder), typesHint) + + private class Visitor(private val holder: ProblemsHolder) : AbstractUastNonRecursiveVisitor() { + + override fun visitElement(node: UElement): Boolean { + if (node is UReferenceExpression) { + doCheck(node) + } + + return super.visitElement(node) } - override fun visitLiteralExpression(expression: PsiLiteralExpression) { - doCheck(expression) + override fun visitLiteralExpression(node: ULiteralExpression): Boolean { + doCheck(node) + return super.visitLiteralExpression(node) } - private fun doCheck(element: PsiElement) { + private fun doCheck(element: UElement) { val result = TranslationInstance.find(element) - if (result == null || result.foldingElement !is PsiCall || result.allowArbitraryArgs) { + if (result == null || result.foldingElement !is UCallExpression || result.allowArbitraryArgs) { return } - val args = result.foldingElement.argumentList ?: return + val args = result.foldingElement.valueArguments - if (!MethodCallUtils.isVarArgCall(result.foldingElement)) { + val javaCall = result.foldingElement.javaPsi as? PsiCall ?: return + if (!MethodCallUtils.isVarArgCall(javaCall)) { return } - val resolvedMethod = result.foldingElement.resolveMethod() ?: return - if ((resolvedMethod.parameterList.parameters.lastOrNull()?.type as? PsiEllipsisType) + val resolvedMethod = result.foldingElement.resolveToUElement() as? UMethod ?: return + val parameters = resolvedMethod.uastParameters + if ((parameters.lastOrNull()?.type as? PsiEllipsisType) ?.componentType?.equalsToText(CommonClassNames.JAVA_LANG_OBJECT) != true ) { return } - val module = element.findModule() ?: return + val elementSourcePsi = element.sourcePsi ?: return + val module = elementSourcePsi.findModule() ?: return val componentName = module.getMappedClass("net.minecraft.network.chat.Component") val translatableName = module.getMappedMethod( "net.minecraft.network.chat.Component", @@ -84,30 +108,31 @@ class WrongTypeInTranslationArgsInspection : TranslationInspection() { "(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/network/chat/MutableComponent;" ) val isComponentTranslatable = resolvedMethod.name == translatableName && - resolvedMethod.containingClass?.qualifiedName == componentName + resolvedMethod.getContainingUClass()?.qualifiedName == componentName + val resolveScope = elementSourcePsi.resolveScope val booleanType = - PsiType.getTypeByName(CommonClassNames.JAVA_LANG_BOOLEAN, holder.project, element.resolveScope) + PsiType.getTypeByName(CommonClassNames.JAVA_LANG_BOOLEAN, holder.project, resolveScope) val numberType = - PsiType.getTypeByName(CommonClassNames.JAVA_LANG_NUMBER, holder.project, element.resolveScope) - val stringType = PsiType.getJavaLangString(PsiManager.getInstance(holder.project), element.resolveScope) - val componentType = PsiType.getTypeByName(componentName, holder.project, element.resolveScope) - for (arg in args.expressions.drop(resolvedMethod.parameterList.parametersCount - 1)) { - val type = arg.type ?: continue + PsiType.getTypeByName(CommonClassNames.JAVA_LANG_NUMBER, holder.project, resolveScope) + val stringType = PsiType.getJavaLangString(PsiManager.getInstance(holder.project), resolveScope) + val componentType = PsiType.getTypeByName(componentName, holder.project, resolveScope) + for (arg in args.drop(parameters.size - 1)) { + val type = arg.getExpressionType() ?: continue if (!booleanType.isAssignableFrom(type) && !numberType.isAssignableFrom(type) && !stringType.isAssignableFrom(type) && !componentType.isAssignableFrom(type) ) { - var fixes = arrayOf(WrapWithStringValueOfFix(arg)) - if (isComponentTranslatable && result.foldingElement is PsiMethodCallExpression) { - val referenceName = result.foldingElement.methodExpression.referenceNameElement + var fixes = arrayOf(WrapWithStringValueOfFix(arg.sourcePsi!!)) + if (isComponentTranslatable && result.foldingElement.isMethodCall()) { + val referenceName = result.foldingElement.methodIdentifier if (referenceName != null) { fixes = arrayOf(ReplaceWithTranslatableEscapedFix(referenceName)) + fixes } } holder.registerProblem( - arg, + arg.sourcePsi!!, "Translation argument is not a 'String', 'Number', 'Boolean' or 'Component'", *fixes ) @@ -117,19 +142,24 @@ class WrongTypeInTranslationArgsInspection : TranslationInspection() { } private class ReplaceWithTranslatableEscapedFix( - referenceName: PsiElement - ) : LocalQuickFixOnPsiElement(referenceName) { + identifier: UIdentifier + ) : LocalQuickFix { + + @FileModifier.SafeFieldForPreview + private val identifierPointer = identifier.createUastSmartPointer() + override fun getFamilyName() = "Replace with 'Component.translatableEscaped'" - override fun getText() = "Replace with 'Component.translatableEscaped'" - override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { - val module = startElement.findModule() ?: return + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val identifier = identifierPointer.element ?: return + val module = identifier.sourcePsi!!.findModule() ?: return val newMethodName = module.getMappedMethod( "net.minecraft.network.chat.Component", "translatableEscape", "(Ljava/lang/String;[Ljava/lang/Object;)Lnet/minecraft/network/chat/MutableComponent;" ) - startElement.replace(JavaPsiFacade.getElementFactory(project).createIdentifier(newMethodName)) + val fakeSourcePsi = JavaPsiFacade.getElementFactory(project).createIdentifier(newMethodName) + identifier.replace(UIdentifier(fakeSourcePsi, identifier.uastParent)) } } diff --git a/src/main/kotlin/translations/intentions/ConvertToTranslationIntention.kt b/src/main/kotlin/translations/intentions/ConvertToTranslationIntention.kt index aa3046dc5..f03fc5785 100644 --- a/src/main/kotlin/translations/intentions/ConvertToTranslationIntention.kt +++ b/src/main/kotlin/translations/intentions/ConvertToTranslationIntention.kt @@ -20,88 +20,119 @@ package com.demonwav.mcdev.translations.intentions +import com.demonwav.mcdev.TranslationSettings +import com.demonwav.mcdev.platform.mcp.mappings.getMappedMethodCall import com.demonwav.mcdev.translations.TranslationFiles +import com.demonwav.mcdev.util.findModule import com.demonwav.mcdev.util.runWriteAction import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction -import com.intellij.lang.java.JavaLanguage import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.openapi.ui.InputValidatorEx import com.intellij.openapi.ui.Messages -import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement -import com.intellij.psi.PsiLiteral -import com.intellij.psi.codeStyle.JavaCodeStyleManager import com.intellij.util.IncorrectOperationException +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.evaluateString +import org.jetbrains.uast.findUElementAt +import org.jetbrains.uast.generate.generationPlugin +import org.jetbrains.uast.textRange +import org.jetbrains.uast.toUElementOfType class ConvertToTranslationIntention : PsiElementBaseIntentionAction() { @Throws(IncorrectOperationException::class) override fun invoke(project: Project, editor: Editor, element: PsiElement) { - if (element.parent is PsiLiteral) { - val value = (element.parent as PsiLiteral).value as? String ?: return - val result = Messages.showInputDialogWithCheckBox( - "Enter translation key:", - "Convert String Literal to Translation", - "Replace literal with call to I18n (only works on clients!)", - true, - true, - Messages.getQuestionIcon(), - null, - object : InputValidatorEx { - override fun getErrorText(inputString: String): String? { - if (inputString.isEmpty()) { - return "Key must not be empty" - } - if (inputString.contains('=')) { - return "Key must not contain separator character ('=')" - } - return null - } + val literal = element.parent.toUElementOfType() ?: return + val value = literal.evaluateString() ?: return - override fun checkInput(inputString: String): Boolean { - return inputString.isNotEmpty() && !inputString.contains('=') - } + val existingKey = TranslationFiles.findTranslationKeyForText(element, value) - override fun canClose(inputString: String): Boolean { - return inputString.isNotEmpty() && !inputString.contains('=') + val result = Messages.showInputDialogWithCheckBox( + "Enter translation key:", + "Convert String Literal to Translation", + "Replace literal with call to I18n (only works on clients!)", + true, + true, + Messages.getQuestionIcon(), + existingKey, + object : InputValidatorEx { + override fun getErrorText(inputString: String): String? { + if (inputString.isEmpty()) { + return "Key must not be empty" } - }, - ) - val key = result.first ?: return - val replaceLiteral = result.second - try { + if (inputString.contains('=')) { + return "Key must not contain separator character ('=')" + } + return null + } + + override fun checkInput(inputString: String): Boolean { + return inputString.isNotEmpty() && !inputString.contains('=') + } + + override fun canClose(inputString: String): Boolean { + return inputString.isNotEmpty() && !inputString.contains('=') + } + }, + ) + val key = result.first ?: return + val replaceLiteral = result.second + try { + if (existingKey != key) { TranslationFiles.add(element, key, value) - if (replaceLiteral) { - val psi = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: return - psi.runWriteAction { - val expression = JavaPsiFacade.getElementFactory(project).createExpressionFromText( - "net.minecraft.client.resources.I18n.format(\"$key\")", - element.context, - ) - if (psi.language === JavaLanguage.INSTANCE) { - JavaCodeStyleManager.getInstance(project) - .shortenClassReferences(element.parent.replace(expression)) - } else { - element.parent.replace(expression) - } + } + if (replaceLiteral) { + val translationSettings = TranslationSettings.getInstance(project) + val documentManager = PsiDocumentManager.getInstance(project) + val psi = documentManager.getPsiFile(editor.document) ?: return + val callCode = if (translationSettings.isUseCustomConvertToTranslationTemplate) { + translationSettings.convertToTranslationTemplate.replace("\$key", key) + } else { + element.findModule()?.getMappedMethodCall( + "net.minecraft.client.resource.language.I18n", + "translate", + "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", + "\"$key\"" + ) ?: "net.minecraft.client.resource.I18n.get(\"$key\")" + } + + val replaceRange = when (literal.lang.id) { + // Special case because in Kotlin, the sourcePsi is a template entry, not the literal itself + "kotlin" -> literal.sourcePsi?.parent?.textRange + else -> literal.textRange + } ?: return + + psi.runWriteAction { + // There is no convenient way to generate a qualified call expression with the UAST factory + // so we simply put the raw code there and assume it's correct + editor.document.replaceString(replaceRange.startOffset, replaceRange.endOffset, callCode) + documentManager.commitDocument(editor.document) + + val callOffset = replaceRange.startOffset + callCode.indexOf('(') + val newExpr = psi.findUElementAt(callOffset - 1, UReferenceExpression::class.java) + if (newExpr != null) { + literal.generationPlugin?.shortenReference(newExpr) } } - } catch (e: Exception) { - Notification( - "Translation support error", - "Error while adding translation", - e.message ?: e.stackTraceToString(), - NotificationType.WARNING, - ).notify(project) } + } catch (e: Exception) { + Notification( + "Translation support error", + "Error while adding translation", + e.message ?: e.stackTraceToString(), + NotificationType.WARNING, + ).notify(project) } } - override fun isAvailable(project: Project, editor: Editor, element: PsiElement) = - (element.parent as? PsiLiteral)?.value is String + override fun isAvailable(project: Project, editor: Editor, element: PsiElement): Boolean { + val literal = element.parent.toUElementOfType() + return literal?.evaluateString() is String + } override fun getFamilyName() = "Convert string literal to translation" diff --git a/src/main/kotlin/translations/intentions/RemoveDuplicatesIntention.kt b/src/main/kotlin/translations/intentions/RemoveDuplicatesIntention.kt index 52d4dcac5..30f227b40 100644 --- a/src/main/kotlin/translations/intentions/RemoveDuplicatesIntention.kt +++ b/src/main/kotlin/translations/intentions/RemoveDuplicatesIntention.kt @@ -23,24 +23,32 @@ package com.demonwav.mcdev.translations.intentions import com.demonwav.mcdev.translations.Translation import com.demonwav.mcdev.translations.TranslationFiles import com.demonwav.mcdev.translations.index.TranslationInverseIndex -import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile import com.intellij.psi.search.GlobalSearchScope -class RemoveDuplicatesIntention(private val translation: Translation) : PsiElementBaseIntentionAction() { +class RemoveDuplicatesIntention( + private val translation: Translation, + element: PsiElement +) : LocalQuickFixAndIntentionActionOnPsiElement(element) { override fun getText() = "Remove duplicates (keep this translation)" override fun getFamilyName() = "Minecraft localization" - override fun isAvailable(project: Project, editor: Editor?, element: PsiElement) = true - - override fun invoke(project: Project, editor: Editor?, element: PsiElement) { - val keep = TranslationFiles.seekTranslation(element) ?: return + override fun invoke( + project: Project, + file: PsiFile, + editor: Editor?, + startElement: PsiElement, + endElement: PsiElement + ) { + val keep = TranslationFiles.seekTranslation(startElement) ?: return val entries = TranslationInverseIndex.findElements( translation.key, - GlobalSearchScope.fileScope(element.containingFile), + GlobalSearchScope.fileScope(file), ) for (other in entries) { if (other !== keep) { diff --git a/src/main/kotlin/translations/intentions/RemoveUnmatchedEntryIntention.kt b/src/main/kotlin/translations/intentions/RemoveUnmatchedEntryIntention.kt index 6185d6f2c..78884869e 100644 --- a/src/main/kotlin/translations/intentions/RemoveUnmatchedEntryIntention.kt +++ b/src/main/kotlin/translations/intentions/RemoveUnmatchedEntryIntention.kt @@ -21,21 +21,24 @@ package com.demonwav.mcdev.translations.intentions import com.demonwav.mcdev.translations.TranslationFiles -import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement -import com.intellij.util.IncorrectOperationException +import com.intellij.psi.PsiFile -class RemoveUnmatchedEntryIntention : PsiElementBaseIntentionAction() { +class RemoveUnmatchedEntryIntention(element: PsiElement) : LocalQuickFixAndIntentionActionOnPsiElement(element) { override fun getText() = "Remove translation" - override fun isAvailable(project: Project, editor: Editor, element: PsiElement) = true - override fun getFamilyName() = "Minecraft" - @Throws(IncorrectOperationException::class) - override fun invoke(project: Project, editor: Editor, element: PsiElement) { - TranslationFiles.remove(TranslationFiles.seekTranslation(element) ?: return) + override fun invoke( + project: Project, + file: PsiFile, + editor: Editor?, + startElement: PsiElement, + endElement: PsiElement + ) { + TranslationFiles.remove(TranslationFiles.seekTranslation(startElement) ?: return) } } diff --git a/src/main/kotlin/translations/intentions/TranslationFileAnnotator.kt b/src/main/kotlin/translations/intentions/TranslationFileAnnotator.kt index 535d3fe6e..302384d1d 100644 --- a/src/main/kotlin/translations/intentions/TranslationFileAnnotator.kt +++ b/src/main/kotlin/translations/intentions/TranslationFileAnnotator.kt @@ -49,7 +49,7 @@ class TranslationFileAnnotator : Annotator { if (translation.key != translation.trimmedKey) { annotations.newAnnotation(HighlightSeverity.WARNING, "Translation key contains whitespace at start or end.") .range(element) - .newFix(TrimKeyIntention()).registerFix() + .newFix(TrimKeyIntention(element)).universal().registerFix() .create() } } @@ -58,7 +58,7 @@ class TranslationFileAnnotator : Annotator { val count = TranslationIndex.getTranslations(element.containingFile).count { it.key == translation.key } if (count > 1) { annotations.newAnnotation(HighlightSeverity.WARNING, "Duplicate translation keys \"${translation.key}\".") - .newFix(RemoveDuplicatesIntention(translation)).registerFix() + .newFix(RemoveDuplicatesIntention(translation, element)).universal().registerFix() .create() } } @@ -71,7 +71,7 @@ class TranslationFileAnnotator : Annotator { } val warningText = "Translation key not included in default localization file." annotations.newAnnotation(HighlightSeverity.WARNING, warningText) - .newFix(RemoveUnmatchedEntryIntention()).registerFix() + .newFix(RemoveUnmatchedEntryIntention(element)).universal().registerFix() .create() } } diff --git a/src/main/kotlin/translations/intentions/TrimKeyIntention.kt b/src/main/kotlin/translations/intentions/TrimKeyIntention.kt index 0e4a9851b..9e532db28 100644 --- a/src/main/kotlin/translations/intentions/TrimKeyIntention.kt +++ b/src/main/kotlin/translations/intentions/TrimKeyIntention.kt @@ -22,28 +22,39 @@ package com.demonwav.mcdev.translations.intentions import com.demonwav.mcdev.translations.TranslationFiles import com.intellij.codeInsight.FileModificationService -import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.PsiElement -import com.intellij.util.IncorrectOperationException +import com.intellij.psi.PsiFile -class TrimKeyIntention : PsiElementBaseIntentionAction() { +class TrimKeyIntention(element: PsiElement) : LocalQuickFixAndIntentionActionOnPsiElement(element) { override fun getText() = "Trim translation key" override fun getFamilyName() = "Minecraft" - override fun isAvailable(project: Project, editor: Editor, element: PsiElement): Boolean { + override fun isAvailable( + project: Project, + file: PsiFile, + editor: Editor?, + startElement: PsiElement, + endElement: PsiElement + ): Boolean { val translation = TranslationFiles.toTranslation( - TranslationFiles.seekTranslation(element) ?: return false, + TranslationFiles.seekTranslation(startElement) ?: return false, ) ?: return false return translation.key != translation.trimmedKey } - @Throws(IncorrectOperationException::class) - override fun invoke(project: Project, editor: Editor, element: PsiElement) { - val entry = TranslationFiles.seekTranslation(element) ?: return + override fun invoke( + project: Project, + file: PsiFile, + editor: Editor?, + startElement: PsiElement, + endElement: PsiElement + ) { + val entry = TranslationFiles.seekTranslation(startElement) ?: return if (!FileModificationService.getInstance().preparePsiElementForWrite(entry)) { return } diff --git a/src/main/kotlin/translations/reference/completion.kt b/src/main/kotlin/translations/reference/completion.kt index 0d17bc146..ada0e77b4 100644 --- a/src/main/kotlin/translations/reference/completion.kt +++ b/src/main/kotlin/translations/reference/completion.kt @@ -38,35 +38,28 @@ import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.json.JsonElementTypes import com.intellij.json.JsonLanguage import com.intellij.json.psi.JsonStringLiteral +import com.intellij.openapi.util.text.StringUtil import com.intellij.patterns.PlatformPatterns import com.intellij.psi.PsiElement import com.intellij.psi.util.PsiUtilCore sealed class TranslationCompletionContributor : CompletionContributor() { protected fun handleKey(text: String, element: PsiElement, domain: String?, result: CompletionResultSet) { - if (text.isEmpty()) { - return - } - val defaultEntries = TranslationIndex.getAllDefaultTranslations(element.project, domain) - val existingKeys = TranslationIndex.getTranslations(element.containingFile ?: return).map { it.key }.toSet() + val availableKeys = TranslationIndex.getTranslations(element.containingFile.originalFile).map { it.key }.toSet() val prefixResult = result.withPrefixMatcher(text) - var counter = 0 for (entry in defaultEntries) { val key = entry.key - if (!key.contains(text) || existingKeys.contains(key)) { + if (!key.contains(text) || availableKeys.contains(key)) { continue } - if (counter++ > 1000) { - break // Prevent insane CPU usage - } - + val textHint = StringUtil.shortenTextWithEllipsis(entry.text, 30, 0) prefixResult.addElement( PrioritizedLookupElement.withPriority( - LookupElementBuilder.create(key).withIcon(PlatformAssets.MINECRAFT_ICON), + LookupElementBuilder.create(key).withIcon(PlatformAssets.MINECRAFT_ICON).withTypeText(textHint), 1.0 + key.getSimilarity(text), ), ) diff --git a/src/main/kotlin/translations/reference/contributors.kt b/src/main/kotlin/translations/reference/contributors.kt index 0ea7f0f68..5ba569a8e 100644 --- a/src/main/kotlin/translations/reference/contributors.kt +++ b/src/main/kotlin/translations/reference/contributors.kt @@ -21,7 +21,6 @@ package com.demonwav.mcdev.translations.reference import com.demonwav.mcdev.translations.TranslationFiles -import com.demonwav.mcdev.translations.identification.TranslationIdentifier import com.demonwav.mcdev.translations.identification.TranslationInstance import com.demonwav.mcdev.translations.lang.gen.psi.LangEntry import com.demonwav.mcdev.translations.lang.gen.psi.LangTypes @@ -34,34 +33,29 @@ import com.intellij.psi.PsiReference import com.intellij.psi.PsiReferenceContributor import com.intellij.psi.PsiReferenceProvider import com.intellij.psi.PsiReferenceRegistrar +import com.intellij.psi.registerUastReferenceProvider +import com.intellij.psi.uastReferenceProvider import com.intellij.util.ProcessingContext +import org.jetbrains.uast.UElement -class JavaReferenceContributor : PsiReferenceContributor() { +class UastReferenceContributor : PsiReferenceContributor() { override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) { - for (identifier in TranslationIdentifier.INSTANCES) { - registrar.registerReferenceProvider( - PlatformPatterns.psiElement(identifier.elementClass()), - object : PsiReferenceProvider() { - override fun getReferencesByElement( - element: PsiElement, - context: ProcessingContext, - ): Array { - val result = identifier.identifyUnsafe(element) - if (result != null) { - val referenceElement = result.referenceElement ?: return emptyArray() - return arrayOf( - TranslationReference( - referenceElement, - TextRange(1, referenceElement.textLength - 1), - result.key, - ), - ) - } - return emptyArray() - } - }, - ) - } + registrar.registerUastReferenceProvider( + { _, _ -> true }, + uastReferenceProvider { uExpr, psi -> + val translation = TranslationInstance.find(uExpr) + ?: return@uastReferenceProvider emptyArray() + val referenceElement = translation.referenceElement + ?: return@uastReferenceProvider emptyArray() + arrayOf( + TranslationReference( + psi, + TextRange(1, referenceElement.asSourceString().length - 1), + translation.key, + ), + ) + } + ) } } diff --git a/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.form b/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.form deleted file mode 100644 index 267a814de..000000000 --- a/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.form +++ /dev/null @@ -1,58 +0,0 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.kt b/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.kt index 87c9200c2..20daa77ee 100644 --- a/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.kt +++ b/src/main/kotlin/translations/sorting/TranslationTemplateConfigurable.kt @@ -20,6 +20,8 @@ package com.demonwav.mcdev.translations.sorting +import com.demonwav.mcdev.TranslationSettings +import com.demonwav.mcdev.asset.MCDevBundle import com.demonwav.mcdev.translations.lang.colors.LangSyntaxHighlighter import com.intellij.codeInsight.template.impl.TemplateEditorUtil import com.intellij.ide.DataManager @@ -30,6 +32,14 @@ import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.editor.ex.util.LexerEditorHighlighter import com.intellij.openapi.options.Configurable import com.intellij.openapi.project.Project +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.selected +import com.intellij.ui.layout.ComponentPredicate import com.intellij.util.ui.JBUI import java.awt.BorderLayout import javax.swing.DefaultComboBoxModel @@ -39,32 +49,66 @@ import javax.swing.JPanel import org.jetbrains.annotations.Nls class TranslationTemplateConfigurable(private val project: Project) : Configurable { - private lateinit var panel: JPanel private lateinit var cmbScheme: JComboBox - private lateinit var editorPanel: JPanel - private lateinit var templateEditor: Editor + private var templateEditor: Editor? = null + + private val editorPanel = JPanel(BorderLayout()).apply { + preferredSize = JBUI.size(250, 350) + minimumSize = preferredSize + } + + private val panel = panel { + row(MCDevBundle("minecraft.settings.lang_template.scheme")) { + cmbScheme = comboBox(emptyList()).component + } + + row { + label(MCDevBundle("minecraft.settings.lang_template.comment")) + } + + row { + cell(editorPanel).align(Align.FILL) + } + + val translationSettings = TranslationSettings.getInstance(project) + row { + checkBox(MCDevBundle("minecraft.settings.translation.force_json_translation_file")) + .bindSelected(translationSettings::isForceJsonTranslationFile) + } + + lateinit var allowConvertToTranslationTemplate: ComponentPredicate + row { + val checkBox = checkBox(MCDevBundle("minecraft.settings.translation.use_custom_convert_template")) + .bindSelected(translationSettings::isUseCustomConvertToTranslationTemplate) + allowConvertToTranslationTemplate = checkBox.selected + } + + row { + textField().bindText(translationSettings::convertToTranslationTemplate) + .enabledIf(allowConvertToTranslationTemplate) + .columns(COLUMNS_LARGE) + } + } @Nls - override fun getDisplayName() = "Localization Template" + override fun getDisplayName() = MCDevBundle("minecraft.settings.lang_template.display_name") override fun getHelpTopic(): String? = null - override fun createComponent(): JComponent { - return panel - } + override fun createComponent(): JComponent = panel private fun getActiveTemplateText() = when { cmbScheme.selectedIndex == 0 -> TemplateManager.getGlobalTemplateText() !project.isDefault -> TemplateManager.getProjectTemplateText(project) - else -> "You must have selected a project for this!" + else -> MCDevBundle("minecraft.settings.lang_template.project_must_be_selected") } private fun init() { if (project.isDefault) { - cmbScheme.selectedIndex = 0 cmbScheme.model = DefaultComboBoxModel(arrayOf("Global")) - } else if (cmbScheme.selectedIndex == 0) { + cmbScheme.selectedIndex = 0 + } else { cmbScheme.model = DefaultComboBoxModel(arrayOf("Global", "Project")) } cmbScheme.addActionListener { @@ -82,28 +126,32 @@ class TranslationTemplateConfigurable(private val project: Project) : Configurab editorColorsScheme, ) (templateEditor as EditorEx).highlighter = highlighter - templateEditor.settings.isLineNumbersShown = true + templateEditor!!.settings.isLineNumbersShown = true - editorPanel.preferredSize = JBUI.size(250, 100) - editorPanel.minimumSize = editorPanel.preferredSize editorPanel.removeAll() - editorPanel.add(templateEditor.component, BorderLayout.CENTER) + editorPanel.add(templateEditor!!.component, BorderLayout.CENTER) } override fun isModified(): Boolean { - return templateEditor.document.text != getActiveTemplateText() + return templateEditor?.document?.text != getActiveTemplateText() != false || panel.isModified() } override fun apply() { + val editor = templateEditor + ?: return + val project = CommonDataKeys.PROJECT.getData(DataManager.getInstance().getDataContext(panel)) if (cmbScheme.selectedIndex == 0) { - TemplateManager.writeGlobalTemplate(templateEditor.document.text) + TemplateManager.writeGlobalTemplate(editor.document.text) } else if (project != null) { - TemplateManager.writeProjectTemplate(project, templateEditor.document.text) + TemplateManager.writeProjectTemplate(project, editor.document.text) } + + panel.apply() } override fun reset() { init() + panel.reset() } } diff --git a/src/main/kotlin/translations/sorting/TranslationTemplateLexerAdapter.kt b/src/main/kotlin/translations/sorting/TranslationTemplateLexerAdapter.kt index ad69540c4..b7413c898 100644 --- a/src/main/kotlin/translations/sorting/TranslationTemplateLexerAdapter.kt +++ b/src/main/kotlin/translations/sorting/TranslationTemplateLexerAdapter.kt @@ -20,7 +20,7 @@ package com.demonwav.mcdev.translations.sorting -import com.demonwav.mcdev.translations.lang.gen.TranslationTemplateLexer +import com.demonwav.mcdev.translations.template.gen.TranslationTemplateLexer import com.intellij.lexer.FlexAdapter class TranslationTemplateLexerAdapter : FlexAdapter(TranslationTemplateLexer()) diff --git a/src/main/kotlin/util/BeforeOrAfter.kt b/src/main/kotlin/util/BeforeOrAfter.kt new file mode 100644 index 000000000..448be3062 --- /dev/null +++ b/src/main/kotlin/util/BeforeOrAfter.kt @@ -0,0 +1,32 @@ +/* + * 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.util + +import com.demonwav.mcdev.asset.MCDevBundle +import java.util.function.Supplier + +enum class BeforeOrAfter(private val myDisplayName: Supplier) { + BEFORE(MCDevBundle.pointer("minecraft.before")), + AFTER(MCDevBundle.pointer("minecraft.after")); + + val displayName get() = myDisplayName.get() + override fun toString() = displayName +} diff --git a/src/main/kotlin/util/MemberReference.kt b/src/main/kotlin/util/MemberReference.kt index 5b5921e73..945746fa5 100644 --- a/src/main/kotlin/util/MemberReference.kt +++ b/src/main/kotlin/util/MemberReference.kt @@ -21,14 +21,12 @@ package com.demonwav.mcdev.util import com.demonwav.mcdev.platform.mixin.reference.MixinSelector -import com.google.gson.JsonDeserializationContext -import com.google.gson.JsonDeserializer -import com.google.gson.JsonElement +import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.PsiClass import com.intellij.psi.PsiField import com.intellij.psi.PsiMethod import java.io.Serializable -import java.lang.reflect.Type +import org.objectweb.asm.Type /** * Represents a reference to a class member (a method or a field). It may @@ -65,6 +63,19 @@ data class MemberReference( override val fieldDescriptor = descriptor?.takeUnless { it.contains("(") } override val displayName = name + val presentableText: String get() = buildString { + if (owner != null) { + append(owner.substringAfterLast('.')) + append('.') + } + append(name) + if (descriptor != null && descriptor.startsWith("(")) { + append('(') + append(Type.getArgumentTypes(descriptor).joinToString { it.className.substringAfterLast('.') }) + append(')') + } + } + override fun canEverMatch(name: String): Boolean { return matchAllNames || this.name == name } @@ -88,13 +99,71 @@ data class MemberReference( (this.descriptor == null || this.descriptor == desc) } - object Deserializer : JsonDeserializer { - override fun deserialize(json: JsonElement, type: Type, ctx: JsonDeserializationContext): MemberReference { - val ref = json.asString - val className = ref.substringBefore('#') - val methodName = ref.substring(className.length + 1, ref.indexOf("(")) - val methodDesc = ref.substring(className.length + methodName.length + 1) - return MemberReference(methodName, methodDesc, className) + companion object { + fun parse(value: String): MemberReference? { + val reference = value.replace(" ", "") + val owner: String? + + var pos = reference.lastIndexOf('.') + if (pos != -1) { + // Everything before the dot is the qualifier/owner + owner = reference.substring(0, pos).replace('/', '.') + } else { + pos = reference.indexOf(';') + if (pos != -1 && reference.startsWith('L')) { + val internalOwner = reference.substring(1, pos) + if (!StringUtil.isJavaIdentifier(internalOwner.replace('/', '_'))) { + // Invalid: Qualifier should only contain slashes + return null + } + + owner = internalOwner.replace('/', '.') + + // if owner is all there is to the selector, match anything with the owner + if (pos == reference.length - 1) { + return MemberReference("", null, owner, matchAllNames = true, matchAllDescs = true) + } + } else { + // No owner/qualifier specified + pos = -1 + owner = null + } + } + + val descriptor: String? + val name: String + val matchAllNames = reference.getOrNull(pos + 1) == '*' + val matchAllDescs: Boolean + + // Find descriptor separator + val methodDescPos = reference.indexOf('(', pos + 1) + if (methodDescPos != -1) { + // Method descriptor + descriptor = reference.substring(methodDescPos) + name = reference.substring(pos + 1, methodDescPos) + matchAllDescs = false + } else { + val fieldDescPos = reference.indexOf(':', pos + 1) + if (fieldDescPos != -1) { + descriptor = reference.substring(fieldDescPos + 1) + name = reference.substring(pos + 1, fieldDescPos) + matchAllDescs = false + } else { + descriptor = null + matchAllDescs = reference.endsWith('*') + name = if (matchAllDescs) { + reference.substring(pos + 1, reference.lastIndex) + } else { + reference.substring(pos + 1) + } + } + } + + if (!matchAllNames && !StringUtil.isJavaIdentifier(name) && name != "" && name != "") { + return null + } + + return MemberReference(if (matchAllNames) "*" else name, descriptor, owner, matchAllNames, matchAllDescs) } } } diff --git a/src/main/kotlin/util/MinecraftVersions.kt b/src/main/kotlin/util/MinecraftVersions.kt index 9d19a575c..7333c5aa3 100644 --- a/src/main/kotlin/util/MinecraftVersions.kt +++ b/src/main/kotlin/util/MinecraftVersions.kt @@ -25,6 +25,7 @@ import com.intellij.openapi.projectRoots.JavaSdkVersion object MinecraftVersions { val MC1_12_2 = SemanticVersion.release(1, 12, 2) val MC1_14_4 = SemanticVersion.release(1, 14, 4) + val MC1_16 = SemanticVersion.release(1, 16) val MC1_16_1 = SemanticVersion.release(1, 16, 1) val MC1_16_5 = SemanticVersion.release(1, 16, 5) val MC1_17 = SemanticVersion.release(1, 17) @@ -35,6 +36,7 @@ object MinecraftVersions { val MC1_19_3 = SemanticVersion.release(1, 19, 3) val MC1_19_4 = SemanticVersion.release(1, 19, 4) val MC1_20 = SemanticVersion.release(1, 20) + val MC1_20_1 = SemanticVersion.release(1, 20, 1) val MC1_20_2 = SemanticVersion.release(1, 20, 2) val MC1_20_3 = SemanticVersion.release(1, 20, 3) val MC1_20_4 = SemanticVersion.release(1, 20, 4) diff --git a/src/main/kotlin/util/bytecode-utils.kt b/src/main/kotlin/util/bytecode-utils.kt index 5eab8bbcf..777210c54 100644 --- a/src/main/kotlin/util/bytecode-utils.kt +++ b/src/main/kotlin/util/bytecode-utils.kt @@ -69,7 +69,9 @@ fun getPrimitiveType(internalName: Char): PsiPrimitiveType? { } val PsiType.descriptor - get() = appendDescriptor(StringBuilder()).toString() + get() = erasure().appendDescriptor(StringBuilder()).toString() + +private fun PsiType.erasure() = TypeConversionUtil.erasure(this)!! fun getPrimitiveWrapperClass(internalName: Char, project: Project): PsiClass? { val type = getPrimitiveType(internalName) ?: return null diff --git a/src/main/kotlin/util/call-uast-utils.kt b/src/main/kotlin/util/call-uast-utils.kt new file mode 100644 index 000000000..af29f8029 --- /dev/null +++ b/src/main/kotlin/util/call-uast-utils.kt @@ -0,0 +1,72 @@ +/* + * 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.util + +import com.intellij.psi.PsiParameter +import com.intellij.psi.PsiType +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UMethod +import org.jetbrains.uast.toUElementOfType +import org.jetbrains.uast.util.isArrayInitializer + +val UCallExpression.referencedMethod: UMethod? + get() = this.resolve()?.toUElementOfType() + +fun UCallExpression.extractVarArgs(index: Int, allowReferences: Boolean, allowTranslations: Boolean): Array? { + val method = this.referencedMethod + val args = this.valueArguments + if (method == null || args.size < (index + 1)) { + return emptyArray() + } + + val psiParam = method.uastParameters[index].javaPsi as? PsiParameter + ?: return null + if (!psiParam.isVarArgs) { + return arrayOf(args[index].evaluate(allowTranslations, allowReferences)) + } + + val elements = args.drop(index) + return extractVarArgs(psiParam.type, elements, allowReferences, allowTranslations) +} + +private fun extractVarArgs( + type: PsiType, + elements: List, + allowReferences: Boolean, + allowTranslations: Boolean, +): Array? { + return if (elements[0].getExpressionType() == type) { + val initializer = elements[0] + if (initializer is UCallExpression && initializer.isArrayInitializer()) { + // We're dealing with an array initializer, let's analyse it! + initializer.valueArguments + .asSequence() + .map { it.evaluate(allowReferences, allowTranslations) } + .toTypedArray() + } else { + // We're dealing with a more complex expression that results in an array, give up + return null + } + } else { + elements.asSequence().map { it.evaluate(allowReferences, allowTranslations) }.toTypedArray() + } +} diff --git a/src/main/kotlin/util/call-utils.kt b/src/main/kotlin/util/call-utils.kt index 8e45a5a3c..672a03ce5 100644 --- a/src/main/kotlin/util/call-utils.kt +++ b/src/main/kotlin/util/call-utils.kt @@ -21,54 +21,15 @@ package com.demonwav.mcdev.util import com.intellij.psi.PsiCall -import com.intellij.psi.PsiExpression +import com.intellij.psi.PsiEnumConstant import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodCallExpression import com.intellij.psi.PsiNewExpression -import com.intellij.psi.PsiSubstitutor -import com.intellij.psi.PsiType val PsiCall.referencedMethod: PsiMethod? get() = when (this) { is PsiMethodCallExpression -> this.methodExpression.advancedResolve(false).element as PsiMethod? is PsiNewExpression -> this.resolveMethod() + is PsiEnumConstant -> this.resolveMethod() else -> null } - -fun PsiCall.extractVarArgs(index: Int, allowReferences: Boolean, allowTranslations: Boolean): Array? { - val method = this.referencedMethod - val args = this.argumentList?.expressions ?: return emptyArray() - if (method == null || args.size < (index + 1)) { - return emptyArray() - } - if (!method.parameterList.parameters[index].isVarArgs) { - return arrayOf(args[index].evaluate(allowTranslations, allowReferences)) - } - - val varargType = method.getSignature(PsiSubstitutor.EMPTY).parameterTypes[index] - val elements = args.drop(index) - return extractVarArgs(varargType, elements, allowReferences, allowTranslations) -} - -private fun extractVarArgs( - type: PsiType, - elements: List, - allowReferences: Boolean, - allowTranslations: Boolean, -): Array? { - return if (elements[0].type == type) { - val initializer = elements[0] - if (initializer is PsiNewExpression && initializer.arrayInitializer != null) { - // We're dealing with an array initializer, let's analyse it! - initializer.arrayInitializer!!.initializers - .asSequence() - .map { it.evaluate(allowReferences, allowTranslations) } - .toTypedArray() - } else { - // We're dealing with a more complex expression that results in an array, give up - return null - } - } else { - elements.asSequence().map { it.evaluate(allowReferences, allowTranslations) }.toTypedArray() - } -} diff --git a/src/main/kotlin/util/expression-utils.kt b/src/main/kotlin/util/expression-utils.kt index 736547c1e..9dca724b2 100644 --- a/src/main/kotlin/util/expression-utils.kt +++ b/src/main/kotlin/util/expression-utils.kt @@ -22,34 +22,44 @@ package com.demonwav.mcdev.util import com.demonwav.mcdev.translations.identification.TranslationInstance import com.demonwav.mcdev.translations.identification.TranslationInstance.Companion.FormattingError -import com.intellij.psi.PsiAnnotationMemberValue -import com.intellij.psi.PsiCall -import com.intellij.psi.PsiLiteral -import com.intellij.psi.PsiReferenceExpression -import com.intellij.psi.PsiTypeCastExpression -import com.intellij.psi.PsiVariable +import org.jetbrains.uast.UBinaryExpressionWithType +import org.jetbrains.uast.UCallExpression +import org.jetbrains.uast.UExpression +import org.jetbrains.uast.UQualifiedReferenceExpression +import org.jetbrains.uast.UReferenceExpression +import org.jetbrains.uast.UVariable +import org.jetbrains.uast.evaluateString +import org.jetbrains.uast.resolveToUElement +import org.jetbrains.uast.util.isTypeCast -fun PsiAnnotationMemberValue.evaluate(allowReferences: Boolean, allowTranslations: Boolean): String? { - val visited = mutableSetOf() +fun UExpression.evaluate(allowReferences: Boolean, allowTranslations: Boolean): String? { + val visited = mutableSetOf() - fun eval(expr: PsiAnnotationMemberValue?, defaultValue: String? = null): String? { + fun eval(expr: UExpression?, defaultValue: String? = null): String? { if (!visited.add(expr)) { return defaultValue } when { - expr is PsiTypeCastExpression && expr.operand != null -> + expr is UBinaryExpressionWithType && expr.isTypeCast() -> return eval(expr.operand, defaultValue) - expr is PsiReferenceExpression -> { - val reference = expr.advancedResolve(false).element - if (reference is PsiVariable && reference.initializer != null) { - return eval(reference.initializer, "\${${expr.text}}") + + expr is UQualifiedReferenceExpression -> { + val selector = expr.selector + if (selector is UCallExpression) { + return eval(selector, "\${${expr.asSourceString()}}") + } + } + + expr is UReferenceExpression -> { + val reference = expr.resolveToUElement() + if (reference is UVariable && reference.uastInitializer != null) { + return eval(reference.uastInitializer, "\${${expr.asSourceString()}}") } } - expr is PsiLiteral -> - return expr.value.toString() - expr is PsiCall && allowTranslations -> - for (argument in expr.argumentList?.expressions ?: emptyArray()) { + + expr is UCallExpression && allowTranslations -> + for (argument in expr.valueArguments) { val translation = TranslationInstance.find(argument) ?: continue if (translation.formattingError == FormattingError.MISSING) { return "{ERROR: Missing formatting arguments for '${translation.text}'}" @@ -57,10 +67,12 @@ fun PsiAnnotationMemberValue.evaluate(allowReferences: Boolean, allowTranslation return translation.text } + + else -> expr?.evaluateString()?.let { return it } } return if (allowReferences && expr != null) { - "\${${expr.text}}" + "\${${expr.asSourceString()}}" } else { defaultValue } diff --git a/src/main/kotlin/util/files.kt b/src/main/kotlin/util/files.kt index a1847d967..a00fd6c21 100644 --- a/src/main/kotlin/util/files.kt +++ b/src/main/kotlin/util/files.kt @@ -20,9 +20,11 @@ package com.demonwav.mcdev.util +import com.intellij.openapi.application.ModalityState import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.newvfs.RefreshQueue import java.io.File import java.io.IOException import java.nio.file.Path @@ -57,6 +59,7 @@ val VirtualFile.mcPath: String? operator fun Manifest.get(attribute: String): String? = mainAttributes.getValue(attribute) operator fun Manifest.get(attribute: Attributes.Name): String? = mainAttributes.getValue(attribute) -fun VirtualFile.refreshFs(): VirtualFile { - return this.parent.findOrCreateChildData(this, this.name) +fun VirtualFile.refreshSync(modalityState: ModalityState): VirtualFile? { + RefreshQueue.getInstance().refresh(false, this.isDirectory, null, modalityState, this) + return this.parent?.findOrCreateChildData(this, this.name) } diff --git a/src/main/kotlin/util/psi-utils.kt b/src/main/kotlin/util/psi-utils.kt index af3363228..548ea40ed 100644 --- a/src/main/kotlin/util/psi-utils.kt +++ b/src/main/kotlin/util/psi-utils.kt @@ -24,6 +24,7 @@ import com.demonwav.mcdev.facet.MinecraftFacet import com.demonwav.mcdev.platform.mcp.McpModule import com.demonwav.mcdev.platform.mcp.McpModuleType import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.lang.injection.InjectedLanguageManager import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleManager import com.intellij.openapi.module.ModuleUtilCore @@ -32,10 +33,12 @@ import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.roots.impl.OrderEntryUtil import com.intellij.openapi.util.Key +import com.intellij.openapi.util.UserDataHolderEx import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.ElementManipulator import com.intellij.psi.ElementManipulators import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiAnnotation import com.intellij.psi.PsiClass import com.intellij.psi.PsiDirectory import com.intellij.psi.PsiElement @@ -45,12 +48,14 @@ import com.intellij.psi.PsiEllipsisType import com.intellij.psi.PsiExpression import com.intellij.psi.PsiFile import com.intellij.psi.PsiKeyword +import com.intellij.psi.PsiLanguageInjectionHost import com.intellij.psi.PsiMember import com.intellij.psi.PsiMethod import com.intellij.psi.PsiMethodReferenceExpression import com.intellij.psi.PsiModifier import com.intellij.psi.PsiModifier.ModifierConstant import com.intellij.psi.PsiModifierList +import com.intellij.psi.PsiNameValuePair import com.intellij.psi.PsiParameter import com.intellij.psi.PsiParameterList import com.intellij.psi.PsiReference @@ -58,14 +63,21 @@ import com.intellij.psi.PsiReferenceExpression import com.intellij.psi.PsiType import com.intellij.psi.ResolveResult import com.intellij.psi.filters.ElementFilter +import com.intellij.psi.util.CachedValue import com.intellij.psi.util.CachedValueProvider import com.intellij.psi.util.CachedValuesManager import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.PsiTypesUtil import com.intellij.psi.util.TypeConversionUtil +import com.intellij.psi.util.parentOfType import com.intellij.refactoring.changeSignature.ChangeSignatureUtil import com.intellij.util.IncorrectOperationException import com.siyeh.ig.psiutils.ImportUtils +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.read +import kotlin.concurrent.write // Parent fun PsiElement.findModule(): Module? = ModuleUtilCore.findModuleForPsiElement(this) @@ -82,6 +94,10 @@ fun PsiElement.findContainingMethod(): PsiMethod? = findParent(resolveReferences fun PsiElement.findContainingModifierList(): PsiModifierList? = findParent(resolveReferences = false) { it is PsiClass } +fun PsiElement.findContainingNameValuePair(): PsiNameValuePair? = findParent(resolveReferences = false) { + it is PsiClass || it is PsiMethod || it is PsiAnnotation +} + private val PsiElement.ancestors: Sequence get() = generateSequence(this) { if (it is PsiFile) null else it.parent } @@ -174,6 +190,18 @@ inline fun PsiElement.childrenOfType(): Collection = inline fun PsiElement.childOfType(): T? = PsiTreeUtil.findChildOfType(this, T::class.java) +/** + * [InjectedLanguageManager.getInjectionHost] returns the first host of a multi-host injection for some reason. + * Use this method as a workaround. + */ +fun PsiElement.findMultiInjectionHost(): PsiLanguageInjectionHost? { + val injectedLanguageManager = InjectedLanguageManager.getInstance(project) + val hostFile = injectedLanguageManager.getInjectionHost(this)?.containingFile ?: return null + val hostOffset = injectedLanguageManager.injectedToHost(this, textRange.startOffset) + val hostElement = hostFile.findElementAt(hostOffset) ?: return null + return hostElement.parentOfType(withSelf = true) +} + fun Sequence.filter(filter: ElementFilter?, context: PsiElement): Sequence { filter ?: return this return filter { filter.isAcceptable(it, context) } @@ -226,6 +254,36 @@ inline fun PsiElement.cached(vararg dependencies: Any, crossinline compute: } } +@PublishedApi +internal val CACHE_LOCKS_KEY = Key.create, ReentrantReadWriteLock>>("mcdev.cacheLock") + +inline fun PsiElement.lockedCached( + key: Key>, + vararg dependencies: Any, + crossinline compute: () -> T, +): T { + val cacheLocks = (this as UserDataHolderEx).putUserDataIfAbsent(CACHE_LOCKS_KEY, ConcurrentHashMap()) + val cacheLock = cacheLocks.computeIfAbsent(key) { ReentrantReadWriteLock() } + + cacheLock.read { + val value = getUserData(key)?.upToDateOrNull + if (value != null) { + return value.get() + } + } + + cacheLock.write { + val value = getUserData(key)?.upToDateOrNull + if (value != null) { + return value.get() + } + + return CachedValuesManager.getCachedValue(this, key) { + CachedValueProvider.Result.create(compute(), *(dependencies.toList() + this).toTypedArray()) + } + } +} + fun LookupElementBuilder.withImportInsertion(toImport: List): LookupElementBuilder = this.withInsertHandler { insertionContext, _ -> toImport.forEach { ImportUtils.addImportIfNeeded(it, insertionContext.file) } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 4f18c81e1..362a1fab0 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -74,6 +74,16 @@ + + + + + + + + + + @@ -123,6 +133,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -159,6 +195,7 @@ + @@ -182,6 +219,8 @@ + + @@ -212,6 +251,10 @@ id="Settings.Minecraft" groupId="language" instance="com.demonwav.mcdev.MinecraftConfigurable"/> + - + - + @@ -267,7 +310,7 @@ - JAVA + UAST com.demonwav.mcdev.translations.intentions.ConvertToTranslationIntention Minecraft convertToTranslation @@ -285,6 +328,7 @@ + @@ -433,6 +477,7 @@ + @@ -454,6 +499,7 @@ + @@ -480,6 +526,35 @@ + + + + + + + + + + + + + + + + + @@ -516,28 +591,28 @@ implementationClass="com.demonwav.mcdev.inspection.IsCancelledInspection"/> - - - @@ -1231,5 +1301,8 @@ description="Copy the reference to clipboard in Access Widener format"> + diff --git a/src/main/resources/messages/MinecraftDevelopment.properties b/src/main/resources/messages/MinecraftDevelopment.properties index a71c4539f..7d8526e81 100644 --- a/src/main/resources/messages/MinecraftDevelopment.properties +++ b/src/main/resources/messages/MinecraftDevelopment.properties @@ -18,7 +18,7 @@ # along with this program. If not, see . # -creator.ui.build_system.label.generic=Build System: +creator.ui.build_system.label=Build System: creator.ui.build_system.label.gradle=Gradle creator.ui.build_system.label.maven=Maven @@ -31,12 +31,38 @@ creator.ui.platform.type.label=Platform Type: creator.ui.platform.label=Platform: creator.ui.platform.mod.name=Mod creator.ui.platform.plugin.name=Plugin +creator.ui.group.default.label=Default +creator.ui.group.mod.label=Mod +creator.ui.group.plugin.label=Plugin +creator.ui.group.proxy.label=Proxy + +creator.ui.custom.step.description=Creating project based on template... +creator.ui.custom.repos.label=Repositories: +creator.ui.custom.groups.label=Groups: +creator.ui.custom.templates.label=Templates: +creator.ui.custom.path.label=Templates Path: +creator.ui.custom.path.dialog.title=Template Root +creator.ui.custom.path.dialog.description=Select the root directory of the template repository +creator.ui.custom.archive.dialog.title=Template Archive +creator.ui.custom.archive.dialog.description=Select the ZIP file containing the template +creator.ui.custom.remote.url.label=Download URL: +creator.ui.custom.remote.url.comment='$version' will be replaced by the template descriptor version currently in use +creator.ui.custom.remote.inner_path.label=Inner Path: +creator.ui.custom.remote.inner_path.comment='$version' will be replaced by the template descriptor version currently in use +creator.ui.custom.remote.auto_update.label=Auto update + +creator.ui.warn.no_properties=This template has no properties +creator.ui.error.template_warns_and_errors=This template contains warnings and errors: +creator.ui.error.template_warns=This template contains warnings: +creator.ui.error.template_errors=This template contains errors: creator.ui.license.label=License: creator.ui.main_class.label=Main Class: -creator.ui.mc_version.label=Minecraft Version: -creator.ui.mod_name.label=Mod Name: -creator.ui.plugin_name.label=Plugin Name: +creator.ui.mc_version.label=Minecraft &Version: +creator.ui.mod_name.label=Mod &Name: +creator.ui.mod_id.label=Mod &ID: +creator.ui.plugin_name.label=Plugin &Name: +creator.ui.plugin_id.label=Plugin &ID: creator.ui.description.label=Description: creator.ui.authors.label=Authors: creator.ui.website.label=Website: @@ -44,13 +70,40 @@ creator.ui.repository.label=Repository: creator.ui.issue_tracker.label=Issue Tracker: creator.ui.update_url.label=Update URL: creator.ui.depend.label=Depend: +creator.ui.log_prefix.label=Log Prefix: +creator.ui.load_at.label=Load At: +creator.ui.load_at.option.startup=Startup: +creator.ui.load_at.option.postworld=Post World: creator.ui.soft_depend.label=Soft Depend: -creator.ui.mixins.label=Use Mixins: +creator.ui.use_mixins.label=Use &Mixins: +creator.ui.split_sources.label=Split Sources: +creator.ui.java_version.label=Java Version: +creator.ui.jdk.label=JDK: +creator.ui.optional_settings.label=Optional Settings creator.ui.parchment.label=Parchment: creator.ui.parchment.include.label=Include: creator.ui.parchment.include.old_mc.label=Older Minecraft versions creator.ui.parchment.include.snapshots.label=Snapshot versions creator.ui.parchment.no_version.message=No versions of Parchment matching your configuration +creator.ui.mod_environment.label=Environment: +creator.ui.mod_environment.option.*=Both +creator.ui.mod_environment.option.client=Client +creator.ui.mod_environment.option.server=Server +creator.ui.forge_version.label=Forge: +creator.ui.neoforge_version.label=NeoForge: +creator.ui.show_snapshots.label=Show snapshots: +creator.ui.loom_version.label=Loom Version: +creator.ui.loader_version.label=Loader Version: +creator.ui.yarn_version.label=Yarn Version: +creator.ui.use_official_mappings.label=Use official mappings +creator.ui.fabricapi_version.label=Fabric API Version: +creator.ui.use_fabricapi.label=Use Fabric API +creator.ui.spongeapi_version.label=Sponge Version: +creator.ui.velocity_version.label=Velocity Version: +creator.ui.versions_download.label=Downloading versions... + +creator.ui.warn.no_yarn_to_mc_match=Unable to match Yarn versions to Minecraft version +creator.ui.warn.no_fabricapi_to_mc_match=Unable to match API versions to Minecraft version creator.ui.outdated.message=Is the Minecraft project wizard outdated? \ Create an issue on the MinecraftDev issue tracker. @@ -61,6 +114,9 @@ creator.ui.generic_unfinished.message=Haven''t finished {0} creator.ui.create_minecraft_project=Create a new Minecraft project creator.step.generic.project_created.message=Your project is being created +creator.step.generic.init_template_providers.message=Initializing templates +creator.step.generic.load_template.message=Loading templates +creator.step.generic.no_templates_available.message=There are no templates available creator.step.gradle.patch_gradle.description=Patching Gradle files creator.step.gradle.import_gradle.description=Importing Gradle project @@ -72,8 +128,15 @@ creator.step.maven.import_maven.description=Importing Maven project creator.step.reformat.description=Reformatting files +creator.validation.custom.path_not_a_directory=Path is not a directory +creator.validation.custom.path_not_a_file=Path is not a file + +creator.validation.blank=Must not be blank creator.validation.group_id_non_example=Group ID must be changed from "org.example" creator.validation.semantic_version=Version must be a valid semantic version +creator.validation.class_fqn=Must be a valid class fully qualified name +creator.validation.regex=Must match regex {0} +creator.validation.invalid_option=Selection is not a valid option creator.validation.jdk_preferred=Java {0} is recommended for {1} creator.validation.jdk_preferred_default_reason=these settings @@ -98,6 +161,9 @@ facet.reimport.failed.content.with_error=Failed to start project refresh, please generate.event_listener.title=Generate Event Listener generate.event_listener.settings=Event Listener Settings +generate.event_listener.event_priority=Event Priority +generate.event_listener.event_order=Event Order +generate.event_listener.ignore_if_canceled=Ignore if event is canceled generate.class.caption=Minecraft Class generate.class.description=Class generation for modders @@ -190,13 +256,81 @@ nbt.file.save_notify.parse_exception.content=An unexpected exception happened, { intention.error.cannot.create.class.message=Cannot create class ''{0}''\n{1} intention.error.cannot.create.class.title=Failed to Create Class -minecraft.settings.display_name=Minecraft Development -minecraft.settings.title=Minecraft Development Settings +translation_sort.title=Select Sort Order +translation_sort.order=Sort Order +translation_sort.keep_comment=Keep Comment + minecraft.settings.change_update_channel=Change Plugin Update Channel -minecraft.settings.show_project_platform_icons=Show project platform icons -minecraft.settings.show_event_listener_gutter_icons=Show event listener gutter icons -minecraft.settings.show_chat_color_gutter_icons=Show chat color gutter icons -minecraft.settings.show_chat_color_underlines=Show chat color underlines minecraft.settings.chat_color_underline_style=Chat color underline style: -minecraft.settings.mixin=Mixin +minecraft.settings.display_name=Minecraft Development +minecraft.settings.creator=Creator +minecraft.settings.creator.repos=Template Repositories: +minecraft.settings.creator.repos.column.name=Name +minecraft.settings.creator.repos.column.provider=Provider +minecraft.settings.creator.repo_config.title={0} Template Repo Configuration +minecraft.settings.creator.repo.default_name=My Repo +minecraft.settings.creator.repo.builtin_name=Built In + +minecraft.settings.lang_template.display_name=Localization Template +minecraft.settings.lang_template.scheme=Scheme: +minecraft.settings.lang_template.project_must_be_selected=You must have selected a project for this! +minecraft.settings.lang_template.comment=You may edit the template used for translation key sorting here.\ +
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.shadow_annotation_same_line=@Shadow annotations on same line +minecraft.settings.mixin=Mixin +minecraft.settings.project.display_name=Project-Specific Settings +minecraft.settings.show_chat_color_gutter_icons=Show chat color gutter icons +minecraft.settings.show_chat_color_underlines=Show chat color underlines +minecraft.settings.show_event_listener_gutter_icons=Show event listener gutter icons +minecraft.settings.show_project_platform_icons=Show project platform icons +minecraft.settings.title=Minecraft Development Settings +minecraft.settings.translation=Translation +minecraft.settings.translation.force_json_translation_file=Force JSON translation file (1.13+) +minecraft.settings.translation.use_custom_convert_template=Use custom template for convert literal to translation + +minecraft.before=Before +minecraft.after=After + +mixinextras.expression.lang.errors.array_access_missing_index=Missing index +mixinextras.expression.lang.errors.array_length_after_empty=Cannot specify array length after an unspecified array length +mixinextras.expression.lang.errors.empty_array_initializer=Array initializer cannot be empty +mixinextras.expression.lang.errors.index_not_expected_in_type=Index not expected in type +mixinextras.expression.lang.errors.instanceof_non_type=Expected type +mixinextras.expression.lang.errors.invalid_number=Invalid number +mixinextras.expression.lang.errors.missing_array_length=Array construction must contain a length +mixinextras.expression.lang.errors.new_array_dim_expr_with_initializer=Cannot use initializer for array with specified length +mixinextras.expression.lang.errors.new_no_constructor_args_or_array=Expected constructor arguments or array creation +mixinextras.expression.lang.errors.unresolved_symbol=Unresolved symbol +mixinextras.expression.lang.errors.unused_definition=Unused definition +mixinextras.expression.lang.errors.unused_symbol.fix=Remove definition + +mixinextras.expression.lang.display_name=MixinExtras Expressions +mixinextras.expression.lang.highlighting.bad_char.display_name=Bad character +mixinextras.expression.lang.highlighting.braces.display_name=Braces +mixinextras.expression.lang.highlighting.brackets.display_name=Brackets +mixinextras.expression.lang.highlighting.call_identifier.display_name=Identifier//Method call +mixinextras.expression.lang.highlighting.capture.display_name=Capture +mixinextras.expression.lang.highlighting.class_name_identifier.display_name=Identifier//Class name +mixinextras.expression.lang.highlighting.comma.display_name=Comma +mixinextras.expression.lang.highlighting.declaration_identifier.display_name=Identifier//Declaration +mixinextras.expression.lang.highlighting.dot.display_name=Dot +mixinextras.expression.lang.highlighting.identifier.display_name=Identifier +mixinextras.expression.lang.highlighting.keyword.display_name=Keyword +mixinextras.expression.lang.highlighting.member_name_identifier.display_name=Identifier//Member name +mixinextras.expression.lang.highlighting.method_reference.display_name=Method reference +mixinextras.expression.lang.highlighting.number.display_name=Number +mixinextras.expression.lang.highlighting.operator.display_name=Operator +mixinextras.expression.lang.highlighting.parens.display_name=Parentheses +mixinextras.expression.lang.highlighting.primitive_type_identifier.display_name=Identifier//Primitive type +mixinextras.expression.lang.highlighting.string.display_name=String +mixinextras.expression.lang.highlighting.string_escape.display_name=String escape +mixinextras.expression.lang.highlighting.type_declaration_identifier.display_name=Identifier//Type declaration +mixinextras.expression.lang.highlighting.variable_identifier.display_name=Identifier//Variable +mixinextras.expression.lang.highlighting.wildcard.display_name=Wildcard + +template.provider.builtin.label=Built In +template.provider.remote.label=Remote +template.provider.local.label=Local +template.provider.zip.label=Archive diff --git a/src/main/resources/messages/MinecraftDevelopment_zh.properties b/src/main/resources/messages/MinecraftDevelopment_zh.properties index 5cac19f5d..c1f1e223e 100644 --- a/src/main/resources/messages/MinecraftDevelopment_zh.properties +++ b/src/main/resources/messages/MinecraftDevelopment_zh.properties @@ -18,7 +18,7 @@ # along with this program. If not, see . # -creator.ui.build_system.label.generic=构建系统: +creator.ui.build_system.label=构建系统: creator.ui.build_system.label.gradle=Gradle creator.ui.build_system.label.maven=Maven @@ -45,7 +45,7 @@ creator.ui.issue_tracker.label=Issue Tracker: creator.ui.update_url.label=更新 URL: creator.ui.depend.label=依赖: creator.ui.soft_depend.label=软依赖: -creator.ui.mixins.label=使用 Mixins: +creator.ui.use_mixins.label=使用 Mixins: creator.ui.parchment.label=Parchment: creator.ui.parchment.include.label=Include: creator.ui.parchment.include.old_mc.label=更旧的 Minecraft 版本 diff --git a/src/test/kotlin/framework/ProjectBuilder.kt b/src/test/kotlin/framework/ProjectBuilder.kt index 45e4dd2ed..07fdc5edf 100644 --- a/src/test/kotlin/framework/ProjectBuilder.kt +++ b/src/test/kotlin/framework/ProjectBuilder.kt @@ -63,6 +63,12 @@ class ProjectBuilder(private val fixture: JavaCodeInsightTestFixture, private va configure: Boolean = true, allowAst: Boolean = false, ) = file(path, code, ".nbtt", configure, allowAst) + fun json( + path: String, + @Language("JSON") code: String, + configure: Boolean = true, + allowAst: Boolean = false, + ) = file(path, code, ".json", configure, allowAst) inline fun dir(path: String, block: ProjectBuilder.() -> Unit) { val oldIntermediatePath = intermediatePath diff --git a/src/test/kotlin/platform/fabric/FabricEntrypointsInspectionTest.kt b/src/test/kotlin/platform/fabric/FabricEntrypointsInspectionTest.kt new file mode 100644 index 000000000..619fc4ad8 --- /dev/null +++ b/src/test/kotlin/platform/fabric/FabricEntrypointsInspectionTest.kt @@ -0,0 +1,264 @@ +/* + * 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.fabric + +import com.demonwav.mcdev.framework.BaseMinecraftTest +import com.demonwav.mcdev.framework.EdtInterceptor +import com.demonwav.mcdev.framework.ProjectBuilder +import com.demonwav.mcdev.framework.createLibrary +import com.demonwav.mcdev.platform.PlatformType +import com.demonwav.mcdev.platform.fabric.inspection.FabricEntrypointsInspection +import com.demonwav.mcdev.util.runWriteTask +import com.intellij.openapi.roots.ModuleRootModificationUtil +import com.intellij.openapi.roots.libraries.Library +import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(EdtInterceptor::class) +@DisplayName("Fabric Entrypoints Inspection Tests") +class FabricEntrypointsInspectionTest : BaseMinecraftTest(PlatformType.FABRIC) { + + private var library: Library? = null + + @BeforeEach + fun initFabric() { + runWriteTask { + library = createLibrary(project, "fabric-loader") + } + + ModuleRootModificationUtil.updateModel(module) { model -> + model.addLibraryEntry(library ?: throw IllegalStateException("Library not created")) + } + } + + @AfterEach + fun cleanupFabric() { + library?.let { l -> + ModuleRootModificationUtil.updateModel(module) { model -> + model.removeOrderEntry( + model.findLibraryOrderEntry(l) ?: throw IllegalStateException("Library not found"), + ) + } + + runWriteTask { + val table = LibraryTablesRegistrar.getInstance().getLibraryTable(project) + table.modifiableModel.let { model -> + model.removeLibrary(l) + model.commit() + } + } + } + } + + private fun doTest(@Language("JSON") json: String, builder: (ProjectBuilder.() -> Unit) = {}) { + buildProject { + java( + "GoodSimpleModInitializer.java", + """ + import net.fabricmc.api.ModInitializer; + + public class GoodSimpleModInitializer implements ModInitializer { + @Override + public void onInitialize() { + } + + public void handle() {} + } + """.trimIndent() + ) + java( + "GoodSimpleClientModInitializer.java", + """ + import net.fabricmc.api.ClientModInitializer; + + public class GoodSimpleClientModInitializer implements ClientModInitializer { + @Override + public void onInitializeClient() { + } + } + """.trimIndent() + ) + java( + "BadSimpleModInitializer.java", + """ + public class BadSimpleModInitializer { + public void handle(String param) {} + } + """.trimIndent() + ) + java( + "BadSimpleClientModInitializer.java", + """ + public class BadSimpleClientModInitializer {} + """.trimIndent() + ) + + builder() + + json("fabric.mod.json", json) + } + + fixture.enableInspections(FabricEntrypointsInspection::class) + fixture.checkHighlighting(false, false, false) + } + + @Test + fun validInitializers() { + doTest( + """ + { + "entrypoints": { + "main": [ + { + "value": "GoodSimpleModInitializer" + }, + "GoodSimpleModInitializer::handle" + ], + "client": [ + "GoodSimpleClientModInitializer" + ] + } + } + """.trimIndent() + ) + } + + @Test + fun invalidInitializers() { + doTest( + """ + { + "entrypoints": { + "main": [ + "GoodSimpleClientModInitializer", + { + "value": "BadSimpleModInitializer" + } + ], + "client": [ + "BadSimpleClientModInitializer", + "GoodSimpleModInitializer" + ] + } + } + """.trimIndent() + ) + } + + @Test + fun missingEmptyConstructor() { + doTest( + """ + { + "entrypoints": { + "main": [ + "BadCtorSimpleModInitializer" + ] + } + } + """.trimIndent() + ) { + java( + "BadCtorSimpleModInitializer.java", + """ + import net.fabricmc.api.ModInitializer; + + public class BadCtorSimpleModInitializer implements ModInitializer { + public BadCtorSimpleModInitializer(String param) {} + } + """.trimIndent() + ) + } + } + + @Test + fun entrypointMethodWithParameter() { + doTest( + """ + { + "entrypoints": { + "main": [ + "BadSimpleModInitializer::handle" + ] + } + } + """.trimIndent() + ) + } + + @Test + fun entrypointInstanceMethodInClassWithNoEmptyCtor() { + doTest( + """ + { + "entrypoints": { + "main": [ + "BadTestInitializer::goodInitialize", + "BadTestInitializer::badInitialize" + ] + } + } + """.trimIndent() + ) { + java( + "BadTestInitializer.java", + """ + public class BadTestInitializer { + public BadTestInitializer(String param) {} + public static void goodInitialize() {} + public void badInitialize() {} + } + """.trimIndent() + ) + } + } + + @Test + fun entrypointFieldInitializers() { + doTest( + """ + { + "entrypoints": { + "main": [ + "ModInitializerContainer::initializer", + "ModInitializerContainer::badTypeInitializer" + ] + } + } + """.trimIndent() + ) { + java( + "ModInitializerContainer.java", + """ + public class ModInitializerContainer { + public static GoodSimpleModInitializer initializer = new GoodSimpleModInitializer(); + public static String badTypeInitializer = "No..."; + } + """.trimIndent() + ) + } + } +} diff --git a/src/test/kotlin/platform/mixin/BaseMixinTest.kt b/src/test/kotlin/platform/mixin/BaseMixinTest.kt index 4b9dbd0ee..caac9bc5d 100644 --- a/src/test/kotlin/platform/mixin/BaseMixinTest.kt +++ b/src/test/kotlin/platform/mixin/BaseMixinTest.kt @@ -34,20 +34,23 @@ import org.junit.jupiter.api.BeforeEach abstract class BaseMixinTest : BaseMinecraftTest(PlatformType.MIXIN) { private var mixinLibrary: Library? = null + private var mixinExtrasLibrary: Library? = null private var testDataLibrary: Library? = null @BeforeEach fun initMixin() { runWriteTask { mixinLibrary = createLibrary(project, "mixin") + mixinExtrasLibrary = createLibrary(project, "mixinextras-common") testDataLibrary = createLibrary(project, "mixin-test-data") } ModuleRootModificationUtil.updateModel(module) { model -> model.addLibraryEntry(mixinLibrary ?: throw IllegalStateException("Mixin library not created")) + model.addLibraryEntry(mixinExtrasLibrary ?: throw IllegalStateException("MixinExtras library not created")) model.addLibraryEntry(testDataLibrary ?: throw IllegalStateException("Test data library not created")) val orderEntries = model.orderEntries - orderEntries.rotate(2) + orderEntries.rotate(3) model.rearrangeOrderEntries(orderEntries) } } diff --git a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt index ae88d95a5..dfa58058f 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt @@ -33,7 +33,7 @@ class InvalidInjectorMethodSignatureFixTest : BaseMixinTest() { private fun doTest(testName: String) { fixture.enableInspections(InvalidInjectorMethodSignatureInspection::class) - testInspectionFix(fixture, "invalidInjectorMethodSignature/$testName", "Fix method parameters") + testInspectionFix(fixture, "invalidInjectorMethodSignature/$testName", "Fix method signature") } @Test diff --git a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt index 769b3894c..ae97fb26e 100644 --- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt +++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt @@ -98,7 +98,7 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() { } @Inject(method = "(Lcom/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureInspection/MixedInOuter;Ljava/lang/String;)V", at = @At("RETURN")) - private void injectCtor(String string, CallbackInfo ci) { + private void injectCtor(String string, CallbackInfo ci) { } } """, @@ -122,7 +122,7 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() { public class TestMixin { @Inject(method = "()V", at = @At("RETURN")) - private void injectCtorWrong(MixedInOuter outer, CallbackInfo ci) { + private void injectCtorWrong(MixedInOuter outer, CallbackInfo ci) { } @Inject(method = "", at = @At("RETURN")) @@ -130,7 +130,7 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() { } @Inject(method = "(Ljava/lang/String;)V", at = @At("RETURN")) - private void injectCtor(MixedInOuter outer, String string, CallbackInfo ci) { + private void injectCtor(MixedInOuter outer, String string, CallbackInfo ci) { } @Inject(method = "(Ljava/lang/String;)V", at = @At("RETURN")) diff --git a/src/test/kotlin/platform/mixin/expression/MEExpressionCompletionTest.kt b/src/test/kotlin/platform/mixin/expression/MEExpressionCompletionTest.kt new file mode 100644 index 000000000..5c12d882d --- /dev/null +++ b/src/test/kotlin/platform/mixin/expression/MEExpressionCompletionTest.kt @@ -0,0 +1,645 @@ +/* + * 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.expression + +import com.demonwav.mcdev.MinecraftProjectSettings +import com.demonwav.mcdev.framework.EdtInterceptor +import com.demonwav.mcdev.platform.mixin.BaseMixinTest +import com.demonwav.mcdev.util.BeforeOrAfter +import com.intellij.codeInsight.lookup.impl.LookupImpl +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.fail +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(EdtInterceptor::class) +@DisplayName("MixinExtras expression completion test") +class MEExpressionCompletionTest : BaseMixinTest() { + private fun assertLookupAppears( + lookupString: String, + @Language("JAVA") code: String, + shouldAppear: Boolean = true + ) { + buildProject { + dir("test") { + java("MEExpressionCompletionTest.java", code) + } + } + + fixture.completeBasic() + + val lookups = fixture.lookupElementStrings + if (lookups != null) { + if (shouldAppear) { + assertTrue(lookupString in lookups) + } else { + assertFalse(lookupString in lookups) + } + } else { + if (shouldAppear) { + assertEquals(lookupString, fixture.elementAtCaret.text) + } else { + assertNotEquals(lookupString, fixture.elementAtCaret.text) + } + } + } + + private fun doBeforeAfterTest( + lookupString: String, + @Language("JAVA") code: String, + @Language("JAVA") expectedAfter: String? + ) { + buildProject { + dir("test") { + java("MEExpressionCompletionTest.java", code) + } + } + + MinecraftProjectSettings.getInstance(fixture.project).definitionPosRelativeToExpression = BeforeOrAfter.BEFORE + + val possibleItems = fixture.completeBasic() + if (possibleItems != null) { + val itemToComplete = possibleItems.firstOrNull { it.lookupString == lookupString } + if (expectedAfter != null) { + assertNotNull(itemToComplete, "Expected a completion matching \"$lookupString\"") + (fixture.lookup as LookupImpl).finishLookup('\n', itemToComplete) + } else { + assertNull(itemToComplete, "Expected no completions matching \"$lookupString\"") + return + } + } else if (expectedAfter == null) { + fail("Expected no completions matching \"$lookupString\"") + return + } + + fixture.checkResult(expectedAfter) + } + + @Test + @DisplayName("Local Variable Implicit Completion Test") + fun localVariableImplicitCompletionTest() { + doBeforeAfterTest( + "one", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import com.llamalad7.mixinextras.sugar.Local; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "one", local = @Local(type = int.class)) + @Expression("one") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + ) + } + + @Test + @DisplayName("Local Variable Ordinal Completion Test") + fun localVariableOrdinalCompletionTest() { + doBeforeAfterTest( + "local1", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import com.llamalad7.mixinextras.sugar.Local; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "local1", local = @Local(type = String.class, ordinal = 0)) + @Expression("local1") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + ) + } + + @Test + @DisplayName("Local Variable Inaccessible Type Completion Test") + fun localVariableInaccessibleTypeCompletionTest() { + doBeforeAfterTest( + "varOfInaccessibleType", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "acceptInaccessibleType", method = "Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData;acceptInaccessibleType(Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData${'$'}InaccessibleType;)V") + @Expression("acceptInaccessibleType()") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import com.llamalad7.mixinextras.sugar.Local; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "acceptInaccessibleType", method = "Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData;acceptInaccessibleType(Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData${'$'}InaccessibleType;)V") + @Definition(id = "varOfInaccessibleType", local = @Local(ordinal = 0)) + @Expression("acceptInaccessibleType(varOfInaccessibleType)") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + ) + } + + @Test + @DisplayName("Field Completion Test") + fun fieldCompletionTest() { + doBeforeAfterTest( + "out", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "out", field = "Ljava/lang/System;out:Ljava/io/PrintStream;") + @Expression("out") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + ) + } + + @Test + @DisplayName("Method Completion Test") + fun methodCompletionTest() { + doBeforeAfterTest( + "acceptInaccessibleType", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "acceptInaccessibleType", method = "Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData;acceptInaccessibleType(Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData${'$'}InaccessibleType;)V") + @Expression("acceptInaccessibleType()") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + ) + } + + @Test + @DisplayName("Method No-Arg Completion Test") + fun methodNoArgCompletionTest() { + doBeforeAfterTest( + "noArgMethod", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "noArgMethod", method = "Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData;noArgMethod()V") + @Expression("noArgMethod()") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + ) + } + + @Test + @DisplayName("Type Completion Test") + fun typeCompletionTest() { + doBeforeAfterTest( + "ArrayList", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("new ") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + import java.util.ArrayList; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "ArrayList", type = ArrayList.class) + @Expression("new ArrayList()") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + ) + } + + @Test + @DisplayName("Inaccessible Type Completion Test") + fun inaccessibleTypeCompletionTest() { + doBeforeAfterTest( + "InaccessibleType", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("new ") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + null, + ) + } + + @Test + @DisplayName("Array Creation Completion Test") + fun arrayCreationCompletionTest() { + doBeforeAfterTest( + "String", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("new ") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent(), + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "String", type = String.class) + @Expression("new String[]") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } + + @Test + @DisplayName("LHS Of Complete Assignment Test") + fun lhsOfCompleteAssignmentTest() { + assertLookupAppears( + "local1", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression(" = 'Hello'") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } + + @Test + @DisplayName("Cast Test") + fun castTest() { + assertLookupAppears( + "Integer", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("()") + @Inject(method = "getStingerCount", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } + + @Test + @DisplayName("Member Function Test") + fun memberFunctionTest() { + assertLookupAppears( + "get", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "Integer", type = Integer.class) + @Definition(id = "synchedData", field = "Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData;synchedData:Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData${'$'}SynchedDataManager;") + @Expression("(Integer) this.synchedData.") + @Inject(method = "getStingerCount", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } + + @Test + @DisplayName("Array Length Test") + fun arrayLengthTest() { + assertLookupAppears( + "one", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "String", type = String.class) + @Expression("new String[]") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } + + @Test + @DisplayName("Array Element Test") + fun arrayElementTest() { + assertLookupAppears( + "local2", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Definition; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Definition(id = "String", type = String.class) + @Expression("new String[]{?, }") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } + + @Test + @DisplayName("Static Method Reference Test") + fun staticMethodReferenceTest() { + assertLookupAppears( + "staticMapper", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("::") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } + + @Test + @DisplayName("Non Static Method Reference Test") + fun nonStaticMethodReferenceTest() { + assertLookupAppears( + "nonStaticMapper", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("this::") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } + + @Test + @DisplayName("Constructor Method Reference Test") + fun constructorMethodReferenceTest() { + assertLookupAppears( + "ConstructedByMethodReference", + """ + package test; + + import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData; + import com.llamalad7.mixinextras.expression.Expression; + import org.spongepowered.asm.mixin.Mixin; + import org.spongepowered.asm.mixin.injection.At; + import org.spongepowered.asm.mixin.injection.Inject; + + @Mixin(MEExpressionTestData.class) + class MEExpressionCompletionTest { + @Expression("") + @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION")) + } + """.trimIndent() + ) + } +} diff --git a/templates b/templates new file mode 160000 index 000000000..c8cf7b83d --- /dev/null +++ b/templates @@ -0,0 +1 @@ +Subproject commit c8cf7b83d9f15903c40e603725318de5bcba85f8