Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support K2 mode in the IJ plugin #5138

Merged
merged 5 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what is the expected behaviour here — the service was marked as an app-level service and the project used as disposable, but using Project as disposable is bad. It feels like the service should be project-level, so this subscription would maintain the same scope, but I don't know if it's the right way to go about it.

}

@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