瀏覽代碼

New Project Wizard (#320)

* Add project wizard

* Add OptionsStep

* Template application fundamentals

* Extract BuildSystemType from BuildSystemStep to top-level

* Complete templates

* Fix build

* Fix build

* Support Java and Groovy, fix strings in Kotlin templates

* Add template for gradle.properties

* Disable `depends on` field

* Fix Java template

* Fix build

* Update tools/compiler-annotations/src/CheckerConstants.kt

Co-authored-by: Karlatemp <[email protected]>

* Update tools/intellij-plugin/src/creator/steps/ValidationUtil.kt

Co-authored-by: Karlatemp <[email protected]>

Co-authored-by: Karlatemp <[email protected]>
Him188 5 年之前
父節點
當前提交
41d0c16ad1
共有 43 個文件被更改,包括 2002 次插入39 次删除
  1. 9 2
      tools/compiler-annotations/src/CheckerConstants.kt
  2. 2 1
      tools/intellij-plugin/.gitignore
  3. 20 10
      tools/intellij-plugin/build.gradle.kts
  4. 11 4
      tools/intellij-plugin/resources/META-INF/plugin.xml
  5. 123 0
      tools/intellij-plugin/resources/fileTemplates/j2ee/.gitignore.ft
  6. 14 0
      tools/intellij-plugin/resources/fileTemplates/j2ee/.gitignore.html
  7. 15 0
      tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.ft
  8. 14 0
      tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.html
  9. 16 0
      tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.kts.ft
  10. 14 0
      tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.kts.html
  11. 27 0
      tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Java.java.ft
  12. 17 0
      tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Java.java.ft.back
  13. 14 0
      tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Java.java.html
  14. 27 0
      tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Kotlin.kt.ft
  15. 14 0
      tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Kotlin.kt.html
  16. 1 0
      tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin settings.gradle.kts.ft
  17. 14 0
      tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin settings.gradle.kts.html
  18. 1 0
      tools/intellij-plugin/resources/fileTemplates/j2ee/gradle.properties.ft
  19. 14 0
      tools/intellij-plugin/resources/fileTemplates/j2ee/gradle.properties.html
  20. 0 18
      tools/intellij-plugin/src/Icons.kt
  21. 35 0
      tools/intellij-plugin/src/assets/Assets.kt
  22. 35 0
      tools/intellij-plugin/src/assets/FileTemplateRegistrar.kt
  23. 120 0
      tools/intellij-plugin/src/creator/MiraiModuleBuilder.kt
  24. 32 0
      tools/intellij-plugin/src/creator/MiraiModuleType.kt
  25. 108 0
      tools/intellij-plugin/src/creator/MiraiProjectModel.kt
  26. 88 0
      tools/intellij-plugin/src/creator/MiraiVersion.kt
  27. 105 0
      tools/intellij-plugin/src/creator/build/ProjectCreator.kt
  28. 111 0
      tools/intellij-plugin/src/creator/steps/BuildSystemStep.form
  29. 80 0
      tools/intellij-plugin/src/creator/steps/BuildSystemStep.kt
  30. 38 0
      tools/intellij-plugin/src/creator/steps/BuildSystemType.kt
  31. 71 0
      tools/intellij-plugin/src/creator/steps/LanguageType.kt
  32. 12 0
      tools/intellij-plugin/src/creator/steps/OptionsStep.form
  33. 26 0
      tools/intellij-plugin/src/creator/steps/OptionsStep.kt
  34. 215 0
      tools/intellij-plugin/src/creator/steps/PluginCoordinatesStep.form
  35. 135 0
      tools/intellij-plugin/src/creator/steps/PluginCoordinatesStep.kt
  36. 128 0
      tools/intellij-plugin/src/creator/steps/ValidationUtil.kt
  37. 68 0
      tools/intellij-plugin/src/creator/tasks/CreateProjectTask.kt
  38. 149 0
      tools/intellij-plugin/src/creator/tasks/TaskUtils.kt
  39. 4 2
      tools/intellij-plugin/src/diagnostics/ContextualParametersChecker.kt
  40. 1 1
      tools/intellij-plugin/src/line/marker/CommandDeclarationLineMarkerProvider.kt
  41. 1 1
      tools/intellij-plugin/src/line/marker/PluginMainLineMarkerProvider.kt
  42. 62 0
      tools/intellij-plugin/test/creator/tasks/TaskUtilsKtTest.kt
  43. 11 0
      tools/intellij-plugin/test/package.kt

+ 9 - 2
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<String> = arrayOf("main", "console", "plugin", "config", "data")
-}
+}

