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")