mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[completion] IJPL-207762 initial support for completion on frontend
GitOrigin-RevId: fbe8f4e0e7f9351a108499d7c085f0055eeda7a0
This commit is contained in:
committed by
intellij-monorepo-bot
parent
848dbbec29
commit
885b621e79
+8
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
+37
@@ -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>()
|
||||
+54
@@ -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.
|
||||
+105
@@ -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
|
||||
)
|
||||
+13
@@ -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) {}
|
||||
}
|
||||
+9
-1
@@ -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);
|
||||
|
||||
+2
-1
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user