Skip to content

Commit 1b877dd

Browse files
rock3rkropp
andauthored
Support K2 mode in the IJ plugin (#5138)
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]>
1 parent 224704b commit 1b877dd

File tree

9 files changed

+247
-126
lines changed

9 files changed

+247
-126
lines changed

idea-plugin/build.gradle.kts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,7 @@ dependencies {
3232
}
3333

3434
intellijPlatform {
35-
pluginConfiguration {
36-
name = "Compose Multiplatform IDE Support"
37-
ideaVersion {
38-
sinceBuild = "231.*"
39-
untilBuild = "243.*"
40-
}
41-
}
35+
pluginConfiguration { name = "Compose Multiplatform IDE Support" }
4236
buildSearchableOptions = false
4337
autoReload = false
4438

@@ -56,6 +50,14 @@ tasks {
5650
targetCompatibility = "21"
5751
}
5852
withType<KotlinJvmCompile> { compilerOptions.jvmTarget.set(JvmTarget.JVM_21) }
53+
54+
runIde {
55+
systemProperty("idea.is.internal", true)
56+
systemProperty("idea.kotlin.plugin.use.k2", true)
57+
jvmArgumentProviders += CommandLineArgumentProvider {
58+
listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005")
59+
}
60+
}
5961
}
6062

6163
class ProjectProperties(private val project: Project) {

idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/ConfigurePreviewTaskNameCache.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.intellij.openapi.project.Project
1515
import com.intellij.util.concurrency.annotations.RequiresReadLock
1616
import org.jetbrains.plugins.gradle.settings.GradleSettings
1717
import org.jetbrains.plugins.gradle.util.GradleConstants
18+
import java.util.Locale
1819

1920
internal val DEFAULT_CONFIGURE_PREVIEW_TASK_NAME = "configureDesktopPreview"
2021

@@ -38,8 +39,11 @@ internal class ConfigurePreviewTaskNameProviderImpl : ConfigurePreviewTaskNamePr
3839
return null
3940
}
4041

41-
private fun previewTaskName(targetName: String = "") =
42-
"$DEFAULT_CONFIGURE_PREVIEW_TASK_NAME${targetName.capitalize()}"
42+
private fun previewTaskName(targetName: String = ""): String {
43+
val capitalizedTargetName =
44+
targetName.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
45+
return "$DEFAULT_CONFIGURE_PREVIEW_TASK_NAME$capitalizedTargetName"
46+
}
4347

4448
private fun moduleDataNodeOrNull(project: Project, modulePath: String): DataNode<ModuleData>? {
4549
val projectDataManager = ProjectDataManager.getInstance()
@@ -87,4 +91,4 @@ internal class ConfigurePreviewTaskNameCache(
8791
cachedTaskName = null
8892
}
8993
}
90-
}
94+
}

idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewLocation.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

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

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

23-
val service = project.getService(PreviewStateService::class.java)
24+
val service = project.service<PreviewStateService>()
2425
val previewTaskName = service.configurePreviewTaskNameOrNull(module) ?: DEFAULT_CONFIGURE_PREVIEW_TASK_NAME
2526
val modulePath = ExternalSystemApiUtil.getExternalProjectPath(module) ?: return null
2627
return PreviewLocation(fqName = fqName, modulePath = modulePath, taskName = previewTaskName)

idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewStateService.kt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@ package org.jetbrains.compose.desktop.ide.preview
77

88
import com.intellij.openapi.Disposable
99
import com.intellij.openapi.components.Service
10-
import com.intellij.openapi.diagnostic.Logger
1110
import com.intellij.openapi.externalSystem.model.task.*
1211
import com.intellij.openapi.externalSystem.service.notification.ExternalSystemProgressNotificationManager
1312
import com.intellij.openapi.module.Module
14-
import com.intellij.openapi.project.Project
1513
import com.intellij.openapi.util.Disposer
1614
import com.intellij.ui.components.JBLoadingPanel
1715
import com.intellij.util.concurrency.annotations.RequiresReadLock
@@ -22,9 +20,8 @@ import javax.swing.JComponent
2220
import javax.swing.event.AncestorEvent
2321
import javax.swing.event.AncestorListener
2422

