Skip to content

Commit

Permalink
Support K2 mode in the IJ plugin (#5138)
Browse files Browse the repository at this point in the history
This PR makes the IJ plugin run in K2 mode with the new Analysis APIs,
instead of the old K1 APIs. It also includes cleaning up a bunch of
compiler warnings, and some mistakes I saw when migrating to K2.

---------

Co-authored-by: Victor Kropp <[email protected]>
(cherry picked from commit 1b877dd)
  • Loading branch information
rock3r authored and kropp committed Dec 12, 2024
1 parent 6e9fdb5 commit 0d971b1
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 126 deletions.
16 changes: 9 additions & 7 deletions idea-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,7 @@ dependencies {
}

intellijPlatform {
pluginConfiguration {
name = "Compose Multiplatform IDE Support"
ideaVersion {
sinceBuild = "231.*"
untilBuild = "243.*"
}
}
pluginConfiguration { name = "Compose Multiplatform IDE Support" }
buildSearchableOptions = false
autoReload = false

Expand All @@ -56,6 +50,14 @@ tasks {
targetCompatibility = "21"
}
withType<KotlinJvmCompile> { compilerOptions.jvmTarget.set(JvmTarget.JVM_21) }

runIde {
systemProperty("idea.is.internal", true)
systemProperty("idea.kotlin.plugin.use.k2", true)
jvmArgumentProviders += CommandLineArgumentProvider {
listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005")
}
}
}

class ProjectProperties(private val project: Project) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.intellij.openapi.project.Project
import com.intellij.util.concurrency.annotations.RequiresReadLock
import org.jetbrains.plugins.gradle.settings.GradleSettings
import org.jetbrains.plugins.gradle.util.GradleConstants
import java.util.Locale

internal val DEFAULT_CONFIGURE_PREVIEW_TASK_NAME = "configureDesktopPreview"

Expand All @@ -38,8 +39,11 @@ internal class ConfigurePreviewTaskNameProviderImpl : ConfigurePreviewTaskNamePr
return null
}

private fun previewTaskName(targetName: String = "") =
"$DEFAULT_CONFIGURE_PREVIEW_TASK_NAME${targetName.capitalize()}"
private fun previewTaskName(targetName: String = ""): String {
val capitalizedTargetName =
targetName.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
return "$DEFAULT_CONFIGURE_PREVIEW_TASK_NAME$capitalizedTargetName"
}

