Code Completion

Abstract. This post explains how code completion works in IntelliJ. It tells you how to implement it in your plugin, how to avoid common mistakes and how to handle advanced scenarios. You can also work through the code of a sample plugin which adds completions from a dictionary.

Introduction

In this article you’ll learn how to implement code completion in your IntelliJ plugin.

Code completion is one of the most important features of your IDE. The IntelliJ help contains a lot of information about using it. But here, we’ll be looking at it from a developer’s point of view.

This article is about the extension point completion.contributor and deals with:

  • basic completion
  • smart completion
  • class name completion

It does not deal with:

  • hippie completion – because it’s unrelated to the extension point we’re covering
  • postfix completion – it’s more relevant than hippie completion, but still not covered by the extension point we’re discussing here

You’ll learn the most relevant points about the different types of completions. After that we’re going to implement word completion from a dictionary. This is what you’ll get:

Simple IntelliJ plugin showing completions from a dictionary

Word completion from a dictionary

Completion types

There are several types of code completion: basic completion, smart completion, class name completion. In code, these completion types are defined by the enum com.intellij.codeInsight.completion.CompletionType.

Basic completion

The is the default completion type. It’s usually invoked by Ctrl + Space on Windows/Linux and ^ + Space on macOS.

Demonstration of IntelliJ basic completion

Basic completion matches by name only

Smart completion

This is the alternative completion mode, usually invoked by Ctrl + Shift + Space on Windows/Linux and + + Space on macOS.

For Java code, smart completion displays only those items which match the current context. For example, when you invoke it after a return keyword, only variables and methods which are compatible with the return type are shown.

It’s your call if you implement smart completion and how smart it’s going to be. Usually, basic completion would return completions which are syntactically valid. Smart completion would filter these to remove the semantically invalid items.

Demonstration of IntelliJ smart completion

Smart completion matches by name and expected type

Class name completion

Used for the completion of class names. Within the IntelliJ Community Edition, this is only relevant for Java code, as far as I can tell. The only reference I could find in the sources is the live-template function classNameComplete().

You won’t need to implement this, unless you want to provide additional data in this specific context.

Extension Point

As you know, plugins implement extension points to tell the IDE what they’re capable of. The extension point for code completions is called completion.contributor.

This is the declaration and all attributes it supports. Check out the next section to learn how to use these attributes – and by the way, IntelliJ 2019.2 includes nice code completion for the attribute values.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!-- add this to your plugin.xml file -->
<extensions defaultExtensionNs="com.intellij">
    <completion.contributor
        implementationClass=""
        factoryClass=""
        factoryArgument=""
        language=""
        order=""
        id=""
        os=""
    />
</extensions>
implementationClass
A class implementing com.intellij.codeInsight.completion.CompletionContributor. Use the factoryClass if you want to choose the implementation at runtime. You have to provide a value for either implementationClass or factoryClass.
factoryClass
Class of a factory to return your implementation. It has to implement com.intellij.openapi.extensions.ExtensionFactory. You’ll rarely need this. Use it to choose one of several implementations at runtime. Or use it to configure the implementation you return.
factoryArgument (optional)
An optional String argument passed to the createInstance method of your factory.
language (required)
This is the language for which your implementation provides the completions. Most often it will provide completions that are only relevant for a certain file type, e.g. Python. You don’t want to show names of Python functions in Java code, for example. Use the value "any" if you want to enable your implementation for all types of files. If you need to support multiple languages, then add the <completion.contributor … /> element multiple times. Use IntelliJ’s code completion to see the available choices for the value.
order (optional)
Contributors are called one after another. Use this attribute to force your provider to be first, last, before <id>, or after <id>. You won’t need this most of the time. A use-case for first is when you’d like to filter completions of other completion contributors. See Filtering completions. A use-case for before and after is when you’d like to order two or more contributors of your plugin.
id (optional)
The ID of your completion contributor. These IDs are referenced by before and after of other completion contributors.
os (optional)
Enable your implementation only for the given operating system, e.g. linux. IntelliJ’s code completion tells you about the supported values.

Implementation

At this point you should decide a few things:

  • which file types you want to support, i.e. the languages you’re supporting
  • which completion types you want to support, i.e. basic and/or smart completion
  • where completions should be shown in a file. You don’t want to suggest a class name where it’s not allowed, for example.

First, define a class which extends the base class of this extension point:

