diff --git a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemotePreviewHost.kt b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemotePreviewHost.kt index 9f5d6a5318..cd02d330ae 100644 --- a/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemotePreviewHost.kt +++ b/gradle-plugins/preview-rpc/src/main/kotlin/org/jetbrains/compose/desktop/ui/tooling/preview/rpc/RemotePreviewHost.kt @@ -62,7 +62,6 @@ internal class PreviewHost(private val log: PreviewLogger, connection: RemoteCon try { val classpath = previewClasspath.get() val request = previewRequest.get() - log { "request != null == ${request != null} && classpath != null == ${classpath != null}" } if (classpath != null && request != null) { if (previewRequest.compareAndSet(request, null)) { val bytes = renderFrame(classpath, request) diff --git a/idea-plugin/examples/desktop-project/build.gradle.kts b/idea-plugin/examples/desktop-project/build.gradle.kts index 7c929bd8ca..7cae07b726 100644 --- a/idea-plugin/examples/desktop-project/build.gradle.kts +++ b/idea-plugin/examples/desktop-project/build.gradle.kts @@ -2,9 +2,9 @@ import org.jetbrains.compose.compose plugins { // __KOTLIN_COMPOSE_VERSION__ - kotlin("jvm") version "1.5.10" + kotlin("jvm") version "1.5.21" // __LATEST_COMPOSE_RELEASE_VERSION__ - id("org.jetbrains.compose") version "0.5.0-build229" + id("org.jetbrains.compose") version "0.5.0-build262" } repositories { 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 new file mode 100644 index 0000000000..1b16ffbb91 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewActions.kt @@ -0,0 +1,92 @@ +/* + * 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.execution.executors.DefaultRunExecutor +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.components.service +import com.intellij.openapi.externalSystem.model.execution.ExternalSystemTaskExecutionSettings +import com.intellij.openapi.externalSystem.service.execution.ProgressExecutionMode +import com.intellij.openapi.externalSystem.task.TaskCallback +import com.intellij.openapi.externalSystem.util.ExternalSystemUtil.runTask +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.UserDataHolderBase +import com.intellij.openapi.wm.ToolWindowManager +import org.jetbrains.plugins.gradle.settings.GradleSettings +import org.jetbrains.plugins.gradle.util.GradleConstants +import javax.swing.SwingUtilities + +class RunPreviewAction( + private val previewLocation: PreviewLocation +) : AnAction({ "Show non-interactive preview" }, PreviewIcons.RUN_PREVIEW) { + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + buildPreviewViaGradle(project, previewLocation) + } +} + +internal const val PREVIEW_EDITOR_TOOLBAR_GROUP_ID = "Compose.Desktop.Preview.Editor.Toolbar" + +class RefreshOrRunPreviewAction : AnAction(PreviewIcons.COMPOSE) { + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val previewLocation = ReadAction.compute { + val editor = e.dataContext.getData(CommonDataKeys.EDITOR) + if (editor != null) { + e.presentation.isEnabled = false + parentPreviewAtCaretOrNull(editor) + } else null + } + if (previewLocation != null) { + buildPreviewViaGradle(project, previewLocation) + } + } +} + +private fun buildPreviewViaGradle(project: Project, previewLocation: PreviewLocation) { + val previewToolWindow = ToolWindowManager.getInstance(project).getToolWindow("Desktop Preview") + previewToolWindow?.setAvailable(true) + + val gradleVmOptions = GradleSettings.getInstance(project).gradleVmOptions + val settings = ExternalSystemTaskExecutionSettings() + settings.executionName = "Preview: ${previewLocation.fqName}" + settings.externalProjectPath = previewLocation.modulePath + settings.taskNames = listOf("configureDesktopPreview") + settings.vmOptions = gradleVmOptions + settings.externalSystemIdString = GradleConstants.SYSTEM_ID.id + val previewService = project.service() + val gradleCallbackPort = previewService.gradleCallbackPort + settings.scriptParameters = + listOf( + "-Pcompose.desktop.preview.target=${previewLocation.fqName}", + "-Pcompose.desktop.preview.ide.port=$gradleCallbackPort" + ).joinToString(" ") + SwingUtilities.invokeLater { + ToolWindowManager.getInstance(project).getToolWindow("Desktop Preview")?.activate { + previewService.buildStarted() + } + } + runTask( + settings, + DefaultRunExecutor.EXECUTOR_ID, + project, + GradleConstants.SYSTEM_ID, + object : TaskCallback { + override fun onSuccess() { + previewService.buildFinished(success = true) + } + override fun onFailure() { + previewService.buildFinished(success = false) + } + }, + ProgressExecutionMode.IN_BACKGROUND_ASYNC, + false, + UserDataHolderBase() + ) +} diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewFloatingToolbarProvider.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewFloatingToolbarProvider.kt new file mode 100644 index 0000000000..edc9dcf105 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewFloatingToolbarProvider.kt @@ -0,0 +1,67 @@ +/* + * 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.Disposable +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.CaretEvent +import com.intellij.openapi.editor.event.CaretListener +import com.intellij.openapi.editor.toolbar.floating.AbstractFloatingToolbarProvider +import com.intellij.openapi.editor.toolbar.floating.FloatingToolbarComponent +import com.intellij.openapi.editor.toolbar.floating.FloatingToolbarComponentImpl +import com.intellij.openapi.project.Project +import com.intellij.util.concurrency.AppExecutorUtil + +class PreviewFloatingToolbarProvider : AbstractFloatingToolbarProvider(PREVIEW_EDITOR_TOOLBAR_GROUP_ID) { + override val autoHideable = false + override val priority: Int = 100 + + // todo: disable if not in Compose JVM module + override fun register(toolbar: FloatingToolbarComponent, parentDisposable: Disposable) { + try { + // todo: use provided data context once 2020.3 is no longer supported + val toolbarImpl = toolbar as? FloatingToolbarComponentImpl ?: return + val editor = toolbarImpl.getData(CommonDataKeys.EDITOR.name) as? Editor ?: return + registerComponent(toolbar, editor, parentDisposable) + } catch (e: Exception) { + LOG.error(e) + } + } + + private fun registerComponent( + component: FloatingToolbarComponent, + editor: Editor, + parentDisposable: Disposable + ) { + val project = editor.project ?: return + val listener = PreviewEditorToolbarVisibilityUpdater(component, project, editor) + editor.caretModel.addCaretListener(listener, parentDisposable) + } +} + +internal class PreviewEditorToolbarVisibilityUpdater( + private val toolbar: FloatingToolbarComponent, + private val project: Project, + private val editor: Editor +) : CaretListener { + override fun caretPositionChanged(event: CaretEvent) { + ReadAction.nonBlocking { updateVisibility() } + .inSmartMode(project) + .submit(AppExecutorUtil.getAppExecutorService()) + } + + private fun updateVisibility() { + val parentPreviewFun = parentPreviewAtCaretOrNull(editor) + if (parentPreviewFun != null) { + toolbar.scheduleShow() + } else { + toolbar.scheduleHide() + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..efea6c2e7b --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewLocation.kt @@ -0,0 +1,27 @@ +/* + * 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.util.ExternalSystemApiUtil +import com.intellij.util.concurrency.annotations.RequiresReadLock +import org.jetbrains.kotlin.idea.util.projectStructure.module +import org.jetbrains.kotlin.psi.KtNamedFunction + +data class PreviewLocation(val fqName: String, val modulePath: 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) + } + } + + return null + +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRootLogger.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRootLogger.kt new file mode 100644 index 0000000000..300ead3ad0 --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRootLogger.kt @@ -0,0 +1,11 @@ +/* + * 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.diagnostic.Logger + +val LOG = Logger.getInstance(PreviewRootLogger::class.java) +private class PreviewRootLogger \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRunLineMarkerContributor.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRunLineMarkerContributor.kt index 41ef1336ba..a8aefef758 100644 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRunLineMarkerContributor.kt +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewRunLineMarkerContributor.kt @@ -35,13 +35,10 @@ class PreviewRunLineMarkerContributor : RunLineMarkerContributor() { if (element !is LeafPsiElement) return null if (element.node.elementType != KtTokens.IDENTIFIER) return null - val parent = element.parent - return when { - parent is KtNamedFunction && parent.isValidComposePreview() -> { - val fqName = parent.composePreviewFunctionFqn() - val module = parent.module?.let { ExternalSystemApiUtil.getExternalProjectPath(it) } - ?: error("Could not determine module for $parent") - val actions = arrayOf(RunPreviewAction(fqName, module)) + return when (val parent = element.parent) { + is KtNamedFunction -> { + val previewFunction = parent.asPreviewFunctionOrNull() ?: return null + val actions = arrayOf(RunPreviewAction(previewFunction)) Info(PreviewIcons.COMPOSE, actions) { PreviewMessages.runPreview(parent.name!!) } } else -> null diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/RunPreviewAction.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/RunPreviewAction.kt deleted file mode 100644 index 566da69f4c..0000000000 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/RunPreviewAction.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.execution.executors.DefaultRunExecutor -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.components.service -import com.intellij.openapi.externalSystem.model.execution.ExternalSystemTaskExecutionSettings -import com.intellij.openapi.externalSystem.service.execution.ProgressExecutionMode -import com.intellij.openapi.externalSystem.task.TaskCallback -import com.intellij.openapi.externalSystem.util.ExternalSystemUtil.runTask -import com.intellij.openapi.util.UserDataHolderBase -import com.intellij.openapi.wm.ToolWindowManager -import org.jetbrains.plugins.gradle.settings.GradleSettings -import org.jetbrains.plugins.gradle.util.GradleConstants -import javax.swing.SwingUtilities - -class RunPreviewAction( - private val fqName: String, - private val modulePath: String -) : AnAction({ "Show non-interactive preview" }, PreviewIcons.RUN_PREVIEW) { - override fun actionPerformed(e: AnActionEvent) { - val project = e.project!! - val previewToolWindow = ToolWindowManager.getInstance(project).getToolWindow("Desktop Preview") - previewToolWindow?.setAvailable(true) - - val gradleVmOptions = GradleSettings.getInstance(project).gradleVmOptions - val settings = ExternalSystemTaskExecutionSettings() - settings.executionName = "Preview: $fqName" - settings.externalProjectPath = modulePath - settings.taskNames = listOf("configureDesktopPreview") - settings.vmOptions = gradleVmOptions - settings.externalSystemIdString = GradleConstants.SYSTEM_ID.id - val previewService = project.service() - val gradleCallbackPort = previewService.gradleCallbackPort - settings.scriptParameters = - listOf( - "-Pcompose.desktop.preview.target=$fqName", - "-Pcompose.desktop.preview.ide.port=$gradleCallbackPort" - ).joinToString(" ") - SwingUtilities.invokeLater { - ToolWindowManager.getInstance(project).getToolWindow("Desktop Preview")?.activate { - previewService.buildStarted() - } - } - runTask( - settings, - DefaultRunExecutor.EXECUTOR_ID, - project, - GradleConstants.SYSTEM_ID, - object : TaskCallback { - override fun onSuccess() { - previewService.buildFinished(success = true) - } - override fun onFailure() { - previewService.buildFinished(success = false) - } - }, - ProgressExecutionMode.IN_BACKGROUND_ASYNC, - false, - UserDataHolderBase() - ) - } -} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/editorUtils.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/editorUtils.kt new file mode 100644 index 0000000000..3e4de4242e --- /dev/null +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/editorUtils.kt @@ -0,0 +1,42 @@ +/* + * 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.editor.Editor +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiManager +import com.intellij.testFramework.LightVirtualFile +import com.intellij.util.concurrency.annotations.RequiresReadLock +import org.jetbrains.kotlin.idea.KotlinFileType +import org.jetbrains.kotlin.psi.KtNamedFunction + +@RequiresReadLock +internal fun parentPreviewAtCaretOrNull(editor: Editor): PreviewLocation? { + val caretModel = editor.caretModel + val psiFile = kotlinPsiFile(editor) + if (psiFile != null) { + var node = psiFile.findElementAt(caretModel.offset) + while (node != null) { + val previewFunction = (node as? KtNamedFunction)?.asPreviewFunctionOrNull() + if (previewFunction != null) { + return previewFunction + } + node = node.parent + } + } + + return null +} + +private fun kotlinPsiFile(editor: Editor): PsiFile? { + val project = editor.project ?: return null + val documentManager = FileDocumentManager.getInstance() + val file = documentManager.getFile(editor.document) + return if (file != null && file.fileType is KotlinFileType) { + PsiManager.getInstance(project).findFile(file) + } else null +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/locationUtils.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/locationUtils.kt index 31d42c52ec..c802e6369d 100644 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/locationUtils.kt +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/locationUtils.kt @@ -17,10 +17,10 @@ package org.jetbrains.compose.desktop.ide.preview import com.intellij.openapi.roots.ProjectRootModificationTracker -import com.intellij.psi.PsiElement import com.intellij.psi.util.CachedValueProvider import com.intellij.psi.util.CachedValuesManager import com.intellij.psi.util.parentOfType +import com.intellij.util.concurrency.annotations.RequiresReadLock import org.jetbrains.kotlin.asJava.findFacadeClass import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.descriptors.ClassKind @@ -39,21 +39,13 @@ internal const val COMPOSABLE_FQ_NAME = "androidx.compose.runtime.Composable" * tools/adt/idea/compose-designer/src/com/android/tools/idea/compose/preview/util/PreviewElement.kt */ -/** - * Whether this function is properly annotated with [PREVIEW_ANNOTATION_FQN] and is defined in a valid location. - * - * @see [isValidPreviewLocation] - */ -internal fun KtNamedFunction.isValidComposePreview() = - isValidPreviewLocation() && annotationEntries.any { it.fqNameMatches(DESKTOP_PREVIEW_ANNOTATION_FQN) } - /** * Returns whether a `@Composable` [PREVIEW_ANNOTATION_FQN] is defined in a valid location, which can be either: * 1. Top-level functions * 2. Non-nested functions defined in top-level classes that have a default (no parameter) constructor * */ -internal fun KtNamedFunction.isValidPreviewLocation(): Boolean { +private fun KtNamedFunction.isValidPreviewLocation(): Boolean { if (valueParameters.size > 0) return false if (receiverTypeReference != null) return false @@ -117,19 +109,39 @@ private fun KtAnnotationEntry.getQualifiedName(): String? = internal fun KtNamedFunction.composePreviewFunctionFqn() = "${getClassName()}.${name}" -// based on AndroidComposePsiUtils.kt from AOSP -internal fun PsiElement.isComposableFunction(): Boolean { - if (this !is KtNamedFunction) return false +@RequiresReadLock +internal fun KtNamedFunction.isValidComposablePreviewFunction(): Boolean { + fun isValidComposablePreviewImpl(): Boolean { + if (!isValidPreviewLocation()) return false + + var hasComposableAnnotation = false + var hasPreviewAnnotation = false + val annotationIt = annotationEntries.iterator() + while (annotationIt.hasNext() && !(hasComposableAnnotation && hasPreviewAnnotation)) { + val annotation = annotationIt.next() + hasComposableAnnotation = hasComposableAnnotation || annotation.fqNameMatches(COMPOSABLE_FQ_NAME) + hasPreviewAnnotation = hasPreviewAnnotation || annotation.fqNameMatches(DESKTOP_PREVIEW_ANNOTATION_FQN) + } + + return hasComposableAnnotation && hasPreviewAnnotation + } return CachedValuesManager.getCachedValue(this) { - val hasComposableAnnotation = annotationEntries.any { it.fqNameMatches(COMPOSABLE_FQ_NAME) } - val containingKtFile = this.containingKtFile - - CachedValueProvider.Result.create( - // TODO: see if we can handle alias imports without ruining performance. - hasComposableAnnotation, - containingKtFile, - ProjectRootModificationTracker.getInstance(project) - ) + cachedResult(isValidComposablePreviewImpl()) } -} \ No newline at end of file +} + +// based on AndroidComposePsiUtils.kt from AOSP +internal fun KtNamedFunction.isComposableFunction(): Boolean { + return CachedValuesManager.getCachedValue(this) { + cachedResult(annotationEntries.any { it.fqNameMatches(COMPOSABLE_FQ_NAME) }) + } +} + +private fun KtNamedFunction.cachedResult(value: T) = + CachedValueProvider.Result.create( + // TODO: see if we can handle alias imports without ruining performance. + value, + this.containingKtFile, + ProjectRootModificationTracker.getInstance(project) + ) \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/org/jetbrains/compose/inspections/ComposeSuppressor.kt b/idea-plugin/src/main/kotlin/org/jetbrains/compose/inspections/ComposeSuppressor.kt index 13fef5f0f7..04407d2f02 100644 --- a/idea-plugin/src/main/kotlin/org/jetbrains/compose/inspections/ComposeSuppressor.kt +++ b/idea-plugin/src/main/kotlin/org/jetbrains/compose/inspections/ComposeSuppressor.kt @@ -21,6 +21,7 @@ import com.intellij.psi.PsiElement import org.jetbrains.compose.desktop.ide.preview.isComposableFunction import org.jetbrains.kotlin.idea.KotlinLanguage import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.KtNamedFunction /** * Suppress inspection that require composable function names to start with a lower case letter. @@ -30,7 +31,7 @@ class ComposeSuppressor : InspectionSuppressor { return toolId == "FunctionName" && element.language == KotlinLanguage.INSTANCE && element.node.elementType == KtTokens.IDENTIFIER && - element.parent.isComposableFunction() + element.parent.let { it is KtNamedFunction && it.isComposableFunction() } } override fun getSuppressActions(element: PsiElement?, toolId: String): Array { diff --git a/idea-plugin/src/main/resources/META-INF/plugin.xml b/idea-plugin/src/main/resources/META-INF/plugin.xml index 73fee8d8ff..9c1c8fcb9f 100644 --- a/idea-plugin/src/main/resources/META-INF/plugin.xml +++ b/idea-plugin/src/main/resources/META-INF/plugin.xml @@ -40,5 +40,13 @@ + + + + + + + +