private fun moduleDataNodeOrNull(project: Project, modulePath: String): DataNode<ModuleData>? {
val projectDataManager = ProjectDataManager.getInstance()
Expand Down Expand Up @@ -87,4 +91,4 @@ internal class ConfigurePreviewTaskNameCache(
cachedTaskName = null
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package org.jetbrains.compose.desktop.ide.preview

import com.intellij.openapi.components.service
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.util.concurrency.annotations.RequiresReadLock
Expand All @@ -20,7 +21,7 @@ internal fun KtNamedFunction.asPreviewFunctionOrNull(): PreviewLocation? {
val module = ProjectFileIndex.getInstance(project).getModuleForFile(containingFile.virtualFile)
if (module == null || module.isDisposed) return null

val service = project.getService(PreviewStateService::class.java)
val service = project.service<PreviewStateService>()
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ package org.jetbrains.compose.desktop.ide.preview

import com.intellij.openapi.Disposable
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.Logger
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
Expand All @@ -22,9 +20,8 @@ import javax.swing.JComponent
import javax.swing.event.AncestorEvent
import javax.swing.event.AncestorListener

@Service
class PreviewStateService(private val myProject: Project) : Disposable {
private val idePreviewLogger = Logger.getInstance("org.jetbrains.compose.desktop.ide.preview")
@Service(Service.Level.PROJECT)
class PreviewStateService : Disposable {
private val previewListener = CompositePreviewListener()
private val previewManager: PreviewManager = PreviewManagerImpl(previewListener)
val gradleCallbackPort: Int
Expand All @@ -35,7 +32,7 @@ class PreviewStateService(private val myProject: Project) : Disposable {
init {
val projectRefreshListener = ConfigurePreviewTaskNameCacheInvalidator(configurePreviewTaskNameCache)
ExternalSystemProgressNotificationManager.getInstance()
.addNotificationListener(projectRefreshListener, myProject)
.addNotificationListener(projectRefreshListener, this)
}

@RequiresReadLock
Expand Down Expand Up @@ -80,7 +77,6 @@ private class PreviewResizeListener(private val previewManager: PreviewManager)

override fun ancestorAdded(event: AncestorEvent) {
updateFrameSize(event.component)

}

override fun ancestorRemoved(event: AncestorEvent) {
Expand Down Expand Up @@ -136,4 +132,4 @@ private class ConfigurePreviewTaskNameCacheInvalidator(
configurePreviewTaskNameCache.invalidate()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,29 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory
import com.intellij.ui.components.JBLoadingPanel
import org.jetbrains.compose.desktop.ide.preview.ui.PreviewPanel
import java.awt.BorderLayout
import org.jetbrains.compose.desktop.ide.preview.ui.PreviewPanel

class PreviewToolWindow : ToolWindowFactory, DumbAware {
override fun isApplicable(project: Project): Boolean =
isPreviewCompatible(project)
@Deprecated("Use isApplicableAsync")
override fun isApplicable(project: Project): Boolean = isPreviewCompatible(project)

override suspend fun isApplicableAsync(project: Project): Boolean = isPreviewCompatible(project)

override fun init(toolWindow: ToolWindow) {
ApplicationManager.getApplication().invokeLater {
toolWindow.setIcon(PreviewIcons.COMPOSE)
}
ApplicationManager.getApplication().invokeLater { toolWindow.setIcon(PreviewIcons.COMPOSE) }
}

override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
toolWindow.contentManager.let { content ->
val panel = PreviewPanel(project)
val loadingPanel = JBLoadingPanel(BorderLayout(), project)
val loadingPanel = JBLoadingPanel(BorderLayout(), toolWindow.disposable)
loadingPanel.add(panel, BorderLayout.CENTER)
content.addContent(content.factory.createContent(loadingPanel, null, false))
project.service<PreviewStateService>().registerPreviewPanels(panel, loadingPanel)
}
}

// don't show the toolwindow until a preview is requested
override fun shouldBeAvailable(project: Project): Boolean =
false
}
override fun shouldBeAvailable(project: Project): Boolean = false
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,39 @@ 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.analysis.api.analyze
import org.jetbrains.kotlin.analysis.api.symbols.KaClassLikeSymbol
import org.jetbrains.kotlin.asJava.findFacadeClass
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.ClassKind
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.allConstructors
import org.jetbrains.kotlin.psi.psiUtil.containingClass
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode

internal const val DESKTOP_PREVIEW_ANNOTATION_FQN = "androidx.compose.desktop.ui.tooling.preview.Preview"
internal const val DESKTOP_PREVIEW_ANNOTATION_FQN =
"androidx.compose.desktop.ui.tooling.preview.Preview"
internal const val COMPOSABLE_FQ_NAME = "androidx.compose.runtime.Composable"

private val ComposableAnnotationClassId = ClassId.topLevel(FqName(COMPOSABLE_FQ_NAME))
private val DesktopPreviewAnnotationClassId =
ClassId.topLevel(FqName(DESKTOP_PREVIEW_ANNOTATION_FQN))

/**
* Utils based on functions from AOSP, taken from
* tools/adt/idea/compose-designer/src/com/android/tools/idea/compose/preview/util/PreviewElement.kt
*/

/**
* Returns whether a `@Composable` [PREVIEW_ANNOTATION_FQN] is defined in a valid location, which can be either:
* Returns whether a `@Composable` [DESKTOP_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
*
* 2. Non-nested functions defined in top-level classes that have a default (no parameter)
* constructor
*/
private fun KtNamedFunction.isValidPreviewLocation(): Boolean {
if (valueParameters.size > 0) return false
if (valueParameters.isNotEmpty()) return false
if (receiverTypeReference != null) return false

if (isTopLevel) return true
Expand All @@ -55,7 +62,8 @@ private fun KtNamedFunction.isValidPreviewLocation(): Boolean {
// This is not a nested method
val containingClass = containingClass()
if (containingClass != null) {
// We allow functions that are not top level defined in top level classes that have a default (no parameter) constructor.
// We allow functions that are not top level defined in top level classes that have a
// default (no parameter) constructor.
if (containingClass.isTopLevel() && containingClass.hasDefaultConstructor()) {
return true
}
Expand All @@ -64,84 +72,67 @@ private fun KtNamedFunction.isValidPreviewLocation(): Boolean {
return false
}


/**
* Computes the qualified name of the class containing this [KtNamedFunction].
*
* For functions defined within a Kotlin class, returns the qualified name of that class. For top-level functions, returns the JVM name of
* the Java facade class generated instead.
*
* For functions defined within a Kotlin class, returns the qualified name of that class. For
* top-level functions, returns the JVM name of the Java facade class generated instead.
*/
internal fun KtNamedFunction.getClassName(): String? =
if (isTopLevel) ((parent as? KtFile)?.findFacadeClass())?.qualifiedName else parentOfType<KtClass>()?.getQualifiedName()

if (isTopLevel) ((parent as? KtFile)?.findFacadeClass())?.qualifiedName
else parentOfType<KtClass>()?.getQualifiedName()

/** Computes the qualified name for a Kotlin Class. Returns null if the class is a kotlin built-in. */
private fun KtClass.getQualifiedName(): String? {
val classDescriptor = analyze(BodyResolveMode.PARTIAL).get(BindingContext.CLASS, this) ?: return null
return if (KotlinBuiltIns.isUnderKotlinPackage(classDescriptor) || classDescriptor.kind != ClassKind.CLASS) {
null
} else {
classDescriptor.fqNameSafe.asString()
/**
* Computes the qualified name for a Kotlin Class. Returns null if the class is a kotlin built-in.
*/
private fun KtClass.getQualifiedName(): String? =
analyze(this) {
val classSymbol = symbol
return when {
classSymbol !is KaClassLikeSymbol -> null
classSymbol.classId.isKotlinPackage() -> null
else -> classSymbol.classId?.asFqNameString()
}
}
}

private fun ClassId?.isKotlinPackage() =
this != null && startsWith(org.jetbrains.kotlin.builtins.StandardNames.BUILT_INS_PACKAGE_NAME)

private fun KtClass.hasDefaultConstructor() =
allConstructors.isEmpty().or(allConstructors.any { it.valueParameters.isEmpty() })

/**
* Determines whether this [KtAnnotationEntry] has the specified qualified name.
* Careful: this does *not* currently take into account Kotlin type aliases (https://kotlinlang.org/docs/reference/type-aliases.html).
* Fortunately, type aliases are extremely uncommon for simple annotation types.
*/
private fun KtAnnotationEntry.fqNameMatches(fqName: String): Boolean {
// For inspiration, see IDELightClassGenerationSupport.KtUltraLightSupportImpl.findAnnotation in the Kotlin plugin.
val shortName = shortName?.asString() ?: return false
return fqName.endsWith(shortName) && fqName == getQualifiedName()
}

/**
* Computes the qualified name of this [KtAnnotationEntry].
* Prefer to use [fqNameMatches], which checks the short name first and thus has better performance.
*/
private fun KtAnnotationEntry.getQualifiedName(): String? =
analyze(BodyResolveMode.PARTIAL).get(BindingContext.ANNOTATION, this)?.fqName?.asString()

internal fun KtNamedFunction.composePreviewFunctionFqn() = "${getClassName()}.${name}"

@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)
}
fun isValidComposablePreviewImpl(): Boolean =
analyze(this) {
if (!isValidPreviewLocation()) return false

return hasComposableAnnotation && hasPreviewAnnotation
}
val mySymbol = symbol
val hasComposableAnnotation = mySymbol.annotations.contains(ComposableAnnotationClassId)
val hasPreviewAnnotation =
mySymbol.annotations.contains(DesktopPreviewAnnotationClassId)

return CachedValuesManager.getCachedValue(this) {
cachedResult(isValidComposablePreviewImpl())
}
return hasComposableAnnotation && hasPreviewAnnotation
}

return CachedValuesManager.getCachedValue(this) { cachedResult(isValidComposablePreviewImpl()) }
}

// based on AndroidComposePsiUtils.kt from AOSP
internal fun KtNamedFunction.isComposableFunction(): Boolean {
return CachedValuesManager.getCachedValue(this) {
cachedResult(annotationEntries.any { it.fqNameMatches(COMPOSABLE_FQ_NAME) })
internal fun KtNamedFunction.isComposableFunction(): Boolean =
CachedValuesManager.getCachedValue(this) {
val hasComposableAnnotation =
analyze(this) { symbol.annotations.contains(ComposableAnnotationClassId) }

cachedResult(hasComposableAnnotation)
}
}

private fun <T> KtNamedFunction.cachedResult(value: T) =
CachedValueProvider.Result.create(
// TODO: see if we can handle alias imports without ruining performance.
value,
this.containingKtFile,
ProjectRootModificationTracker.getInstance(project)
)
ProjectRootModificationTracker.getInstance(project),
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ class WebRunLineMarkerContributor : RunLineMarkerContributor() {
override fun getInfo(element: PsiElement): Info? {
if (element !is LeafPsiElement) return null
if (element.node.elementType != KtTokens.IDENTIFIER) return null
if (element.parent.getAsJsMainFunctionOrNull() == null) return null

val jsMain = element.parent.getAsJsMainFunctionOrNull() ?: return null
val icon = AllIcons.RunConfigurations.TestState.Run
return Info(icon, null, ExecutorAction.getActions()[0])
return Info(icon, arrayOf(ExecutorAction.getActions()[0]))
}
}
Loading

0 comments on commit 0d971b1

Please sign in to comment.