From d6cffffa816af37cd36d47cb20c63dc7f480be15 Mon Sep 17 00:00:00 2001 From: Tagir Valeev Date: Tue, 28 Oct 2025 18:17:38 +0100 Subject: [PATCH] [mod-commands] IJPL-214795 (part) simplest proof-of-concept: complete a few keywords for Java Under a registry key ide.completion.modcommand GitOrigin-RevId: e0d6a25a811089aa0b72c2269ae450afc81b4d46 --- .../resources/META-INF/JavaPlugin.xml | 2 + .../intellij/codeInsight/JavaTailTypes.java | 36 ++--- .../modcompletion/KeywordCompletionItem.java | 53 ++++++++ .../KeywordCompletionItemProvider.java | 125 ++++++++++++++++++ .../simple/ParenthesesTailType.java | 34 +++-- platform/analysis-api/api-dump-unreviewed.txt | 2 - platform/analysis-api/api-dump.txt | 12 ++ .../messages/AnalysisBundle.properties | 1 + .../intellij/codeInsight/CharTailType.java | 9 +- .../codeInsight/ModNavigatorTailType.java | 27 ++++ .../com/intellij/codeInsight/TailType.java | 19 ++- .../com/intellij/codeInsight/TailTypes.java | 34 +++-- .../completion/CompletionContributor.java | 9 +- .../completion/CompletionItemContributor.java | 40 ++++++ .../CompletionItemLookupElement.java | 84 ++++++++++++ .../modcompletion/CompletionItem.java | 66 +++++++++ .../CompletionItemPresentation.java | 42 ++++++ .../modcompletion/CompletionItemProvider.java | 68 ++++++++++ .../PsiUpdateCompletionItem.java | 49 +++++++ platform/core-api/api-dump-experimental.txt | 1 + .../intellij/openapi/editor/ModNavigator.java | 13 ++ .../completion/CodeCompletionHandlerBase.java | 24 ++++ platform/platform-impl/api-dump.txt | 1 + .../intellij/openapi/editor/ex/EditorEx.java | 31 +++++ .../META-INF/CompletionExtensionPoints.xml | 8 ++ 25 files changed, 739 insertions(+), 51 deletions(-) create mode 100644 java/java-impl/src/com/intellij/codeInsight/completion/modcompletion/KeywordCompletionItem.java create mode 100644 java/java-impl/src/com/intellij/codeInsight/completion/modcompletion/KeywordCompletionItemProvider.java create mode 100644 platform/analysis-api/src/com/intellij/codeInsight/ModNavigatorTailType.java create mode 100644 platform/analysis-api/src/com/intellij/codeInsight/completion/CompletionItemContributor.java create mode 100644 platform/analysis-api/src/com/intellij/codeInsight/completion/CompletionItemLookupElement.java create mode 100644 platform/analysis-api/src/com/intellij/modcompletion/CompletionItem.java create mode 100644 platform/analysis-api/src/com/intellij/modcompletion/CompletionItemPresentation.java create mode 100644 platform/analysis-api/src/com/intellij/modcompletion/CompletionItemProvider.java create mode 100644 platform/analysis-api/src/com/intellij/modcompletion/PsiUpdateCompletionItem.java 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 @@ + + +