From 867427c56e5a80afc412ab7aea432c56b401206b Mon Sep 17 00:00:00 2001 From: Max Medvedev Date: Tue, 2 Dec 2025 13:03:05 +0100 Subject: [PATCH] IJPL-207762 extract common code for registering serialization extension point for RD GitOrigin-RevId: 5e73ac6f91ad6933416be5e7c193368d1866d8cc --- .../resource/intellij.java.frontback.impl.xml | 10 +- .../resources/META-INF/AnalysisImpl.xml | 11 +- .../FrontendFriendlyInsertHandler.kt | 11 +- ...FrontendFriendlyInsertHandlerSerializer.kt | 105 ---------- ...FrontendFriendlyInsertHandlerSerializer.kt | 21 ++ .../serialization/ExtensionPointSerializer.kt | 181 ++++++++++++++++++ .../common/src/protocol/RpcInsertHandler.kt | 2 + 7 files changed, 223 insertions(+), 118 deletions(-) delete mode 100644 platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyInsertHandlerSerializer.kt create mode 100644 platform/analysis-impl/src/com/intellij/codeInsight/completion/serialization/FrontendFriendlyInsertHandlerSerializer.kt create mode 100644 platform/analysis-impl/src/com/intellij/codeInsight/serialization/ExtensionPointSerializer.kt diff --git a/java/java-frontback-impl/resource/intellij.java.frontback.impl.xml b/java/java-frontback-impl/resource/intellij.java.frontback.impl.xml index f82369c63206..7e50474ac47f 100644 --- a/java/java-frontback-impl/resource/intellij.java.frontback.impl.xml +++ b/java/java-frontback-impl/resource/intellij.java.frontback.impl.xml @@ -75,11 +75,11 @@ - - - - - + + + + + - - + + + + @@ -95,9 +97,8 @@ - + target="com.intellij.codeInsight.completion.CompositeFrontendFriendlyInsertHandler"/> + target="com.intellij.codeInsight.completion.NoOpFrontendFriendlyInsertHandler"/> diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyInsertHandler.kt b/platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyInsertHandler.kt index 1103987e9345..6a51bf552942 100644 --- a/platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyInsertHandler.kt +++ b/platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyInsertHandler.kt @@ -1,6 +1,7 @@ // 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.codeInsight.completion +import com.intellij.codeInsight.completion.serialization.FrontendFriendlyInsertHandlerSerializer import com.intellij.codeInsight.lookup.LookupElement import kotlinx.serialization.Serializable import org.jetbrains.annotations.ApiStatus @@ -9,15 +10,19 @@ import org.jetbrains.annotations.ApiStatus /** * Marker interface to be used for insert handlers of Backend's lookup elements that are safe to run on Frontend in Remote Development environment. * - * Must be [kotlinx.serialization.Serializable] - * * Must not contain any heavy computations, resolve, or index access. * * Must be registered in `plugin.xml` as `completion.frontendFriendlyInsertHandler` extension point. * That said, it's allowed and encouraged to make frontend-friendly insert handlers stateful. * Their constructors should accept their state as a parameter. * - * Note: it's explicitly forbidden to specify a custom [com.intellij.codeInsight.lookup.LookupElement] as a type parameter because + * To allow transferring FFIHs to Frontend, you either need to make the class @kotlinx.Serializable or add a converter to a serializable Data Transfer Object. + * If you prefer DTO way, you must register the converter and DTO classes in plugin.xml: + * ``` + * + * ``` + * + * Note: it's explicitly forbidden to specify a custom [LookupElement] as a type parameter because * it is going to be called with a generic LookupElement instance on Frontend. * */ diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyInsertHandlerSerializer.kt b/platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyInsertHandlerSerializer.kt deleted file mode 100644 index 521b451deb9e..000000000000 --- a/platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyInsertHandlerSerializer.kt +++ /dev/null @@ -1,105 +0,0 @@ -// 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.codeInsight.completion - -import com.intellij.openapi.components.ComponentManager -import com.intellij.openapi.extensions.ExtensionPointName -import com.intellij.openapi.extensions.PluginDescriptor -import com.intellij.openapi.extensions.RequiredElement -import com.intellij.serviceContainer.BaseKeyedLazyInstance -import com.intellij.util.KeyedLazyInstance -import com.intellij.util.xmlb.annotations.Attribute -import kotlinx.coroutines.CancellationException -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement - -/** - * Serializer for [FrontendFriendlyInsertHandler]. - * It uses [ep_name] to collect serializers for all known [FrontendFriendlyInsertHandler] implementations. - */ -internal class FrontendFriendlyInsertHandlerSerializer : KSerializer { - private val json = Json { - ignoreUnknownKeys = true - encodeDefaults = true - } - - override val descriptor: SerialDescriptor = SerialDescriptor( - serialName = "com.intellij.codeInsight.completion.FrontendFriendlyInsertHandlerSerializer", - original = FFIHWrapper.serializer().descriptor - ) - - override fun serialize(encoder: Encoder, value: FrontendFriendlyInsertHandler) { - val fqn = value.javaClass.name - val serializer = findSerializer(fqn) - val element = json.encodeToJsonElement(serializer, value) - val wrapper = FFIHWrapper(fqn, element) - encoder.encodeSerializableValue(FFIHWrapper.serializer(), wrapper) - } - - override fun deserialize(decoder: Decoder): FrontendFriendlyInsertHandler { - val wrapper = decoder.decodeSerializableValue(FFIHWrapper.serializer()) - val serializer = findSerializer(wrapper.fqn) - return json.decodeFromJsonElement(serializer, wrapper.value) - } -} - -private fun findSerializer(fqn: String): KSerializer { - // todo maybe cache serializers? - val bean = ep_name.extensionList.find { bean -> - bean.implementationClass == fqn - } - if (bean == null) { - throw IllegalArgumentException("Cannot find serializer for $fqn") - } - return bean.instance as KSerializer -} - -private val ep_name = ExtensionPointName("com.intellij.completion.frontendFriendlyInsertHandler") - -internal class FrontendFriendlyInsertHandlerSerializerBean : BaseKeyedLazyInstance>(), KeyedLazyInstance> { - @Attribute("handler") - @RequiredElement - @JvmField - var implementationClass: String? = null - - override fun getImplementationClassName(): String? = throw UnsupportedOperationException() - - override fun createInstance(componentManager: ComponentManager, pluginDescriptor: PluginDescriptor): KSerializer { - val fqn = implementationClass - requireNotNull(fqn) { "implementationClass is not specified for $fqn" } - try { - val clazz = componentManager.loadClass(fqn, pluginDescriptor) - - val companionField = clazz.getField("Companion") - companionField.trySetAccessible() - val companion = companionField.get(null)!! - - val serializerMethod = companion.javaClass.getMethod("serializer") - serializerMethod.trySetAccessible() - val serializer = serializerMethod.invoke(companion) as KSerializer - - return serializer - } - catch (e: CancellationException) { - throw e - } - catch (e: Throwable) { - throw IllegalStateException("Cannot obtain kotlinx-serializer instance of $fqn." + - "Please make sure $fqn has @kotlinx.serialization.Serializable annotation, " + - "and it is compiled with the kotlinx-serialization compiler plugin" + - "(see kotlinx-serialization documentation for details).", e) - } - } - - override fun getKey(): String = implementationClass!! -} - -@Serializable -private data class FFIHWrapper( - val fqn: String, - val value: JsonElement -) \ No newline at end of file diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/completion/serialization/FrontendFriendlyInsertHandlerSerializer.kt b/platform/analysis-impl/src/com/intellij/codeInsight/completion/serialization/FrontendFriendlyInsertHandlerSerializer.kt new file mode 100644 index 000000000000..99854a3e3d9f --- /dev/null +++ b/platform/analysis-impl/src/com/intellij/codeInsight/completion/serialization/FrontendFriendlyInsertHandlerSerializer.kt @@ -0,0 +1,21 @@ +// 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.codeInsight.completion.serialization + +import com.intellij.codeInsight.completion.FrontendFriendlyInsertHandler +import com.intellij.codeInsight.serialization.ExtensionPointSerializer +import com.intellij.codeInsight.serialization.ExtensionPointSerializerBean +import com.intellij.openapi.extensions.ExtensionPointName +import org.jetbrains.annotations.ApiStatus + +/** + * Serializer for [FrontendFriendlyInsertHandler]. + * + * It uses [ep_name] to collect serializers for all known [FrontendFriendlyInsertHandlerSerializer] implementations. + */ +@ApiStatus.Internal +object FrontendFriendlyInsertHandlerSerializer : ExtensionPointSerializer( + epName = ep_name, + descriptorClass = FrontendFriendlyInsertHandler::class +) + +private val ep_name = ExtensionPointName("com.intellij.completion.frontendFriendlyInsertHandler") \ No newline at end of file diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/serialization/ExtensionPointSerializer.kt b/platform/analysis-impl/src/com/intellij/codeInsight/serialization/ExtensionPointSerializer.kt new file mode 100644 index 000000000000..6e3f3750df0f --- /dev/null +++ b/platform/analysis-impl/src/com/intellij/codeInsight/serialization/ExtensionPointSerializer.kt @@ -0,0 +1,181 @@ +// 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.codeInsight.serialization + +import com.intellij.openapi.components.ComponentManager +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.extensions.PluginDescriptor +import com.intellij.openapi.extensions.RequiredElement +import com.intellij.serviceContainer.BaseKeyedLazyInstance +import com.intellij.util.KeyedLazyInstance +import com.intellij.util.xmlb.annotations.Attribute +import kotlinx.coroutines.CancellationException +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import org.jetbrains.annotations.ApiStatus +import kotlin.reflect.KClass + +// TODO IJPL-207762 mark experimental +@ApiStatus.Internal +abstract class ExtensionPointSerializer( + private val epName: ExtensionPointName, + private val descriptorClass: KClass, +) : KSerializer { + + fun toDescriptor(target: Target): Descriptor? { + val fqn = target.javaClass.name + val helper = findSerializerHelper(fqn) ?: return null + return helper.converter.toDescriptor(target) + } + + private fun findNotNullSerializerHelper(fqn: String): SerializationHelper = + findSerializerHelper(fqn) ?: error("Cannot find serializer for $fqn") + + private fun findSerializerHelper(fqn: String): SerializationHelper? { + // todo maybe cache serializers? + val bean = epName.extensionList.find { bean -> + bean.implementationClass == fqn || bean.descriptor == fqn + } ?: return null + + @Suppress("UNCHECKED_CAST") + return bean.instance as SerializationHelper + } + + private val json: Json // TODO investigate if we need caching here + get() = Json { + ignoreUnknownKeys = true + encodeDefaults = true + serializersModule = SerializersModule { + polymorphic(descriptorClass, this@ExtensionPointSerializer) { + epName.extensionList.map { bean -> + val actualDescriptorClass = bean.instance.serializableClass.kotlin as KClass + subclass(actualDescriptorClass, this@ExtensionPointSerializer) + } + } + } + } + + override val descriptor: SerialDescriptor = SerialDescriptor( + serialName = epName.name, + original = SerializerWrapper.serializer().descriptor + ) + + override fun serialize(encoder: Encoder, value: Descriptor) { + val fqn = value.javaClass.name + val (serializer, _) = findNotNullSerializerHelper(fqn) + val element = json.encodeToJsonElement(serializer, value) + val wrapper = SerializerWrapper(fqn, element) + encoder.encodeSerializableValue(SerializerWrapper.serializer(), wrapper) + } + + override fun deserialize(decoder: Decoder): Descriptor { + val wrapper = decoder.decodeSerializableValue(SerializerWrapper.serializer()) + val (serializer, _) = findNotNullSerializerHelper(wrapper.fqn) + @Suppress("UNCHECKED_CAST") + return json.decodeFromJsonElement(serializer, wrapper.value) as Descriptor + } + //} +} + +@Serializable +private data class SerializerWrapper( + val fqn: String, + val value: JsonElement, +) + +/** + * Returns the instance of serializer for the given implementation class. + * + * TODO IJPL-207762 mark experimental + */ +@ApiStatus.Internal +class ExtensionPointSerializerBean : BaseKeyedLazyInstance>(), + KeyedLazyInstance> { + @Attribute("target") + @RequiredElement + @JvmField + var implementationClass: String? = null + + @Attribute("descriptor") + @JvmField + var descriptor: String? = null + + @Attribute("converter") + @JvmField + var converter: String? = null + + override fun getImplementationClassName(): String = throw UnsupportedOperationException() + + override fun createInstance(componentManager: ComponentManager, pluginDescriptor: PluginDescriptor): SerializationHelper { + val fqn = descriptor ?: implementationClass + requireNotNull(fqn) { "implementationClass is not specified for $fqn" } + + val converterClass = converter.takeIf { descriptor != null } + + try { + val serializer = instantiateSerializer(fqn, componentManager, pluginDescriptor) + + val converter = when (converterClass) { + null -> IdConverter + else -> componentManager.instantiateClass>(converterClass, pluginDescriptor) + } + + return SerializationHelper(serializer, converter, componentManager.loadClass(fqn, pluginDescriptor)) + } + catch (e: CancellationException) { + throw e + } + catch (e: Throwable) { + throw IllegalStateException("Cannot obtain kotlinx-serializer instance of $fqn." + + "Please make sure $fqn has @kotlinx.serialization.Serializable annotation, " + + "and it is compiled with the kotlinx-serialization compiler plugin" + + "(see kotlinx-serialization documentation for details).", e) + } + } + + private fun instantiateSerializer( + fqn: String, + componentManager: ComponentManager, + pluginDescriptor: PluginDescriptor, + ): KSerializer { + val serializableClass = componentManager.loadClass(fqn, pluginDescriptor) + + val companionField = serializableClass.getField("Companion") + companionField.trySetAccessible() + val companion = companionField.get(null)!! + + val serializerMethod = companion.javaClass.getMethod("serializer") + serializerMethod.trySetAccessible() + + @Suppress("UNCHECKED_CAST") + return serializerMethod.invoke(companion) as KSerializer + } + + override fun getKey(): String = implementationClass!! +} + +@ApiStatus.Internal +data class SerializationHelper( + val serializer: KSerializer, + val converter: DescriptorConverter, + val serializableClass: Class, +) + +// TODO IJPL-207762 mark experimental +@ApiStatus.Internal +interface DescriptorConverter { + /** + * @return data transfer object for [target], or null if [target] cannot be serialized + */ + fun toDescriptor(target: Target): Descriptor? +} + +private object IdConverter : DescriptorConverter { + override fun toDescriptor(target: Any): Any = target +} \ No newline at end of file diff --git a/platform/completion/common/src/protocol/RpcInsertHandler.kt b/platform/completion/common/src/protocol/RpcInsertHandler.kt index 548ef6ba572b..b812f6b4fe9e 100644 --- a/platform/completion/common/src/protocol/RpcInsertHandler.kt +++ b/platform/completion/common/src/protocol/RpcInsertHandler.kt @@ -2,6 +2,7 @@ package com.intellij.platform.completion.common.protocol import com.intellij.codeInsight.completion.* +import com.intellij.codeInsight.completion.serialization.FrontendFriendlyInsertHandlerSerializer import com.intellij.codeInsight.lookup.LookupElement import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.codeInsight.lookup.LookupElementDecorator @@ -34,6 +35,7 @@ sealed interface RpcInsertHandler { */ @Serializable data class Frontend( + @Serializable(with = FrontendFriendlyInsertHandlerSerializer::class) val insertHandler: FrontendFriendlyInsertHandler, ) : RpcInsertHandler { override fun toString(): String = buildToString("Frontend") {