diff --git a/java/java-impl/resources/META-INF/JavaPlugin.xml b/java/java-impl/resources/META-INF/JavaPlugin.xml
index 5c68d92c938d..c333c77e4652 100644
--- a/java/java-impl/resources/META-INF/JavaPlugin.xml
+++ b/java/java-impl/resources/META-INF/JavaPlugin.xml
@@ -1249,6 +1249,8 @@
+
+
diff --git a/java/java-impl/src/com/intellij/codeInsight/JavaTailTypes.java b/java/java-impl/src/com/intellij/codeInsight/JavaTailTypes.java
index 66a7d91578ef..320ad44faba0 100644
--- a/java/java-impl/src/com/intellij/codeInsight/JavaTailTypes.java
+++ b/java/java-impl/src/com/intellij/codeInsight/JavaTailTypes.java
@@ -55,69 +55,69 @@ public final class JavaTailTypes {
}
};
- public static final TailType SYNCHRONIZED_LPARENTH = new ParenthesesTailType() {
+ public static final ModNavigatorTailType SYNCHRONIZED_LPARENTH = new ParenthesesTailType() {
@Override
- protected boolean isSpaceBeforeParentheses(final CommonCodeStyleSettings styleSettings, final Editor editor, final int tailOffset) {
+ protected boolean isSpaceBeforeParentheses(final CommonCodeStyleSettings styleSettings, final int tailOffset) {
return styleSettings.SPACE_BEFORE_SYNCHRONIZED_PARENTHESES;
}
@Override
- protected boolean isSpaceWithinParentheses(final CommonCodeStyleSettings styleSettings, final Editor editor, final int tailOffset) {
+ protected boolean isSpaceWithinParentheses(final CommonCodeStyleSettings styleSettings, final int tailOffset) {
return styleSettings.SPACE_WITHIN_SYNCHRONIZED_PARENTHESES;
}
};
- public static final TailType CATCH_LPARENTH = new ParenthesesTailType() {
+ public static final ModNavigatorTailType CATCH_LPARENTH = new ParenthesesTailType() {
@Override
- protected boolean isSpaceBeforeParentheses(final CommonCodeStyleSettings styleSettings, final Editor editor, final int tailOffset) {
+ protected boolean isSpaceBeforeParentheses(final CommonCodeStyleSettings styleSettings, final int tailOffset) {
return styleSettings.SPACE_BEFORE_CATCH_PARENTHESES;
}
@Override
- protected boolean isSpaceWithinParentheses(final CommonCodeStyleSettings styleSettings, final Editor editor, final int tailOffset) {
+ protected boolean isSpaceWithinParentheses(final CommonCodeStyleSettings styleSettings, final int tailOffset) {
return styleSettings.SPACE_WITHIN_CATCH_PARENTHESES;
}
};
- public static final TailType SWITCH_LPARENTH = new ParenthesesTailType() {
+ public static final ModNavigatorTailType SWITCH_LPARENTH = new ParenthesesTailType() {
@Override
- protected boolean isSpaceBeforeParentheses(final CommonCodeStyleSettings styleSettings, final Editor editor, final int tailOffset) {
+ protected boolean isSpaceBeforeParentheses(final CommonCodeStyleSettings styleSettings, final int tailOffset) {
return styleSettings.SPACE_BEFORE_SWITCH_PARENTHESES;
}
@Override
- protected boolean isSpaceWithinParentheses(final CommonCodeStyleSettings styleSettings, final Editor editor, final int tailOffset) {
+ protected boolean isSpaceWithinParentheses(final CommonCodeStyleSettings styleSettings, final int tailOffset) {
return styleSettings.SPACE_WITHIN_SWITCH_PARENTHESES;
}
};
- public static final TailType WHILE_LPARENTH = new ParenthesesTailType() {
+ public static final ModNavigatorTailType WHILE_LPARENTH = new ParenthesesTailType() {
@Override
- protected boolean isSpaceBeforeParentheses(final CommonCodeStyleSettings styleSettings, final Editor editor, final int tailOffset) {
+ protected boolean isSpaceBeforeParentheses(final CommonCodeStyleSettings styleSettings, final int tailOffset) {
return styleSettings.SPACE_BEFORE_WHILE_PARENTHESES;
}
@Override
- protected boolean isSpaceWithinParentheses(final CommonCodeStyleSettings styleSettings, final Editor editor, final int tailOffset) {
+ protected boolean isSpaceWithinParentheses(final CommonCodeStyleSettings styleSettings, final int tailOffset) {
return styleSettings.SPACE_WITHIN_WHILE_PARENTHESES;
}
};
- public static final TailType FOR_LPARENTH = new ParenthesesTailType() {
+ public static final ModNavigatorTailType FOR_LPARENTH = new ParenthesesTailType() {
@Override
- protected boolean isSpaceBeforeParentheses(final CommonCodeStyleSettings styleSettings, final Editor editor, final int tailOffset) {
+ protected boolean isSpaceBeforeParentheses(final CommonCodeStyleSettings styleSettings, final int tailOffset) {
return styleSettings.SPACE_BEFORE_FOR_PARENTHESES;
}
@Override
- protected boolean isSpaceWithinParentheses(final CommonCodeStyleSettings styleSettings, final Editor editor, final int tailOffset) {
+ protected boolean isSpaceWithinParentheses(final CommonCodeStyleSettings styleSettings, final int tailOffset) {
return styleSettings.SPACE_WITHIN_FOR_PARENTHESES;
}
};
- public static final TailType IF_LPARENTH = new ParenthesesTailType() {
+ public static final ModNavigatorTailType IF_LPARENTH = new ParenthesesTailType() {
@Override
- protected boolean isSpaceBeforeParentheses(final CommonCodeStyleSettings styleSettings, final Editor editor, final int tailOffset) {
+ protected boolean isSpaceBeforeParentheses(final CommonCodeStyleSettings styleSettings, final int tailOffset) {
return styleSettings.SPACE_BEFORE_IF_PARENTHESES;
}
@Override
- protected boolean isSpaceWithinParentheses(final CommonCodeStyleSettings styleSettings, final Editor editor, final int tailOffset) {
+ protected boolean isSpaceWithinParentheses(final CommonCodeStyleSettings styleSettings, final int tailOffset) {
return styleSettings.SPACE_WITHIN_IF_PARENTHESES;
}
};
diff --git a/java/java-impl/src/com/intellij/codeInsight/completion/modcompletion/KeywordCompletionItem.java b/java/java-impl/src/com/intellij/codeInsight/completion/modcompletion/KeywordCompletionItem.java
new file mode 100644
index 000000000000..f2e9ce871bac
--- /dev/null
+++ b/java/java-impl/src/com/intellij/codeInsight/completion/modcompletion/KeywordCompletionItem.java
@@ -0,0 +1,53 @@
+// 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.modcompletion;
+
+import com.intellij.codeInsight.ModNavigatorTailType;
+import com.intellij.modcommand.ActionContext;
+import com.intellij.modcommand.ModPsiUpdater;
+import com.intellij.modcompletion.CompletionItemPresentation;
+import com.intellij.modcompletion.PsiUpdateCompletionItem;
+import com.intellij.openapi.util.NlsSafe;
+import com.intellij.openapi.util.text.MarkupText;
+import org.jetbrains.annotations.NotNullByDefault;
+
+/**
+ * A completion item for a Java keyword.
+ */
+@NotNullByDefault
+final class KeywordCompletionItem extends PsiUpdateCompletionItem {
+ private final @NlsSafe String myKeyword;
+ private final ModNavigatorTailType myTail;
+
+ KeywordCompletionItem(@NlsSafe String keyword, ModNavigatorTailType tail) {
+ myKeyword = keyword;
+ myTail = tail;
+ }
+
+ @Override
+ public String mainLookupString() {
+ return myKeyword;
+ }
+
+ @Override
+ public KeywordInfo contextObject() {
+ return new KeywordInfo(myKeyword);
+ }
+
+ @Override
+ public CompletionItemPresentation presentation() {
+ return new CompletionItemPresentation(MarkupText.plainText(myKeyword).highlightAll(MarkupText.Kind.STRONG));
+ }
+
+ @Override
+ public void update(ActionContext actionContext, InsertionContext insertionContext, ModPsiUpdater updater) {
+ myTail.processTail(actionContext.project(), updater, actionContext.offset());
+ }
+
+ public record KeywordInfo(String keyword) {
+ }
+
+ @Override
+ public String toString() {
+ return myKeyword;
+ }
+}
diff --git a/java/java-impl/src/com/intellij/codeInsight/completion/modcompletion/KeywordCompletionItemProvider.java b/java/java-impl/src/com/intellij/codeInsight/completion/modcompletion/KeywordCompletionItemProvider.java
new file mode 100644
index 000000000000..d78f76d289a5
--- /dev/null
+++ b/java/java-impl/src/com/intellij/codeInsight/completion/modcompletion/KeywordCompletionItemProvider.java
@@ -0,0 +1,125 @@
+// 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.modcompletion;
+
+import com.intellij.codeInsight.JavaTailTypes;
+import com.intellij.codeInsight.ModNavigatorTailType;
+import com.intellij.codeInsight.TailTypes;
+import com.intellij.java.syntax.parser.JavaKeywords;
+import com.intellij.modcompletion.CompletionItem;
+import com.intellij.modcompletion.CompletionItemProvider;
+import com.intellij.psi.*;
+import com.intellij.psi.filters.FilterPositionUtil;
+import com.intellij.psi.javadoc.PsiDocComment;
+import com.intellij.psi.templateLanguages.OuterLanguageElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import org.jetbrains.annotations.NotNullByDefault;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.function.Consumer;
+
+import static com.intellij.patterns.PsiJavaPatterns.psiAnnotation;
+import static com.intellij.patterns.PsiJavaPatterns.psiElement;
+import static com.intellij.patterns.StandardPatterns.string;
+
+/**
+ * A provider for Java keywords completion.
+ */
+@NotNullByDefault
+final class KeywordCompletionItemProvider implements CompletionItemProvider {
+ @Override
+ public void provideItems(CompletionContext context, Consumer sink) {
+ PsiElement element = context.element();
+ if (!context.isSmart()) {
+ if (canAddKeywords(element)) {
+ if (isStatementPosition(element)) {
+ sink.accept(new KeywordCompletionItem(JavaKeywords.SWITCH, JavaTailTypes.SWITCH_LPARENTH));
+ sink.accept(new KeywordCompletionItem(JavaKeywords.WHILE, JavaTailTypes.WHILE_LPARENTH));
+ //sink.accept(new KeywordCompletionItem(JavaKeywords.DO, JavaTailTypes.DO_LBRACE));
+ sink.accept(new KeywordCompletionItem(JavaKeywords.FOR, JavaTailTypes.FOR_LPARENTH));
+ sink.accept(new KeywordCompletionItem(JavaKeywords.IF, JavaTailTypes.IF_LPARENTH));
+ //sink.accept(new KeywordCompletionItem(JavaKeywords.TRY, JavaTailTypes.TRY_LBRACE));
+ sink.accept(new KeywordCompletionItem(JavaKeywords.SYNCHRONIZED, JavaTailTypes.SYNCHRONIZED_LPARENTH));
+ sink.accept(new KeywordCompletionItem(JavaKeywords.THROW, (ModNavigatorTailType)TailTypes.insertSpaceType()));
+ sink.accept(new KeywordCompletionItem(JavaKeywords.NEW, (ModNavigatorTailType)TailTypes.insertSpaceType()));
+ }
+ }
+ }
+ }
+
+ private static boolean canAddKeywords(PsiElement position) {
+ if (PsiTreeUtil.getNonStrictParentOfType(position, PsiLiteralExpression.class, PsiComment.class) != null) {
+ return false;
+ }
+
+ if (psiElement().afterLeaf("::").accepts(position)) {
+ return false;
+ }
+ return true;
+ }
+
+ private static boolean isStatementPosition(PsiElement position) {
+ if (psiElement()
+ .withSuperParent(2, PsiConditionalExpression.class)
+ .andNot(psiElement().insideStarting(psiElement(PsiConditionalExpression.class)))
+ .accepts(position)) {
+ return false;
+ }
+
+ if (isEndOfBlock(position) &&
+ PsiTreeUtil.getParentOfType(position, PsiCodeBlock.class, true, PsiMember.class) != null) {
+ return !isForLoopMachinery(position);
+ }
+
+ if (psiElement().withParents(PsiReferenceExpression.class, PsiExpressionStatement.class, PsiIfStatement.class).andNot(
+ psiElement().afterLeaf(".")).accepts(position)) {
+ PsiElement stmt = position.getParent().getParent();
+ PsiIfStatement ifStatement = (PsiIfStatement)stmt.getParent();
+ return ifStatement.getElseBranch() == stmt || ifStatement.getThenBranch() == stmt;
+ }
+
+ return false;
+ }
+
+ static boolean isEndOfBlock(PsiElement element) {
+ PsiElement prev = prevSignificantLeaf(element);
+ if (prev == null) {
+ PsiFile file = element.getContainingFile();
+ return !(file instanceof PsiCodeFragment) || isStatementCodeFragment(file);
+ }
+
+ if (psiElement().inside(psiAnnotation()).accepts(prev)) return false;
+
+ if (prev instanceof OuterLanguageElement) return true;
+ if (psiElement().withText(string().oneOf("{", "}", ";", ":", "else")).accepts(prev)) return true;
+ if (prev.textMatches(")")) {
+ PsiElement parent = prev.getParent();
+ if (parent instanceof PsiParameterList) {
+ return PsiTreeUtil.getParentOfType(PsiTreeUtil.prevVisibleLeaf(element), PsiDocComment.class) != null;
+ }
+
+ return !(parent instanceof PsiExpressionList || parent instanceof PsiTypeCastExpression
+ || parent instanceof PsiRecordHeader);
+ }
+
+ return false;
+ }
+
+ private static @Nullable PsiElement prevSignificantLeaf(PsiElement position) {
+ return FilterPositionUtil.searchNonSpaceNonCommentBack(position);
+ }
+
+ private static boolean isStatementCodeFragment(PsiFile file) {
+ return file instanceof JavaCodeFragment &&
+ !(file instanceof PsiExpressionCodeFragment ||
+ file instanceof PsiJavaCodeReferenceCodeFragment ||
+ file instanceof PsiTypeCodeFragment);
+ }
+
+ private static boolean isForLoopMachinery(PsiElement position) {
+ PsiStatement statement = PsiTreeUtil.getParentOfType(position, PsiStatement.class);
+ if (statement == null) return false;
+
+ return statement instanceof PsiForStatement ||
+ statement.getParent() instanceof PsiForStatement && statement != ((PsiForStatement)statement.getParent()).getBody();
+ }
+}
diff --git a/java/java-impl/src/com/intellij/codeInsight/completion/simple/ParenthesesTailType.java b/java/java-impl/src/com/intellij/codeInsight/completion/simple/ParenthesesTailType.java
index 15a8ec015b38..069cc502225c 100644
--- a/java/java-impl/src/com/intellij/codeInsight/completion/simple/ParenthesesTailType.java
+++ b/java/java-impl/src/com/intellij/codeInsight/completion/simple/ParenthesesTailType.java
@@ -3,23 +3,35 @@
package com.intellij.codeInsight.completion.simple;
import com.intellij.application.options.CodeStyle;
-import com.intellij.codeInsight.TailType;
-import com.intellij.codeInsight.editorActions.TabOutScopesTracker;
+import com.intellij.codeInsight.ModNavigatorTailType;
+import com.intellij.lang.Language;
import com.intellij.openapi.editor.Document;
-import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.ModNavigator;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
+import com.intellij.psi.util.PsiUtilCore;
import org.jetbrains.annotations.NotNull;
-public abstract class ParenthesesTailType extends TailType {
+public abstract class ParenthesesTailType extends ModNavigatorTailType {
- protected abstract boolean isSpaceBeforeParentheses(CommonCodeStyleSettings styleSettings, Editor editor, final int tailOffset);
+ protected boolean isSpaceBeforeParentheses(CommonCodeStyleSettings styleSettings, final int tailOffset) {
+ return false;
+ }
- protected abstract boolean isSpaceWithinParentheses(CommonCodeStyleSettings styleSettings, Editor editor, final int tailOffset);
+ protected boolean isSpaceWithinParentheses(CommonCodeStyleSettings styleSettings, final int tailOffset) {
+ return false;
+ }
@Override
- public int processTail(final @NotNull Editor editor, int tailOffset) {
- CommonCodeStyleSettings styleSettings = CodeStyle.getLocalLanguageSettings(editor, tailOffset);
- if (isSpaceBeforeParentheses(styleSettings, editor, tailOffset)) {
+ public int processTail(@NotNull Project project, @NotNull ModNavigator editor, int tailOffset) {
+ PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
+ assert psiFile != null;
+ Language language = PsiUtilCore.getLanguageAtOffset(psiFile, tailOffset);
+ CommonCodeStyleSettings styleSettings = CodeStyle.getLanguageSettings(psiFile, language);
+ if (isSpaceBeforeParentheses(styleSettings, tailOffset)) {
tailOffset = insertChar(editor, tailOffset, ' ');
}
Document document = editor.getDocument();
@@ -28,7 +40,7 @@ public abstract class ParenthesesTailType extends TailType {
}
tailOffset = insertChar(editor, tailOffset, '(');
- if (isSpaceWithinParentheses(styleSettings, editor, tailOffset)) {
+ if (isSpaceWithinParentheses(styleSettings, tailOffset)) {
tailOffset = insertChar(editor, tailOffset, ' ');
tailOffset = insertChar(editor, tailOffset, ' ');
tailOffset = insertChar(editor, tailOffset, ')');
@@ -36,7 +48,7 @@ public abstract class ParenthesesTailType extends TailType {
} else {
tailOffset = insertChar(editor, tailOffset, ')');
moveCaret(editor, tailOffset, -1);
- TabOutScopesTracker.getInstance().registerEmptyScopeAtCaret(editor);
+ editor.registerTabOut(TextRange.from(editor.getCaretOffset(), 0), editor.getCaretOffset() + 1);
}
return tailOffset;
}
diff --git a/platform/analysis-api/api-dump-unreviewed.txt b/platform/analysis-api/api-dump-unreviewed.txt
index 5dd199d5e322..516ec0d20ef0 100644
--- a/platform/analysis-api/api-dump-unreviewed.txt
+++ b/platform/analysis-api/api-dump-unreviewed.txt
@@ -96,13 +96,11 @@ com.intellij.codeHighlighting.HighlightDisplayLevelColoredIcon
- a:getColor():java.awt.Color
- a:getIcon():javax.swing.Icon
c:com.intellij.codeInsight.CharTailType
-- com.intellij.codeInsight.TailType
- (C):V
- (C,Z):V
- equals(java.lang.Object):Z
- hashCode():I
- isApplicable(com.intellij.codeInsight.completion.InsertionContext):Z
-- processTail(com.intellij.openapi.editor.Editor,I):I
a:com.intellij.codeInsight.TailType
- sf:CASE_COLON:com.intellij.codeInsight.TailType
- sf:DOT:com.intellij.codeInsight.TailType
diff --git a/platform/analysis-api/api-dump.txt b/platform/analysis-api/api-dump.txt
index 8752c8168fc2..18f99201fc26 100644
--- a/platform/analysis-api/api-dump.txt
+++ b/platform/analysis-api/api-dump.txt
@@ -1,3 +1,15 @@
+c:com.intellij.codeInsight.CharTailType
+- com.intellij.codeInsight.ModNavigatorTailType
+- processTail(com.intellij.openapi.project.Project,com.intellij.openapi.editor.ModNavigator,I):I
+a:com.intellij.codeInsight.ModNavigatorTailType
+- com.intellij.codeInsight.TailType
+- ():V
+- processTail(com.intellij.openapi.editor.Editor,I):I
+- a:processTail(com.intellij.openapi.project.Project,com.intellij.openapi.editor.ModNavigator,I):I
+a:com.intellij.codeInsight.TailType
+- s:insertChar(com.intellij.openapi.editor.ModNavigator,I,C):I
+- s:insertChar(com.intellij.openapi.editor.ModNavigator,I,C,Z):I
+- ps:moveCaret(com.intellij.openapi.editor.ModNavigator,I,I):I
com.intellij.codeInsight.completion.BatchConsumer
- com.intellij.util.Consumer
- endBatch():V
diff --git a/platform/analysis-api/resources/messages/AnalysisBundle.properties b/platform/analysis-api/resources/messages/AnalysisBundle.properties
index 96e2cbcfae51..ad12c4d74be3 100644
--- a/platform/analysis-api/resources/messages/AnalysisBundle.properties
+++ b/platform/analysis-api/resources/messages/AnalysisBundle.properties
@@ -255,3 +255,4 @@ modcommand.executor.cannot.rename.file=Cannot rename ''{0}'' into ''{1}'': {2}
modcommand.executor.unable.to.find.the.new.file=Unable to find the new file {0}
modcommand.executor.modification.of.guarded.region=Action tries to modify guarded region
preview.binary.content=(binary content)
+complete=Complete
diff --git a/platform/analysis-api/src/com/intellij/codeInsight/CharTailType.java b/platform/analysis-api/src/com/intellij/codeInsight/CharTailType.java
index b601e1072e4c..a3dd306dd9ac 100644
--- a/platform/analysis-api/src/com/intellij/codeInsight/CharTailType.java
+++ b/platform/analysis-api/src/com/intellij/codeInsight/CharTailType.java
@@ -2,7 +2,8 @@
package com.intellij.codeInsight;
import com.intellij.codeInsight.completion.InsertionContext;
-import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.ModNavigator;
+import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
@@ -10,7 +11,7 @@ import org.jetbrains.annotations.NotNull;
* Use {@link TailTypes#charType(char)} factory method instead of constructor to avoid possible deadlock
* until deprecated static fields are not removed from {@link TailType},
*/
-public class CharTailType extends TailType {
+public class CharTailType extends ModNavigatorTailType {
private final char myChar;
private final boolean myOverwrite;
@@ -29,8 +30,8 @@ public class CharTailType extends TailType {
}
@Override
- public int processTail(final @NotNull Editor editor, final int tailOffset) {
- return insertChar(editor, tailOffset, myChar, myOverwrite);
+ public int processTail(@NotNull Project project, @NotNull ModNavigator navigator, int tailOffset) {
+ return insertChar(navigator, tailOffset, myChar, myOverwrite);
}
@Override
diff --git a/platform/analysis-api/src/com/intellij/codeInsight/ModNavigatorTailType.java b/platform/analysis-api/src/com/intellij/codeInsight/ModNavigatorTailType.java
new file mode 100644
index 000000000000..0a166eae1aa6
--- /dev/null
+++ b/platform/analysis-api/src/com/intellij/codeInsight/ModNavigatorTailType.java
@@ -0,0 +1,27 @@
+// 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;
+
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.ModNavigator;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+
+/**
+ * A base class for tail types that use {@link ModNavigator} instead of {@link Editor} to insert the tail.
+ */
+public abstract class ModNavigatorTailType extends TailType {
+ @Override
+ public int processTail(final @NotNull Editor editor, int tailOffset) {
+ return processTail(Objects.requireNonNull(editor.getProject()), editor.asPsiNavigator(), tailOffset);
+ }
+
+ /**
+ * @param project current project
+ * @param navigator {@link ModNavigator} to use
+ * @param tailOffset tail offset
+ * @return new tail offset
+ */
+ public abstract int processTail(@NotNull Project project, @NotNull ModNavigator navigator, int tailOffset);
+}
diff --git a/platform/analysis-api/src/com/intellij/codeInsight/TailType.java b/platform/analysis-api/src/com/intellij/codeInsight/TailType.java
index 83e922c243d6..b9ff8fe690de 100644
--- a/platform/analysis-api/src/com/intellij/codeInsight/TailType.java
+++ b/platform/analysis-api/src/com/intellij/codeInsight/TailType.java
@@ -2,9 +2,9 @@
package com.intellij.codeInsight;
import com.intellij.codeInsight.completion.InsertionContext;
-import com.intellij.openapi.editor.CaretModel;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.ModNavigator;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiDocumentManager;
@@ -39,7 +39,15 @@ public abstract class TailType {
return insertChar(editor, tailOffset, c, true);
}
+ public static int insertChar(@NotNull ModNavigator editor, int tailOffset, char c) {
+ return insertChar(editor, tailOffset, c, true);
+ }
+
public static int insertChar(@NotNull Editor editor, int tailOffset, char c, boolean overwrite) {
+ return insertChar(editor.asPsiNavigator(), tailOffset, c, overwrite);
+ }
+
+ public static int insertChar(@NotNull ModNavigator editor, int tailOffset, char c, boolean overwrite) {
Document document = editor.getDocument();
int textLength = document.getTextLength();
CharSequence chars = document.getCharsSequence();
@@ -54,9 +62,12 @@ public abstract class TailType {
}
protected static int moveCaret(@NotNull Editor editor, int tailOffset, int delta) {
- CaretModel model = editor.getCaretModel();
- if (model.getOffset() == tailOffset) {
- model.moveToOffset(tailOffset + delta);
+ return moveCaret(editor.asPsiNavigator(), tailOffset, delta);
+ }
+
+ protected static int moveCaret(@NotNull ModNavigator editor, int tailOffset, int delta) {
+ if (editor.getCaretOffset() == tailOffset) {
+ editor.moveCaretTo(tailOffset + delta);
}
return tailOffset + delta;
}
diff --git a/platform/analysis-api/src/com/intellij/codeInsight/TailTypes.java b/platform/analysis-api/src/com/intellij/codeInsight/TailTypes.java
index 810eb2d9e03e..1dc5ce3a23fc 100644
--- a/platform/analysis-api/src/com/intellij/codeInsight/TailTypes.java
+++ b/platform/analysis-api/src/com/intellij/codeInsight/TailTypes.java
@@ -4,13 +4,20 @@ package com.intellij.codeInsight;
import com.intellij.codeInsight.completion.InsertionContext;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.ModNavigator;
+import com.intellij.openapi.project.Project;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
public final class TailTypes {
private TailTypes() { }
- private static final TailType UNKNOWN = new TailType() {
+ private static final ModNavigatorTailType UNKNOWN = new ModNavigatorTailType() {
+ @Override
+ public int processTail(@NotNull Project project, @NotNull ModNavigator navigator, int tailOffset) {
+ return tailOffset;
+ }
+
@Override
public int processTail(final @NotNull Editor editor, final int tailOffset) {
return tailOffset;
@@ -22,7 +29,12 @@ public final class TailTypes {
}
};
- private static final TailType NONE = new TailType() {
+ private static final ModNavigatorTailType NONE = new ModNavigatorTailType() {
+ @Override
+ public int processTail(@NotNull Project project, @NotNull ModNavigator navigator, int tailOffset) {
+ return tailOffset;
+ }
+
@Override
public int processTail(final @NotNull Editor editor, final int tailOffset) {
return tailOffset;
@@ -34,13 +46,13 @@ public final class TailTypes {
}
};
- private static final TailType SEMICOLON = new CharTailType(';');
+ private static final ModNavigatorTailType SEMICOLON = new CharTailType(';');
- private static final TailType SPACE = new CharTailType(' ');
+ private static final ModNavigatorTailType SPACE = new CharTailType(' ');
- private static final TailType INSERT_SPACE = new CharTailType(' ', false);
+ private static final ModNavigatorTailType INSERT_SPACE = new CharTailType(' ', false);
- private static final TailType HUMBLE_SPACE_BEFORE_WORD = new CharTailType(' ', false) {
+ private static final ModNavigatorTailType HUMBLE_SPACE_BEFORE_WORD = new CharTailType(' ', false) {
@Override
public boolean isApplicable(@NotNull InsertionContext context) {
CharSequence text = context.getDocument().getCharsSequence();
@@ -60,15 +72,15 @@ public final class TailTypes {
}
};
- private static final TailType DOT = new CharTailType('.');
+ private static final ModNavigatorTailType DOT = new CharTailType('.');
- private static final TailType CASE_COLON = new CharTailType(':');
+ private static final ModNavigatorTailType CASE_COLON = new CharTailType(':');
- private static final TailType EQUALS = new CharTailType('=');
+ private static final ModNavigatorTailType EQUALS = new CharTailType('=');
- private static final TailType COND_EXPR_COLON = new TailType() {
+ private static final ModNavigatorTailType COND_EXPR_COLON = new ModNavigatorTailType() {
@Override
- public int processTail(final @NotNull Editor editor, final int tailOffset) {
+ public int processTail(@NotNull Project project, @NotNull ModNavigator editor, int tailOffset) {
Document document = editor.getDocument();
int textLength = document.getTextLength();
CharSequence chars = document.getCharsSequence();
diff --git a/platform/analysis-api/src/com/intellij/codeInsight/completion/CompletionContributor.java b/platform/analysis-api/src/com/intellij/codeInsight/completion/CompletionContributor.java
index efff6748b4ee..61d7b1790db0 100644
--- a/platform/analysis-api/src/com/intellij/codeInsight/completion/CompletionContributor.java
+++ b/platform/analysis-api/src/com/intellij/codeInsight/completion/CompletionContributor.java
@@ -6,6 +6,7 @@ import com.intellij.codeInsight.lookup.LookupElementPresentation;
import com.intellij.lang.Language;
import com.intellij.lang.LanguageExtension;
import com.intellij.lang.LanguageExtensionWithAny;
+import com.intellij.modcompletion.CompletionItemProvider;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.extensions.ExtensionPointName;
@@ -14,6 +15,7 @@ import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.PossiblyDumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.NlsContexts;
+import com.intellij.openapi.util.registry.Registry;
import com.intellij.patterns.ElementPattern;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
@@ -232,7 +234,12 @@ public abstract class CompletionContributor implements PossiblyDumbAware {
}
public static @NotNull List forLanguage(@NotNull Language language) {
- return INSTANCE.forKey(language);
+ List contributors = INSTANCE.forKey(language);
+ if (Registry.is("ide.completion.modcommand")) {
+ contributors =
+ ContainerUtil.concat(ContainerUtil.map(CompletionItemProvider.forLanguage(language), CompletionItemContributor::new), contributors);
+ }
+ return contributors;
}
public static @NotNull List forLanguageHonorDumbness(@NotNull Language language, @NotNull Project project) {
diff --git a/platform/analysis-api/src/com/intellij/codeInsight/completion/CompletionItemContributor.java b/platform/analysis-api/src/com/intellij/codeInsight/completion/CompletionItemContributor.java
new file mode 100644
index 000000000000..efbdac15a551
--- /dev/null
+++ b/platform/analysis-api/src/com/intellij/codeInsight/completion/CompletionItemContributor.java
@@ -0,0 +1,40 @@
+// 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.modcommand.ActionContext;
+import com.intellij.modcompletion.CompletionItemProvider;
+import com.intellij.openapi.diagnostic.ReportingClassSubstitutor;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A wrapper for {@link CompletionItemProvider} to be used as a {@link CompletionContributor}.
+ */
+final class CompletionItemContributor extends CompletionContributor implements ReportingClassSubstitutor {
+ private final CompletionItemProvider myProvider;
+
+ @SuppressWarnings("NonDefaultConstructor")
+ CompletionItemContributor(CompletionItemProvider provider) {
+ myProvider = provider;
+ }
+
+ @Override
+ public void fillCompletionVariants(@NotNull CompletionParameters parameters, @NotNull CompletionResultSet result) {
+ ActionContext actionContext = ActionContext.from(parameters.getEditor(), parameters.getOriginalFile())
+ .withElement(parameters.getPosition());
+ CompletionItemProvider.CompletionContext context = new CompletionItemProvider.CompletionContext(
+ actionContext, result.getPrefixMatcher().getPrefix(), parameters.getInvocationCount(), parameters.getCompletionType());
+ myProvider.provideItems(context, item -> {
+ result.addElement(new CompletionItemLookupElement(item));
+ });
+ }
+
+ @Override
+ public @NotNull Class> getSubstitutedClass() {
+ return myProvider.getClass();
+ }
+
+ @Override
+ public String toString() {
+ return "Adapter for " + myProvider;
+ }
+}
diff --git a/platform/analysis-api/src/com/intellij/codeInsight/completion/CompletionItemLookupElement.java b/platform/analysis-api/src/com/intellij/codeInsight/completion/CompletionItemLookupElement.java
new file mode 100644
index 000000000000..9be87142091e
--- /dev/null
+++ b/platform/analysis-api/src/com/intellij/codeInsight/completion/CompletionItemLookupElement.java
@@ -0,0 +1,84 @@
+// 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.analysis.AnalysisBundle;
+import com.intellij.codeInsight.lookup.Lookup;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementPresentation;
+import com.intellij.modcommand.ActionContext;
+import com.intellij.modcommand.ModCommandExecutor;
+import com.intellij.modcompletion.CompletionItem;
+import com.intellij.modcompletion.CompletionItemPresentation;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNullByDefault;
+import org.jetbrains.annotations.Unmodifiable;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * A wrapper around {@link CompletionItem} that adapts it to {@link LookupElement}.
+ */
+@NotNullByDefault
+@ApiStatus.Internal
+public final class CompletionItemLookupElement extends LookupElement {
+ private final CompletionItem item;
+
+ CompletionItemLookupElement(CompletionItem item) {
+ this.item = item;
+ }
+
+ public CompletionItem item() {
+ return item;
+ }
+
+ @Override
+ public String getLookupString() {
+ return item.mainLookupString();
+ }
+
+ @Override
+ public @Unmodifiable Set getAllLookupStrings() {
+ Set strings = item.additionalLookupStrings();
+ return strings.isEmpty() ? Set.of(item.mainLookupString()) :
+ Stream.concat(Stream.of(item.mainLookupString()), strings.stream()).collect(Collectors.toSet());
+ }
+
+ @Override
+ public Object getObject() {
+ return item.contextObject();
+ }
+
+ @Override
+ public void renderElement(LookupElementPresentation presentation) {
+ CompletionItemPresentation itemPresentation = item.presentation();
+ // TODO: apply styles when possible
+ presentation.setItemText(itemPresentation.mainText().toText());
+ presentation.setTailText(" (MC)");
+ presentation.setTypeText(itemPresentation.detailText().toText());
+ }
+
+ @Override
+ public boolean requiresCommittedDocuments() {
+ return false;
+ }
+
+ @Override
+ public void handleInsert(InsertionContext context) {
+ CompletionItem.InsertionContext insertionContext = new CompletionItem.InsertionContext(
+ context.getCompletionChar() == Lookup.REPLACE_SELECT_CHAR ?
+ CompletionItem.InsertionMode.OVERWRITE : CompletionItem.InsertionMode.INSERT,
+ context.getCompletionChar());
+ ActionContext actionContext = ActionContext.from(context.getEditor(), context.getFile());
+ ModCommandExecutor.executeInteractively(
+ actionContext,
+ AnalysisBundle.message("complete"), context.getEditor(),
+ () -> item.perform(actionContext, insertionContext));
+ }
+
+ @Override
+ public String toString() {
+ return "Adapter for " + item;
+ }
+}
diff --git a/platform/analysis-api/src/com/intellij/modcompletion/CompletionItem.java b/platform/analysis-api/src/com/intellij/modcompletion/CompletionItem.java
new file mode 100644
index 000000000000..9cb94cef8dc1
--- /dev/null
+++ b/platform/analysis-api/src/com/intellij/modcompletion/CompletionItem.java
@@ -0,0 +1,66 @@
+// 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.modcompletion;
+
+import com.intellij.modcommand.ActionContext;
+import com.intellij.modcommand.ModCommand;
+import org.jetbrains.annotations.NotNullByDefault;
+
+import java.util.Set;
+
+/**
+ * An item in the code completion list
+ */
+@NotNullByDefault
+public interface CompletionItem {
+ /**
+ * @return the string searched for
+ */
+ String mainLookupString();
+
+ /**
+ * @return set of additional lookup strings, if necessary
+ */
+ default Set additionalLookupStrings() {
+ return Set.of();
+ }
+
+ /**
+ * @return a context object which could be used for item weighing
+ */
+ Object contextObject();
+
+ /**
+ * @return a presentation of the completion item
+ */
+ CompletionItemPresentation presentation();
+
+ /**
+ * @param actionContext action context where the completion is performed
+ * @param insertionContext an insertion context, which describes how exactly the user invoked the completion
+ * @return the command to perform the completion (e.g., insert the lookup string)
+ */
+ ModCommand perform(ActionContext actionContext, InsertionContext insertionContext);
+
+ /**
+ * Context for the item insertion
+ *
+ * @param mode whether to insert or overwrite the existing text
+ * @param insertionCharacter the character used to finish the completion
+ */
+ record InsertionContext(InsertionMode mode, char insertionCharacter) {
+
+ }
+
+ enum InsertionMode {
+ /**
+ * Insert mode: we should insert the text to the caret position
+ */
+ INSERT,
+
+ /**
+ * Overwrite mode: we should overwrite the existing text at the caret position
+ * The exact behavior (e.g. overwrite to the end of current word) is up to the specific completion item.
+ */
+ OVERWRITE
+ }
+}
diff --git a/platform/analysis-api/src/com/intellij/modcompletion/CompletionItemPresentation.java b/platform/analysis-api/src/com/intellij/modcompletion/CompletionItemPresentation.java
new file mode 100644
index 000000000000..dc72773df381
--- /dev/null
+++ b/platform/analysis-api/src/com/intellij/modcompletion/CompletionItemPresentation.java
@@ -0,0 +1,42 @@
+// 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.modcompletion;
+
+import com.intellij.openapi.util.text.MarkupText;
+import org.jetbrains.annotations.NotNullByDefault;
+
+/**
+ * Visual representation of {@link CompletionItem}.
+ *
+ * @param mainText main text describing the completion item (usually the same text that will be inserted, with optional suffix information)
+ * @param detailText optional text describing the completion item in more detail (like method type, etc.)
+ */
+@NotNullByDefault
+public record CompletionItemPresentation(
+ MarkupText mainText,
+ MarkupText detailText
+) {
+ /**
+ * Creates a presentation with the given main text and an empty detail text.
+ *
+ * @param mainText main text to use
+ */
+ public CompletionItemPresentation(MarkupText mainText) {
+ this(mainText, MarkupText.empty());
+ }
+
+ /**
+ * @param mainText new main text
+ * @return a new presentation with the given main text and the same detail text
+ */
+ public CompletionItemPresentation withMainText(MarkupText mainText) {
+ return new CompletionItemPresentation(mainText, detailText);
+ }
+
+ /**
+ * @param detailText new detail text
+ * @return a new presentation with the same main text and the given detail text
+ */
+ public CompletionItemPresentation withDetailText(MarkupText detailText) {
+ return new CompletionItemPresentation(mainText, detailText);
+ }
+}
diff --git a/platform/analysis-api/src/com/intellij/modcompletion/CompletionItemProvider.java b/platform/analysis-api/src/com/intellij/modcompletion/CompletionItemProvider.java
new file mode 100644
index 000000000000..0345fd3243d7
--- /dev/null
+++ b/platform/analysis-api/src/com/intellij/modcompletion/CompletionItemProvider.java
@@ -0,0 +1,68 @@
+// 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.modcompletion;
+
+import com.intellij.codeInsight.completion.CompletionType;
+import com.intellij.lang.Language;
+import com.intellij.lang.LanguageExtension;
+import com.intellij.modcommand.ActionContext;
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.NotNullByDefault;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * A language-specific provider for {@link CompletionItem} completion options
+ */
+@NotNullByDefault
+public interface CompletionItemProvider {
+ LanguageExtension EP_NAME = new LanguageExtension<>("com.intellij.modcompletion.completionItemProvider");
+
+ /**
+ * Provide completion items for given context
+ *
+ * @param context context to use
+ * @param sink a consumer to pass completion items to
+ */
+ void provideItems(CompletionContext context, Consumer sink);
+
+ /**
+ * @param language language to get providers for
+ * @return language-specific completion providers
+ */
+ static List forLanguage(Language language) {
+ return EP_NAME.forKey(language);
+ }
+
+ /**
+ * Completion context
+ *
+ * @param context an action context to use
+ * @param prefix current completion prefix
+ * @param invocationCount invocation count (0 = auto-popup)
+ * @param type completion type
+ */
+ record CompletionContext(ActionContext context, String prefix, int invocationCount, CompletionType type) {
+ /**
+ * @return a context PSI element
+ */
+ public PsiElement element() {
+ return Objects.requireNonNull(context.element());
+ }
+
+ /**
+ * @return true if the smart completion is invoked (Ctrl+Shift+Space)
+ */
+ public boolean isSmart() {
+ return type == CompletionType.SMART;
+ }
+
+ /**
+ * @return true if the basic completion is invoked (Ctrl+Space)
+ */
+ public boolean isBasic() {
+ return type == CompletionType.BASIC;
+ }
+ }
+}
diff --git a/platform/analysis-api/src/com/intellij/modcompletion/PsiUpdateCompletionItem.java b/platform/analysis-api/src/com/intellij/modcompletion/PsiUpdateCompletionItem.java
new file mode 100644
index 000000000000..4b395c4487eb
--- /dev/null
+++ b/platform/analysis-api/src/com/intellij/modcompletion/PsiUpdateCompletionItem.java
@@ -0,0 +1,49 @@
+// 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.modcompletion;
+
+import com.intellij.modcommand.ActionContext;
+import com.intellij.modcommand.ModCommand;
+import com.intellij.modcommand.ModPsiUpdater;
+import com.intellij.openapi.editor.Document;
+import org.jetbrains.annotations.NotNullByDefault;
+
+/**
+ * A {@link CompletionItem} that performs an update via {@link ModCommand#psiUpdate} API. The overwrite mode is handled automatically.
+ */
+@NotNullByDefault
+public abstract class PsiUpdateCompletionItem implements CompletionItem {
+ @Override
+ public ModCommand perform(ActionContext actionContext, InsertionContext insertionContext) {
+ return ModCommand.psiUpdate(actionContext, updater -> {
+ if (insertionContext.mode() == InsertionMode.OVERWRITE) {
+ updater.getDocument().deleteString(
+ actionContext.offset(), calculateEndOffsetForOverwrite(updater.getDocument(), actionContext.offset()));
+ }
+ update(actionContext, insertionContext, updater);
+ });
+ }
+
+ /**
+ * @param document the document to use
+ * @param startFrom a caret position
+ * @return the position of the identifier end. The default implementation uses Java identifier rules.
+ * Override this method to use different rules for ID boundary.
+ */
+ protected int calculateEndOffsetForOverwrite(Document document, int startFrom) {
+ final CharSequence text = document.getCharsSequence();
+ int idEnd = startFrom;
+ while (idEnd < text.length() && Character.isJavaIdentifierPart(text.charAt(idEnd))) {
+ idEnd++;
+ }
+ return idEnd;
+ }
+
+ /**
+ * Performs PSI/document update of the file copy to generate a final {@link ModCommand}.
+ *
+ * @param actionContext context of the action
+ * @param insertionContext context of the insertion (like which character was used to finish the completion)
+ * @param updater an updater to use
+ */
+ public abstract void update(ActionContext actionContext, InsertionContext insertionContext, ModPsiUpdater updater);
+}
diff --git a/platform/core-api/api-dump-experimental.txt b/platform/core-api/api-dump-experimental.txt
index 1f4d8130b31a..60059d5a4927 100644
--- a/platform/core-api/api-dump-experimental.txt
+++ b/platform/core-api/api-dump-experimental.txt
@@ -249,6 +249,7 @@ f:com.intellij.openapi.components.ServicesKt
- a:getCaretOffset():I
- a:getDocument():com.intellij.openapi.editor.Document
- a:moveCaretTo(I):V
+- registerTabOut(com.intellij.openapi.util.TextRange,I):V
- a:select(com.intellij.openapi.util.TextRange):V
f:com.intellij.openapi.editor.markup.AttributesFlyweight
- *s:create(java.awt.Color,java.awt.Color,I,java.awt.Color,com.intellij.openapi.editor.markup.EffectType,java.util.Map,java.awt.Color):com.intellij.openapi.editor.markup.AttributesFlyweight
diff --git a/platform/core-api/src/com/intellij/openapi/editor/ModNavigator.java b/platform/core-api/src/com/intellij/openapi/editor/ModNavigator.java
index f5e8599aa939..36623bb27e5a 100644
--- a/platform/core-api/src/com/intellij/openapi/editor/ModNavigator.java
+++ b/platform/core-api/src/com/intellij/openapi/editor/ModNavigator.java
@@ -35,4 +35,17 @@ public interface ModNavigator {
* @return the document being edited
*/
@NotNull Document getDocument();
+
+ /**
+ * Registers a tab out scope, so pressing tab inside the scope moves the caret to the specified offset
+ * instead of adding a tab character.
+ *
+ * May do nothing if tab out is not supported by the implementation.
+ *
+ * @param range scope range
+ * @param tabOutOffset target offset for tab-out
+ */
+ default void registerTabOut(@NotNull TextRange range, int tabOutOffset) {
+
+ }
}
diff --git a/platform/lang-impl/src/com/intellij/codeInsight/completion/CodeCompletionHandlerBase.java b/platform/lang-impl/src/com/intellij/codeInsight/completion/CodeCompletionHandlerBase.java
index d7f1c646782c..9283fdc43e64 100644
--- a/platform/lang-impl/src/com/intellij/codeInsight/completion/CodeCompletionHandlerBase.java
+++ b/platform/lang-impl/src/com/intellij/codeInsight/completion/CodeCompletionHandlerBase.java
@@ -2,6 +2,7 @@
package com.intellij.codeInsight.completion;
+import com.intellij.analysis.AnalysisBundle;
import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.CodeInsightSettings;
import com.intellij.codeInsight.completion.CompletionAssertions.WatchingInsertionContext;
@@ -14,6 +15,9 @@ import com.intellij.codeInsight.lookup.impl.LookupImpl;
import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.ide.DataManager;
import com.intellij.lang.Language;
+import com.intellij.modcommand.ActionContext;
+import com.intellij.modcommand.ModCommandExecutor;
+import com.intellij.modcompletion.CompletionItem;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.impl.ActionManagerImpl;
import com.intellij.openapi.application.*;
@@ -714,6 +718,11 @@ public class CodeCompletionHandlerBase {
WatchingInsertionContext context =
CompletionUtil.createInsertionContext(lookupItems, item, completionChar, editor, psiFile, caretOffset, idEndOffset, offsetMap);
+ if (item instanceof CompletionItemLookupElement wrapper) {
+ insertItem(completionChar, editor, psiFile, wrapper);
+ return context;
+ }
+
int initialStartOffset = Math.max(0, caretOffset - item.getLookupString().length());
ApplicationManager.getApplication().runWriteAction(() -> {
try {
@@ -746,6 +755,21 @@ public class CodeCompletionHandlerBase {
return context;
}
+ private static void insertItem(char completionChar,
+ @NotNull Editor editor,
+ @NotNull PsiFile psiFile,
+ CompletionItemLookupElement wrapper) {
+ CompletionItem.InsertionContext insertionContext = new CompletionItem.InsertionContext(
+ completionChar == Lookup.REPLACE_SELECT_CHAR ?
+ CompletionItem.InsertionMode.OVERWRITE : CompletionItem.InsertionMode.INSERT,
+ completionChar);
+ ActionContext actionContext = ActionContext.from(editor, psiFile);
+ ModCommandExecutor.executeInteractively(
+ actionContext,
+ AnalysisBundle.message("complete"), editor,
+ () -> wrapper.item().perform(actionContext, insertionContext));
+ }
+
private static @NotNull WatchingInsertionContext callHandleInsert(@NotNull CompletionProcessEx indicator,
@NotNull LookupElement item,
@NotNull List items,
diff --git a/platform/platform-impl/api-dump.txt b/platform/platform-impl/api-dump.txt
index 35593f39310c..f5c4f3dea411 100644
--- a/platform/platform-impl/api-dump.txt
+++ b/platform/platform-impl/api-dump.txt
@@ -556,6 +556,7 @@ com.intellij.openapi.editor.ex.EditorEx
- a:addFocusListener(com.intellij.openapi.editor.ex.FocusChangeListener,com.intellij.openapi.Disposable):V
- a:addPropertyChangeListener(java.beans.PropertyChangeListener):V
- a:addPropertyChangeListener(java.beans.PropertyChangeListener,com.intellij.openapi.Disposable):V
+- asPsiNavigator():com.intellij.openapi.editor.ModNavigator
- a:createBoundColorSchemeDelegate(com.intellij.openapi.editor.colors.EditorColorsScheme):com.intellij.openapi.editor.colors.EditorColorsScheme
- a:getBackgroundColor():java.awt.Color
- a:getContentSize():java.awt.Dimension
diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/ex/EditorEx.java b/platform/platform-impl/src/com/intellij/openapi/editor/ex/EditorEx.java
index 3c058038d052..dde848f8e3fb 100644
--- a/platform/platform-impl/src/com/intellij/openapi/editor/ex/EditorEx.java
+++ b/platform/platform-impl/src/com/intellij/openapi/editor/ex/EditorEx.java
@@ -15,6 +15,7 @@
*/
package com.intellij.openapi.editor.ex;
+import com.intellij.codeInsight.editorActions.TabOutScopesTracker;
import com.intellij.ide.CopyProvider;
import com.intellij.ide.CutProvider;
import com.intellij.ide.DeleteProvider;
@@ -374,4 +375,34 @@ public interface EditorEx extends Editor {
default int getStickyLinesPanelHeight() {
return 0;
}
+
+ @Override
+ default @NotNull ModNavigator asPsiNavigator() {
+ return new ModNavigator() {
+ @Override
+ public @NotNull Document getDocument() {
+ return EditorEx.this.getDocument();
+ }
+
+ @Override
+ public void select(@NotNull TextRange range) {
+ getSelectionModel().setSelection(range.getStartOffset(), range.getEndOffset());
+ }
+
+ @Override
+ public void moveCaretTo(int offset) {
+ getCaretModel().moveToOffset(offset);
+ }
+
+ @Override
+ public int getCaretOffset() {
+ return getCaretModel().getOffset();
+ }
+
+ @Override
+ public void registerTabOut(@NotNull TextRange range, int tabOutOffset) {
+ TabOutScopesTracker.getInstance().registerScopeRange(EditorEx.this, range.getStartOffset(), range.getEndOffset(), tabOutOffset);
+ }
+ };
+ }
}
diff --git a/platform/platform-resources/src/META-INF/CompletionExtensionPoints.xml b/platform/platform-resources/src/META-INF/CompletionExtensionPoints.xml
index 401a18aa4245..f6a7e3155f81 100644
--- a/platform/platform-resources/src/META-INF/CompletionExtensionPoints.xml
+++ b/platform/platform-resources/src/META-INF/CompletionExtensionPoints.xml
@@ -59,6 +59,10 @@
+
+
+
@@ -79,4 +83,8 @@
+
+
+