1
2
3
4
import com.intellij.codeInsight.completion.CompletionContributor;

public class MyCompletionContributor extends CompletionContributor {
}

Now, you have a choice to make:

  • You can override fillCompletionVariants() of your CompletionContributor.
  • Or you can implement a CompletionProvider and use extend() in the constructor to tell IntelliJ which completion type(s) and context you’re supporting.

Both options are discussed in the subsections below.

How to implement fillCompletionVariants()

This is the generic approach. IntelliJ’s implementation just iterates over the data which was added by extend(). If you don’t want to use extend(), then override this method.

This is a bit easier to implement than a CompletionProvider.

How to implement a CompletionProvider

This is the recommended approach. It’s a bit more complicated to understand and to debug because it is an abstraction of common use-cases of code completion.

extend’s signature is:

1
2
3
4
// class CompletionContributor:
void extend(@Nullable CompletionType type,
            @NotNull ElementPattern<? extends PsiElement> place,
            CompletionProvider<CompletionParameters> provider)
type
It defines which completion type you’re supporting. You’ll usually use CompletionType.BASIC or CompletionType.SMART here. Pass null to support all.
place
This defines the context which is supported by your provider. The SDK already comes with a lot of patterns you can use here. See below for more information on patterns and where to find them.
provider
This is your implementation to provide completions specifically for the given type and context. If you’re using extend() multiple times, then each provider usually serves a subset of the possible completions.
For example, it could compute a list of all class names to show after the extends keyword in Java. Another provider could compute a list of interface names to suggest after the implements keyword.

ElementPattern

Patterns are a bit hard to grasp at first. It always feels a bit over-engineered to me, but once you get it it’s a nice tool in your belt. But beware, getting used to it might take a while.

Let’s take a visual approach to understand how this thing works. Suppose you’d like to suggest a list of valid locale names in this code snippet:

1
Locale.forLanguageTag("<completions here>");

A pattern starts with the PSI element at the text cursor’s position. The PSI structure of this code snippet looks like this:

IntelliJ PSI structure of Locale.forLanguageTag

PSI structure of Locale.forLanguageTag

We have to make sure that:

  • we’re only completing a value in a string literal
  • the literal is the first argument of java.util.Locale#forLanguageTag

Now, a pattern which matches the innermost String literal (highlighted in the illustration above), is

1
PlatformPatterns.psiElement(JavaTokenType.STRING_LITERAL)

But this is matching all strings, not just the argument to Locale.forLanguageTag. We have to make this pattern more restrictive.

Let’s take another look at the PSI structure. The parent of the literal is a PsiLiteralExpression. The grandparent is a PsiExpressionList. And the next ancestor finally is the PsiMethodCallExpression we’re looking for.

You can use withParent(...) on a PsiElementPattern to check the hierarchy. You can add your own logic via with(...). Chained calls of withParent() all operate on the same element. Therefore we have to nest these calls.

With this knowledge we finally get this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import static com.intellij.patterns.PlatformPatterns.psiElement;
// ...

PatternCondition<PsiMethodCallExpression> = new PatternCondition<PsiMethodCallExpression>("") {
    @Override
    public boolean accepts(@NotNull PsiMethodCallExpression call, ProcessingContext context) {
        PsiReferenceExpression method = call.getMethodExpression();
        return method.getQualifiedName().equals("Locale.forLanguageTag");
    }
};

psiElement(JavaTokenType.STRING_LITERAL).withParent(
    psiElement(PsiLiteralExpression.class).withParent(
        psiElement(PsiExpressionList.class).withParent(
            psiElement(PsiMethodCallExpression.class).with(
                methodCallPattern
            )
        )
    )
)

It’s still not good enough, though:

  • It’s not checking that Locale is java.util.Locale. It still could be another class of the same name.
  • It’s not limiting the completion to the first argument.

After digging into the sources of the IntelliJ SDK, I was able to come up with this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import static com.intellij.patterns.PlatformPatterns.psiElement;
import static com.intellij.patterns.PsiJavaPatterns.psiLiteral;
import static com.intellij.patterns.PsiJavaPatterns.psiMethod;
//  ...
psiElement(JavaTokenType.STRING_LITERAL).withParent(
               psiLiteral().methodCallParameter(
                    0,
                    psiMethod()
                        .withName("forLanguageTag")
                        .definedInClass("java.util.Locale")))

