From 6b08ef19038731441de9e18408f7f24afef407c8 Mon Sep 17 00:00:00 2001 From: Max Medvedev Date: Sun, 15 Feb 2026 19:33:56 +0100 Subject: [PATCH] IJPL-207762 introduce frontend-friendly tail types GitOrigin-RevId: 1dc3f250ae1c85f4453ec4b337b37ff8e8f0cfcc --- .../resource/intellij.java.frontback.impl.xml | 2 + ...ndFriendlyOverridableSpaceInsertHandler.kt | 45 +++++++ .../completion/OverridableSpace.java | 15 ++- .../codeInsight/ExpectedTypesProvider.java | 5 +- .../intellij/codeInsight/TailTypeFactory.kt | 32 +++++ .../com/intellij/codeInsight/TailTypes.java | 114 ++---------------- .../META-INF/AnalysisImpl.analyzer.xml | 4 + .../resources/META-INF/AnalysisImpl.xml | 21 +++- .../completion/CompletionUtil.java | 20 +-- .../completion/CompletionVariant.java | 5 +- .../completion/FrontendFriendlyTailType.kt | 38 ++++++ .../FrontendFriendlyTailTypeFactory.kt | 40 ++++++ .../completion/FrontendFriendlyTailTypes.kt | 65 ++++++++++ .../completion/StandardTailTypeConverters.kt | 23 ++++ .../serialization/TailTypeSerializer.kt | 24 ++++ 15 files changed, 329 insertions(+), 124 deletions(-) create mode 100644 java/java-frontback-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyOverridableSpaceInsertHandler.kt create mode 100644 platform/analysis-api/src/com/intellij/codeInsight/TailTypeFactory.kt create mode 100644 platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyTailType.kt create mode 100644 platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyTailTypeFactory.kt create mode 100644 platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyTailTypes.kt create mode 100644 platform/analysis-impl/src/com/intellij/codeInsight/completion/StandardTailTypeConverters.kt create mode 100644 platform/analysis-impl/src/com/intellij/codeInsight/completion/serialization/TailTypeSerializer.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 e9d4f8cfe85f..e3e02c0a1c85 100644 --- a/java/java-frontback-impl/resource/intellij.java.frontback.impl.xml +++ b/java/java-frontback-impl/resource/intellij.java.frontback.impl.xml @@ -97,5 +97,7 @@ + + diff --git a/java/java-frontback-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyOverridableSpaceInsertHandler.kt b/java/java-frontback-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyOverridableSpaceInsertHandler.kt new file mode 100644 index 000000000000..1c49eb013d4b --- /dev/null +++ b/java/java-frontback-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyOverridableSpaceInsertHandler.kt @@ -0,0 +1,45 @@ +// Copyright 2000-2026 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.TailType +import com.intellij.codeInsight.completion.serialization.InsertHandlerSerializer +import com.intellij.codeInsight.completion.serialization.TailTypeSerializer +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder +import kotlinx.serialization.Serializable + + +@Serializable +internal data class FrontendFriendlyOverridableSpaceInsertHandler( + val delegateInsertHandler: FrontendFriendlyInsertHandler?, + val tailType: FrontendFriendlyTailType, +) : FrontendFriendlyInsertHandler { + + override fun handleInsert(context: InsertionContext, item: LookupElement) { + require(tailType is TailType) { "Tail type must extend TailType: $tailType" } + val wrapper = LookupElementBuilder.create("").withInsertHandler(delegateInsertHandler) + val lookupElement = OverridableSpace.create(wrapper, tailType) + lookupElement.handleInsert(context) + } + + companion object { + @JvmStatic + fun createIfFrontendFriendly(os: OverridableSpace): FrontendFriendlyOverridableSpaceInsertHandler? { + // Try to convert tail type to frontend-friendly version + val tail = os.myTail + val ffTailType = tail as? FrontendFriendlyTailType ?: TailTypeSerializer.toDescriptor(tail) ?: return null + + // Try to get delegate's effective handler + val delegateHandler = os.delegateEffectiveInsertHandler + val ffDelegate = if (delegateHandler != null) { + delegateHandler as? FrontendFriendlyInsertHandler ?: InsertHandlerSerializer.toDescriptor(delegateHandler) + ?: return null // delegate handler must be frontend-friendly + } + else { + null // missing delegate handler is OK + } + + return FrontendFriendlyOverridableSpaceInsertHandler(ffDelegate, ffTailType) + } + } +} \ No newline at end of file diff --git a/java/java-frontback-impl/src/com/intellij/codeInsight/completion/OverridableSpace.java b/java/java-frontback-impl/src/com/intellij/codeInsight/completion/OverridableSpace.java index 70f023680d73..3d7b16fe8a41 100644 --- a/java/java-frontback-impl/src/com/intellij/codeInsight/completion/OverridableSpace.java +++ b/java/java-frontback-impl/src/com/intellij/codeInsight/completion/OverridableSpace.java @@ -6,9 +6,10 @@ import com.intellij.codeInsight.TailTypes; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.TailTypeDecorator; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -public final class OverridableSpace extends TailTypeDecorator { - private final @NotNull TailType myTail; +public final class OverridableSpace extends TailTypeDecorator implements LookupElementWithEffectiveInsertHandler { + final @NotNull TailType myTail; private OverridableSpace(@NotNull LookupElement keyword, @NotNull TailType tail) { super(keyword); @@ -20,6 +21,16 @@ public final class OverridableSpace extends TailTypeDecorator { return context.shouldAddCompletionChar() ? TailTypes.noneType() : myTail; } + @Override + protected @Nullable InsertHandler getDelegateEffectiveInsertHandler() { + return super.getDelegateEffectiveInsertHandler(); + } + + @Override + public @Nullable InsertHandler getEffectiveInsertHandler() { + return FrontendFriendlyOverridableSpaceInsertHandler.createIfFrontendFriendly(this); + } + public static LookupElement create(@NotNull LookupElement delegate, @NotNull TailType tail) { return new OverridableSpace(delegate, tail); } diff --git a/java/java-impl/src/com/intellij/codeInsight/ExpectedTypesProvider.java b/java/java-impl/src/com/intellij/codeInsight/ExpectedTypesProvider.java index cbbf4424c2de..071fa034877a 100644 --- a/java/java-impl/src/com/intellij/codeInsight/ExpectedTypesProvider.java +++ b/java/java-impl/src/com/intellij/codeInsight/ExpectedTypesProvider.java @@ -148,9 +148,6 @@ import java.util.function.Supplier; @Service(Service.Level.PROJECT) public final class ExpectedTypesProvider { - private static final ExpectedTypeInfo VOID_EXPECTED = createInfoImpl(PsiTypes.voidType(), ExpectedTypeInfo.TYPE_OR_SUBTYPE, - PsiTypes.voidType(), TailTypes.semicolonType()); - private static final Logger LOG = Logger.getInstance(ExpectedTypesProvider.class); public static ExpectedTypesProvider getInstance(@NotNull Project project) { @@ -462,7 +459,7 @@ public final class ExpectedTypesProvider { } } if (myVoidable) { - myResult.add(VOID_EXPECTED); + myResult.add(createInfoImpl(PsiTypes.voidType(), ExpectedTypeInfo.TYPE_OR_SUBTYPE, PsiTypes.voidType(), TailTypes.semicolonType())); } } diff --git a/platform/analysis-api/src/com/intellij/codeInsight/TailTypeFactory.kt b/platform/analysis-api/src/com/intellij/codeInsight/TailTypeFactory.kt new file mode 100644 index 000000000000..02fe7425bf9e --- /dev/null +++ b/platform/analysis-api/src/com/intellij/codeInsight/TailTypeFactory.kt @@ -0,0 +1,32 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInsight + +import com.intellij.openapi.components.service +import org.jetbrains.annotations.ApiStatus + +/** + * Factory service for creating [TailType] instances. + * + * This abstraction allows returning different implementations (e.g., frontend-friendly) + * depending on the runtime environment, while keeping [TailTypes] as the stable API. + */ +@ApiStatus.Internal +interface TailTypeFactory { + fun noneType(): TailType + fun semicolonType(): TailType + fun spaceType(): TailType + fun insertSpaceType(): TailType + fun humbleSpaceBeforeWordType(): TailType + fun dotType(): TailType + fun caseColonType(): TailType + fun equalsType(): TailType + fun conditionalExpressionColonType(): TailType + fun charType(char: Char): TailType + fun charType(char: Char, overwrite: Boolean): TailType + fun unknownType(): TailType + + companion object { + @JvmStatic + fun getInstance(): TailTypeFactory = service() + } +} diff --git a/platform/analysis-api/src/com/intellij/codeInsight/TailTypes.java b/platform/analysis-api/src/com/intellij/codeInsight/TailTypes.java index f6389b5ed6ba..6375e3d343b9 100644 --- a/platform/analysis-api/src/com/intellij/codeInsight/TailTypes.java +++ b/platform/analysis-api/src/com/intellij/codeInsight/TailTypes.java @@ -1,155 +1,65 @@ // Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.codeInsight; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.ModNavigator; -import com.intellij.util.text.CharArrayUtil; import org.jetbrains.annotations.NotNull; public final class TailTypes { private TailTypes() { } - private static final ModNavigatorTailType UNKNOWN = new ModNavigatorTailType() { - @Override - public int processTail(@NotNull ModNavigator navigator, int tailOffset) { - return tailOffset; - } - - @Override - public int processTail(final @NotNull Editor editor, final int tailOffset) { - return tailOffset; - } - - @Override - public String toString() { - return "UNKNOWN"; - } - }; - - private static final ModNavigatorTailType NONE = new ModNavigatorTailType() { - @Override - public int processTail(@NotNull ModNavigator navigator, int tailOffset) { - return tailOffset; - } - - @Override - public int processTail(final @NotNull Editor editor, final int tailOffset) { - return tailOffset; - } - - @Override - public String toString() { - return "NONE"; - } - }; - - private static final ModNavigatorTailType SEMICOLON = new CharTailType(';'); - - private static final ModNavigatorTailType SPACE = new CharTailType(' '); - - private static final ModNavigatorTailType INSERT_SPACE = new CharTailType(' ', false); - - private static final ModNavigatorTailType HUMBLE_SPACE_BEFORE_WORD = new CharTailType(' ', false) { - @Override - public int processTail(@NotNull ModNavigator navigator, int tailOffset) { - CharSequence text = navigator.getDocument().getCharsSequence(); - if (text.length() > tailOffset + 1 && text.charAt(tailOffset) == ' ') { - char ch = text.charAt(tailOffset + 1); - if (ch == '@' || Character.isLetter(ch)) { - return tailOffset; - } - } - return super.processTail(navigator, tailOffset); - } - - @Override - public String toString() { - return "HUMBLE_SPACE_BEFORE_WORD"; - } - }; - - private static final ModNavigatorTailType DOT = new CharTailType('.'); - - private static final ModNavigatorTailType CASE_COLON = new CharTailType(':'); - - private static final ModNavigatorTailType EQUALS = new CharTailType('='); - - private static final ModNavigatorTailType COND_EXPR_COLON = new ModNavigatorTailType() { - @Override - public int processTail(@NotNull ModNavigator editor, int tailOffset) { - Document document = editor.getDocument(); - int textLength = document.getTextLength(); - CharSequence chars = document.getCharsSequence(); - - int afterWhitespace = CharArrayUtil.shiftForward(chars, tailOffset, " \n\t"); - if (afterWhitespace < textLength && chars.charAt(afterWhitespace) == ':') { - return moveCaret(editor, tailOffset, afterWhitespace - tailOffset + 1); - } - document.insertString(tailOffset, " : "); - return moveCaret(editor, tailOffset, 3); - } - - @Override - public String toString() { - return "COND_EXPR_COLON"; - } - }; - public static @NotNull TailType unknownType() { - return UNKNOWN; + return TailTypeFactory.getInstance().unknownType(); } public static @NotNull TailType noneType() { - return NONE; + return TailTypeFactory.getInstance().noneType(); } public static @NotNull TailType semicolonType() { - return SEMICOLON; + return TailTypeFactory.getInstance().semicolonType(); } /** * insert a space, overtype if already present */ public static @NotNull TailType spaceType() { - return SPACE; + return TailTypeFactory.getInstance().spaceType(); } /** * always insert a space */ public static @NotNull TailType insertSpaceType() { - return INSERT_SPACE; + return TailTypeFactory.getInstance().insertSpaceType(); } /** * insert a space unless there's one at the caret position already, followed by a word or '@' */ public static @NotNull TailType humbleSpaceBeforeWordType() { - return HUMBLE_SPACE_BEFORE_WORD; + return TailTypeFactory.getInstance().humbleSpaceBeforeWordType(); } public static @NotNull TailType dotType() { - return DOT; + return TailTypeFactory.getInstance().dotType(); } public static @NotNull TailType caseColonType() { - return CASE_COLON; + return TailTypeFactory.getInstance().caseColonType(); } public static @NotNull TailType equalsType() { - return EQUALS; + return TailTypeFactory.getInstance().equalsType(); } public static @NotNull TailType conditionalExpressionColonType() { - return COND_EXPR_COLON; + return TailTypeFactory.getInstance().conditionalExpressionColonType(); } public static @NotNull TailType charType(char aChar) { - return new CharTailType(aChar); + return TailTypeFactory.getInstance().charType(aChar); } public static @NotNull TailType charType(char aChar, boolean overwrite) { - return new CharTailType(aChar, overwrite); + return TailTypeFactory.getInstance().charType(aChar, overwrite); } } diff --git a/platform/analysis-impl/resources/META-INF/AnalysisImpl.analyzer.xml b/platform/analysis-impl/resources/META-INF/AnalysisImpl.analyzer.xml index 53ce48e70e51..1d09abdff7bc 100644 --- a/platform/analysis-impl/resources/META-INF/AnalysisImpl.analyzer.xml +++ b/platform/analysis-impl/resources/META-INF/AnalysisImpl.analyzer.xml @@ -19,5 +19,9 @@ + + + diff --git a/platform/analysis-impl/resources/META-INF/AnalysisImpl.xml b/platform/analysis-impl/resources/META-INF/AnalysisImpl.xml index 2ad5e2f7430c..a6cda3e794aa 100644 --- a/platform/analysis-impl/resources/META-INF/AnalysisImpl.xml +++ b/platform/analysis-impl/resources/META-INF/AnalysisImpl.xml @@ -36,6 +36,11 @@ + + + + + - - + + + + + + + + + + diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/completion/CompletionUtil.java b/platform/analysis-impl/src/com/intellij/codeInsight/completion/CompletionUtil.java index 99dbe5281b84..c195b19a68d5 100644 --- a/platform/analysis-impl/src/com/intellij/codeInsight/completion/CompletionUtil.java +++ b/platform/analysis-impl/src/com/intellij/codeInsight/completion/CompletionUtil.java @@ -1,4 +1,4 @@ -// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2026 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.TailTypes; @@ -35,20 +35,22 @@ import java.util.Iterator; import java.util.List; public final class CompletionUtil { - - private static final CompletionData ourGenericCompletionData = new CompletionData() { - { - CompletionVariant variant = new CompletionVariant(PsiElement.class, TrueFilter.INSTANCE); - variant.addCompletionFilter(TrueFilter.INSTANCE, TailTypes.noneType()); - registerVariant(variant); - } - }; + private static volatile CompletionData ourGenericCompletionData; public static final @NonNls String DUMMY_IDENTIFIER = CompletionInitializationContext.DUMMY_IDENTIFIER; public static final @NonNls String DUMMY_IDENTIFIER_TRIMMED = CompletionInitializationContext.DUMMY_IDENTIFIER_TRIMMED; @ApiStatus.Internal public static @Nullable CompletionData getCompletionDataByElement(@Nullable PsiElement position, @NotNull PsiFile originalFile) { if (position == null) return null; + if (ourGenericCompletionData == null) { + ourGenericCompletionData = new CompletionData() { + { + CompletionVariant variant = new CompletionVariant(PsiElement.class, TrueFilter.INSTANCE); + variant.addCompletionFilter(TrueFilter.INSTANCE, TailTypes.noneType()); + registerVariant(variant); + } + }; + } return ourGenericCompletionData; } diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/completion/CompletionVariant.java b/platform/analysis-impl/src/com/intellij/codeInsight/completion/CompletionVariant.java index a2ed86a163f3..aebcead450bf 100644 --- a/platform/analysis-impl/src/com/intellij/codeInsight/completion/CompletionVariant.java +++ b/platform/analysis-impl/src/com/intellij/codeInsight/completion/CompletionVariant.java @@ -1,4 +1,4 @@ -// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.codeInsight.completion; @@ -26,7 +26,6 @@ import java.util.Set; @ApiStatus.Internal @Deprecated(forRemoval = true) public class CompletionVariant { - protected static final TailType DEFAULT_TAIL_TYPE = TailTypes.spaceType(); private final Set myScopeClasses = new HashSet<>(); private ElementFilter myPosition; @@ -106,7 +105,7 @@ public class CompletionVariant { } public void addCompletion(@NonNls String keyword){ - addCompletion(keyword, DEFAULT_TAIL_TYPE); + addCompletion(keyword, TailTypes.spaceType()); } public void addCompletion(@NonNls String keyword, TailType tailType){ diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyTailType.kt b/platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyTailType.kt new file mode 100644 index 000000000000..40f38a89c88a --- /dev/null +++ b/platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyTailType.kt @@ -0,0 +1,38 @@ +// Copyright 2000-2026 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.TailType +import com.intellij.codeInsight.completion.serialization.TailTypeSerializer +import com.intellij.codeInsight.serialization.DescriptorConverter +import kotlinx.serialization.Serializable +import org.jetbrains.annotations.ApiStatus + +/** + * Marker interface for tail types that are safe to run on Frontend in Remote Development environment. + * + * Similar to [FrontendFriendlyInsertHandler], tail types implementing this interface + * must not contain any heavy computations, resolve, or index access. + * + * Must be registered in `plugin.xml` as `completion.frontendFriendlyTailType` extension point. + * + * To allow transferring FFTTs 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: + * ``` + * + * ``` + * + * @see FrontendFriendlyInsertHandler + * @see LookupElementWithEffectiveInsertHandler + */ +@Serializable(with = TailTypeSerializer::class) +@ApiStatus.Internal +interface FrontendFriendlyTailType + +/** + * Converter from target [TailType] implementation to [FrontendFriendlyTailType]. + * + * Similar to [InsertHandlerToFrontendFriendlyConverter]. + */ +@ApiStatus.Internal +interface TailTypeToFrontendFriendlyConverter : DescriptorConverter diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyTailTypeFactory.kt b/platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyTailTypeFactory.kt new file mode 100644 index 000000000000..354e46acce50 --- /dev/null +++ b/platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyTailTypeFactory.kt @@ -0,0 +1,40 @@ +// Copyright 2000-2026 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.TailType +import com.intellij.codeInsight.TailTypeFactory +import org.jetbrains.annotations.ApiStatus + +/** + * Frontend-friendly implementation of [TailTypeFactory][com.intellij.codeInsight.TailTypeFactory] + * that returns [FrontendFriendlyTailType] implementations. + * + * These implementations are safe to serialize and execute on the frontend + * in Remote Development environments. + */ +@ApiStatus.Internal +class FrontendFriendlyTailTypeFactory : TailTypeFactory { + override fun noneType(): TailType = NoneTailType + + override fun semicolonType(): TailType = FrontendFriendlyCharTailType(';') + + override fun spaceType(): TailType = FrontendFriendlyCharTailType(' ') + + override fun insertSpaceType(): TailType = FrontendFriendlyCharTailType(' ', false) + + override fun unknownType(): TailType = FrontendFriendlyUnknownTailType + + override fun humbleSpaceBeforeWordType(): TailType = HumbleSpaceBeforeWordTailType + + override fun dotType(): TailType = FrontendFriendlyCharTailType('.') + + override fun caseColonType(): TailType = FrontendFriendlyCharTailType(':') + + override fun equalsType(): TailType = FrontendFriendlyCharTailType('=') + + override fun conditionalExpressionColonType(): TailType = CondExprColonTailType + + override fun charType(char: Char): TailType = FrontendFriendlyCharTailType(char) + + override fun charType(char: Char, overwrite: Boolean): TailType = FrontendFriendlyCharTailType(char, overwrite) +} diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyTailTypes.kt b/platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyTailTypes.kt new file mode 100644 index 000000000000..3ec7ec8dbada --- /dev/null +++ b/platform/analysis-impl/src/com/intellij/codeInsight/completion/FrontendFriendlyTailTypes.kt @@ -0,0 +1,65 @@ +// Copyright 2000-2026 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.ModNavigatorTailType +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.ModNavigator +import com.intellij.util.text.CharArrayUtil +import kotlinx.serialization.Serializable + +@Serializable +internal data class FrontendFriendlyCharTailType( + val char: Char, + val overwrite: Boolean = true, +) : ModNavigatorTailType(), FrontendFriendlyTailType { + override fun processTail(navigator: ModNavigator, tailOffset: Int): Int { + return insertChar(navigator, tailOffset, char, overwrite) + } + + @Suppress("OVERRIDE_DEPRECATION") + override fun isApplicable(context: InsertionContext): Boolean { + return !context.shouldAddCompletionChar() || context.completionChar != char + } +} + +@Serializable +internal object NoneTailType : ModNavigatorTailType(), FrontendFriendlyTailType { + override fun processTail(navigator: ModNavigator, tailOffset: Int): Int = tailOffset +} + +@Serializable +internal object HumbleSpaceBeforeWordTailType : ModNavigatorTailType(), FrontendFriendlyTailType { + override fun processTail(navigator: ModNavigator, tailOffset: Int): Int { + val text = navigator.document.charsSequence + if (text.length > tailOffset + 1 && text[tailOffset] == ' ') { + val ch = text[tailOffset + 1] + if (ch == '@' || ch.isLetter()) { + return tailOffset + } + } + return insertChar(navigator, tailOffset, ' ', false) + } +} + +@Serializable +internal object CondExprColonTailType : ModNavigatorTailType(), FrontendFriendlyTailType { + override fun processTail(navigator: ModNavigator, tailOffset: Int): Int { + val document = navigator.document + val textLength = document.textLength + val chars = document.charsSequence + + val afterWhitespace = CharArrayUtil.shiftForward(chars, tailOffset, " \n\t") + if (afterWhitespace < textLength && chars[afterWhitespace] == ':') { + return moveCaret(navigator, tailOffset, afterWhitespace - tailOffset + 1) + } + document.insertString(tailOffset, " : ") + return moveCaret(navigator, tailOffset, 3) + } +} + +@Serializable +internal object FrontendFriendlyUnknownTailType : ModNavigatorTailType() { + override fun processTail(navigator: ModNavigator, tailOffset: Int): Int = tailOffset + override fun processTail(editor: Editor, tailOffset: Int): Int = tailOffset + override fun toString(): String = "UNKNOWN" +} diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/completion/StandardTailTypeConverters.kt b/platform/analysis-impl/src/com/intellij/codeInsight/completion/StandardTailTypeConverters.kt new file mode 100644 index 000000000000..5c0a1e4bc2ba --- /dev/null +++ b/platform/analysis-impl/src/com/intellij/codeInsight/completion/StandardTailTypeConverters.kt @@ -0,0 +1,23 @@ +// Copyright 2000-2026 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.CharTailType +import org.jetbrains.annotations.ApiStatus + +/** + * Converter for [CharTailType] to [FrontendFriendlyCharTailType]. + */ +@ApiStatus.Internal +class CharTailTypeConverter : TailTypeToFrontendFriendlyConverter { + override fun toDescriptor(target: CharTailType): FrontendFriendlyTailType? { + // CharTailType stores the char and overwrite flag in private fields + // We need to extract them - using reflection or toString() parsing + val str = target.toString() // "CharTailType:'c'" + if (!str.startsWith("CharTailType:'") || !str.endsWith("'")) { + return null + } + val char = str[str.length - 2] + // Default CharTailType uses overwrite=true + return FrontendFriendlyCharTailType(char, true) + } +} diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/completion/serialization/TailTypeSerializer.kt b/platform/analysis-impl/src/com/intellij/codeInsight/completion/serialization/TailTypeSerializer.kt new file mode 100644 index 000000000000..9641c497cd58 --- /dev/null +++ b/platform/analysis-impl/src/com/intellij/codeInsight/completion/serialization/TailTypeSerializer.kt @@ -0,0 +1,24 @@ +// Copyright 2000-2026 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.TailType +import com.intellij.codeInsight.completion.FrontendFriendlyTailType +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 [TailType]. + * + * It uses [EP_NAME] to collect serializers for all known [FrontendFriendlyTailType] implementations. + * + * @see InsertHandlerSerializer + */ +@ApiStatus.Internal +object TailTypeSerializer : ExtensionPointSerializer( + epName = EP_NAME, + descriptorClass = FrontendFriendlyTailType::class +) + +private val EP_NAME = ExtensionPointName("com.intellij.completion.frontendFriendlyTailType")