+ 2 - 1
tools/intellij-plugin/.gitignore

@@ -1 +1,2 @@
-run/idea-sandbox
+run/idea-sandbox
+!src/creator/build

+ 20 - 10
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<org.jetbrains.intellij.tasks.RunIdeTask> {
-    // 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<org.jetbrains.intellij.tasks.PatchPluginXmlTask> {
     sinceBuild("201.*")
     untilBuild("215.*")
@@ -85,9 +87,17 @@ tasks.withType<org.jetbrains.intellij.tasks.PatchPluginXmlTask> {
 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`)
 }

+ 11 - 4
tools/intellij-plugin/resources/META-INF/plugin.xml

@@ -1,10 +1,10 @@
 <!--
-  ~ Copyright 2019-2020 Mamoe Technologies and contributors.
+  ~ Copyright 2019-2021 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.
+  ~  此源代码的使用受 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
+  ~  https://github.com/mamoe/mirai/blob/master/LICENSE
   -->
 
 <idea-plugin>
@@ -22,7 +22,14 @@
     <depends>com.intellij.modules.platform</depends>
     <depends>org.jetbrains.kotlin</depends>
 
+    <depends>org.jetbrains.idea.maven</depends>
+    <depends>com.intellij.gradle</depends>
+
     <extensions defaultExtensionNs="com.intellij">
+        <moduleType id="MIRAI_MODULE_TYPE" implementationClass="net.mamoe.mirai.console.intellij.creator.MiraiModuleType"/>
+        <moduleBuilder id="MIRAI_MODULE" builderClass="net.mamoe.mirai.console.intellij.creator.MiraiModuleBuilder"/>
+
+        <fileTemplateGroup implementation="net.mamoe.mirai.console.intellij.assets.FileTemplateRegistrar"/>
         <codeInsight.lineMarkerProvider language="JAVA"
                                         implementationClass="net.mamoe.mirai.console.intellij.line.marker.PluginMainLineMarkerProvider"/>
         <codeInsight.lineMarkerProvider language="kotlin"

+ 123 - 0
tools/intellij-plugin/resources/fileTemplates/j2ee/.gitignore.ft

@@ -0,0 +1,123 @@
+# User-specific stuff
+.idea/
+
+*.iml
+*.ipr
+*.iws
+
+# IntelliJ
+out/
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+### Gradle ###
+.gradle
+build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Cache of project
+.gradletasknamecache
+
+### Gradle Patch ###
+**/build/
+
+# Common working directory
+run/
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar

+ 14 - 0
tools/intellij-plugin/resources/fileTemplates/j2ee/.gitignore.html

@@ -0,0 +1,14 @@
+<!--
+  ~ 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
+  -->
+
+<html>
+<body>
+<p>This is a built-in file template used to create a new .gitignore for Gradle projects.</p>
+</body>
+</html>

+ 15 - 0
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()
+}

+ 14 - 0
tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.html

@@ -0,0 +1,14 @@
+<!--
+  ~ 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
+  -->
+
+<html>
+<body>
+<p>This is a built-in file template used to create a new build.gradle for Mirai Console Plugin projects.</p>
+</body>
+</html>

+ 16 - 0
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()
+}

+ 14 - 0
tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin build.gradle.kts.html

@@ -0,0 +1,14 @@
+<!--
+  ~ 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
+  -->
+
+<html>
+<body>
+<p>This is a built-in file template used to create a new build.gradle.kts for Mirai Console Plugin projects.</p>
+</body>
+</html>

+ 27 - 0
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!");
+    }
+}

+ 17 - 0
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!");
+    }
+}

+ 14 - 0
tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Java.java.html

@@ -0,0 +1,14 @@
+<!--
+  ~ 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
+  -->
+
+<html>
+<body>
+<p>This is a built-in file template used to create a new plugin main class for Mirai Console Plugin projects.</p>
+</body>
+</html>

+ 27 - 0
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" }
+    }
+}

+ 14 - 0
tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin main class Kotlin.kt.html

@@ -0,0 +1,14 @@
+<!--
+  ~ 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
+  -->
+
+<html>
+<body>
+<p>This is a built-in file template used to create a new plugin main class for Mirai Console Plugin projects.</p>
+</body>
+</html>

+ 1 - 0
tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin settings.gradle.kts.ft

@@ -0,0 +1 @@
+rootProject.name = "$ARTIFACT_ID"

+ 14 - 0
tools/intellij-plugin/resources/fileTemplates/j2ee/Plugin settings.gradle.kts.html

@@ -0,0 +1,14 @@
+<!--
+  ~ 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
+  -->
+
+<html>
+<body>
+<p>This is a built-in file template used to create a new settings.gradle.kts for Mirai Console Plugin projects.</p>
+</body>
+</html>

+ 1 - 0
tools/intellij-plugin/resources/fileTemplates/j2ee/gradle.properties.ft

@@ -0,0 +1 @@
+kotlin.code.style=official

+ 14 - 0
tools/intellij-plugin/resources/fileTemplates/j2ee/gradle.properties.html

@@ -0,0 +1,14 @@
+<!--
+  ~ 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
+  -->
+
+<html>
+<body>
+<p>This is a built-in file template used to create a new gradle.properties for Mirai Console Plugin projects.</p>
+</body>
+</html>

+ 0 - 18
tools/intellij-plugin/src/Icons.kt

@@ -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)
-}

+ 35 - 0
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"
+}

+ 35 - 0
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))
+        }
+    }
+
+}
+

+ 120 - 0
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<Path, VirtualFile> {
+        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<ModuleWizardStep> {
+        return arrayOf(
+            BuildSystemStep(model),
+            PluginCoordinatesStep(model),
+        )
+    }
+
+    override fun getCustomOptionsStep(context: WizardContext?, parentDisposable: Disposable?): ModuleWizardStep =
+        OptionsStep()
+
+    companion object {
+        const val ID = "MIRAI_MODULE"
+    }
+}

+ 32 - 0
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 <b>Mirai Console</b>"
+
+    companion object {
+        private const val ID = "MIRAI_MODULE_TYPE"
+        const val NAME = "Mirai"
+
+        val instance: MiraiModuleType
+            get() = ModuleTypeManager.getInstance().findByID(ID) as MiraiModuleType
+    }
+}

+ 108 - 0
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<Set<MiraiVersion>>? = 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<String, String?>
+    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 : Any> T?.checkNotNull(name: String): T {
+    contract {
+        returns() implies (this@checkNotNull != null)
+    }
+    checkNotNull(this) {
+        "$name is not yet initialized."
+    }
+    return this
+}

+ 88 - 0
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<MiraiVersion>? {
+            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("""<version>\s*(.*?)\s*</version>""").findAll(xml).mapNotNull { it.groupValues[1] }.toSet()
+        }
+
+        fun CoroutineScope.getMiraiVersionListAsync(): Deferred<Set<MiraiVersion>> {
+            return async(CoroutineName("getMiraiVersionListAsync")) {
+               getMiraiVersionList()?: setOf("+")
+            }
+        }
+    }
+}
+
+
+/*
+
+<?xml version="1.0" encoding="UTF-8"?>
+
+<metadata>
+  <groupId>net.mamoe</groupId>
+  <artifactId>mirai-core</artifactId>
+  <versioning>
+    <latest>2.5.0-dev-2</latest>
+    <release>2.5.0-dev-2</release>
+    <versions>
+      <version>2.4-RC</version>
+      <version>2.4-M1-dev-publish-3</version>
+      <version>2.4.0-dev-publish-2</version>
+      <version>2.4.0</version>
+      <version>2.4.1</version>
+      <version>2.4.2</version>
+      <version>2.5-RC-dev-1</version>
+      <version>2.5-M1</version>
+      <version>2.5-M2-dev-2</version>
+      <version>2.5-M2</version>
+      <version>2.5.0-dev-android-1</version>
+      <version>2.5.0-dev-1</version>
+      <version>2.5.0-dev-2</version>
+    </versions>
+    <lastUpdated>20210319014025</lastUpdated>
+  </versioning>
+</metadata>
+
+ */

+ 105 - 0
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<VirtualFile>()
+
+    @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))
+        }
+    }
+}

+ 111 - 0
tools/intellij-plugin/src/creator/steps/BuildSystemStep.form

@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="net.mamoe.mirai.console.intellij.creator.steps.BuildSystemStep">
+  <grid id="27dc6" binding="panel" layout-manager="GridLayoutManager" row-count="5" column-count="4" same-size-horizontally="false" same-size-vertically="false" hgap="10" vgap="10">
+    <margin top="10" left="10" bottom="10" right="10"/>
+    <constraints>
+      <xy x="20" y="20" width="589" height="400"/>
+    </constraints>
+    <properties/>
+    <border type="none" title-justification="1" title-position="3"/>
+    <children>
+      <component id="539d6" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="ArtifactId:"/>
+        </properties>
+      </component>
+      <component id="f1b7a" class="javax.swing.JTextField" binding="artifactIdField">
+        <constraints>
+          <grid row="1" column="1" row-span="1" col-span="3" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+            <preferred-size width="150" height="-1"/>
+          </grid>
+        </constraints>
+        <properties>
+          <text value="plugin"/>
+        </properties>
+      </component>
+      <component id="2c1ec" class="javax.swing.JTextField" binding="groupIdField">
+        <constraints>
+          <grid row="0" column="1" row-span="1" col-span="3" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+            <preferred-size width="150" height="-1"/>
+          </grid>
+        </constraints>
+        <properties>
+          <text value="org.example"/>
+        </properties>
+      </component>
+      <component id="2e485" class="javax.swing.JTextField" binding="versionField">
+        <constraints>
+          <grid row="2" column="1" row-span="1" col-span="3" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+            <preferred-size width="150" height="-1"/>
+          </grid>
+        </constraints>
+        <properties>
+          <text value="1.0-SNAPSHOT"/>
+        </properties>
+      </component>
+      <component id="6d341" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="Version:"/>
+        </properties>
+      </component>
+      <vspacer id="3151a">
+        <constraints>
+          <grid row="4" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
+        </constraints>
+      </vspacer>
+      <hspacer id="8d42b">
+        <constraints>
+          <grid row="3" column="1" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false">
+            <preferred-size width="229" height="11"/>
+          </grid>
+        </constraints>
+      </hspacer>
+      <component id="33f22" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="GroupId:"/>
+        </properties>
+      </component>
+      <component id="452df" class="javax.swing.JComboBox" binding="buildSystemBox">
+        <constraints>
+          <grid row="3" column="3" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <model/>
+          <toolTipText value="Build system"/>
+        </properties>
+      </component>
+      <component id="45fb1" class="javax.swing.JComboBox" binding="languageBox">
+        <constraints>
+          <grid row="3" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <model/>
+          <toolTipText value="Language"/>
+        </properties>
+      </component>
+      <component id="303a9" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="4" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value=""/>
+        </properties>
+      </component>
+    </children>
+  </grid>
+  <buttonGroups>
+    <group name="radioButtonGroup">
+      <member id="80d0b"/>
+      <member id="9d2d8"/>
+    </group>
+  </buttonGroups>
+</form>

+ 80 - 0
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<BuildSystemType>
+    private lateinit var languageBox: JComboBox<LanguageType>
+
+    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 <br/>
+            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)
+}

+ 38 - 0
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
+    }
+}

+ 71 - 0
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 <T: String?> escapeString(string: T): T {
+        string ?: return null as T
+        return string
+            .replace("\\", "\\\\")
+            .replace("\n", "\\n")
+            .replace("\"", "\\\"") as T
+    }
+    abstract fun <T: String?> 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 <T : String?> 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 <T : String?> escapeRawString(string: T): T = escapeString(string)
+    }
+}

+ 12 - 0
tools/intellij-plugin/src/creator/steps/OptionsStep.form

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="net.mamoe.mirai.console.intellij.creator.steps.OptionsStep">
+  <grid id="27dc6" binding="panel" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
+    <margin top="0" left="0" bottom="0" right="0"/>
+    <constraints>
+      <xy x="20" y="20" width="500" height="400"/>
+    </constraints>
+    <properties/>
+    <border type="none"/>
+    <children/>
+  </grid>
+</form>

+ 26 - 0
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() {
+    }
+}

+ 215 - 0
tools/intellij-plugin/src/creator/steps/PluginCoordinatesStep.form

@@ -0,0 +1,215 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="net.mamoe.mirai.console.intellij.creator.steps.PluginCoordinatesStep">
+  <grid id="27dc6" binding="panel" layout-manager="GridLayoutManager" row-count="11" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="10" vgap="10">
+    <margin top="10" left="10" bottom="10" right="10"/>
+    <constraints>
+      <xy x="20" y="20" width="531" height="541"/>
+    </constraints>
+    <properties/>
+    <border type="none" title-justification="1" title-position="3"/>
+    <children>
+      <vspacer id="3151a">
+        <constraints>
+          <grid row="10" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
+        </constraints>
+      </vspacer>
+      <component id="303a9" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="10" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value=""/>
+          <visible value="false"/>
+        </properties>
+      </component>
+      <component id="539d6" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="10" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value=""/>
+          <toolTipText value=""/>
+          <visible value="false"/>
+        </properties>
+      </component>
+      <component id="33f22" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="8" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="9" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="Info:"/>
+          <toolTipText value="描述"/>
+        </properties>
+      </component>
+      <vspacer id="b4a73">
+        <constraints>
+          <grid row="9" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
+        </constraints>
+      </vspacer>
+      <component id="980c2" class="javax.swing.JTextArea" binding="infoArea">
+        <constraints>
+          <grid row="8" column="1" row-span="1" col-span="2" vsize-policy="6" hsize-policy="6" anchor="0" fill="3" indent="0" use-parent-layout="false">
+            <preferred-size width="150" height="119"/>
+          </grid>
+        </constraints>
+        <properties>
+          <dragEnabled value="true"/>
+          <text value=""/>
+          <toolTipText value="描述, 可选"/>
+        </properties>
+      </component>
+      <component id="3ca24" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="6" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="Author:"/>
+          <toolTipText value="作者"/>
+        </properties>
+      </component>
+      <component id="45051" class="javax.swing.JTextField" binding="authorField">
+        <constraints>
+          <grid row="6" column="1" row-span="1" col-span="2" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+            <preferred-size width="150" height="-1"/>
+          </grid>
+        </constraints>
+        <properties>
+          <text value=""/>
+          <toolTipText value="作者名称, 可选"/>
+        </properties>
+      </component>
+      <component id="c6d49" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="5" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="Name:"/>
+          <toolTipText value="显示名称"/>
+        </properties>
+      </component>
+      <component id="27fb6" class="javax.swing.JTextField" binding="nameField">
+        <constraints>
+          <grid row="5" column="1" row-span="1" col-span="2" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+            <preferred-size width="150" height="-1"/>
+          </grid>
+        </constraints>
+        <properties>
+          <text value=""/>
+          <toolTipText value="显示名称, 可选"/>
+        </properties>
+      </component>
+      <component id="28e8e" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="7" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="Depends on:"/>
+          <toolTipText value="依赖的插件列表"/>
+        </properties>
+      </component>
+      <component id="44e5c" class="javax.swing.JTextField" binding="dependsOnField">
+        <constraints>
+          <grid row="7" column="1" row-span="1" col-span="2" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+            <preferred-size width="150" height="-1"/>
+          </grid>
+        </constraints>
+        <properties>
+          <enabled value="false"/>
+          <text value=""/>
+          <toolTipText value="依赖的插件列表, 还不支持编辑, 请在创建项目后修改"/>
+        </properties>
+      </component>
+      <component id="53007" class="javax.swing.JSeparator">
+        <constraints>
+          <grid row="1" column="0" row-span="1" col-span="3" vsize-policy="6" hsize-policy="6" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties/>
+      </component>
+      <component id="45fb1" class="javax.swing.JComboBox" binding="miraiVersionBox">
+        <constraints>
+          <grid row="0" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <editable value="true"/>
+          <enabled value="false"/>
+          <model>
+            <item value="Loading..."/>
+          </model>
+        </properties>
+      </component>
+      <component id="6d341" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <opaque value="true"/>
+          <text value="Mirai version:"/>
+        </properties>
+      </component>
+      <component id="452df" class="javax.swing.JComboBox" binding="miraiVersionKindBox">
+        <constraints>
+          <grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <enabled value="false"/>
+          <model>
+            <item value="Stable"/>
+            <item value="Prerelease"/>
+          </model>
+          <toolTipText value="Mirai 版本类型 &lt;br/&gt; Stable: 稳定 Prerelease: -M 和 -RC 测试版" noi18n="true"/>
+        </properties>
+      </component>
+      <component id="6ddd1" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="*ID:"/>
+          <toolTipText value=""/>
+        </properties>
+      </component>
+      <component id="a76eb" class="javax.swing.JTextField" binding="idField">
+        <constraints>
+          <grid row="2" column="1" row-span="1" col-span="2" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+            <preferred-size width="150" height="-1"/>
+          </grid>
+        </constraints>
+        <properties>
+          <text value=""/>
+        </properties>
+      </component>
+      <component id="9ee7e" class="javax.swing.JTextField" binding="mainClassField">
+        <constraints>
+          <grid row="3" column="1" row-span="1" col-span="2" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
+            <preferred-size width="150" height="-1"/>
+          </grid>
+        </constraints>
+        <properties>
+          <text value=""/>
+          <toolTipText value="依赖的插件列表, 可选"/>
+        </properties>
+      </component>
+      <component id="f4c2b" class="javax.swing.JLabel">
+        <constraints>
+          <grid row="3" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties>
+          <text value="*Main class:"/>
+          <toolTipText value="依赖的插件列表"/>
+        </properties>
+      </component>
+      <component id="f7d3f" class="javax.swing.JSeparator">
+        <constraints>
+          <grid row="4" column="0" row-span="1" col-span="3" vsize-policy="6" hsize-policy="6" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
+        </constraints>
+        <properties/>
+      </component>
+    </children>
+  </grid>
+  <buttonGroups>
+    <group name="radioButtonGroup">
+      <member id="80d0b"/>
+      <member id="9d2d8"/>
+    </group>
+  </buttonGroups>
+</form>

+ 135 - 0
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<MiraiVersionKind>
+
+    @field:NotBlank("Mirai version")
+    @field:Pattern("Mirai version", ContextualParametersChecker.SEMANTIC_VERSIONING_PATTERN)
+    private lateinit var miraiVersionBox: JComboBox<String>
+
+    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..."
+    }
+}

+ 128 - 0
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<out Validator<WithValidator>>) {
+        companion object {
+            init {
+                registerValidator<WithValidator> { annotation, component ->
+                    val instance = annotation.clazz.objectInstance ?: annotation.clazz.createInstance()
+                    instance.validate(annotation, component)
+                }
+            }
+        }
+    }
+
+    annotation class NotBlank(val tipName: String) {
+        companion object {
+            init {
+                registerValidator<NotBlank> { annotation, component ->
+                    if (component.text.isNullOrBlank()) {
+                        report("请填写 ${annotation.tipName}")
+                    }
+                }
+            }
+        }
+    }
+
+    annotation class Pattern(val tipName: String, @Language("RegExp") val value: String) {
+        companion object {
+            init {
+                registerValidator<Pattern> { annotation, component ->
+                    if (component.text?.matches(Regex(annotation.value)) != true) {
+                        report("请正确填写 ${annotation.tipName}")
+                    }
+                }
+            }
+        }
+    }
+
+    fun interface Validator<in A : Annotation> {
+        @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<A : Annotation>(val type: KClass<A>, val validator: Validator<A>)
+
+        private val validators: MutableCollection<RegisteredValidator<*>> = ConcurrentLinkedQueue()
+
+        private inline fun <reified A : Annotation> registerValidator(validator: Validator<A>) {
+            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<out Annotation>) ->
+                        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<Validator<Annotation>>().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
+        }
+    }
+}

+ 68 - 0
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
+}

+ 149 - 0
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 <T> 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 <T> runWriteActionAndWait(modalityState: ModalityState? = null, runnable: () -> T) {
+    invokeAndWait(modalityState) {
+        runWriteAction(runnable)
+    }
+}
+
+@PublishedApi
+internal inline fun <T> 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<String, String?>? = 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, String?>
+): 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 <T, R> lateinitReadWriteProperty(initializer: () -> R) = object : ReadWriteProperty<T, R> {
+    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)
+    }
+}

+ 4 - 2
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")

+ 1 - 1
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

+ 1 - 1
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

+ 62 - 0
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() }
+    }
+}

+ 11 - 0
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