When I’m unable to get a pattern to work, then I usually add a call to with(...) returning true with a breakpoint. If you’d like to quickly iterate on this, then add a test case and incrementally fix the pattern logic.

Common patterns in the SDK

There’s a bunch of patterns available in the JetBrains SDK:

com.intellij.patterns.StandardPatterns
Very basic patterns. This offers the useful utilities and(), or(), and not().
com.intellij.patterns.PlatformPatterns
This offers PSI based patterns. Most useful are psiElement() and psiFile.
Don’t forget that the patterns returned by these methods provide chainable methods. For example psiElement().withParent(…), psiElement().inside(…) and more.
com.intellij.patterns.PsiJavaPatterns
Most useful when you’re working with Java PSI. Before you write your own pattern for Java code, take a look at this class.

How to implement a test case

With a fixture-based test, testing completions isn’t difficult.

Here’s a simple test which tests basic completions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// You can use JUnit4 annotations in this test.
public class MyCompletionContributorTest extends LightPlatformCodeInsightFixture4TestCase {
    @Test
    public void completionItems() {
        myFixture.configureByText("test.txt", "");

        myFixture.type("foo");
        LookupElement[] items = myFixture.completeBasic();
        // fixme: test the completion items
    }
}

Testing with automatic invocation

There’s a nice utility class CompletionAutoPopupTester. It helps to test completions with the automatic popup enabled.

Here’s a simple test which makes use of CompletionAutoPopupTester:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// based on BasePlatformTestCase to get runInDispatchThread() to work
public class StringLiteralDictionaryAutoPopupContributorTest extends BasePlatformTestCase {
    @Override
    protected boolean runInDispatchThread() {
        return false;
    }

    public void testAutoPopupCompletions() {
        CompletionAutoPopupTester tester = new CompletionAutoPopupTester(myFixture);
        tester.runWithAutoPopupEnabled(() -> {
            myFixture.configureByText("test.txt", "");

            tester.typeWithPauses("ob");

            tester.joinCompletion();
            // fixme: test completion items via tester.getLookup()
        });
    }
}

Example: Dictionary completion

Finally, we’re ready to implement our plugin!

We’d like to complete words with items from a dictionary. IntelliJ comes with built-in dictionaries, so we’re reusing this data. Accessing this data isn’t straight-forward, though.

But let’s get started!

The result should look similar to this:

Demonstration of our sample plugin

Word completion from a dictionary

For our plugin, we’d like to implement these features:

  • automatic completion anywhere inside of plain text files
  • automatic completion inside of string literals of any language
  • manual completion at any place in any file
  • support to complete upper-cased and lower-cased words, i.e. both The and the have to be supported
  • no completion immediately after whitespace. We always require a prefix for completion as we don’t want to add the whole dictionary to the popup.

The code is available at jansorg/intellij-code-completion on GitHub.

Update your plugin.xml

First, let’s add the extension point to the plugin.xml file:

1
2
3
<completion.contributor
    language="any"
    implementationClass="dev.ja.samples.completion.StringLiteralDictionaryContributor"/>

You now can use the quick-fix on StringLiteralDictionaryContributor to create the missing class.

We’re using language=any to enable it in all file types, as we want to support completions in string literals of any language.

Add a test case

Let’s do it “test-first” and start with a test case. We’ll extend this later on.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class StringLiteralDictionaryContributorTest extends LightPlatformCodeInsightFixture4TestCase {
    @Test
    public void noEmptyPrefix() {
        // empty file
        myFixture.configureByText("test.txt", "");
        Assert.assertEquals("expected no completions for an empty file",
            0, myFixture.completeBasic().length);

        // whitespace suffix
        myFixture.configureByText("test.txt", "foo");
        myFixture.type(" ");
        Assert.assertEquals("expected no completions after whitespace",
            0, myFixture.completeBasic().length);
    }

    @Test
    public void completions() {
        myFixture.configureByText("test.txt", "");

        myFixture.type("ob");
        Assert.assertTrue(myFixture.completeBasic().length > 0);
    }
}

Our CompletionContributor

Let’s implement the completion contributor next.

We’re starting with completions in plain text files. So, we need to find out about the internal structure of a plain text file.

Create a text file and check out the PSI structure via Tools → View PSI Structure of Current File. You’ll see that this type of file does not display a PSI structure. Go and dig into the IntelliJ Community Edition sources to find out more about the plain text type. Finally, you’ll come across PlainTextParserDefinition.

