[compose] IJPL-208258 Isolate Compose UI Preview application on rendering from the host IDE

GitOrigin-RevId: fe2ed7d0684897037f112c848aa5a2810d18fb35
This commit is contained in:
Yuriy Artamonov
2025-09-27 21:48:49 +02:00
committed by intellij-monorepo-bot
parent 9e6bbb34cf
commit 4da98930a0
5 changed files with 44 additions and 9 deletions

View File

@@ -643,11 +643,21 @@ abstract class ComponentManagerImpl(
return getOrCreateInstanceBlocking(holder = adapter.holder, debugString = key.name, keyClass = key) as T
}
private fun isDevelopmentTime(): Boolean {
return Thread.currentThread().contextClassLoader is DevTimeClassLoader
}
final override fun <T : Any> getService(serviceClass: Class<T>): T? {
if (isDevelopmentTime()) return null
return doGetService(serviceClass, true) ?: return postGetService(serviceClass, createIfNeeded = true)
}
final override suspend fun <T : Any> getServiceAsync(keyClass: Class<T>): T {
if (isDevelopmentTime()) {
throw IllegalStateException("Getting services is not allowed from development tools threads")
}
return serviceContainer.instance(keyClass)
}
@@ -660,6 +670,8 @@ abstract class ComponentManagerImpl(
protected open fun <T : Any> postGetService(serviceClass: Class<T>, createIfNeeded: Boolean): T? = null
final override fun <T : Any> getServiceIfCreated(serviceClass: Class<T>): T? {
if (isDevelopmentTime()) return null
return doGetService(serviceClass, createIfNeeded = false) ?: postGetService(serviceClass, createIfNeeded = false)
}

View File

@@ -8,6 +8,7 @@ import com.intellij.BundleBase.SHOW_LOCALIZED_MESSAGES
import com.intellij.BundleBase.appendLocalizationSuffix
import com.intellij.BundleBase.getDefaultMessage
import com.intellij.BundleBase.replaceMnemonicAmpersand
import com.intellij.openapi.application.DevTimeClassLoader
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.util.NlsSafe
@@ -61,7 +62,7 @@ object BundleBase {
}
/**
* Performs partial application of the pattern message from the bundle leaving some parameters unassigned.
* Performs partial application of the pattern message from the bundle, leaving some parameters unassigned.
* It's expected that the message contains `params.length + unassignedParams` placeholders. Parameters
* `{0}..{params.length-1}` will be substituted using a passed params array. The remaining parameters
* will be renumbered: `{params.length}` will become `{0}` and so on, so the resulting template
@@ -284,8 +285,9 @@ internal fun useDefaultValue(bundle: ResourceBundle, @NlsSafe key: String): @Nls
return "!$key!"
}
private fun isDevelopmentTime(classLoader: ClassLoader): Boolean {
return classLoader.javaClass.simpleName == "DevKitClassLoader"
private fun isDevelopmentTime(bundleClassLoader: ClassLoader): Boolean {
return bundleClassLoader is DevTimeClassLoader
|| Thread.currentThread().contextClassLoader is DevTimeClassLoader
}
internal fun postProcessResolvedValue(

View File

@@ -0,0 +1,10 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.application
import org.jetbrains.annotations.ApiStatus
/**
* Magic delegating class loader for UI previews.
*/
@ApiStatus.Internal
interface DevTimeClassLoader

View File

@@ -28,6 +28,9 @@ internal class ComposableFunctionFinder(private val classLoader: ClassLoader) {
fun findPreviewFunctions(clazzFqn: String, composableMethodNames: Collection<String>): List<ComposablePreviewFunction> {
val previewFunctions = mutableListOf<ComposablePreviewFunction>()
val contextClassLoader = Thread.currentThread().contextClassLoader
Thread.currentThread().contextClassLoader = classLoader
try {
val clazz = classLoader.loadClass(clazzFqn)
val functions = findPreviewFunctionsInClass(clazz, composableMethodNames)
@@ -39,6 +42,9 @@ internal class ComposableFunctionFinder(private val classLoader: ClassLoader) {
catch (e: Exception) {
logger.error("Error processing class: $clazzFqn", e)
}
finally {
Thread.currentThread().contextClassLoader = contextClassLoader
}
return previewFunctions
}

View File

@@ -6,6 +6,7 @@ import com.intellij.codeInsight.AnnotationUtil
import com.intellij.debugger.ui.HotSwapUIImpl
import com.intellij.devkit.compose.hasCompose
import com.intellij.ide.plugins.PluginManager
import com.intellij.openapi.application.DevTimeClassLoader
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.readAction
import com.intellij.openapi.diagnostic.logger
@@ -36,12 +37,18 @@ internal data class ModulePaths(val module: Module, val paths: List<String>)
internal data class ContentProvider(val function: Method, val classLoader: URLClassLoader) {
fun build(currentComposer: Composer, currentCompositeKeyHashCode: Long) {
val contextClassLoader = Thread.currentThread().contextClassLoader
Thread.currentThread().contextClassLoader = classLoader
try {
function.invoke(null, currentComposer, currentCompositeKeyHashCode.toInt())
}
catch (t: Throwable) {
thisLogger().warn("Unable to build preview", t)
}
finally {
Thread.currentThread().contextClassLoader = contextClassLoader
}
}
}
@@ -75,17 +82,15 @@ internal suspend fun compileCode(fileToCompile: VirtualFile, project: Project):
val pluginByClass = PluginManager.getPluginByClass(ComposePreviewToolWindowFactory::class.java)
val filteringClassLoader = FilteringClassLoader(pluginByClass!!.classLoader)
val loader = DevKitClassLoader(diskPaths, filteringClassLoader)
val loader = ComposeUIPreviewClassLoader(diskPaths, filteringClassLoader)
val functions = ComposableFunctionFinder(loader).findPreviewFunctions(analysis.targetClassName, analysis.composableMethodNames)
return functions.firstOrNull()?.method
?.let { ContentProvider(it, loader) }
}
/**
* Here the magic name `DevKitClassLoader` is used in the IDE process to check if we are in development time classloader.
*/
internal class DevKitClassLoader(urls: Array<URL>, parent: ClassLoader) : URLClassLoader("ComposeUIPreview", urls, parent)
internal class ComposeUIPreviewClassLoader(urls: Array<URL>, parent: ClassLoader)
: URLClassLoader("ComposeUIPreview", urls, parent), DevTimeClassLoader
private suspend fun compileFiles(fileToCompile: VirtualFile, project: Project): List<VirtualFile> {
val taskManager = ProjectTaskManager.getInstance(project) as ProjectTaskManagerImpl
@@ -145,7 +150,7 @@ private fun analyzeClass(project: Project, vFile: VirtualFile): FileAnalysisResu
}
/**
* Isolates project code from attempts to load unrelated classes via parent classloader.
* Isolates project code from attempts to load unrelated classes via the parent classloader.
*/
private class FilteringClassLoader(parent: ClassLoader) : ClassLoader(parent) {
override fun loadClass(name: String, resolve: Boolean): Class<*>? {