diff --git a/libraries/tools/new-project-wizard/build.gradle.kts b/libraries/tools/new-project-wizard/build.gradle.kts index 26e7ab671fa..e769beb2b54 100644 --- a/libraries/tools/new-project-wizard/build.gradle.kts +++ b/libraries/tools/new-project-wizard/build.gradle.kts @@ -9,11 +9,10 @@ dependencies { api("org.apache.velocity:velocity:1.7") // we have to use the old version as it is the same as bundled into IntelliJ compileOnly(project(":kotlin-reflect-api")) - //needed only for message bundles - implementation(intellijDep()) { includeJars("util") } + implementation(intellijDep()) { includeJars("util") } //needed only for message bundles + testImplementation(intellijDep()) { includeJars("trove4j") } //needed only for message bundles testImplementation(project(":kotlin-test:kotlin-test-junit")) - testImplementation(commonDep("junit:junit")) implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:${property("versions.kotlinx-collections-immutable")}") } diff --git a/libraries/tools/new-project-wizard/src/org/jetbrains/kotlin/tools/projectWizard/moduleConfigurators/MppModuleConfigurator.kt b/libraries/tools/new-project-wizard/src/org/jetbrains/kotlin/tools/projectWizard/moduleConfigurators/MppModuleConfigurator.kt new file mode 100644 index 00000000000..2ca8f94e2ef --- /dev/null +++ b/libraries/tools/new-project-wizard/src/org/jetbrains/kotlin/tools/projectWizard/moduleConfigurators/MppModuleConfigurator.kt @@ -0,0 +1,271 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.tools.projectWizard.moduleConfigurators + +import org.jetbrains.annotations.NonNls +import org.jetbrains.kotlin.tools.projectWizard.KotlinNewProjectWizardBundle +import org.jetbrains.kotlin.tools.projectWizard.core.* +import org.jetbrains.kotlin.tools.projectWizard.core.entity.properties.ModuleConfiguratorProperty +import org.jetbrains.kotlin.tools.projectWizard.ir.buildsystem.KotlinBuildSystemPluginIR +import org.jetbrains.kotlin.tools.projectWizard.plugins.kotlin.ModuleSubType +import org.jetbrains.kotlin.tools.projectWizard.plugins.kotlin.ModulesToIrConversionData +import org.jetbrains.kotlin.tools.projectWizard.plugins.projectPath +import org.jetbrains.kotlin.tools.projectWizard.plugins.templates.TemplatesPlugin +import org.jetbrains.kotlin.tools.projectWizard.settings.JavaPackage +import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.Module +import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.ModuleKind +import org.jetbrains.kotlin.tools.projectWizard.templates.FileTemplate +import org.jetbrains.kotlin.tools.projectWizard.templates.FileTextDescriptor +import java.nio.file.Path + +object MppModuleConfigurator : ModuleConfigurator, + ModuleConfiguratorWithSettings, + ModuleConfiguratorSettings(), + ModuleConfiguratorProperties, + ModuleConfiguratorWithProperties { + + override val moduleKind = ModuleKind.multiplatform + + @NonNls + override val suggestedModuleName = "shared" + + @NonNls + override val id = "multiplatform" + override val text = KotlinNewProjectWizardBundle.message("module.configurator.mpp") + override val canContainSubModules = true + + override fun createKotlinPluginIR(configurationData: ModulesToIrConversionData, module: Module): KotlinBuildSystemPluginIR? = + KotlinBuildSystemPluginIR( + KotlinBuildSystemPluginIR.Type.multiplatform, + version = configurationData.kotlinVersion + ) + + val mppFiles by listProperty() + + override fun getConfiguratorProperties(): List> = + listOf(mppFiles) + + override fun Writer.runArbitraryTask( + configurationData: ModulesToIrConversionData, + module: Module, + modulePath: Path, + ): TaskResult = inContextOfModuleConfigurator(module) { + val mppFiles = mppFiles.reference.propertyValue.distinctBy { it.fileNameWithPackage } // todo merge files with the same name here + mppFiles.mapSequenceIgnore { file -> + module.subModules.mapSequenceIgnore mapTargets@{ target -> + val moduleSubType = //TODO handle for non-simple target configurator + target.configurator.safeAs()?.moduleSubType ?: return@mapTargets UNIT_SUCCESS + val fileTemplate = FileTemplate( + FileTextDescriptor(file.printForModuleSubType(moduleSubType), pathForFileInTarget(modulePath, module, file, target)), + projectPath, + ) + TemplatesPlugin.fileTemplatesToRender.addValues(fileTemplate) + } + } + } + + private fun pathForFileInTarget(mppModulePath: Path, mppModule: Module, file: MppFile, target: Module) = + mppModulePath / + Defaults.SRC_DIR / + "${target.name}Main" / + mppModule.configurator.kotlinDirectoryName / + file.javaPackage?.asPath() / + file.filename +} + + +@DslMarker +annotation class ExpectFileDSL + +data class MppFile( + @NonNls val filename: String, + @NonNls val javaPackage: JavaPackage? = null, + val declarations: List +) { + + val fileNameWithPackage + get() = javaPackage?.let { "${it.asCodePackage()}." }.orEmpty() + filename + + fun printForModuleSubType(moduleSubType: ModuleSubType): String = buildString { + javaPackage?.let { appendLine("package ${it.asCodePackage()}"); appendLine() } + if (moduleSubType == ModuleSubType.common) { + declarations.joinTo(this, separator = "\n\n") { it.printExpect() } + } else { + printImports(declarations.flatMap { it.importsFor(moduleSubType) }) + declarations.joinTo(this, separator = "\n\n") { it.printActualFor(moduleSubType) } + } + }.trim() + + private fun StringBuilder.printImports(imports: List) { + if (imports.isEmpty()) return + imports.distinct().joinTo(this, separator = "\n", prefix = "\n", postfix = "\n\n") { import -> + "import $import" + } + } + + @ExpectFileDSL + class Builder(private val filename: String) { + var `package`: String? = null + private val declarations = mutableListOf() + + fun function(signature: String, init: MppFunction.Builder.() -> Unit = {}) { + declarations += MppFunction.Builder(signature).apply(init).build() + } + + fun `class`(name: String, init: MppClass.Builder.() -> Unit = {}) { + declarations += MppClass.Builder(name).apply(init).build() + } + + fun build() = MppFile(filename, `package`?.let(::JavaPackage), declarations) + } +} + +@ExpectFileDSL +class MppFilesListBuilder { + private val expectFiles = mutableListOf() + + fun mppfile(filename: String, init: MppFile.Builder.() -> Unit) { + expectFiles += MppFile.Builder(filename).apply(init).build() + } + + fun build(): List = expectFiles +} + +fun mppFiles(init: MppFilesListBuilder.() -> Unit): List = + MppFilesListBuilder().apply(init).build() + +sealed class MppDeclaration { + @Suppress("SpellCheckingInspection") + abstract val actuals: Actuals + + abstract fun printExpect(): String + + abstract fun printActualFor(moduleSubType: ModuleSubType): String + + fun importsFor(moduleSubType: ModuleSubType): List = actuals.getBodyFor(moduleSubType).imports + + @ExpectFileDSL + abstract class Builder { + private var defaultActualDeclarationBody = DefaultActualDeclarationBody(text = "", imports = emptyList()) + + @Suppress("SpellCheckingInspection") + private val actuals = mutableMapOf() + + fun default(defaultText: String, init: DefaultActualDeclarationBody.Builder.() -> Unit = {}) { + defaultActualDeclarationBody = DefaultActualDeclarationBody.Builder(defaultText).also(init).build() + } + + fun actualFor(vararg moduleSubTypes: ModuleSubType, actualBody: String, init: ActualDeclarationBody.Builder.() -> Unit = {}) { + moduleSubTypes.forEach { moduleSubType -> + actuals[moduleSubType] = ActualDeclarationBody.Builder(actualBody).apply(init).build() + } + } + + protected fun buildActuals() = Actuals(defaultActualDeclarationBody, actuals) + } +} + +data class MppFunction( + val signature: String, + @Suppress("SpellCheckingInspection") + override val actuals: Actuals, +) : MppDeclaration() { + override fun printExpect(): String = + "expect fun $signature" + + override fun printActualFor(moduleSubType: ModuleSubType): String { + val body = actuals.getBodyFor(moduleSubType) + return buildString { + appendLine("actual fun $signature {") + appendLine(body.text.indented(4)) + append("}") + } + } + + @ExpectFileDSL + class Builder(private val signature: String) : MppDeclaration.Builder() { + fun build(): MppFunction = MppFunction( + signature, + buildActuals(), + ) + } +} + +data class MppClass( + val name: String, + val expectBody: String?, + @Suppress("SpellCheckingInspection") + override val actuals: Actuals, +) : MppDeclaration() { + override fun printExpect(): String = buildString { + append("expect class $name()") + expectBody?.let { body -> + appendLine(" {") + appendLine(body.indented(4)) + append("}") + } + } + + override fun printActualFor(moduleSubType: ModuleSubType): String { + val body = actuals.getBodyFor(moduleSubType) + return buildString { + appendLine("actual class $name actual constructor() {") + appendLine(body.text.indented(4)) + append("}") + } + } + + @ExpectFileDSL + class Builder(private val name: String) : MppDeclaration.Builder() { + var expectBody: String? = null + + fun build(): MppClass = MppClass( + name, + expectBody, + buildActuals(), + ) + } +} + + +private fun String.indented(indentValue: Int): String { + val indent = " ".repeat(indentValue) + return split('\n').joinToString(separator = "\n") { line -> indent + line.trim() } +} + +@Suppress("SpellCheckingInspection") +data class Actuals( + val defaultActualDeclarationBody: DefaultActualDeclarationBody, + val actuals: Map, +) { + fun getBodyFor(moduleSubType: ModuleSubType): DeclarationBody = + actuals[moduleSubType] ?: defaultActualDeclarationBody +} + +sealed class DeclarationBody { + abstract val text: String + abstract val imports: List + + abstract class Builder { + protected val imports = mutableListOf() + + fun import(import: String) { + imports += import + } + } +} + +data class ActualDeclarationBody(override val text: String, override val imports: List) : DeclarationBody() { + class Builder(private val text: String) : DeclarationBody.Builder() { + fun build() = ActualDeclarationBody(text, imports) + } +} + +data class DefaultActualDeclarationBody(override val text: String, override val imports: List) : DeclarationBody() { + class Builder(private val text: String) : DeclarationBody.Builder() { + fun build() = DefaultActualDeclarationBody(text, imports) + } +} \ No newline at end of file diff --git a/libraries/tools/new-project-wizard/src/org/jetbrains/kotlin/tools/projectWizard/moduleConfigurators/configurators.kt b/libraries/tools/new-project-wizard/src/org/jetbrains/kotlin/tools/projectWizard/moduleConfigurators/configurators.kt index 1b63859d984..d5957686fc6 100644 --- a/libraries/tools/new-project-wizard/src/org/jetbrains/kotlin/tools/projectWizard/moduleConfigurators/configurators.kt +++ b/libraries/tools/new-project-wizard/src/org/jetbrains/kotlin/tools/projectWizard/moduleConfigurators/configurators.kt @@ -17,6 +17,7 @@ import org.jetbrains.kotlin.tools.projectWizard.plugins.buildSystem.isGradle import org.jetbrains.kotlin.tools.projectWizard.plugins.kotlin.ModuleType import org.jetbrains.kotlin.tools.projectWizard.plugins.kotlin.ModulesToIrConversionData import org.jetbrains.kotlin.tools.projectWizard.plugins.kotlin.ModuleType +import org.jetbrains.kotlin.tools.projectWizard.plugins.kotlin.ModulesToIrConversionData import org.jetbrains.kotlin.tools.projectWizard.settings.DisplayableSettingItem import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.Module import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.ModuleKind @@ -58,24 +59,6 @@ interface ModuleConfiguratorWithModuleType : ModuleConfigurator { val ModuleConfigurator.moduleType: ModuleType? get() = safeAs()?.moduleType -object MppModuleConfigurator : ModuleConfigurator { - override val moduleKind = ModuleKind.multiplatform - - @NonNls - override val suggestedModuleName = "shared" - - @NonNls - override val id = "multiplatform" - override val text = KotlinNewProjectWizardBundle.message("module.configurator.mpp") - override val canContainSubModules = true - - override fun createKotlinPluginIR(configurationData: ModulesToIrConversionData, module: Module): KotlinBuildSystemPluginIR? = - KotlinBuildSystemPluginIR( - KotlinBuildSystemPluginIR.Type.multiplatform, - version = configurationData.kotlinVersion - ) -} - interface SinglePlatformModuleConfigurator : ModuleConfigurator { val needCreateBuildFile: Boolean get() = true diff --git a/libraries/tools/new-project-wizard/src/org/jetbrains/kotlin/tools/projectWizard/settings/JavaPackage.kt b/libraries/tools/new-project-wizard/src/org/jetbrains/kotlin/tools/projectWizard/settings/JavaPackage.kt index a2c0a76eb24..05535313263 100644 --- a/libraries/tools/new-project-wizard/src/org/jetbrains/kotlin/tools/projectWizard/settings/JavaPackage.kt +++ b/libraries/tools/new-project-wizard/src/org/jetbrains/kotlin/tools/projectWizard/settings/JavaPackage.kt @@ -6,7 +6,7 @@ import org.jetbrains.kotlin.tools.projectWizard.settings.buildsystem.Module import java.nio.file.Paths class JavaPackage(val parts: List) { - constructor(vararg parts: String) : this(parts.toList()) + constructor(`package`: String) : this(`package`.split('.')) fun asPath() = Paths.get(parts.joinToString(separator = "/")) fun asCodePackage() = parts.joinToString(separator = ".") diff --git a/libraries/tools/new-project-wizard/tests/org/jetbrains/kotlin/tools/projectWizard/core/ExpectedFileTest.kt b/libraries/tools/new-project-wizard/tests/org/jetbrains/kotlin/tools/projectWizard/core/ExpectedFileTest.kt new file mode 100644 index 00000000000..11b14df5235 --- /dev/null +++ b/libraries/tools/new-project-wizard/tests/org/jetbrains/kotlin/tools/projectWizard/core/ExpectedFileTest.kt @@ -0,0 +1,138 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.tools.projectWizard.core + +import org.jetbrains.kotlin.tools.projectWizard.moduleConfigurators.mppFiles +import org.jetbrains.kotlin.tools.projectWizard.plugins.kotlin.ModuleSubType +import org.junit.Test +import kotlin.test.assertEquals + +class ExpectedFileTest { + @Test + fun `it should generate valid expect functions`() { + val file = mppFiles { + mppfile("file") { + function("f(x: Int): String") + function("y(x: Int): String") + } + }.first() + val generatedFileText = file.printForModuleSubType(ModuleSubType.common) + val expectedFileText = """ + expect fun f(x: Int): String + + expect fun y(x: Int): String + """.trimIndent().trim() + assertEquals(expectedFileText, generatedFileText) + } + + @Test + fun `it should generate valid expect classes`() { + val file = mppFiles { + mppfile("file") { + `class`("A") + `class`("B") + } + }.first() + val generatedFileText = file.printForModuleSubType(ModuleSubType.common) + val expectedFileText = """ + expected class A expected constructor() + + expected class B expected constructor() + """.trimIndent().trim() + assertEquals(expectedFileText, generatedFileText) + } + + @Test + fun `it should generate valid actual functions`() { + val file = mppFiles { + mppfile("file") { + function("foo()") { + actualFor( + ModuleSubType.jvm, + actualBody = "println(\"jvm\")\nval a = 10" + ) + actualFor(ModuleSubType.js, actualBody = """println("js")""") + default("TODO()") + } + } + }.first() + run { + val generatedFileText = file.printForModuleSubType(ModuleSubType.jvm) + val expectedFileText = """ + actual fun foo() { + println("jvm") + val a = 10 + } + """.trimIndent().trim() + assertEquals(expectedFileText, generatedFileText) + } + + run { + val generatedFileText = file.printForModuleSubType(ModuleSubType.js) + val expectedFileText = """ + actual fun foo() { + println("js") + } + """.trimIndent().trim() + assertEquals(expectedFileText, generatedFileText) + } + run { + val generatedFileText = file.printForModuleSubType(ModuleSubType.android) + val expectedFileText = """ + actual fun foo() { + TODO() + } + """.trimIndent().trim() + assertEquals(expectedFileText, generatedFileText) + } + } + + @Test + fun `it should generate valid actual classes`() { + val file = mppFiles { + mppfile("file") { + `class`("A") { + actualFor( + ModuleSubType.jvm, + actualBody = "val a = 10\n fun x(): String" + ) + actualFor(ModuleSubType.js, actualBody = "val b = 10\n fun y(): String") + default("// nothing here") + } + } + }.first() + run { + val generatedFileText = file.printForModuleSubType(ModuleSubType.jvm) + val expectedFileText = """ + actual class A actual constructor() { + val a = 10 + fun x(): String + } + """.trimIndent().trim() + assertEquals(expectedFileText, generatedFileText) + } + + run { + val generatedFileText = file.printForModuleSubType(ModuleSubType.js) + val expectedFileText = """ + actual class A actual constructor() { + val b = 10 + fun y(): String + } + """.trimIndent().trim() + assertEquals(expectedFileText, generatedFileText) + } + run { + val generatedFileText = file.printForModuleSubType(ModuleSubType.android) + val expectedFileText = """ + actual class A actual constructor() { + // nothing here + } + """.trimIndent().trim() + assertEquals(expectedFileText, generatedFileText) + } + } +} \ No newline at end of file