PlainTextParserDefinition is always returning a single element which covers all of the content. Its type is PlainTextTokenTypes.PLAIN_TEXT This means that you only need to check for this element type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package dev.ja.samples.completion;

import com.intellij.codeInsight.completion.CompletionContributor;
import com.intellij.codeInsight.completion.CompletionType;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PlainTextTokenTypes;

public class StringLiteralDictionaryContributor extends CompletionContributor {
    public StringLiteralDictionaryContributor() {
        // completions for plain text files
        extend(CompletionType.BASIC,
                PlatformPatterns.psiElement(PlainTextTokenTypes.PLAIN_TEXT),
                new DictionaryCompletionProvider(false));
    }
}

At this point you’ll have to create your DictionaryCompletionProvider. We’re passing a flag to tell if it’s only enabled in manual popup mode.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package dev.ja.samples.completion;

import com.intellij.codeInsight.completion.CompletionParameters;
import com.intellij.codeInsight.completion.CompletionProvider;
import com.intellij.codeInsight.completion.CompletionResultSet;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.spellchecker.compress.CompressedDictionary;
import com.intellij.util.ProcessingContext;
import org.jetbrains.annotations.NotNull;

import java.util.List;

class DictionaryCompletionProvider extends CompletionProvider<CompletionParameters> {
    private final boolean onlyManual;

    DictionaryCompletionProvider(boolean onlyManual) {
        this.onlyManual = onlyManual;
    }

    @Override
    protected void addCompletions(@NotNull CompletionParameters parameters,
                                  @NotNull ProcessingContext context,
                                  @NotNull CompletionResultSet result) {

        // return early if we're not supposed to show items
        // in the automatic popup
        if (parameters.isAutoPopup() && onlyManual) {
            return;
        }

        // return early when there's not prefix
        String prefix = result.getPrefixMatcher().getPrefix();
        if (prefix.isEmpty()) {
            return;
        }

        // make sure that our prefix is the last word
        // (In plain text files, the prefix initially contains the whole
        // file up to the cursor. We don't want that, as we're only
        // completing a single word.)
        CompletionResultSet dictResult;
        int lastSpace = prefix.lastIndexOf(' ');
        if (lastSpace >= 0 && lastSpace < prefix.length() - 1) {
            prefix = prefix.substring(lastSpace + 1);
            dictResult = result.withPrefixMatcher(prefix);
        } else {
            dictResult = result;
        }

        int length = prefix.length();
        char firstChar = prefix.charAt(0);
        boolean isUppercase = Character.isUpperCase(firstChar);

        // fixme: implement dictionary lookup
        // fixme: add found words to dictResult
    }
}

Accessing IntelliJ’s dictionaries

We want to access all built-in dictionaries.

After poking around in the sources of IntelliJ Community Edition, I finally encountered Dictionary.java. I’m not pasting the source here, because it’s not directly relevant. The source code is linked above.

The data is loaded at startup via a preloading activity. The implementation is available here: DictionaryLoader.java.

We want to add dictionary data to the completion result:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// in DictionaryCompletionProvider.java
for (CompressedDictionary dict : Dictionary.get()) {
    // limit completions to 20 additional characters max
    dict.getWords(Character.toLowerCase(firstChar), length, length + 20, word -> {
        // return early when the user modified the data of our editor
        ProgressManager.checkCanceled();

        LookupElementBuilder element;
        if (isUppercase) {
            element = LookupElementBuilder.create
                          (word.substring(0, 1).toUpperCase() + word.substring(1)
                      );
        } else {
            element = LookupElementBuilder.create(word);
        }

        // finally, add it to the completions  
        dictResult.addElement(element);
    });
}

Complete any string literal

We’re not yet showing completions in string literals of any language.
We have to implement our own pattern, because there’s no suitable implementation in the SDK.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// StringLiteralPattern.java
class StringLiteralPattern extends PatternCondition<PsiElement> {
    // ...

    @Override
    public boolean accepts(@NotNull PsiElement psi, ProcessingContext context) {
        // find out about the current language's string and comment token types  
        Language lang = PsiUtilCore.findLanguageFromElement(psi);
        ParserDefinition definition = LanguageParserDefinitions.INSTANCE.forLanguage(lang);
        if (definition == null) {
            return false;
        }

        ASTNode node = psi.getNode();
        if (node == null) {
            return false;
        }

        // support completions in string and comment literals
        TokenSet tokens = TokenSet.orSet(
                definition.getStringLiteralElements(),
                definition.getCommentTokens());    
        if (tokens.contains(node.getElementType())) {
            return true;
        }

        node = node.getTreeParent();
        return node != null && tokens.contains(node.getElementType());
    }
}

