diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt index eb512363..ed1b7cb6 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/application/internal/configureApplication.kt @@ -9,7 +9,6 @@ import org.gradle.api.* import org.gradle.api.file.Directory import org.gradle.api.file.DuplicatesStrategy import org.gradle.api.file.FileCollection -import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.provider.Provider import org.gradle.api.tasks.* import org.gradle.jvm.tasks.Jar @@ -17,9 +16,10 @@ import org.jetbrains.compose.desktop.application.dsl.Application import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.jetbrains.compose.desktop.application.internal.validation.validatePackageVersions import org.jetbrains.compose.desktop.application.tasks.* -import org.jetbrains.compose.desktop.preview.internal.configureConfigureDesktopPreviewTask -import org.jetbrains.compose.desktop.preview.tasks.AbstractConfigureDesktopPreviewTask -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.compose.internal.KOTLIN_JVM_PLUGIN_ID +import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID +import org.jetbrains.compose.internal.javaExt +import org.jetbrains.compose.internal.mppExt import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType import java.io.File import java.util.* @@ -31,10 +31,10 @@ private val defaultJvmArgs = listOf("-Dcompose.application.configure.swing.globa // todo: use workers fun configureApplicationImpl(project: Project, app: Application) { if (app._isDefaultConfigurationEnabled) { - if (project.plugins.hasPlugin("org.jetbrains.kotlin.multiplatform")) { + if (project.plugins.hasPlugin(KOTLIN_MPP_PLUGIN_ID)) { project.configureFromMppPlugin(app) - } else if (project.plugins.hasPlugin("org.jetbrains.kotlin.jvm")) { - val mainSourceSet = project.convention.getPlugin(JavaPluginConvention::class.java).sourceSets.getByName("main") + } else if (project.plugins.hasPlugin(KOTLIN_JVM_PLUGIN_ID)) { + val mainSourceSet = project.javaExt.sourceSets.getByName("main") app.from(mainSourceSet) } } @@ -44,9 +44,8 @@ fun configureApplicationImpl(project: Project, app: Application) { } internal fun Project.configureFromMppPlugin(mainApplication: Application) { - val kotlinExt = extensions.getByType(KotlinMultiplatformExtension::class.java) var isJvmTargetConfigured = false - kotlinExt.targets.all { target -> + mppExt.targets.all { target -> if (target.platformType == KotlinPlatformType.jvm) { if (!isJvmTargetConfigured) { mainApplication.from(target) @@ -159,10 +158,6 @@ internal fun Project.configurePackagingTasks(apps: Collection) { val run = project.tasks.composeTask(taskName("run", app)) { configureRunTask(app) } - - val configureDesktopPreviewTask = project.tasks.composeTask("configureDesktopPreview") { - configureConfigureDesktopPreviewTask(app) - } } } diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/internal/configurePreview.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/internal/configurePreview.kt index bed2520c..ac075059 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/internal/configurePreview.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/desktop/preview/internal/configurePreview.kt @@ -1,19 +1,39 @@ package org.jetbrains.compose.desktop.preview.internal import org.gradle.api.Project -import org.jetbrains.compose.desktop.application.dsl.Application -import org.jetbrains.compose.desktop.application.internal.javaHomeOrDefault -import org.jetbrains.compose.desktop.application.internal.provider +import org.jetbrains.compose.desktop.application.dsl.ConfigurationSource import org.jetbrains.compose.desktop.preview.tasks.AbstractConfigureDesktopPreviewTask +import org.jetbrains.compose.internal.KOTLIN_JVM_PLUGIN_ID +import org.jetbrains.compose.internal.KOTLIN_MPP_PLUGIN_ID +import org.jetbrains.compose.internal.javaExt +import org.jetbrains.compose.internal.mppExt +import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType +import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget fun Project.initializePreview() { + plugins.withId(KOTLIN_MPP_PLUGIN_ID) { + mppExt.targets.all { target -> + if (target.platformType == KotlinPlatformType.jvm) { + val config = ConfigurationSource.KotlinMppTarget(target as KotlinJvmTarget) + registerConfigurePreviewTask(project, config, targetName = target.name) + } + } + } + plugins.withId(KOTLIN_JVM_PLUGIN_ID) { + val config = ConfigurationSource.GradleSourceSet(project.javaExt.sourceSets.getByName("main")) + registerConfigurePreviewTask(project, config) + } } -internal fun AbstractConfigureDesktopPreviewTask.configureConfigureDesktopPreviewTask(app: Application) { - app._configurationSource?.let { configSource -> - dependsOn(configSource.jarTaskName) - previewClasspath = configSource.runtimeClasspath(project) - javaHome.set(provider { app.javaHomeOrDefault() }) - jvmArgs.set(provider { app.jvmArgs }) +private fun registerConfigurePreviewTask(project: Project, config: ConfigurationSource, targetName: String = "") { + project.tasks.register( + previewTaskName(targetName), + AbstractConfigureDesktopPreviewTask::class.java + ) { previewTask -> + previewTask.dependsOn(config.jarTask(project)) + previewTask.previewClasspath = config.runtimeClasspath(project) } -} \ No newline at end of file +} + +private fun previewTaskName(targetName: String) = + "configureDesktopPreview${targetName.capitalize()}" \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/constants.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/constants.kt new file mode 100644 index 00000000..1c8bd903 --- /dev/null +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/constants.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.internal + +internal const val KOTLIN_MPP_PLUGIN_ID = "org.jetbrains.kotlin.multiplatform" +internal const val KOTLIN_JVM_PLUGIN_ID = "org.jetbrains.kotlin.jvm" \ No newline at end of file diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/projectExtensions.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/projectExtensions.kt index 9a09e04f..39578e56 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/projectExtensions.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/internal/projectExtensions.kt @@ -6,6 +6,7 @@ package org.jetbrains.compose.internal import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension import org.jetbrains.compose.ComposeExtension import org.jetbrains.compose.web.WebExtension import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension @@ -16,5 +17,14 @@ internal val Project.composeExt: ComposeExtension? internal val Project.webExt: WebExtension? get() = composeExt?.extensions?.findByType(WebExtension::class.java) -internal val Project.mppExt: KotlinMultiplatformExtension? - get() = extensions.findByType(KotlinMultiplatformExtension::class.java) \ No newline at end of file +internal val Project.mppExt: KotlinMultiplatformExtension + get() = mppExtOrNull ?: error("Could not find KotlinMultiplatformExtension ($project)") + +internal val Project.mppExtOrNull: KotlinMultiplatformExtension? + get() = extensions.findByType(KotlinMultiplatformExtension::class.java) + +internal val Project.javaExt: JavaPluginExtension + get() = javaExtOrNull ?: error("Could not find JavaPluginExtension ($project)") + +internal val Project.javaExtOrNull: JavaPluginExtension? + get() = extensions.findByType(JavaPluginExtension::class.java) diff --git a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/web/WebExtension.kt b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/web/WebExtension.kt index 079fac26..4f91e2b1 100644 --- a/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/web/WebExtension.kt +++ b/gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/web/WebExtension.kt @@ -8,6 +8,7 @@ package org.jetbrains.compose.web import org.gradle.api.Project import org.gradle.api.plugins.ExtensionAware import org.jetbrains.compose.internal.mppExt +import org.jetbrains.compose.internal.mppExtOrNull import org.jetbrains.kotlin.gradle.plugin.KotlinTarget import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget @@ -45,7 +46,7 @@ abstract class WebExtension : ExtensionAware { } private fun defaultJsTargetsToConfigure(project: Project): Set { - val mppTargets = project.mppExt?.targets?.asMap?.values ?: emptySet() + val mppTargets = project.mppExtOrNull?.targets?.asMap?.values ?: emptySet() val jsIRTargets = mppTargets.filterIsInstanceTo(LinkedHashSet()) return if (jsIRTargets.size > 1) { diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/GradlePluginTest.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/GradlePluginTest.kt index 42e0e75c..ee70c984 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/GradlePluginTest.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/gradle/GradlePluginTest.kt @@ -6,11 +6,20 @@ package org.jetbrains.compose.gradle import org.gradle.testkit.runner.TaskOutcome +import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.PreviewLogger +import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.RemoteConnection +import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.receiveConfigFromGradle import org.jetbrains.compose.test.GradlePluginTestBase import org.jetbrains.compose.test.TestKotlinVersion import org.jetbrains.compose.test.TestProjects import org.jetbrains.compose.test.checks import org.junit.jupiter.api.Test +import java.net.ServerSocket +import java.net.Socket +import java.net.SocketTimeoutException +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import kotlin.concurrent.thread class GradlePluginTest : GradlePluginTestBase() { @Test @@ -25,4 +34,103 @@ class GradlePluginTest : GradlePluginTestBase() { check.taskOutcome(":compileKotlinJs", TaskOutcome.SUCCESS) } } + + @Test + fun configurePreview() { + val isAlive = AtomicBoolean(true) + val receivedConfigCount = AtomicInteger(0) + val port = AtomicInteger(-1) + val connectionThread = thread { + val serverSocket = ServerSocket(0).apply { + soTimeout = 10_000 + } + port.set(serverSocket.localPort) + try { + while (isAlive.get()) { + try { + val socket = serverSocket.accept() + val connection = RemoteConnectionImpl(socket, TestPreviewLogger("SERVER")) + val previewConfig = connection.receiveConfigFromGradle() + if (previewConfig != null) { + receivedConfigCount.incrementAndGet() + } + } catch (e: Exception) { + if (!isAlive.get()) break + + if (e !is SocketTimeoutException) { + e.printStackTrace() + throw e + } + } + } + + } finally { + serverSocket.close() + } + } + + val startTimeNs = System.nanoTime() + while (port.get() <= 0) { + val elapsedTimeNs = System.nanoTime() - startTimeNs + val elapsedTimeMs = elapsedTimeNs / 1_000_000L + if (elapsedTimeMs > 10_000) { + error("Server socket initialization timeout!") + } + Thread.sleep(200) + } + + try { + testConfigureDesktopPreivewImpl(port.get()) + } finally { + isAlive.set(false) + connectionThread.interrupt() + connectionThread.join(5000) + } + + val expectedReceivedConfigCount = 2 + val actualReceivedConfigCount = receivedConfigCount.get() + check(actualReceivedConfigCount == 2) { + "Expected to receive $expectedReceivedConfigCount preview configs, got $actualReceivedConfigCount" + } + } + + private fun testConfigureDesktopPreivewImpl(port: Int) { + check(port > 0) { "Invalid port: $port" } + with(testProject(TestProjects.jvmPreview)) { + val portProperty = "-Pcompose.desktop.preview.ide.port=$port" + val previewTargetProperty = "-Pcompose.desktop.preview.target=PreviewKt.ExamplePreview" + val jvmTask = ":jvm:configureDesktopPreview" + gradle(jvmTask, portProperty, previewTargetProperty) + .build() + .checks { check -> + check.taskOutcome(jvmTask, TaskOutcome.SUCCESS) + } + + val mppTask = ":mpp:configureDesktopPreviewDesktop" + gradle(mppTask, portProperty, previewTargetProperty) + .build() + .checks { check -> + check.taskOutcome(mppTask, TaskOutcome.SUCCESS) + } + } + } + + private class TestPreviewLogger(private val prefix: String) : PreviewLogger() { + override val isEnabled: Boolean + get() = true + + override fun log(s: String) { + println("$prefix: $s") + } + } + + private fun RemoteConnectionImpl( + socket: Socket, logger: PreviewLogger + ): RemoteConnection { + val connectionClass = Class.forName("org.jetbrains.compose.desktop.ui.tooling.preview.rpc.RemoteConnectionImpl") + val constructor = connectionClass.constructors.first { + it.parameterCount == 3 + } + return constructor.newInstance(socket, logger, {}) as RemoteConnection + } } \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProjects.kt b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProjects.kt index e831eb1b..1f7f1a67 100644 --- a/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProjects.kt +++ b/gradle-plugins/compose/src/test/kotlin/org/jetbrains/compose/test/TestProjects.kt @@ -18,4 +18,5 @@ object TestProjects { const val defaultArgsOverride = "application/defaultArgsOverride" const val unpackSkiko = "application/unpackSkiko" const val jsMpp = "misc/jsMpp" + const val jvmPreview = "misc/jvmPreview" } \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/build.gradle b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/build.gradle new file mode 100644 index 00000000..dccfb966 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/build.gradle @@ -0,0 +1,9 @@ +subprojects { + repositories { + mavenLocal() + mavenCentral() + maven { + url 'https://maven.pkg.jetbrains.space/public/p/compose/dev' + } + } +} \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/jvm/build.gradle b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/jvm/build.gradle new file mode 100644 index 00000000..75721fd9 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/jvm/build.gradle @@ -0,0 +1,10 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' + id 'org.jetbrains.compose' +} + +dependencies { + implementation 'org.jetbrains.kotlin:kotlin-stdlib' + implementation compose.uiTooling + implementation compose.desktop.currentOs +} \ No newline at end of file diff --git a/idea-plugin/examples/desktop-project/src/main/kotlin/preview.kt b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/jvm/src/main/kotlin/preview.kt similarity index 100% rename from idea-plugin/examples/desktop-project/src/main/kotlin/preview.kt rename to gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/jvm/src/main/kotlin/preview.kt diff --git a/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/build.gradle b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/build.gradle new file mode 100644 index 00000000..884f3528 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'org.jetbrains.kotlin.multiplatform' + id 'org.jetbrains.compose' +} + +kotlin { + jvm('desktop') {} + + sourceSets { + commonMain.dependencies { + api compose.runtime + api compose.foundation + api compose.material + api compose.uiTooling + } + desktopMain.dependencies { + implementation compose.desktop.currentOs + } + } +} diff --git a/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/src/commonMain/kotlin/composable.kt b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/src/commonMain/kotlin/composable.kt new file mode 100644 index 00000000..370b617b --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/src/commonMain/kotlin/composable.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +import androidx.compose.material.Text +import androidx.compose.material.Button +import androidx.compose.runtime.* + +@Composable +fun ExampleComposable() { + var text by remember { mutableStateOf("Hello, World!") } + + Button(onClick = { + text = "Hello, $platformName!" + }) { + Text(text) + } +} + +expect val platformName: String \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/src/desktopMain/kotlin/preview.kt b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/src/desktopMain/kotlin/preview.kt new file mode 100644 index 00000000..d601d043 --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/mpp/src/desktopMain/kotlin/preview.kt @@ -0,0 +1,11 @@ +import androidx.compose.runtime.Composable +import androidx.compose.desktop.ui.tooling.preview.Preview + +@Preview +@Composable +fun ExamplePreview() { + ExampleComposable() +} + +actual val platformName: String + get() = "Desktop" \ No newline at end of file diff --git a/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/settings.gradle b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/settings.gradle new file mode 100644 index 00000000..2db23b4d --- /dev/null +++ b/gradle-plugins/compose/src/test/test-projects/misc/jvmPreview/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + plugins { + id 'org.jetbrains.kotlin.multiplatform' version 'KOTLIN_VERSION_PLACEHOLDER' + id 'org.jetbrains.kotlin.jvm' version 'KOTLIN_VERSION_PLACEHOLDER' + id 'org.jetbrains.compose' version 'COMPOSE_VERSION_PLACEHOLDER' + } + repositories { + mavenLocal() + gradlePluginPortal() + maven { + url 'https://maven.pkg.jetbrains.space/public/p/compose/dev' + } + } +} +rootProject.name = 'jvmPreview' +include(':jvm', ':mpp') \ No newline at end of file diff --git a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemoteConnection.kt b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemoteConnection.kt index 249510f5..692cbc75 100644 --- a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemoteConnection.kt +++ b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemoteConnection.kt @@ -39,6 +39,7 @@ abstract class RemoteConnection : AutoCloseable { } } +// Constructor is also used in GradlePluginTest#configurePreview via reflection internal class RemoteConnectionImpl( private val socket: Socket, private val log: PreviewLogger, diff --git a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/commands.kt b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/commands.kt index 4cfe928c..bd12ec18 100644 --- a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/commands.kt +++ b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/commands.kt @@ -63,7 +63,7 @@ data class ConfigFromGradle( val previewHostConfig: PreviewHostConfig ) -internal fun RemoteConnection.receiveConfigFromGradle(): ConfigFromGradle? { +fun RemoteConnection.receiveConfigFromGradle(): ConfigFromGradle? { var previewClasspath: String? = null var previewFqName: String? = null var previewHostConfig: PreviewHostConfig? = null diff --git a/idea-plugin/examples/desktop-project/README.md b/idea-plugin/examples/desktop-project/README.md deleted file mode 100644 index 64d40add..00000000 --- a/idea-plugin/examples/desktop-project/README.md +++ /dev/null @@ -1,5 +0,0 @@ -1. Run from `idea-plugin`: -``` -./gradlew runIde -``` -2. Open `idea-plugin/examples/desktop-project` with the test IDE. \ No newline at end of file diff --git a/idea-plugin/examples/desktop-project/build.gradle.kts b/idea-plugin/examples/desktop-project/build.gradle.kts deleted file mode 100644 index 7cae07b7..00000000 --- a/idea-plugin/examples/desktop-project/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -import org.jetbrains.compose.compose - -plugins { - // __KOTLIN_COMPOSE_VERSION__ - kotlin("jvm") version "1.5.21" - // __LATEST_COMPOSE_RELEASE_VERSION__ - id("org.jetbrains.compose") version "0.5.0-build262" -} - -repositories { - mavenCentral() - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") -} - -dependencies { - implementation(compose.uiTooling) - implementation(compose.desktop.currentOs) -} - -compose.desktop { - application { - mainClass = "MainKt" - } -} diff --git a/idea-plugin/examples/desktop-project/gradle.properties b/idea-plugin/examples/desktop-project/gradle.properties deleted file mode 100644 index 87ec412e..00000000 --- a/idea-plugin/examples/desktop-project/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -kotlin.code.style=official -#org.gradle.unsafe.configuration-cache=true -#org.gradle.unsafe.configuration-cache-problems=warn diff --git a/idea-plugin/examples/desktop-project/settings.gradle.kts b/idea-plugin/examples/desktop-project/settings.gradle.kts deleted file mode 100644 index 781ae938..00000000 --- a/idea-plugin/examples/desktop-project/settings.gradle.kts +++ /dev/null @@ -1,6 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") - } -} \ No newline at end of file diff --git a/idea-plugin/examples/desktop-project/.gitignore b/idea-plugin/examples/simple-preview-example/.gitignore similarity index 100% rename from idea-plugin/examples/desktop-project/.gitignore rename to idea-plugin/examples/simple-preview-example/.gitignore diff --git a/idea-plugin/examples/simple-preview-example/README.md b/idea-plugin/examples/simple-preview-example/README.md new file mode 100644 index 00000000..e8bca9eb --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/README.md @@ -0,0 +1,5 @@ +1. Run from `idea-plugin`: +``` +./gradlew runIde +``` +2. Open the project with the test IDE. \ No newline at end of file diff --git a/idea-plugin/examples/simple-preview-example/build.gradle.kts b/idea-plugin/examples/simple-preview-example/build.gradle.kts new file mode 100644 index 00000000..7b2e0a3b --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/build.gradle.kts @@ -0,0 +1,21 @@ +buildscript { + repositories { + mavenLocal() + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + } + dependencies { + // __LATEST_COMPOSE_RELEASE_VERSION__ + classpath("org.jetbrains.compose:compose-gradle-plugin:0.5.0-build262") + // __KOTLIN_COMPOSE_VERSION__ + classpath(kotlin("gradle-plugin", version = "1.5.21")) + } +} + +subprojects { + repositories { + mavenLocal() + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + } +} \ No newline at end of file diff --git a/idea-plugin/examples/simple-preview-example/gradle.properties b/idea-plugin/examples/simple-preview-example/gradle.properties new file mode 100644 index 00000000..2c8ed06f --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/gradle.properties @@ -0,0 +1,3 @@ +kotlin.code.style=official +#org.gradle.unsafe.configuration-cache=true +#org.gradle.unsafe.configuration-cache-problems=warn \ No newline at end of file diff --git a/idea-plugin/examples/desktop-project/gradle/wrapper/gradle-wrapper.jar b/idea-plugin/examples/simple-preview-example/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from idea-plugin/examples/desktop-project/gradle/wrapper/gradle-wrapper.jar rename to idea-plugin/examples/simple-preview-example/gradle/wrapper/gradle-wrapper.jar diff --git a/idea-plugin/examples/desktop-project/gradle/wrapper/gradle-wrapper.properties b/idea-plugin/examples/simple-preview-example/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from idea-plugin/examples/desktop-project/gradle/wrapper/gradle-wrapper.properties rename to idea-plugin/examples/simple-preview-example/gradle/wrapper/gradle-wrapper.properties diff --git a/idea-plugin/examples/desktop-project/gradlew b/idea-plugin/examples/simple-preview-example/gradlew similarity index 100% rename from idea-plugin/examples/desktop-project/gradlew rename to idea-plugin/examples/simple-preview-example/gradlew diff --git a/idea-plugin/examples/desktop-project/gradlew.bat b/idea-plugin/examples/simple-preview-example/gradlew.bat similarity index 100% rename from idea-plugin/examples/desktop-project/gradlew.bat rename to idea-plugin/examples/simple-preview-example/gradlew.bat diff --git a/idea-plugin/examples/simple-preview-example/mpp-jvm/build.gradle.kts b/idea-plugin/examples/simple-preview-example/mpp-jvm/build.gradle.kts new file mode 100644 index 00000000..58ca836f --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/mpp-jvm/build.gradle.kts @@ -0,0 +1,26 @@ +import org.jetbrains.compose.compose + +plugins { + kotlin("multiplatform") + id("org.jetbrains.compose") +} + +kotlin { + jvm("desktop") + + sourceSets { + named("commonMain") { + dependencies { + api(compose.runtime) + api(compose.foundation) + api(compose.material) + api(compose.uiTooling) + } + } + named("desktopMain") { + dependencies { + implementation(compose.desktop.currentOs) + } + } + } +} \ No newline at end of file diff --git a/idea-plugin/examples/simple-preview-example/mpp-jvm/src/commonMain/kotlin/App.kt b/idea-plugin/examples/simple-preview-example/mpp-jvm/src/commonMain/kotlin/App.kt new file mode 100644 index 00000000..788fc394 --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/mpp-jvm/src/commonMain/kotlin/App.kt @@ -0,0 +1,15 @@ +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* + +@Composable +fun App() { + MaterialTheme { + Button(onClick = {}) { + Text("Hello, ${getPlatformName()}!") + } + } +} + +expect fun getPlatformName(): String \ No newline at end of file diff --git a/idea-plugin/examples/simple-preview-example/mpp-jvm/src/desktopMain/kotlin/DesktopApp.kt b/idea-plugin/examples/simple-preview-example/mpp-jvm/src/desktopMain/kotlin/DesktopApp.kt new file mode 100644 index 00000000..0be86dcc --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/mpp-jvm/src/desktopMain/kotlin/DesktopApp.kt @@ -0,0 +1,10 @@ +import androidx.compose.runtime.* +import androidx.compose.desktop.ui.tooling.preview.Preview + +actual fun getPlatformName(): String = "Desktop" + +@Preview +@Composable +fun DesktopAppPreview() { + App() +} \ No newline at end of file diff --git a/idea-plugin/examples/simple-preview-example/pure-jvm/build.gradle.kts b/idea-plugin/examples/simple-preview-example/pure-jvm/build.gradle.kts new file mode 100644 index 00000000..22bf3d8f --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/pure-jvm/build.gradle.kts @@ -0,0 +1,12 @@ +import org.jetbrains.compose.compose + +plugins { + kotlin("jvm") + id("org.jetbrains.compose") +} + +dependencies { + implementation(compose.desktop.currentOs) + // todo: remove after update + implementation(compose.uiTooling) +} \ No newline at end of file diff --git a/idea-plugin/examples/simple-preview-example/pure-jvm/src/main/kotlin/preview.kt b/idea-plugin/examples/simple-preview-example/pure-jvm/src/main/kotlin/preview.kt new file mode 100644 index 00000000..9c8d883b --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/pure-jvm/src/main/kotlin/preview.kt @@ -0,0 +1,16 @@ +import androidx.compose.material.Text +import androidx.compose.material.Button +import androidx.compose.runtime.* +import androidx.compose.desktop.ui.tooling.preview.Preview + +@Preview +@Composable +fun ExamplePreview() { + var text by remember { mutableStateOf("Hello, World!") } + + Button(onClick = { + text = "Hello, Desktop!" + }) { + Text(text) + } +} \ No newline at end of file diff --git a/idea-plugin/examples/simple-preview-example/settings.gradle.kts b/idea-plugin/examples/simple-preview-example/settings.gradle.kts new file mode 100644 index 00000000..3e91a0b4 --- /dev/null +++ b/idea-plugin/examples/simple-preview-example/settings.gradle.kts @@ -0,0 +1 @@ +include(":mpp-jvm", ":pure-jvm") \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/ConfigurePreviewTaskNameCache.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/ConfigurePreviewTaskNameCache.kt new file mode 100644 index 00000000..83948e2c --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/ConfigurePreviewTaskNameCache.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2020-2021 JetBrains s.r.o. and respective authors and developers. + * Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. + */ + +package org.jetbrains.compose.desktop.ide.preview + +import com.intellij.openapi.externalSystem.model.DataNode +import com.intellij.openapi.externalSystem.model.ProjectKeys +import com.intellij.openapi.externalSystem.model.project.ModuleData +import com.intellij.openapi.externalSystem.service.project.ProjectDataManager +import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.util.concurrency.annotations.RequiresReadLock +import org.jetbrains.kotlin.idea.configuration.KotlinTargetData +import org.jetbrains.plugins.gradle.settings.GradleSettings +import org.jetbrains.plugins.gradle.util.GradleConstants + +internal val DEFAULT_CONFIGURE_PREVIEW_TASK_NAME = "configureDesktopPreview" + +internal interface ConfigurePreviewTaskNameProvider { + @RequiresReadLock + fun configurePreviewTaskNameOrNull(module: Module): String? +} + +internal class ConfigurePreviewTaskNameProviderImpl : ConfigurePreviewTaskNameProvider { + @RequiresReadLock + override fun configurePreviewTaskNameOrNull(module: Module): String? { + val modulePath = ExternalSystemApiUtil.getExternalProjectPath(module) ?: return null + val moduleNode = moduleDataNodeOrNull(module.project, modulePath) + if (moduleNode != null) { + val target = ExternalSystemApiUtil.getChildren(moduleNode, KotlinTargetData.KEY).singleOrNull() + if (target != null) { + return previewTaskName(target.data.externalName) + } + } + + return null + } + + private fun previewTaskName(targetName: String = "") = + "$DEFAULT_CONFIGURE_PREVIEW_TASK_NAME${targetName.capitalize()}" + + private fun moduleDataNodeOrNull(project: Project, modulePath: String): DataNode? { + val projectDataManager = ProjectDataManager.getInstance() + for (settings in GradleSettings.getInstance(project).linkedProjectsSettings) { + val projectInfo = projectDataManager.getExternalProjectData(project, GradleConstants.SYSTEM_ID, settings.externalProjectPath) + val projectNode = projectInfo?.externalProjectStructure ?: continue + val moduleNodes = ExternalSystemApiUtil.getChildren(projectNode, ProjectKeys.MODULE) + for (moduleNode in moduleNodes) { + val externalProjectPath = moduleNode.data.linkedExternalProjectPath + if (externalProjectPath == modulePath) { + return moduleNode + } + } + } + return null + } +} + +internal class ConfigurePreviewTaskNameCache( + private val provider: ConfigurePreviewTaskNameProvider +) : ConfigurePreviewTaskNameProvider { + private var cachedModuleId: String? = null + private var cachedTaskName: String? = null + + @RequiresReadLock + override fun configurePreviewTaskNameOrNull(module: Module): String? { + val externalProjectPath = ExternalSystemApiUtil.getExternalProjectPath(module) ?: return null + val moduleId = "$externalProjectPath#${module.name}" + + synchronized(this) { + if (moduleId == cachedModuleId) return cachedTaskName + } + + val taskName = provider.configurePreviewTaskNameOrNull(module) + synchronized(this) { + cachedTaskName = taskName + cachedModuleId = moduleId + } + return taskName + } + + fun invalidate() { + synchronized(this) { + cachedModuleId = null + cachedTaskName = null + } + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewActions.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewActions.kt index 1b16ffbb..93851ff3 100644 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewActions.kt +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewActions.kt @@ -57,7 +57,7 @@ private fun buildPreviewViaGradle(project: Project, previewLocation: PreviewLoca val settings = ExternalSystemTaskExecutionSettings() settings.executionName = "Preview: ${previewLocation.fqName}" settings.externalProjectPath = previewLocation.modulePath - settings.taskNames = listOf("configureDesktopPreview") + settings.taskNames = listOf(previewLocation.taskName) settings.vmOptions = gradleVmOptions settings.externalSystemIdString = GradleConstants.SYSTEM_ID.id val previewService = project.service() diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewLocation.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewLocation.kt index efea6c2e..ceb50191 100644 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewLocation.kt +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewLocation.kt @@ -6,22 +6,23 @@ package org.jetbrains.compose.desktop.ide.preview import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil +import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.util.concurrency.annotations.RequiresReadLock -import org.jetbrains.kotlin.idea.util.projectStructure.module +import org.jetbrains.kotlin.idea.debugger.getService import org.jetbrains.kotlin.psi.KtNamedFunction -data class PreviewLocation(val fqName: String, val modulePath: String) +data class PreviewLocation(val fqName: String, val modulePath: String, val taskName: String) @RequiresReadLock internal fun KtNamedFunction.asPreviewFunctionOrNull(): PreviewLocation? { - if (isValidComposablePreviewFunction()) { - val fqName = composePreviewFunctionFqn() - val module = module?.let { ExternalSystemApiUtil.getExternalProjectPath(it) } - if (module != null) { - return PreviewLocation(fqName = fqName, modulePath = module) - } - } + if (!isValidComposablePreviewFunction()) return null - return null + val fqName = composePreviewFunctionFqn() + val module = ProjectFileIndex.getInstance(project).getModuleForFile(containingFile.virtualFile) + if (module == null || module.isDisposed) return null + val service = project.getService() + val previewTaskName = service.configurePreviewTaskNameOrNull(module) ?: DEFAULT_CONFIGURE_PREVIEW_TASK_NAME + val modulePath = ExternalSystemApiUtil.getExternalProjectPath(module) ?: return null + return PreviewLocation(fqName = fqName, modulePath = modulePath, taskName = previewTaskName) } \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewStateService.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewStateService.kt index f683c410..1beba8ab 100644 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewStateService.kt +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewStateService.kt @@ -9,10 +9,15 @@ import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service +import com.intellij.openapi.externalSystem.model.task.* +import com.intellij.openapi.externalSystem.service.notification.ExternalSystemProgressNotificationManager +import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.ui.components.JBLoadingPanel +import com.intellij.util.concurrency.annotations.RequiresReadLock import org.jetbrains.compose.desktop.ui.tooling.preview.rpc.* +import org.jetbrains.kotlin.idea.framework.GRADLE_SYSTEM_ID import javax.swing.JComponent import javax.swing.event.AncestorEvent import javax.swing.event.AncestorListener @@ -23,9 +28,22 @@ class PreviewStateService(private val myProject: Project) : Disposable { private val previewManager: PreviewManager = PreviewManagerImpl(previewListener) val gradleCallbackPort: Int get() = previewManager.gradleCallbackPort + private val configurePreviewTaskNameCache = + ConfigurePreviewTaskNameCache(ConfigurePreviewTaskNameProviderImpl()) + + init { + val projectRefreshListener = ConfigurePreviewTaskNameCacheInvalidator(configurePreviewTaskNameCache) + ExternalSystemProgressNotificationManager.getInstance() + .addNotificationListener(projectRefreshListener, myProject) + } + + @RequiresReadLock + internal fun configurePreviewTaskNameOrNull(module: Module): String? = + configurePreviewTaskNameCache.configurePreviewTaskNameOrNull(module) override fun dispose() { previewManager.close() + configurePreviewTaskNameCache.invalidate() } internal fun registerPreviewPanels( @@ -111,4 +129,21 @@ private class LoadingPanelUpdater(private val panel: JBLoadingPanel) : PreviewLi override fun onRenderedFrame(frame: RenderedFrame) { panel.stopLoading() } +} + +// ExternalSystemTaskNotificationListenerAdapter is used, +// because ExternalSystemTaskNotificationListener interface's API +// was changed between 2020.3 and 2021.1, so a direct implementation +// would not work with both 2020.3 and 2021.1 +private class ConfigurePreviewTaskNameCacheInvalidator( + private val configurePreviewTaskNameCache: ConfigurePreviewTaskNameCache +) : ExternalSystemTaskNotificationListenerAdapter(null) { + override fun onStart(id: ExternalSystemTaskId, workingDir: String?) { + if ( + id.projectSystemId == GRADLE_SYSTEM_ID && + id.type == ExternalSystemTaskType.RESOLVE_PROJECT + ) { + configurePreviewTaskNameCache.invalidate() + } + } } \ No newline at end of file