[completion] IJPL-207762 initial support for completion on frontend

GitOrigin-RevId: fbe8f4e0e7f9351a108499d7c085f0055eeda7a0
This commit is contained in:
Max Medvedev
2025-10-14 00:05:43 +00:00
committed by intellij-monorepo-bot
parent 848dbbec29
commit 885b621e79
12 changed files with 272 additions and 3 deletions
@@ -1,10 +1,11 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.lookup;
import com.intellij.codeInsight.completion.InsertHandler;
import com.intellij.codeInsight.completion.InsertionContext;
import com.intellij.openapi.util.ClassConditionKey;
import com.intellij.psi.PsiElement;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
@@ -275,4 +276,10 @@ public abstract class LookupElementDecorator<T extends LookupElement> extends Lo
return result;
}
}
/** a way to check if `element` is exactly InsertingDecorator */
@ApiStatus.Internal
public static boolean isDecoratedWithInsertHandler(@NotNull LookupElement element) {
return element instanceof InsertingDecorator;
}
}
+2
View File
@@ -46,6 +46,8 @@ jvm_library(
"//libraries/jackson/jackson",
"//libraries/jackson/module-kotlin:libraries-jackson-module-kotlin",
"//platform/util-class-loader:util-classLoader",
"//libraries/kotlinx/serialization/core",
"//libraries/kotlinx/serialization/json",
],
exports = [
"//platform/core-impl",
@@ -1,5 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="kotlin-language" name="Kotlin">
<configuration version="5" platform="JVM 21" allPlatforms="JVM [21]" useProjectSettings="false">
<compilerSettings>
<option name="additionalArguments" value="-Xjvm-default=all -opt-in=com.intellij.openapi.util.IntellijInternalApi -XXLanguage:+AllowEagerSupertypeAccessibilityChecks" />
</compilerSettings>
<compilerArguments>
<stringArguments>
<stringArg name="jvmTarget" arg="21" />
<stringArg name="apiVersion" arg="2.2" />
<stringArg name="languageVersion" arg="2.2" />
</stringArguments>
<arrayArguments>
<arrayArg name="pluginClasspaths">
<args>$KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar</args>
</arrayArg>
<arrayArg name="pluginOptions" />
</arrayArguments>
</compilerArguments>
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
@@ -41,5 +63,7 @@
<orderEntry type="module" module-name="intellij.libraries.jackson" />
<orderEntry type="module" module-name="intellij.libraries.jackson.module.kotlin" />
<orderEntry type="module" module-name="intellij.platform.util.classLoader" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.serialization.core" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.serialization.json" />
</component>
</module>
@@ -23,6 +23,9 @@
<extensionPoint name="daemon.highlightInfoFilter" interface="com.intellij.codeInsight.daemon.impl.HighlightInfoFilter" dynamic="true"/>
<extensionPoint name="daemon.intentionActionFilter" interface="com.intellij.codeInsight.daemon.impl.IntentionActionFilter" dynamic="true"/>
<extensionPoint name="completion.frontendFriendlyInsertHandler" beanClass="com.intellij.codeInsight.completion.FrontendFriendlyInsertHandlerSerializerBean" dynamic="true">
<with attribute="implementationClass" implements="com.intellij.codeInsight.completion.FrontendFriendlyInsertHandler"/>
</extensionPoint>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<projectService serviceInterface="com.intellij.profile.codeInspection.InspectionProjectProfileManager"
@@ -91,5 +94,7 @@
<daemon.highlightInfoFilter implementation="com.intellij.codeInsight.daemon.SyntheticPsiFileSupport$HighlightFilter"/>
<daemon.intentionActionFilter implementation="com.intellij.codeInsight.daemon.SyntheticPsiFileSupport$IntentionFilter"/>
<completion.frontendFriendlyInsertHandler
implementationClass="com.intellij.codeInsight.completion.CompositeFrontendFriendlyInsertHandler"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,37 @@
// 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.lookup.LookupElement
import com.intellij.openapi.diagnostic.debug
import kotlinx.serialization.Serializable
import org.jetbrains.annotations.ApiStatus
//TODO IJPL-207762 mark experimental
//TODO IJPL-207762 This class should be moved to `intellij.platform.completion.common` module once Java plugin is migrated to plugin model v2.
// At the moment, it's impossible as v1 modules don't see v2 modules, so Java could not have seen this class if it was in v2 module.
/**
* A frontend-friendly insert handler that delegates to a list of other frontend-friendly insert handlers.
* Can be useful if you want to split a complex insert handler into multiple independent pieces.
*
* @see FrontendFriendlyInsertHandler
*/
@Suppress("NonDefaultConstructor")
@ApiStatus.Internal
@Serializable
data class CompositeFrontendFriendlyInsertHandler(
val debugName: String,
private val children: List<FrontendFriendlyInsertHandler>,
) : FrontendFriendlyInsertHandler {
override fun handleInsert(context: InsertionContext, item: LookupElement) {
logger.debug { "Handling insert \"$debugName\", item = $item, children = $children" }
children.forEach {
it.handleInsert(context, item)
}
}
}
private val logger = com.intellij.openapi.diagnostic.logger<CompositeFrontendFriendlyInsertHandler>()
@@ -0,0 +1,54 @@
// 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.lookup.LookupElement
import kotlinx.serialization.Serializable
import org.jetbrains.annotations.ApiStatus
//TODO IJPL-207762 mark experimental
/**
* 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
* it is going to be called with a generic LookupElement instance on Frontend.
*
*/
@Serializable(with = FrontendFriendlyInsertHandlerSerializer::class)
@ApiStatus.Internal
interface FrontendFriendlyInsertHandler : InsertHandler<LookupElement>
/**
* An insert handler that can be converted to a [FrontendFriendlyInsertHandler] depending on the context.
*/
@ApiStatus.Internal
interface FrontendConvertibleInsertHandler<T : LookupElement> : InsertHandler<T> {
fun asFrontendFriendly(): FrontendFriendlyInsertHandler?
}
/**
* Marker interface for Backend's lookup elements which [LookupElement.handleInsert] are safe to run on Frontend in Remote Development environment.
* Must not contain any heavy computations, resolve, or index access.
*/
@ApiStatus.Internal
interface FrontendFriendlyLookupElement {
/**
* @return the frontend-friendly insert handler for this lookup element to be transferred to Frontend, or `null` if the frontend-friendly insert handler is not available
*/
@get:ApiStatus.OverrideOnly
val frontendFriendlyInsertHandler: FrontendFriendlyInsertHandler?
}
/**
* Tries to convert the given [InsertHandler] to a [FrontendFriendlyInsertHandler].
*/
@ApiStatus.Internal
fun InsertHandler<*>.asFrontendFriendly(): FrontendFriendlyInsertHandler? =
this as? FrontendFriendlyInsertHandler ?: (this as? FrontendConvertibleInsertHandler<*>)?.asFrontendFriendly()// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
@@ -0,0 +1,105 @@
// 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<FrontendFriendlyInsertHandler> {
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<FrontendFriendlyInsertHandler>(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<FrontendFriendlyInsertHandler>(wrapper.fqn)
return json.decodeFromJsonElement(serializer, wrapper.value)
}
}
private fun <T : FrontendFriendlyInsertHandler> findSerializer(fqn: String): KSerializer<T> {
// 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<T>
}
private val ep_name = ExtensionPointName<FrontendFriendlyInsertHandlerSerializerBean>("com.intellij.completion.frontendFriendlyInsertHandler")
internal class FrontendFriendlyInsertHandlerSerializerBean : BaseKeyedLazyInstance<KSerializer<Any>>(), KeyedLazyInstance<KSerializer<Any>> {
@Attribute("implementationClass")
@RequiredElement
@JvmField
var implementationClass: String? = null
override fun getImplementationClassName(): String? = throw UnsupportedOperationException()
override fun createInstance(componentManager: ComponentManager, pluginDescriptor: PluginDescriptor): KSerializer<Any> {
val fqn = implementationClass
requireNotNull(fqn) { "implementationClass is not specified for $fqn" }
try {
val clazz = componentManager.loadClass<Any>(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<Any>
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
)
@@ -0,0 +1,13 @@
// 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.lookup.LookupElement
import kotlinx.serialization.Serializable
import org.jetbrains.annotations.ApiStatus
//TODO IJPL-207762 mark experimental
@ApiStatus.Internal
@Serializable
object NoOpFrontendFriendlyInsertHandler : FrontendFriendlyInsertHandler {
override fun handleInsert(context: InsertionContext, item: LookupElement) {}
}
@@ -70,6 +70,9 @@ public class CodeCompletionHandlerBase {
*/
public static final Key<Boolean> DIRECT_INSERTION = Key.create("CodeCompletionHandlerBase.directInsertion");
@ApiStatus.Internal
public static final Key<FinishCompletionInfo> ITEM_PATTERN_AND_PREFIX_LENGTH = Key.create("CodeCompletionHandlerBase.prefix-length");
final @NotNull CompletionType completionType;
final boolean invokedExplicitly;
final boolean synchronous;
@@ -260,7 +263,12 @@ public class CodeCompletionHandlerBase {
}
LookupImpl lookup = (LookupImpl)LookupManager.getInstance(project).createLookup(editor, LookupElement.EMPTY_ARRAY, "",
new LookupArranger.DefaultArranger());
new LookupArranger.DefaultArranger() {
@Override
public boolean isCompletion() {
return true;
}
});
if (editor.isOneLineMode()) {
lookup.setCancelOnClickOutside(true);
lookup.setCancelOnOtherWindowOpen(true);
@@ -335,7 +335,8 @@ public final class CompletionProgressIndicator extends ProgressIndicatorBase imp
public void dispose() {
}
private static int findReplacementOffset(int selectionEndOffset, @NotNull PsiReference reference) {
@ApiStatus.Internal
public static int findReplacementOffset(int selectionEndOffset, @NotNull PsiReference reference) {
final List<TextRange> ranges = ReferenceRange.getAbsoluteRanges(reference);
for (TextRange range : ranges) {
if (range.contains(selectionEndOffset)) {
@@ -0,0 +1,11 @@
// 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 org.jetbrains.annotations.ApiStatus
// hack for remote-development to pass item pattern and prefix length to the backend
@ApiStatus.Internal
class FinishCompletionInfo(
val itemPattern: String,
val prefixLength: Int,
)
@@ -711,6 +711,8 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable,
int prefixLength) {
String lookupString = LookupUtil.getCaseCorrectedLookupString(item, matcher, itemPattern);
item.putUserData(CodeCompletionHandlerBase.ITEM_PATTERN_AND_PREFIX_LENGTH, new FinishCompletionInfo(itemPattern, prefixLength));
editor.getCaretModel().runForEachCaret(__ -> {
EditorModificationUtilEx.deleteSelectedText(editor);
int caretOffset = editor.getCaretModel().getOffset();