We’re registering this pattern with another call to extend():

1
2
3
4
// in StringLiteralDictionaryContributor.java:
extend(CompletionType.BASIC,
    PlatformPatterns.psiElement().with(new StringLiteralPattern()),
    new DictionaryCompletionProvider(false));

Manual invocation

We’re still missing the last piece of the puzzle: manual invocation should always display completions, no matter where. This is easy to add: use a pattern which accepts all elements.

1
2
3
4
5
6
// in the constructor of StringLiteralDictionaryContributor
// ...
// always suggest when invoked manually
extend(CompletionType.BASIC,
        PlatformPatterns.not(PlatformPatterns.alwaysFalse()),
        new DictionaryCompletionProvider(true));

Source code

You’re done! Make sure that your tests are passing and play around with it in your IDE.

You can find the full set of sources on GitHub:

Common mistakes

Referencing originalFile or its PsiElements

If you add orignalFile or any of its PSI elements to the result, then IntelliJ will display invalid element in the list of completions. This usually happens when the user modifies the file while completions are displayed. Modifications invalidate PsiElements of your original file, which then become unusable in the completion item you just added.

Advanced topics

DumbAware completions

By default, a completion contributor is not called when indexing is in progress.

Implement com.intellij.openapi.project.DumbAware to provide completions during indexing. Of course, you must not access indexes or stubs to provide completions while indexing is in progress.

Completions based on the number of invocations

Code completion can be invoked multiple times in a row. The number of invocations is passed to the extension of your plugin. This means that you’re able to return more or different results for the 2nd or 3rd invocation, for example.

The number of invocations is provided by CompletionParameters.getInvocationCount().

Hiding other completions

You can use the method stopHere() of your CompletionResultSet to exclude all following contributors. Don’t do this unless you have very good reasons. You wouldn’t like it either, if other plugins disabled completions of your own plugin.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// do this in a CompletionProvider:
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
                              @NotNull ProcessingContext context,
                              @NotNull CompletionResultSet result) {
    result.stopHere();
}

// do this in a CompletionContributor:
@Override
public void fillCompletionVariants(@NotNull CompletionParameters parameters,
                                   @NotNull CompletionResultSet result) {
    // ...
    result.stopHere();
}

If you only want to show your own completions, then you typically want to run your contributor first:

1
2
3
<extensions defaultExtensionNs="com.intellij">
    <completion.contributor order="first" impplementationClass="..." />
</extensions>

Filtering other completions

You can use CompletionResultSet.runRemainingContributors(...) to filter the completions of the remaining contributors.

Set order="first" if you want to filter the results of all other contributors.

But this is rude. Always be nice and kind to other plugins in your runtime space 😉

Handle automatic invocation

Code completion is either automatically invoked while typing or manually by the user. Use CompletionParameters.isAutoPopup() to find out it was invoked automatically.

Interruptable completions

Completions are wrapped with com.intellij.openapi.progress.ProgressManager. If your completion contributor is doing a lot of work, then make sure to call ProgressManager.checkCanceled. For example, when the user continues to type while completions are shown, the progress will be cancelled.

This method throws an exception when completions are cancelled. This exception is then caught by ProgressManager, so you don’t need to worry about it.

Custom presentation of items

Use LookupItem to customize the appearance. The easiest way to create a LookupItem is LookupElementBuilder. It provides chainable methods to configure the different aspects of a LookupElement.

This is what you can do:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
LookupElement e = LookupElementBuilder.create("Lookup string")
    .withPresentableText("Presentable text")
    .withItemTextForeground(JBColor.RED)
    .bold()
    .withIcon(PlatformIcons.VARIABLE_ICON)
    .withTailText(" my tail text")
    .withTypeText("my type text", PlatformIcons.CLASS_ICON, true)
    .withTypeIconRightAligned(true);

result.addElement(e);

This will be rendered like this:

Rendering of a IntelliJ LookupElement

Resources on code completion

A few more articles on code completion:

Use of patterns in the IntellJ Community Edition SDK:

More on code completion in the SDK: