[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
This commit is contained in:
Tagir Valeev
2025-10-31 02:31:40 +00:00
committed by intellij-monorepo-bot
parent 1f1f4fa579
commit d6cffffa81
25 changed files with 739 additions and 51 deletions
@@ -1249,6 +1249,8 @@
<extendWordSelectionHandler implementation="com.intellij.codeInsight.editorActions.wordSelection.VarargsSelectioner"/>
<modcompletion.completionItemProvider language="JAVA" implementationClass="com.intellij.codeInsight.completion.modcompletion.KeywordCompletionItemProvider"/>
<completion.skip implementation="com.intellij.codeInsight.completion.AbstractExpectedTypeSkipper" id="skipAbstract"/>
<completion.skip implementation="com.intellij.codeInsight.completion.DeprecatedSkipper" id="skipDeprecated"/>
<statistician key="completion" implementationClass="com.intellij.codeInsight.completion.JavaCompletionStatistician"/>
@@ -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;
}
};
@@ -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;
}
}
@@ -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<CompletionItem> 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();
}
}
@@ -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;
}
@@ -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
- <init>(C):V
- <init>(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
+12
View File
@@ -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
- <init>():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
@@ -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
@@ -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
@@ -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);
}
@@ -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;
}
@@ -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();
@@ -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<CompletionContributor> forLanguage(@NotNull Language language) {
return INSTANCE.forKey(language);
List<CompletionContributor> 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<CompletionContributor> forLanguageHonorDumbness(@NotNull Language language, @NotNull Project project) {
@@ -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;
}
}
@@ -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<String> getAllLookupStrings() {
Set<String> 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;
}
}
@@ -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<String> 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
}
}
@@ -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);
}
}
@@ -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<CompletionItemProvider> 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<CompletionItem> sink);
/**
* @param language language to get providers for
* @return language-specific completion providers
*/
static List<CompletionItemProvider> 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;
}
}
}
@@ -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);
}
@@ -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
@@ -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) {
}
}
@@ -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<LookupElement> items,
+1
View File
@@ -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
@@ -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);
}
};
}
}
@@ -59,6 +59,10 @@
<extensionPoint qualifiedName="com.intellij.mlCompletionFeaturesCollector" dynamic="true" beanClass="com.intellij.lang.LanguageExtensionPoint">
<with attribute="implementationClass" implements="com.intellij.codeInsight.inline.completion.features.InlineCompletionFeaturesCollector"/>
</extensionPoint>
<extensionPoint qualifiedName="com.intellij.modcompletion.completionItemProvider"
beanClass="com.intellij.lang.LanguageExtensionPoint" dynamic="true">
<with attribute="implementationClass" implements="com.intellij.modcompletion.CompletionItemProvider"/>
</extensionPoint>
<extensionPoint qualifiedName="com.intellij.codeInsight.completion.command.factory"
beanClass="com.intellij.lang.LanguageExtensionPoint" dynamic="true">
@@ -79,4 +83,8 @@
<with attribute="implementationClass" implements="com.intellij.codeInsight.completion.command.commands.IntentionCommandOffsetProvider"/>
</extensionPoint>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<registryKey defaultValue="false" description="ModCompletion contributors (see com.intellij.modcompletion.CompletionItemProvider)"
key="ide.completion.modcommand"/>
</extensions>
</idea-plugin>