diff --git a/tools/compiler-annotations/src/CheckerConstants.kt b/tools/compiler-annotations/src/CheckerConstants.kt index a45f7f2b8..b8aed8dc3 100644 --- a/tools/compiler-annotations/src/CheckerConstants.kt +++ b/tools/compiler-annotations/src/CheckerConstants.kt @@ -9,12 +9,19 @@ package net.mamoe.mirai.console.compiler.common +import org.intellij.lang.annotations.Language + /** * @suppress 这是内部 API. 可能在任意时刻变动 */ public object CheckerConstants { + @Language("RegExp") + public const val PLUGIN_ID_PATTERN: String = """([a-zA-Z]\w*(?:\.[a-zA-Z]\w*)*)\.([a-zA-Z]\w*(?:-\w+)*)""" + @JvmField - public val PLUGIN_ID_REGEX: Regex = Regex("""([a-zA-Z]\w*(?:\.[a-zA-Z]\w*)*)\.([a-zA-Z]\w*(?:-\w+)*)""") + public val PLUGIN_ID_REGEX: Regex = Regex(PLUGIN_ID_PATTERN) + + @JvmField public val PLUGIN_FORBIDDEN_NAMES: Array = arrayOf("main", "console", "plugin", "config", "data") -} \ No newline at end of file +} diff --git a/tools/intellij-plugin/.gitignore b/tools/intellij-plugin/.gitignore index ab8038013..420965f5b 100644 --- a/tools/intellij-plugin/.gitignore +++ b/tools/intellij-plugin/.gitignore @@ -1 +1,2 @@ -run/idea-sandbox \ No newline at end of file +run/idea-sandbox +!src/creator/build \ No newline at end of file diff --git a/tools/intellij-plugin/build.gradle.kts b/tools/intellij-plugin/build.gradle.kts index 1bf485283..306eb741a 100644 --- a/tools/intellij-plugin/build.gradle.kts +++ b/tools/intellij-plugin/build.gradle.kts @@ -19,21 +19,30 @@ plugins { } repositories { - maven("http://maven.aliyun.com/nexus/content/groups/public/") + maven("https://maven.aliyun.com/repository/public") } version = Versions.console description = "IntelliJ plugin for Mirai Console" +// JVM fails to compile +kotlin.target.compilations.forEach { kotlinCompilation -> + kotlinCompilation.kotlinOptions.freeCompilerArgs += "-Xuse-ir" +} // don't use `useIr()`, compatibility with mirai-console dedicated builds + // See https://github.com/JetBrains/gradle-intellij-plugin/ intellij { version = Versions.intellij isDownloadSources = true updateSinceUntilBuild = false + sandboxDirectory = projectDir.resolve("run/idea-sandbox").absolutePath + setPlugins( "org.jetbrains.kotlin:${Versions.kotlinIntellijPlugin}", // @eap - "java" + "java", + "gradle", + "maven" ) } @@ -53,13 +62,6 @@ fun File.resolveMkdir(relative: String): File { return this.resolve(relative).apply { mkdirs() } } -tasks.withType { - // redirect config and cache files so as not to be cleared by task 'clean' - val ideaSandbox = project.file("run/idea-sandbox") - configDirectory(ideaSandbox.resolveMkdir("config")) - systemDirectory(ideaSandbox.resolveMkdir("system")) -} - tasks.withType { sinceBuild("201.*") untilBuild("215.*") @@ -85,9 +87,17 @@ tasks.withType { dependencies { api(`jetbrains-annotations`) api(`kotlinx-coroutines-jdk8`) + api(`kotlinx-coroutines-swing`) api(project(":mirai-console-compiler-common")) - compileOnly(`kotlin-compiler`) + compileOnly(`kotlin-stdlib-jdk8`) + compileOnly("com.jetbrains:ideaIC:${Versions.intellij}") + // compileOnly(`kotlin-compiler`) + compileOnly(files("libs/ide-common.jar")) + compileOnly(fileTree("build/idea-sandbox/plugins/Kotlin/lib").filter { + !it.name.contains("stdlib") + }) + compileOnly(`kotlin-reflect`) } diff --git a/tools/intellij-plugin/resources/META-INF/plugin.xml b/tools/intellij-plugin/resources/META-INF/plugin.xml index a597d1735..6bff4de9a 100644 --- a/tools/intellij-plugin/resources/META-INF/plugin.xml +++ b/tools/intellij-plugin/resources/META-INF/plugin.xml @@ -1,10 +1,10 @@ @@ -22,7 +22,14 @@ com.intellij.modules.platform org.jetbrains.kotlin + org.jetbrains.idea.maven + com.intellij.gradle + + + + + + + + +

This is a built-in file template used to create a new .gitignore for Gradle projects.

+ + diff --git a/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.ft b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.ft new file mode 100644 index 000000000..3f289e301 --- /dev/null +++ b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.ft @@ -0,0 +1,15 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '$KOTLIN_VERSION' + id 'org.jetbrains.kotlin.plugin.serialization' version '$KOTLIN_VERSION' + + id 'net.mamoe.mirai-console' version '$MIRAI_VERSION' +} + +group = '$GROUP_ID' +version = '$VERSION' + +repositories { + #if ($USE_PROXY_REPO) maven { url 'https://maven.aliyun.com/repository/public' } #end + + mavenCentral() +} diff --git a/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.html b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.html new file mode 100644 index 000000000..50ffe062d --- /dev/null +++ b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.html @@ -0,0 +1,14 @@ + + + + +

This is a built-in file template used to create a new build.gradle for Mirai Console Plugin projects.

+ + diff --git a/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.kts.ft b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.kts.ft new file mode 100644 index 000000000..7d8c41b0b --- /dev/null +++ b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.kts.ft @@ -0,0 +1,16 @@ +plugins { + val kotlinVersion = "$KOTLIN_VERSION" + kotlin("jvm") version kotlinVersion + kotlin("plugin.serialization") version kotlinVersion + + id("net.mamoe.mirai-console") version "$MIRAI_VERSION" +} + +group = "$GROUP_ID" +version = "$VERSION" + +repositories { + #if ($USE_PROXY_REPO) maven("https://maven.aliyun.com/repository/public") #end + + mavenCentral() +} diff --git a/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.kts.html b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.kts.html new file mode 100644 index 000000000..f852dcf1b --- /dev/null +++ b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.kts.html @@ -0,0 +1,14 @@ + + + + +

This is a built-in file template used to create a new build.gradle.kts for Mirai Console Plugin projects.

+ + diff --git a/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Java.java.ft b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Java.java.ft new file mode 100644 index 000000000..8e0564332 --- /dev/null +++ b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Java.java.ft @@ -0,0 +1,27 @@ +package $PACKAGE_NAME; + +import net.mamoe.mirai.console.plugin.jvm.JavaPlugin; +import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescriptionBuilder; + +public final class ${CLASS_NAME} extends JavaPlugin { + public static final ${CLASS_NAME} INSTANCE = new ${CLASS_NAME}(); + + #set($HAS_DETAILS = ${PLUGIN_AUTHOR} != "" || ${PLUGIN_DEPENDS_ON} != "" || ${PLUGIN_INFO} != "" || ${PLUGIN_NAME} != "") + private ${CLASS_NAME}() { + #if($HAS_DETAILS == false) + super(new JvmPluginDescriptionBuilder("$PLUGIN_ID", "$PLUGIN_VERSION").build());#end + #if($HAS_DETAILS) + super(new JvmPluginDescriptionBuilder("$PLUGIN_ID", "$PLUGIN_VERSION") + #if($PLUGIN_NAME != "").name("$PLUGIN_NAME") + #end#if($PLUGIN_INFO != "").info("$PLUGIN_INFO") + #end#if($PLUGIN_AUTHOR != "").author("$PLUGIN_AUTHOR") + #end#if($PLUGIN_DEPENDS_ON != "").dependsOn("$PLUGIN_DEPENDS_ON") + #end + .build());#end + } + + @Override + public void onEnable() { + getLogger().info("Plugin loaded!"); + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Java.java.ft.back b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Java.java.ft.back new file mode 100644 index 000000000..7fd8f4005 --- /dev/null +++ b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Java.java.ft.back @@ -0,0 +1,17 @@ +package $PACKAGE_NAME; + +import net.mamoe.mirai.console.plugin.jvm.JavaPlugin; +import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription; + +public final class ${CLASS_NAME} extends JavaPlugin { + public static final ${CLASS_NAME} INSTANCE = new ${CLASS_NAME}(); + + private ${CLASS_NAME}() { + super(JvmPluginDescription.loadFromResource("plugin.yml", ${CLASS_NAME}.class.getClassLoader())); + } + + @Override + public void onEnable() { + getLogger().info("Plugin loaded!"); + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Java.java.html b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Java.java.html new file mode 100644 index 000000000..74fe05d7c --- /dev/null +++ b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Java.java.html @@ -0,0 +1,14 @@ + + + + +

This is a built-in file template used to create a new plugin main class for Mirai Console Plugin projects.

+ + diff --git a/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Kotlin.kt.ft b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Kotlin.kt.ft new file mode 100644 index 000000000..4d7cca817 --- /dev/null +++ b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Kotlin.kt.ft @@ -0,0 +1,27 @@ +package $PACKAGE_NAME + +import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription +import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin +import net.mamoe.mirai.utils.info +#set($HAS_DETAILS = ${PLUGIN_AUTHOR} != "" || ${PLUGIN_DEPENDS_ON} != "" || ${PLUGIN_INFO} != "") +object $CLASS_NAME : KotlinPlugin( + JvmPluginDescription( + id = "${PLUGIN_ID}", + #if(${PLUGIN_NAME} != "")name = "${PLUGIN_NAME}", +#end + version = "${PLUGIN_VERSION}", + ) #if($HAS_DETAILS){ +#end +#if(${PLUGIN_AUTHOR} != "")author("${PLUGIN_AUTHOR}") +#end +#if(${PLUGIN_DEPENDS_ON} != "")dependsOn("${PLUGIN_DEPENDS_ON}") +#end +#if(${PLUGIN_INFO} != "")info("""${PLUGIN_INFO}""") +#end +#if($HAS_DETAILS) } +#end +) { + override fun onEnable() { + logger.info { "Plugin loaded" } + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Kotlin.kt.html b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Kotlin.kt.html new file mode 100644 index 000000000..74fe05d7c --- /dev/null +++ b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Kotlin.kt.html @@ -0,0 +1,14 @@ + + + + +

This is a built-in file template used to create a new plugin main class for Mirai Console Plugin projects.

+ + diff --git a/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin settings.gradle.kts.ft b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin settings.gradle.kts.ft new file mode 100644 index 000000000..460df44e7 --- /dev/null +++ b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin settings.gradle.kts.ft @@ -0,0 +1 @@ +rootProject.name = "$ARTIFACT_ID" \ No newline at end of file diff --git a/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin settings.gradle.kts.html b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin settings.gradle.kts.html new file mode 100644 index 000000000..eca0260e5 --- /dev/null +++ b/tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin settings.gradle.kts.html @@ -0,0 +1,14 @@ + + + + +

This is a built-in file template used to create a new settings.gradle.kts for Mirai Console Plugin projects.

+ + diff --git a/tools/intellij-plugin/resources/fileTemplates/j2ee/gradle.properties.ft b/tools/intellij-plugin/resources/fileTemplates/j2ee/gradle.properties.ft new file mode 100644 index 000000000..7fc6f1ff2 --- /dev/null +++ b/tools/intellij-plugin/resources/fileTemplates/j2ee/gradle.properties.ft @@ -0,0 +1 @@ +kotlin.code.style=official diff --git a/tools/intellij-plugin/resources/fileTemplates/j2ee/gradle.properties.html b/tools/intellij-plugin/resources/fileTemplates/j2ee/gradle.properties.html new file mode 100644 index 000000000..707aed9d5 --- /dev/null +++ b/tools/intellij-plugin/resources/fileTemplates/j2ee/gradle.properties.html @@ -0,0 +1,14 @@ + + + + +

This is a built-in file template used to create a new gradle.properties for Mirai Console Plugin projects.

+ + diff --git a/tools/intellij-plugin/src/Icons.kt b/tools/intellij-plugin/src/Icons.kt deleted file mode 100644 index 2fbfe6f2d..000000000 --- a/tools/intellij-plugin/src/Icons.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2019-2020 Mamoe Technologies and contributors. - * - * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. - * Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 license that can be found through the following link. - * - * https://github.com/mamoe/mirai/blob/master/LICENSE - */ - -package net.mamoe.mirai.console.intellij - -import com.intellij.openapi.util.IconLoader -import javax.swing.Icon - -object Icons { - val CommandDeclaration: Icon = IconLoader.getIcon("/icons/commandDeclaration.svg", Icons::class.java) - val PluginMainDeclaration: Icon = IconLoader.getIcon("/icons/pluginMainDeclaration.png", Icons::class.java) -} \ No newline at end of file diff --git a/tools/intellij-plugin/src/assets/Assets.kt b/tools/intellij-plugin/src/assets/Assets.kt new file mode 100644 index 000000000..a83ef452a --- /dev/null +++ b/tools/intellij-plugin/src/assets/Assets.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.intellij.assets + +import com.intellij.openapi.util.IconLoader +import javax.swing.Icon + +object Icons { + val CommandDeclaration: Icon = IconLoader.getIcon("/icons/commandDeclaration.svg", Icons::class.java) + val PluginMainDeclaration: Icon = IconLoader.getIcon("/icons/pluginMainDeclaration.png", Icons::class.java) + + val MainIcon: Icon = PluginMainDeclaration +} + +object FT { // file template + const val BuildGradleKts = "Plugin build.gradle.kts" + const val BuildGradle = "Plugin build.gradle" + + const val SettingsGradleKts = "Plugin settings.gradle.kts" + const val SettingsGradle = "Plugin settings.gradle" + + const val GradleProperties = "Gradle gradle.properties" + + const val PluginMainKt = "Plugin main class Kotlin.kt" + const val PluginMainJava = "Plugin main class Java.java" + + const val Gitignore = ".gitignore" +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/assets/FileTemplateRegistrar.kt b/tools/intellij-plugin/src/assets/FileTemplateRegistrar.kt new file mode 100644 index 000000000..1e1f8515d --- /dev/null +++ b/tools/intellij-plugin/src/assets/FileTemplateRegistrar.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + + +package net.mamoe.mirai.console.intellij.assets + +import com.intellij.ide.fileTemplates.FileTemplateDescriptor +import com.intellij.ide.fileTemplates.FileTemplateGroupDescriptor + +class FileTemplateRegistrar : com.intellij.ide.fileTemplates.FileTemplateGroupDescriptorFactory { + override fun getFileTemplatesDescriptor(): FileTemplateGroupDescriptor { + return FileTemplateGroupDescriptor("Mirai", Icons.PluginMainDeclaration).apply { + addTemplate(FileTemplateDescriptor(FT.BuildGradleKts)) + addTemplate(FileTemplateDescriptor(FT.BuildGradle)) + + addTemplate(FileTemplateDescriptor(FT.PluginMainKt)) + addTemplate(FileTemplateDescriptor(FT.PluginMainJava)) + + addTemplate(FileTemplateDescriptor(FT.GradleProperties)) + + addTemplate(FileTemplateDescriptor(FT.SettingsGradleKts)) + addTemplate(FileTemplateDescriptor(FT.SettingsGradle)) + + addTemplate(FileTemplateDescriptor(FT.Gitignore)) + } + } + +} + diff --git a/tools/intellij-plugin/src/creator/MiraiModuleBuilder.kt b/tools/intellij-plugin/src/creator/MiraiModuleBuilder.kt new file mode 100644 index 000000000..95ae3be64 --- /dev/null +++ b/tools/intellij-plugin/src/creator/MiraiModuleBuilder.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + + +package net.mamoe.mirai.console.intellij.creator + +import com.intellij.ide.util.projectWizard.JavaModuleBuilder +import com.intellij.ide.util.projectWizard.ModuleWizardStep +import com.intellij.ide.util.projectWizard.WizardContext +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.module.JavaModuleType +import com.intellij.openapi.module.ModuleType +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.project.DumbAwareRunnable +import com.intellij.openapi.project.DumbService +import com.intellij.openapi.roots.ModifiableRootModel +import com.intellij.openapi.roots.ui.configuration.ModulesProvider +import com.intellij.openapi.startup.StartupManager +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VirtualFile +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import net.mamoe.mirai.console.intellij.assets.Icons +import net.mamoe.mirai.console.intellij.creator.steps.BuildSystemStep +import net.mamoe.mirai.console.intellij.creator.steps.OptionsStep +import net.mamoe.mirai.console.intellij.creator.steps.PluginCoordinatesStep +import net.mamoe.mirai.console.intellij.creator.tasks.CreateProjectTask +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +class MiraiModuleBuilder : JavaModuleBuilder() { + override fun getPresentableName() = MiraiModuleType.NAME + override fun getNodeIcon() = Icons.MainIcon + override fun getGroupName() = MiraiModuleType.NAME + override fun getWeight() = BUILD_SYSTEM_WEIGHT - 1 + override fun getBuilderId() = ID + override fun getModuleType(): ModuleType<*> = JavaModuleType.getModuleType() + override fun getParentGroup() = MiraiModuleType.NAME + + override fun setupRootModel(rootModel: ModifiableRootModel) { + val project = rootModel.project + val (root, vFile) = createAndGetRoot() + rootModel.addContentEntry(vFile) + + if (moduleJdk != null) { + rootModel.sdk = moduleJdk + } else { + rootModel.inheritSdk() + } + + val r = DumbAwareRunnable { + ProgressManager.getInstance().run(CreateProjectTask(root, rootModel.module, model)) + } + + if (project.isDisposed) return + + if ( + ApplicationManager.getApplication().isUnitTestMode || + ApplicationManager.getApplication().isHeadlessEnvironment + ) { + r.run() + return + } + + if (!project.isInitialized) { + StartupManager.getInstance(project).registerPostStartupActivity(r) + return + } + + DumbService.getInstance(project).runWhenSmart(r) + } + + private fun createAndGetRoot(): Pair { + val temp = contentEntryPath ?: throw IllegalStateException("Failed to get content entry path") + + val pathName = FileUtil.toSystemIndependentName(temp) + + val path = Paths.get(pathName) + Files.createDirectories(path) + val vFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(pathName) + ?: throw IllegalStateException("Failed to refresh and file file: $path") + + return path to vFile + } + + private val scope = CoroutineScope(SupervisorJob()) + private val model = MiraiProjectModel.create(scope) + + override fun cleanup() { + super.cleanup() + scope.cancel() + } + + override fun createWizardSteps( + wizardContext: WizardContext, + modulesProvider: ModulesProvider + ): Array { + return arrayOf( + BuildSystemStep(model), + PluginCoordinatesStep(model), + ) + } + + override fun getCustomOptionsStep(context: WizardContext?, parentDisposable: Disposable?): ModuleWizardStep = + OptionsStep() + + companion object { + const val ID = "MIRAI_MODULE" + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/creator/MiraiModuleType.kt b/tools/intellij-plugin/src/creator/MiraiModuleType.kt new file mode 100644 index 000000000..b03d9931c --- /dev/null +++ b/tools/intellij-plugin/src/creator/MiraiModuleType.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + + +package net.mamoe.mirai.console.intellij.creator + +import com.intellij.openapi.module.JavaModuleType +import com.intellij.openapi.module.ModuleTypeManager +import net.mamoe.mirai.console.intellij.assets.Icons + +class MiraiModuleType : JavaModuleType() { + override fun createModuleBuilder() = MiraiModuleBuilder() + override fun getIcon() = Icons.MainIcon + override fun getNodeIcon(isOpened: Boolean) = Icons.MainIcon + override fun getName() = NAME + override fun getDescription() = + "Modules used for developing plugins for Mirai Console" + + companion object { + private const val ID = "MIRAI_MODULE_TYPE" + const val NAME = "Mirai" + + val instance: MiraiModuleType + get() = ModuleTypeManager.getInstance().findByID(ID) as MiraiModuleType + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/creator/MiraiProjectModel.kt b/tools/intellij-plugin/src/creator/MiraiProjectModel.kt new file mode 100644 index 000000000..28193d9a6 --- /dev/null +++ b/tools/intellij-plugin/src/creator/MiraiProjectModel.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + + +package net.mamoe.mirai.console.intellij.creator + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import net.mamoe.mirai.console.intellij.creator.MiraiVersionKind.Companion.getMiraiVersionListAsync +import net.mamoe.mirai.console.intellij.creator.steps.BuildSystemType +import net.mamoe.mirai.console.intellij.creator.steps.LanguageType +import net.mamoe.mirai.console.intellij.creator.tasks.adjustToClassName +import net.mamoe.mirai.console.intellij.creator.tasks.lateinitReadWriteProperty +import kotlin.contracts.contract + +data class ProjectCoordinates( + val groupId: String, // already checked by pattern + val artifactId: String, + val version: String +) { + val packageName: String get() = groupId +} + +data class PluginCoordinates( + val id: String?, + val name: String?, + val author: String?, + val info: String?, + val dependsOn: String?, +) + +class MiraiProjectModel private constructor() { + // STEP: ProjectCreator + + var projectCoordinates: ProjectCoordinates? = null + var buildSystemType: BuildSystemType = BuildSystemType.DEFAULT + var languageType: LanguageType = LanguageType.DEFAULT + + var miraiVersion: String? = null + var pluginCoordinates: PluginCoordinates? = null + + var mainClassQualifiedName: String by lateinitReadWriteProperty { "$packageName.$mainClassSimpleName" } + var mainClassSimpleName: String by lateinitReadWriteProperty { + pluginCoordinates?.run { + name?.adjustToClassName() ?: id?.substringAfterLast('.')?.adjustToClassName() + } ?: "PluginMain" + } + var packageName: String by lateinitReadWriteProperty { projectCoordinates.checkNotNull("projectCoordinates").groupId } + + + var availableMiraiVersions: Deferred>? = null + val availableMiraiVersionsOrFail get() = availableMiraiVersions.checkNotNull("availableMiraiVersions") + + fun checkValuesNotNull() { + checkNotNull(miraiVersion) { "miraiVersion" } + checkNotNull(pluginCoordinates) { "pluginCoordinates" } + checkNotNull(projectCoordinates) { "projectCoordinates" } + } + + companion object { + fun create(scope: CoroutineScope): MiraiProjectModel { + return MiraiProjectModel().apply { + availableMiraiVersions = scope.getMiraiVersionListAsync() + } + } + } + +} + +val MiraiProjectModel.templateProperties: Map + get() { + val projectCoordinates = projectCoordinates!! + val pluginCoordinates = pluginCoordinates!! + return mapOf( + "KOTLIN_VERSION" to KotlinVersion.CURRENT.toString(), + "MIRAI_VERSION" to miraiVersion!!, + "GROUP_ID" to projectCoordinates.groupId, + "VERSION" to projectCoordinates.version, + "USE_PROXY_REPO" to "true", + "ARTIFACT_ID" to projectCoordinates.artifactId, + + "PLUGIN_ID" to pluginCoordinates.id, + "PLUGIN_NAME" to languageType.escapeString(pluginCoordinates.name), + "PLUGIN_AUTHOR" to languageType.escapeString(pluginCoordinates.author), + "PLUGIN_INFO" to languageType.escapeRawString(pluginCoordinates.info), + "PLUGIN_DEPENDS_ON" to pluginCoordinates.dependsOn, + "PLUGIN_VERSION" to projectCoordinates.version, + + "PACKAGE_NAME" to packageName, + "CLASS_NAME" to mainClassSimpleName, + ) + } + +fun T?.checkNotNull(name: String): T { + contract { + returns() implies (this@checkNotNull != null) + } + checkNotNull(this) { + "$name is not yet initialized." + } + return this +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/creator/MiraiVersion.kt b/tools/intellij-plugin/src/creator/MiraiVersion.kt new file mode 100644 index 000000000..711ba0c6e --- /dev/null +++ b/tools/intellij-plugin/src/creator/MiraiVersion.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + + +package net.mamoe.mirai.console.intellij.creator + +import kotlinx.coroutines.* +import org.jsoup.Jsoup + +typealias MiraiVersion = String + +enum class MiraiVersionKind { + Stable { + override fun isThatKind(version: String): Boolean = version matches REGEX_STABLE + }, + Prerelease { + override fun isThatKind(version: String): Boolean = !version.contains("-dev") // && (version.contains("-M") || version.contains("-RC")) + }, + Nightly { + override fun isThatKind(version: String): Boolean = true // version.contains("-dev") + }, ; + + abstract fun isThatKind(version: String): Boolean + + companion object { + val DEFAULT = Stable + + private val REGEX_STABLE = Regex("""^\d+\.\d+(?:\.\d+)?$""") + + private suspend fun getMiraiVersionList(): Set? { + val xml = runInterruptible { + // https://maven.aliyun.com/repository/central/net/mamoe/mirai-core/maven-metadata.xml + // https://repo.maven.apache.org/maven2/net/mamoe/mirai-core/maven-metadata.xml + kotlin.runCatching { + Jsoup.connect("https://maven.aliyun.com/repository/central/net/mamoe/mirai-core/maven-metadata.xml").get() + }.recoverCatching { + Jsoup.connect("https://repo.maven.apache.org/maven2/net/mamoe/mirai-core/maven-metadata.xml").get() + }.getOrNull() + }?.body()?.toString() ?: return null + + return Regex("""\s*(.*?)\s*""").findAll(xml).mapNotNull { it.groupValues[1] }.toSet() + } + + fun CoroutineScope.getMiraiVersionListAsync(): Deferred> { + return async(CoroutineName("getMiraiVersionListAsync")) { + getMiraiVersionList()?: setOf("+") + } + } + } +} + + +/* + + + + + net.mamoe + mirai-core + + 2.5.0-dev-2 + 2.5.0-dev-2 + + 2.4-RC + 2.4-M1-dev-publish-3 + 2.4.0-dev-publish-2 + 2.4.0 + 2.4.1 + 2.4.2 + 2.5-RC-dev-1 + 2.5-M1 + 2.5-M2-dev-2 + 2.5-M2 + 2.5.0-dev-android-1 + 2.5.0-dev-1 + 2.5.0-dev-2 + + 20210319014025 + + + + */ \ No newline at end of file diff --git a/tools/intellij-plugin/src/creator/build/ProjectCreator.kt b/tools/intellij-plugin/src/creator/build/ProjectCreator.kt new file mode 100644 index 000000000..9dd38e8e3 --- /dev/null +++ b/tools/intellij-plugin/src/creator/build/ProjectCreator.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + + +package net.mamoe.mirai.console.intellij.creator.build + +import com.intellij.codeInsight.actions.ReformatCodeProcessor +import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.testFramework.writeChild +import net.mamoe.mirai.console.intellij.assets.FT +import net.mamoe.mirai.console.intellij.creator.MiraiProjectModel +import net.mamoe.mirai.console.intellij.creator.tasks.getTemplate +import net.mamoe.mirai.console.intellij.creator.tasks.invokeAndWait +import net.mamoe.mirai.console.intellij.creator.tasks.runWriteActionAndWait +import net.mamoe.mirai.console.intellij.creator.tasks.writeChild +import net.mamoe.mirai.console.intellij.creator.templateProperties +import org.jetbrains.kotlin.idea.core.util.toPsiFile + +sealed class ProjectCreator( + val module: Module, + val root: VirtualFile, + val model: MiraiProjectModel, +) { + val project get() = module.project + + init { + model.checkValuesNotNull() + } + + protected val filesChanged = mutableListOf() + + @Synchronized + protected fun addFileChanged(vf: VirtualFile) { + filesChanged.add(vf) + } + + protected fun getTemplate(name: String) = project.getTemplate(name, model.templateProperties) + + fun doFinish(indicator: ProgressIndicator) { + indicator.text2 = "Reformatting files" + invokeAndWait { + for (file in filesChanged) { + val psi = file.toPsiFile(project) ?: continue + ReformatCodeProcessor(psi, false).run() + } + } + } + + abstract fun createProject( + module: Module, + root: VirtualFile, + model: MiraiProjectModel, + ) +} + +sealed class GradleProjectCreator( + module: Module, root: VirtualFile, model: MiraiProjectModel, +) : ProjectCreator(module, root, model) { + override fun createProject(module: Module, root: VirtualFile, model: MiraiProjectModel) { + runWriteActionAndWait { + VfsUtil.createDirectoryIfMissing(root, "src/main/${model.languageType.sourceSetDirName}") + VfsUtil.createDirectoryIfMissing(root, "src/main/resources") + filesChanged += root.writeChild(model.languageType.pluginMainClassFile(this)) + filesChanged += root.writeChild("src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin", model.mainClassQualifiedName) + filesChanged += root.writeChild("gradle.properties", getTemplate(FT.GradleProperties)) + } + } +} + +class GradleKotlinProjectCreator( + module: Module, root: VirtualFile, model: MiraiProjectModel, +) : GradleProjectCreator( + module, root, model, +) { + override fun createProject(module: Module, root: VirtualFile, model: MiraiProjectModel) { + super.createProject(module, root, model) + runWriteActionAndWait { + filesChanged += root.writeChild("build.gradle.kts", getTemplate(FT.BuildGradleKts)) + filesChanged += root.writeChild("settings.gradle.kts", getTemplate(FT.SettingsGradleKts)) + } + } +} + +class GradleGroovyProjectCreator( + module: Module, root: VirtualFile, model: MiraiProjectModel, +) : GradleProjectCreator( + module, root, model, +) { + override fun createProject(module: Module, root: VirtualFile, model: MiraiProjectModel) { + super.createProject(module, root, model) + runWriteActionAndWait { + filesChanged += root.writeChild("build.gradle", getTemplate(FT.BuildGradle)) + filesChanged += root.writeChild("settings.gradle", getTemplate(FT.SettingsGradle)) + } + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/creator/steps/BuildSystemStep.form b/tools/intellij-plugin/src/creator/steps/BuildSystemStep.form new file mode 100644 index 000000000..5db60065e --- /dev/null +++ b/tools/intellij-plugin/src/creator/steps/BuildSystemStep.form @@ -0,0 +1,111 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/tools/intellij-plugin/src/creator/steps/BuildSystemStep.kt b/tools/intellij-plugin/src/creator/steps/BuildSystemStep.kt new file mode 100644 index 000000000..f5063de75 --- /dev/null +++ b/tools/intellij-plugin/src/creator/steps/BuildSystemStep.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + + +package net.mamoe.mirai.console.intellij.creator.steps + +import com.intellij.ide.util.projectWizard.ModuleWizardStep +import net.mamoe.mirai.console.intellij.creator.MiraiProjectModel +import net.mamoe.mirai.console.intellij.creator.ProjectCoordinates +import net.mamoe.mirai.console.intellij.creator.tasks.PACKAGE_PATTERN +import net.mamoe.mirai.console.intellij.diagnostics.ContextualParametersChecker.Companion.SEMANTIC_VERSIONING_PATTERN +import javax.swing.JComboBox +import javax.swing.JPanel +import javax.swing.JTextField + +/** + * @see MiraiProjectModel.projectCoordinates + * @see MiraiProjectModel.languageType + * @see MiraiProjectModel.buildSystemType + */ +class BuildSystemStep( + private val model: MiraiProjectModel +) : ModuleWizardStep() { + + private lateinit var panel: JPanel + + @field:Validation.NotBlank("Group ID") + @field:Validation.Pattern("Group ID", PACKAGE_PATTERN) + private lateinit var groupIdField: JTextField + + @field:Validation.NotBlank("Artifact ID") + @field:Validation.Pattern("Artifact ID", PACKAGE_PATTERN) + private lateinit var artifactIdField: JTextField + + @field:Validation.NotBlank("Version") + @field:Validation.Pattern("Version", SEMANTIC_VERSIONING_PATTERN) + private lateinit var versionField: JTextField + + private lateinit var buildSystemBox: JComboBox + private lateinit var languageBox: JComboBox + + override fun getComponent() = panel + + override fun updateStep() { + buildSystemBox.removeAllItems() + buildSystemBox.isEnabled = true + BuildSystemType.values().forEach { buildSystemBox.addItem(it) } + buildSystemBox.selectedItem = BuildSystemType.DEFAULT + buildSystemBox.toolTipText = """ + Gradle Kotlin DSL: build.gradle.kts
+ Gradle Groovy DSL: build.gradle + """.trimIndent() + + languageBox.removeAllItems() + languageBox.isEnabled = true + LanguageType.values().forEach { languageBox.addItem(it) } + languageBox.selectedItem = LanguageType.DEFAULT + buildSystemBox.toolTipText = """ + Language for main class. + """.trimIndent() + } + + override fun updateDataModel() { + model.buildSystemType = this.buildSystemBox.selectedItem as BuildSystemType + model.languageType = this.languageBox.selectedItem as LanguageType + model.projectCoordinates = ProjectCoordinates( + groupId = groupIdField.text, + artifactId = artifactIdField.text, + version = versionField.text + ) + } + + override fun validate() = Validation.doValidation(this) +} diff --git a/tools/intellij-plugin/src/creator/steps/BuildSystemType.kt b/tools/intellij-plugin/src/creator/steps/BuildSystemType.kt new file mode 100644 index 000000000..7ed5585eb --- /dev/null +++ b/tools/intellij-plugin/src/creator/steps/BuildSystemType.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.intellij.creator.steps + +import com.intellij.openapi.module.Module +import com.intellij.openapi.vfs.VirtualFile +import net.mamoe.mirai.console.intellij.creator.MiraiProjectModel +import net.mamoe.mirai.console.intellij.creator.build.GradleGroovyProjectCreator +import net.mamoe.mirai.console.intellij.creator.build.GradleKotlinProjectCreator +import net.mamoe.mirai.console.intellij.creator.build.ProjectCreator + +enum class BuildSystemType { + GradleKt { + override fun createBuildSystem(module: Module, root: VirtualFile, model: MiraiProjectModel): ProjectCreator = + GradleKotlinProjectCreator(module, root, model) + + override fun toString(): String = "Gradle Kotlin DSL" + }, + GradleGroovy { + override fun createBuildSystem(module: Module, root: VirtualFile, model: MiraiProjectModel): ProjectCreator = + GradleGroovyProjectCreator(module, root, model) + + override fun toString(): String = "Gradle Groovy DSL" + }, ; + + abstract fun createBuildSystem(module: Module, root: VirtualFile, model: MiraiProjectModel): ProjectCreator + + companion object { + val DEFAULT = GradleKt + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/creator/steps/LanguageType.kt b/tools/intellij-plugin/src/creator/steps/LanguageType.kt new file mode 100644 index 000000000..8985660fb --- /dev/null +++ b/tools/intellij-plugin/src/creator/steps/LanguageType.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.intellij.creator.steps + +import net.mamoe.mirai.console.intellij.assets.FT +import net.mamoe.mirai.console.intellij.creator.build.ProjectCreator +import net.mamoe.mirai.console.intellij.creator.tasks.getTemplate +import net.mamoe.mirai.console.intellij.creator.templateProperties + +data class NamedFile( + val path: String, + val content: String +) + +interface ILanguageType { + val sourceSetDirName: String + fun pluginMainClassFile(creator: ProjectCreator): NamedFile +} + +sealed class LanguageType : ILanguageType { + @Suppress("UNCHECKED_CAST") + fun escapeString(string: T): T { + string ?: return null as T + return string + .replace("\\", "\\\\") + .replace("\n", "\\n") + .replace("\"", "\\\"") as T + } + abstract fun escapeRawString(string: T): T + + companion object { + val DEFAULT = Kotlin + fun values() = arrayOf(Kotlin, Java) + } + + object Kotlin : LanguageType() { + override fun toString(): String = "Kotlin" // display in UI + override val sourceSetDirName: String get() = "kotlin" + override fun pluginMainClassFile(creator: ProjectCreator): NamedFile = creator.model.run { + return NamedFile( + path = "src/main/kotlin/$mainClassSimpleName.kt", + content = creator.project.getTemplate(FT.PluginMainKt, templateProperties) + ) + } + + @Suppress("UNCHECKED_CAST") + override fun escapeRawString(string: T): T { + string ?: return null as T + return string.replace("$", "\${'\$'}").replace("\n", "\\n") as T + } + } + + object Java : LanguageType() { + override fun toString(): String = "Java" // display in UI + override val sourceSetDirName: String get() = "java" + override fun pluginMainClassFile(creator: ProjectCreator): NamedFile = creator.model.run { + return NamedFile( + path = "src/main/java/${packageName.replace('.', '/')}/$mainClassSimpleName.java", + content = creator.project.getTemplate(FT.PluginMainJava, templateProperties) + ) + } + override fun escapeRawString(string: T): T = escapeString(string) + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/creator/steps/OptionsStep.form b/tools/intellij-plugin/src/creator/steps/OptionsStep.form new file mode 100644 index 000000000..afd117a54 --- /dev/null +++ b/tools/intellij-plugin/src/creator/steps/OptionsStep.form @@ -0,0 +1,12 @@ + +
+ + + + + + + + + +
diff --git a/tools/intellij-plugin/src/creator/steps/OptionsStep.kt b/tools/intellij-plugin/src/creator/steps/OptionsStep.kt new file mode 100644 index 000000000..d11529fd4 --- /dev/null +++ b/tools/intellij-plugin/src/creator/steps/OptionsStep.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + + +package net.mamoe.mirai.console.intellij.creator.steps + +import com.intellij.ide.util.projectWizard.ModuleWizardStep +import javax.swing.JComponent +import javax.swing.JPanel + +class OptionsStep : ModuleWizardStep() { + private lateinit var panel: JPanel + + override fun getComponent(): JComponent { + return panel + } + + override fun updateDataModel() { + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/creator/steps/PluginCoordinatesStep.form b/tools/intellij-plugin/src/creator/steps/PluginCoordinatesStep.form new file mode 100644 index 000000000..0d1c6908c --- /dev/null +++ b/tools/intellij-plugin/src/creator/steps/PluginCoordinatesStep.form @@ -0,0 +1,215 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/tools/intellij-plugin/src/creator/steps/PluginCoordinatesStep.kt b/tools/intellij-plugin/src/creator/steps/PluginCoordinatesStep.kt new file mode 100644 index 000000000..b2ba99134 --- /dev/null +++ b/tools/intellij-plugin/src/creator/steps/PluginCoordinatesStep.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + + +package net.mamoe.mirai.console.intellij.creator.steps + +import com.intellij.ide.util.projectWizard.ModuleWizardStep +import kotlinx.coroutines.* +import net.mamoe.mirai.console.compiler.common.CheckerConstants.PLUGIN_ID_PATTERN +import net.mamoe.mirai.console.intellij.creator.MiraiProjectModel +import net.mamoe.mirai.console.intellij.creator.MiraiVersionKind +import net.mamoe.mirai.console.intellij.creator.PluginCoordinates +import net.mamoe.mirai.console.intellij.creator.checkNotNull +import net.mamoe.mirai.console.intellij.creator.steps.Validation.NotBlank +import net.mamoe.mirai.console.intellij.creator.steps.Validation.Pattern +import net.mamoe.mirai.console.intellij.creator.tasks.QUALIFIED_CLASS_NAME_PATTERN +import net.mamoe.mirai.console.intellij.creator.tasks.adjustToClassName +import net.mamoe.mirai.console.intellij.diagnostics.ContextualParametersChecker +import java.awt.event.ItemEvent +import java.awt.event.ItemListener +import javax.swing.* + +class PluginCoordinatesStep( + private val model: MiraiProjectModel +) : ModuleWizardStep() { + + private lateinit var panel: JPanel + + @field:NotBlank("ID") + @field:Pattern("ID", PLUGIN_ID_PATTERN) + private lateinit var idField: JTextField + + @field:NotBlank("Main class") + @field:Pattern("Main class", QUALIFIED_CLASS_NAME_PATTERN) + private lateinit var mainClassField: JTextField + private lateinit var nameField: JTextField + private lateinit var authorField: JTextField + private lateinit var dependsOnField: JTextField + private lateinit var infoArea: JTextArea + private lateinit var miraiVersionKindBox: JComboBox + + @field:NotBlank("Mirai version") + @field:Pattern("Mirai version", ContextualParametersChecker.SEMANTIC_VERSIONING_PATTERN) + private lateinit var miraiVersionBox: JComboBox + + override fun getComponent() = panel + + private val versionKindChangeListener: ItemListener = ItemListener { event -> + if (event.stateChange != ItemEvent.SELECTED) return@ItemListener + + updateVersionItems() + } + + override fun getPreferredFocusedComponent(): JComponent = idField + + override fun updateStep() { + miraiVersionKindBox.removeAllItems() + miraiVersionKindBox.isEnabled = true + MiraiVersionKind.values().forEach { miraiVersionKindBox.addItem(it) } + miraiVersionKindBox.selectedItem = MiraiVersionKind.DEFAULT + miraiVersionKindBox.addItemListener(versionKindChangeListener) // when selected, change versions + + miraiVersionBox.removeAllItems() + miraiVersionBox.addItem(VERSION_LOADING_PLACEHOLDER) + miraiVersionBox.selectedItem = VERSION_LOADING_PLACEHOLDER + + model.availableMiraiVersionsOrFail.invokeOnCompletion { + updateVersionItems() + } + + if (idField.text.isNullOrEmpty()) { + model.projectCoordinates.checkNotNull("projectCoordinates").run { + idField.text = "$groupId.$artifactId" + } + } + + if (mainClassField.text.isNullOrEmpty()) { + model.projectCoordinates.checkNotNull("projectCoordinates").run { + mainClassField.text = "$groupId.${artifactId.adjustToClassName()}" + } + } + } + + private fun updateVersionItems() { + GlobalScope.launch(Dispatchers.Main + CoroutineName("updateVersionItems")) { + if (!model.availableMiraiVersionsOrFail.isCompleted) return@launch + miraiVersionBox.removeAllItems() + val expectingKind = miraiVersionKindBox.selectedItem as? MiraiVersionKind ?: MiraiVersionKind.DEFAULT + model.availableMiraiVersionsOrFail.await() + .sortedDescending() + .filter { v -> + expectingKind.isThatKind(v) + } + .forEach { v -> miraiVersionBox.addItem(v) } + miraiVersionBox.isEnabled = true + } + } + + override fun updateDataModel() { + model.pluginCoordinates = PluginCoordinates( + id = idField.text.trim(), + author = authorField.text, + name = nameField.text?.trim(), + info = infoArea.text?.trim(), + dependsOn = dependsOnField.text?.trim(), + ) + model.miraiVersion = miraiVersionBox.selectedItem?.toString()?.trim() ?: "+" + model.packageName = mainClassField.text.substringBeforeLast('.') + model.mainClassSimpleName = mainClassField.text.substringAfterLast('.') + model.mainClassQualifiedName = mainClassField.text + } + + override fun validate(): Boolean { + if (miraiVersionBox.selectedItem?.toString() == VERSION_LOADING_PLACEHOLDER) { + Validation.popup("请等待获取版本号", miraiVersionBox) + return false + } + if (!Validation.doValidation(this)) return false + if (!mainClassField.text.contains('.')) { + Validation.popup("Main class 需要包含包名", mainClassField) + return false + } + return true + } + + companion object { + const val VERSION_LOADING_PLACEHOLDER = "Loading..." + } +} diff --git a/tools/intellij-plugin/src/creator/steps/ValidationUtil.kt b/tools/intellij-plugin/src/creator/steps/ValidationUtil.kt new file mode 100644 index 000000000..5d9f354ab --- /dev/null +++ b/tools/intellij-plugin/src/creator/steps/ValidationUtil.kt @@ -0,0 +1,128 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + + +package net.mamoe.mirai.console.intellij.creator.steps + +import com.intellij.ide.util.projectWizard.ModuleWizardStep +import com.intellij.openapi.ui.MessageType +import com.intellij.openapi.ui.popup.Balloon +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.ui.awt.RelativePoint +import net.mamoe.mirai.console.compiler.common.cast +import org.intellij.lang.annotations.Language +import java.lang.reflect.Field +import java.util.concurrent.ConcurrentLinkedQueue +import javax.swing.JComponent +import javax.swing.text.JTextComponent +import kotlin.reflect.KClass +import kotlin.reflect.full.createInstance + + +class Validation { + + annotation class WithValidator(val clazz: KClass>) { + companion object { + init { + registerValidator { annotation, component -> + val instance = annotation.clazz.objectInstance ?: annotation.clazz.createInstance() + instance.validate(annotation, component) + } + } + } + } + + annotation class NotBlank(val tipName: String) { + companion object { + init { + registerValidator { annotation, component -> + if (component.text.isNullOrBlank()) { + report("请填写 ${annotation.tipName}") + } + } + } + } + } + + annotation class Pattern(val tipName: String, @Language("RegExp") val value: String) { + companion object { + init { + registerValidator { annotation, component -> + if (component.text?.matches(Regex(annotation.value)) != true) { + report("请正确填写 ${annotation.tipName}") + } + } + } + } + } + + fun interface Validator { + @Throws(ValidationException::class) + fun ValidationContext.validate(annotation: A, component: JTextComponent) + + @Throws(ValidationException::class) + fun validate(annotation: A, component: JTextComponent) { + ValidationContext.run { validate(annotation, component) } + } + + object ValidationContext { + fun report(message: String): Nothing = throw ValidationException(message) + } + } + + class ValidationException(message: String) : Exception(message) + + companion object { + private data class RegisteredValidator(val type: KClass, val validator: Validator) + + private val validators: MutableCollection> = ConcurrentLinkedQueue() + + private inline fun registerValidator(validator: Validator) { + validators.add(RegisteredValidator(A::class, validator)) + } + + fun popup(message: String, component: JComponent) { + JBPopupFactory.getInstance() + .createHtmlTextBalloonBuilder(message, MessageType.ERROR, null) + .setFadeoutTime(2000) + .createBalloon() + .show(RelativePoint.getSouthWestOf(component), Balloon.Position.below) + } + + /** + * @return `true` if no error + */ + fun doValidation(step: ModuleWizardStep): Boolean { + fun validateProperty(field: Field): Boolean { + field.isAccessible = true + val annotationsToValidate = + validators.associateBy { (type: KClass) -> + field.annotations.find { it::class == type } + } + + for ((annotation, validator) in annotationsToValidate) { + if (annotation == null) continue + val component = field.get(step) as JTextComponent + try { + validator.validator.cast>().validate(annotation, component) + } catch (e: ValidationException) { + popup(e.message ?: e.toString(), component) + return false // report one error only + } + } + return true + } + var result = true + for (prop in step::class.java.declaredFields) { + if (!validateProperty(prop)) result = false + } + return result + } + } +} diff --git a/tools/intellij-plugin/src/creator/tasks/CreateProjectTask.kt b/tools/intellij-plugin/src/creator/tasks/CreateProjectTask.kt new file mode 100644 index 000000000..6a0ce801f --- /dev/null +++ b/tools/intellij-plugin/src/creator/tasks/CreateProjectTask.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + + +package net.mamoe.mirai.console.intellij.creator.tasks + +import com.intellij.ide.ui.UISettings +import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.wm.WindowManager +import com.intellij.openapi.wm.ex.StatusBarEx +import net.mamoe.mirai.console.intellij.creator.MiraiProjectModel +import org.jetbrains.kotlin.idea.util.application.invokeLater +import org.jetbrains.plugins.gradle.service.project.open.linkAndRefreshGradleProject +import java.nio.file.Files +import java.nio.file.Path + +class CreateProjectTask( + private val root: Path, + private val module: Module, + private val model: MiraiProjectModel, +) : Task.Backgroundable(module.project, "Creating project", false) { + override fun shouldStartInBackground() = false + + override fun run(indicator: ProgressIndicator) { + if (module.isDisposed || project.isDisposed) return + + Files.createDirectories(root) + + invokeAndWait { + VfsUtil.markDirtyAndRefresh(false, true, true, root.vf) + } + + val build = model.buildSystemType.createBuildSystem(module, root.vf, model) + + build.createProject(module, root.vf, model) + build.doFinish(indicator) + + invokeLater { + VfsUtil.markDirtyAndRefresh(false, true, true, root.vf) + } + + invokeLater { + @Suppress("UnstableApiUsage") + (linkAndRefreshGradleProject(root.toAbsolutePath().toString(), project)) + showProgress(project) + } + } + +} + +private fun showProgress(project: Project) { + if (!UISettings.instance.showStatusBar || UISettings.instance.presentationMode) { + return + } + + val statusBar = WindowManager.getInstance().getStatusBar(project) as? StatusBarEx ?: return + statusBar.isProcessWindowOpen = true +} diff --git a/tools/intellij-plugin/src/creator/tasks/TaskUtils.kt b/tools/intellij-plugin/src/creator/tasks/TaskUtils.kt new file mode 100644 index 000000000..2b76d5086 --- /dev/null +++ b/tools/intellij-plugin/src/creator/tasks/TaskUtils.kt @@ -0,0 +1,149 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + + +package net.mamoe.mirai.console.intellij.creator.tasks + +import com.intellij.ide.fileTemplates.FileTemplateManager +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.testFramework.writeChild +import net.mamoe.mirai.console.intellij.creator.steps.NamedFile +import org.intellij.lang.annotations.Language +import java.nio.file.Path +import java.util.concurrent.atomic.AtomicReference +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +val Path.vfOrNull: VirtualFile? + get() = LocalFileSystem.getInstance().refreshAndFindFileByPath(this.toAbsolutePath().toString()) + +val Path.vf: VirtualFile + get() = vfOrNull ?: error("Failed to resolve VirtualFile ${this.toAbsolutePath()}") + +fun VirtualFile.readText(): String? = if (this.exists() && !this.isDirectory) String(inputStream.use { it.readBytes() }) else null +fun VirtualFile.readChildText(relative: String): String? = this.resolve(relative)?.readText() + +fun VirtualFile.resolve(relative: String): VirtualFile? = VfsUtil.findRelativeFile( + this, + *relative.replace('\\', '/').split('/').toTypedArray() +) + +fun invokeAndWait(modalityState: ModalityState? = null, runnable: () -> T): T { + val app = ApplicationManager.getApplication() + if (app.isDispatchThread) return runnable() + return computeDelegated { + app.invokeAndWait({ it(runnable()) }, modalityState ?: ModalityState.defaultModalityState()) + } +} + +fun runWriteActionAndWait(modalityState: ModalityState? = null, runnable: () -> T) { + invokeAndWait(modalityState) { + runWriteAction(runnable) + } +} + +@PublishedApi +internal inline fun computeDelegated(executor: (setter: (T) -> Unit) -> Unit): T { + var resultRef: T? = null + executor { resultRef = it } + @Suppress("UNCHECKED_CAST") + return resultRef as T +} + +fun Project.getTemplate( + templateName: String, + properties: Map? = null +): String { + val manager = FileTemplateManager.getInstance(this) + val template = manager.getJ2eeTemplate(templateName) + + val allProperties = manager.defaultProperties + properties?.let { prop -> allProperties.putAll(prop.mapValues { it.value.orEmpty() }) } + + return template.getText(allProperties) +} + +fun Project.getTemplate( + templateName: String, + vararg properties: Pair +): String = getTemplate(templateName, properties.toMap()) + + +fun VirtualFile.writeChild(namedFile: NamedFile): VirtualFile = this.writeChild(namedFile.path, namedFile.content) + +@Language("RegExp") +const val CLASS_NAME_PATTERN = "[a-zA-Z]+[0-9a-zA-Z_]*" // self written + +@Language("RegExp") +const val PACKAGE_PATTERN = """[a-zA-Z]+[0-9a-zA-Z_]*(\.[a-zA-Z]+[0-9a-zA-Z_]*)*""" + +@Language("RegExp") +const val QUALIFIED_CLASS_NAME_PATTERN = """($PACKAGE_PATTERN\.)?$CLASS_NAME_PATTERN""" // self written + +fun String.isValidQualifiedClassName(): Boolean = this matches Regex(QUALIFIED_CLASS_NAME_PATTERN) +fun String.isValidPackageName(): Boolean = this matches Regex(PACKAGE_PATTERN) +fun String.isValidSimpleClassName(): Boolean = this matches Regex(CLASS_NAME_PATTERN) +fun String.adjustToClassName(): String? { + val result = buildString { + var doCapitalization = true + + fun Char.isAllowed() = isLetterOrDigit() || this in "_-" + + for (char in this@adjustToClassName) { + if (!char.isAllowed()) continue + + if (doCapitalization) { + when { + char.isDigit() -> { + if (this.isEmpty()) append('_') + append(char) + } + char.isLetter() -> append(char.toUpperCase()) + char == '-' -> append("_") + else -> append(char) + } + doCapitalization = false + } else { + if (char in "_-") { + doCapitalization = true + } else { + append(char) + } + } + } + } + + if (result.isValidSimpleClassName()) return result + + return null +} + +@Suppress("RedundantNullableReturnType") +private val UNINITIALIZED: Any? = Any() + +@Suppress("UNCHECKED_CAST") +fun lateinitReadWriteProperty(initializer: () -> R) = object : ReadWriteProperty { + private var field = AtomicReference(UNINITIALIZED) + override fun setValue(thisRef: T, property: KProperty<*>, value: R) { + field.set(value) + } + + override tailrec fun getValue(thisRef: T, property: KProperty<*>): R { + val v = field.get() + if (v !== UNINITIALIZED) return v as R + field.compareAndSet(UNINITIALIZED, initializer()) + return getValue(thisRef, property) + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/src/diagnostics/ContextualParametersChecker.kt b/tools/intellij-plugin/src/diagnostics/ContextualParametersChecker.kt index 8cf5b11ba..5226037b5 100644 --- a/tools/intellij-plugin/src/diagnostics/ContextualParametersChecker.kt +++ b/tools/intellij-plugin/src/diagnostics/ContextualParametersChecker.kt @@ -82,11 +82,13 @@ class ContextualParametersChecker : DeclarationChecker { private const val syntax = """类似于 "net.mamoe.mirai.example-plugin", 其中 "net.mamoe.mirai" 为 groupId, "example-plugin" 为插件名""" + const val SEMANTIC_VERSIONING_PATTERN = + """^(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?${'$'}""" + /** * https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string */ - private val SEMANTIC_VERSIONING_REGEX = - Regex("""^(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?${'$'}""") + private val SEMANTIC_VERSIONING_REGEX = Regex(SEMANTIC_VERSIONING_PATTERN) fun checkPluginId(inspectionTarget: KtElement, value: String): Diagnostic? { if (value.isBlank()) return ILLEGAL_PLUGIN_DESCRIPTION.on(inspectionTarget, "插件 Id 不能为空. \n插件 Id$syntax") diff --git a/tools/intellij-plugin/src/line/marker/CommandDeclarationLineMarkerProvider.kt b/tools/intellij-plugin/src/line/marker/CommandDeclarationLineMarkerProvider.kt index 6d1d2d0e6..c4ba064b1 100644 --- a/tools/intellij-plugin/src/line/marker/CommandDeclarationLineMarkerProvider.kt +++ b/tools/intellij-plugin/src/line/marker/CommandDeclarationLineMarkerProvider.kt @@ -14,7 +14,7 @@ import com.intellij.codeInsight.daemon.LineMarkerProvider import com.intellij.openapi.editor.markup.GutterIconRenderer import com.intellij.psi.PsiElement import com.intellij.psi.PsiMethod -import net.mamoe.mirai.console.intellij.Icons +import net.mamoe.mirai.console.intellij.assets.Icons import net.mamoe.mirai.console.intellij.resolve.getElementForLineMark import net.mamoe.mirai.console.intellij.resolve.isSimpleCommandHandlerOrCompositeCommandSubCommand import net.mamoe.mirai.console.intellij.util.runIgnoringErrors diff --git a/tools/intellij-plugin/src/line/marker/PluginMainLineMarkerProvider.kt b/tools/intellij-plugin/src/line/marker/PluginMainLineMarkerProvider.kt index e47761290..01741098e 100644 --- a/tools/intellij-plugin/src/line/marker/PluginMainLineMarkerProvider.kt +++ b/tools/intellij-plugin/src/line/marker/PluginMainLineMarkerProvider.kt @@ -14,7 +14,7 @@ import com.intellij.codeInsight.daemon.LineMarkerProvider import com.intellij.openapi.editor.markup.GutterIconRenderer import com.intellij.psi.PsiElement import net.mamoe.mirai.console.compiler.common.resolve.PLUGIN_FQ_NAME -import net.mamoe.mirai.console.intellij.Icons +import net.mamoe.mirai.console.intellij.assets.Icons import net.mamoe.mirai.console.intellij.resolve.allSuperNames import net.mamoe.mirai.console.intellij.resolve.getElementForLineMark import net.mamoe.mirai.console.intellij.util.runIgnoringErrors diff --git a/tools/intellij-plugin/test/creator/tasks/TaskUtilsKtTest.kt b/tools/intellij-plugin/test/creator/tasks/TaskUtilsKtTest.kt new file mode 100644 index 000000000..571a8afb6 --- /dev/null +++ b/tools/intellij-plugin/test/creator/tasks/TaskUtilsKtTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + +package net.mamoe.mirai.console.intellij.creator.tasks + +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +internal class TaskUtilsKtTest { + + private fun useClassNameCases(mustBeTrue: (String) -> Boolean) { + val success = listOf("A", "A_B", "A0", "A_0", "A_B0") + val failure = listOf("", "0", "_", "-", ".", "/", "A/", "A.", "A.") + + success.forEach { assertEquals(true, mustBeTrue(it), it) } + failure.forEach { assertEquals(false, mustBeTrue(it), it) } + } + + @Test + fun isValidPackageName() { + useClassNameCases { it.isValidPackageName() } + } + + @Test + fun isValidClassName() { + useClassNameCases { it.isValidSimpleClassName() } + } + + @Test + fun adjustToClassName() { + assertEquals("Test", "Test".adjustToClassName()) + assertEquals("TeSt", "Te_st".adjustToClassName()) + assertEquals("TeSt", "Te_St".adjustToClassName()) + assertEquals("TeSt", "Te-st".adjustToClassName()) + assertEquals("TeSt", "Te-St".adjustToClassName()) + + assertEquals("TestAA", "Test//!@#$%^&*()AA".adjustToClassName()) + + assertEquals(null, "0".adjustToClassName()) + assertEquals(null, "_0".adjustToClassName()) + assertEquals(null, "_0A".adjustToClassName()) + assertEquals("A1", "A1".adjustToClassName()) + + assertEquals("A1", "A_1".adjustToClassName()) + assertEquals("A1", "A-1".adjustToClassName()) + + assertEquals("MiraiConsoleExample", "mirai-console-example".adjustToClassName()) + } + + @Test + fun qualifiedClassname() { + useClassNameCases { it.isValidQualifiedClassName() } + assertTrue { "a.b.c".isValidQualifiedClassName() } + } +} \ No newline at end of file diff --git a/tools/intellij-plugin/test/package.kt b/tools/intellij-plugin/test/package.kt new file mode 100644 index 000000000..8b9fb73af --- /dev/null +++ b/tools/intellij-plugin/test/package.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2019-2021 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/master/LICENSE + */ + + +package net.mamoe.mirai.console.intellij \ No newline at end of file