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 <karlatemp@vip.qq.com>

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

Co-authored-by: Karlatemp <karlatemp@vip.qq.com>

Co-authored-by: Karlatemp <karlatemp@vip.qq.com>
This commit is contained in:
Him188 2021-03-29 18:17:50 +08:00 committed by GitHub
parent 0a0caeeb3a
commit 41d0c16ad1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 2002 additions and 39 deletions

View File

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

View File

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

View File

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

View File

@ -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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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!");
}
}

View File

@ -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!");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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..."
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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