25-
@Service
26-
class PreviewStateService(private val myProject: Project) : Disposable {
27-
private val idePreviewLogger = Logger.getInstance("org.jetbrains.compose.desktop.ide.preview")
23+
@Service(Service.Level.PROJECT)
24+
class PreviewStateService : Disposable {
2825
private val previewListener = CompositePreviewListener()
2926
private val previewManager: PreviewManager = PreviewManagerImpl(previewListener)
3027
val gradleCallbackPort: Int
@@ -35,7 +32,7 @@ class PreviewStateService(private val myProject: Project) : Disposable {
3532
init {
3633
val projectRefreshListener = ConfigurePreviewTaskNameCacheInvalidator(configurePreviewTaskNameCache)
3734
ExternalSystemProgressNotificationManager.getInstance()
38-
.addNotificationListener(projectRefreshListener, myProject)
35+
.addNotificationListener(projectRefreshListener, this)
3936
}
4037

4138
@RequiresReadLock
@@ -80,7 +77,6 @@ private class PreviewResizeListener(private val previewManager: PreviewManager)
8077

8178
override fun ancestorAdded(event: AncestorEvent) {
8279
updateFrameSize(event.component)
83-
8480
}
8581

8682
override fun ancestorRemoved(event: AncestorEvent) {
@@ -136,4 +132,4 @@ private class ConfigurePreviewTaskNameCacheInvalidator(
136132
configurePreviewTaskNameCache.invalidate()
137133
}
138134
}
139-
}
135+
}

idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/PreviewToolWindow.kt

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,29 @@ import com.intellij.openapi.project.Project
1111
import com.intellij.openapi.wm.ToolWindow
1212
import com.intellij.openapi.wm.ToolWindowFactory
1313
import com.intellij.ui.components.JBLoadingPanel
14-
import org.jetbrains.compose.desktop.ide.preview.ui.PreviewPanel
1514
import java.awt.BorderLayout
15+
import org.jetbrains.compose.desktop.ide.preview.ui.PreviewPanel
1616

1717
class PreviewToolWindow : ToolWindowFactory, DumbAware {
18-
override fun isApplicable(project: Project): Boolean =
19-
isPreviewCompatible(project)
18+
@Deprecated("Use isApplicableAsync")
19+
override fun isApplicable(project: Project): Boolean = isPreviewCompatible(project)
20+
21+
override suspend fun isApplicableAsync(project: Project): Boolean = isPreviewCompatible(project)
2022

2123
override fun init(toolWindow: ToolWindow) {
22-
ApplicationManager.getApplication().invokeLater {
23-
toolWindow.setIcon(PreviewIcons.COMPOSE)
24-
}
24+
ApplicationManager.getApplication().invokeLater { toolWindow.setIcon(PreviewIcons.COMPOSE) }
2525
}
2626

2727
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
2828
toolWindow.contentManager.let { content ->
2929
val panel = PreviewPanel(project)
30-
val loadingPanel = JBLoadingPanel(BorderLayout(), project)
30+
val loadingPanel = JBLoadingPanel(BorderLayout(), toolWindow.disposable)
3131
loadingPanel.add(panel, BorderLayout.CENTER)
3232
content.addContent(content.factory.createContent(loadingPanel, null, false))
3333
project.service<PreviewStateService>().registerPreviewPanels(panel, loadingPanel)
3434
}
3535
}
3636

3737
// don't show the toolwindow until a preview is requested
38-
override fun shouldBeAvailable(project: Project): Boolean =
39-
false
40-
}
38+
override fun shouldBeAvailable(project: Project): Boolean = false
39+
}

idea-plugin/src/main/kotlin/org/jetbrains/compose/desktop/ide/preview/locationUtils.kt

Lines changed: 58 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -21,32 +21,39 @@ import com.intellij.psi.util.CachedValueProvider
2121
import com.intellij.psi.util.CachedValuesManager
2222
import com.intellij.psi.util.parentOfType
2323
import com.intellij.util.concurrency.annotations.RequiresReadLock
24+
import org.jetbrains.kotlin.analysis.api.analyze
25+
import org.jetbrains.kotlin.analysis.api.symbols.KaClassLikeSymbol
2426
import org.jetbrains.kotlin.asJava.findFacadeClass
25-
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
26-
import org.jetbrains.kotlin.descriptors.ClassKind
27-
import org.jetbrains.kotlin.idea.caches.resolve.analyze
28-
import org.jetbrains.kotlin.psi.*
27+
import org.jetbrains.kotlin.name.ClassId
28+
import org.jetbrains.kotlin.name.FqName
29+
import org.jetbrains.kotlin.psi.KtClass
30+
import org.jetbrains.kotlin.psi.KtFile
31+
import org.jetbrains.kotlin.psi.KtNamedFunction
32+
import org.jetbrains.kotlin.psi.allConstructors
2933
import org.jetbrains.kotlin.psi.psiUtil.containingClass
30-
import org.jetbrains.kotlin.resolve.BindingContext
31-
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
32-
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
3334

34-
internal const val DESKTOP_PREVIEW_ANNOTATION_FQN = "androidx.compose.desktop.ui.tooling.preview.Preview"
35+
internal const val DESKTOP_PREVIEW_ANNOTATION_FQN =
36+
"androidx.compose.desktop.ui.tooling.preview.Preview"
3537
internal const val COMPOSABLE_FQ_NAME = "androidx.compose.runtime.Composable"
3638

39+
private val ComposableAnnotationClassId = ClassId.topLevel(FqName(COMPOSABLE_FQ_NAME))
40+
private val DesktopPreviewAnnotationClassId =
41+
ClassId.topLevel(FqName(DESKTOP_PREVIEW_ANNOTATION_FQN))
42+
3743
/**
3844
* Utils based on functions from AOSP, taken from
3945
* tools/adt/idea/compose-designer/src/com/android/tools/idea/compose/preview/util/PreviewElement.kt
4046
*/
4147

4248
/**
43-
* Returns whether a `@Composable` [PREVIEW_ANNOTATION_FQN] is defined in a valid location, which can be either:
49+
* Returns whether a `@Composable` [DESKTOP_PREVIEW_ANNOTATION_FQN] is defined in a valid location,
50+
* which can be either:
4451
* 1. Top-level functions
45-
* 2. Non-nested functions defined in top-level classes that have a default (no parameter) constructor
46-
*
52+
* 2. Non-nested functions defined in top-level classes that have a default (no parameter)
53+
* constructor
4754
*/
4855
private fun KtNamedFunction.isValidPreviewLocation(): Boolean {
49-
if (valueParameters.size > 0) return false
56+
if (valueParameters.isNotEmpty()) return false
5057
if (receiverTypeReference != null) return false
5158

5259
if (isTopLevel) return true
@@ -55,7 +62,8 @@ private fun KtNamedFunction.isValidPreviewLocation(): Boolean {
5562
// This is not a nested method
5663
val containingClass = containingClass()
5764
if (containingClass != null) {
58-
// We allow functions that are not top level defined in top level classes that have a default (no parameter) constructor.
65+
// We allow functions that are not top level defined in top level classes that have a
66+
// default (no parameter) constructor.
5967
if (containingClass.isTopLevel() && containingClass.hasDefaultConstructor()) {
6068
return true
6169
}
@@ -64,84 +72,67 @@ private fun KtNamedFunction.isValidPreviewLocation(): Boolean {
6472
return false
6573
}
6674

67-
6875
/**
6976
* Computes the qualified name of the class containing this [KtNamedFunction].
7077
*
71-
* For functions defined within a Kotlin class, returns the qualified name of that class. For top-level functions, returns the JVM name of
72-
* the Java facade class generated instead.
73-
*
78+
* For functions defined within a Kotlin class, returns the qualified name of that class. For
79+
* top-level functions, returns the JVM name of the Java facade class generated instead.
7480
*/
7581
internal fun KtNamedFunction.getClassName(): String? =
76-
if (isTopLevel) ((parent as? KtFile)?.findFacadeClass())?.qualifiedName else parentOfType<KtClass>()?.getQualifiedName()
77-
82+
if (isTopLevel) ((parent as? KtFile)?.findFacadeClass())?.qualifiedName
83+
else parentOfType<KtClass>()?.getQualifiedName()
7884

79-
/** Computes the qualified name for a Kotlin Class. Returns null if the class is a kotlin built-in. */
80-
private fun KtClass.getQualifiedName(): String? {
81-
val classDescriptor = analyze(BodyResolveMode.PARTIAL).get(BindingContext.CLASS, this) ?: return null
82-
return if (KotlinBuiltIns.isUnderKotlinPackage(classDescriptor) || classDescriptor.kind != ClassKind.CLASS) {
83-
null
84-
} else {
85-
classDescriptor.fqNameSafe.asString()
85+
/**
86+
* Computes the qualified name for a Kotlin Class. Returns null if the class is a kotlin built-in.
87+
*/
88+
private fun KtClass.getQualifiedName(): String? =
89+
analyze(this) {
90+
val classSymbol = symbol
91+
return when {
92+
classSymbol !is KaClassLikeSymbol -> null
93+
classSymbol.classId.isKotlinPackage() -> null
94+
else -> classSymbol.classId?.asFqNameString()
95+
}
8696
}
87-
}
97+
98+
private fun ClassId?.isKotlinPackage() =
99+
this != null && startsWith(org.jetbrains.kotlin.builtins.StandardNames.BUILT_INS_PACKAGE_NAME)
88100

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

92-
/**
93-
* Determines whether this [KtAnnotationEntry] has the specified qualified name.
94-
* Careful: this does *not* currently take into account Kotlin type aliases (https://kotlinlang.org/docs/reference/type-aliases.html).
95-
* Fortunately, type aliases are extremely uncommon for simple annotation types.
96-
*/
97-
private fun KtAnnotationEntry.fqNameMatches(fqName: String): Boolean {
98-
// For inspiration, see IDELightClassGenerationSupport.KtUltraLightSupportImpl.findAnnotation in the Kotlin plugin.
99-
val shortName = shortName?.asString() ?: return false
100-
return fqName.endsWith(shortName) && fqName == getQualifiedName()
101-
}
102-
103-
/**
104-
* Computes the qualified name of this [KtAnnotationEntry].
105-
* Prefer to use [fqNameMatches], which checks the short name first and thus has better performance.
106-
*/
107-
private fun KtAnnotationEntry.getQualifiedName(): String? =
108-
analyze(BodyResolveMode.PARTIAL).get(BindingContext.ANNOTATION, this)?.fqName?.asString()
109-
110104
internal fun KtNamedFunction.composePreviewFunctionFqn() = "${getClassName()}.${name}"
111105

112106
@RequiresReadLock
113107
internal fun KtNamedFunction.isValidComposablePreviewFunction(): Boolean {
114-
fun isValidComposablePreviewImpl(): Boolean {
115-
if (!isValidPreviewLocation()) return false
116-
117-
var hasComposableAnnotation = false
118-
var hasPreviewAnnotation = false
119-
val annotationIt = annotationEntries.iterator()
120-
while (annotationIt.hasNext() && !(hasComposableAnnotation && hasPreviewAnnotation)) {
121-
val annotation = annotationIt.next()
122-
hasComposableAnnotation = hasComposableAnnotation || annotation.fqNameMatches(COMPOSABLE_FQ_NAME)
123-
hasPreviewAnnotation = hasPreviewAnnotation || annotation.fqNameMatches(DESKTOP_PREVIEW_ANNOTATION_FQN)
124-
}
108+
fun isValidComposablePreviewImpl(): Boolean =
109+
analyze(this) {
110+
if (!isValidPreviewLocation()) return false
125111

126-
return hasComposableAnnotation && hasPreviewAnnotation
127-
}
112+
val mySymbol = symbol
113+
val hasComposableAnnotation = mySymbol.annotations.contains(ComposableAnnotationClassId)
114+
val hasPreviewAnnotation =
115+
mySymbol.annotations.contains(DesktopPreviewAnnotationClassId)
128116

129-
return CachedValuesManager.getCachedValue(this) {
130-
cachedResult(isValidComposablePreviewImpl())
131-
}
117+
return hasComposableAnnotation && hasPreviewAnnotation
118+
}
119+
120+
return CachedValuesManager.getCachedValue(this) { cachedResult(isValidComposablePreviewImpl()) }
132121
}
133122

134123
// based on AndroidComposePsiUtils.kt from AOSP
135-
internal fun KtNamedFunction.isComposableFunction(): Boolean {
136-
return CachedValuesManager.getCachedValue(this) {
137-
cachedResult(annotationEntries.any { it.fqNameMatches(COMPOSABLE_FQ_NAME) })
124+
internal fun KtNamedFunction.isComposableFunction(): Boolean =
125+
CachedValuesManager.getCachedValue(this) {
126+
val hasComposableAnnotation =
127+
analyze(this) { symbol.annotations.contains(ComposableAnnotationClassId) }
128+
129+
cachedResult(hasComposableAnnotation)
138130
}
139-
}
140131

141132
private fun <T> KtNamedFunction.cachedResult(value: T) =
142133
CachedValueProvider.Result.create(
143134
// TODO: see if we can handle alias imports without ruining performance.
144135
value,
145136
this.containingKtFile,
146-
ProjectRootModificationTracker.getInstance(project)
147-
)
137+
ProjectRootModificationTracker.getInstance(project),
138+
)

idea-plugin/src/main/kotlin/org/jetbrains/compose/web/ide/run/WebRunLineMarkerContributor.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ class WebRunLineMarkerContributor : RunLineMarkerContributor() {
1515
override fun getInfo(element: PsiElement): Info? {
1616
if (element !is LeafPsiElement) return null
1717
if (element.node.elementType != KtTokens.IDENTIFIER) return null
18+
if (element.parent.getAsJsMainFunctionOrNull() == null) return null
1819

19-
val jsMain = element.parent.getAsJsMainFunctionOrNull() ?: return null
2020
val icon = AllIcons.RunConfigurations.TestState.Run
21-
return Info(icon, null, ExecutorAction.getActions()[0])
21+
return Info(icon, arrayOf(ExecutorAction.getActions()[0]))
2222
}
2323
}

0 commit comments

Comments
 (0)