Writing a Postfix Completion Provider  Editor

Writing a Postfix Completion Provider

This post explains how you can add your own PostfixTemplateProvider to your IntelliJ plugin. Postfix completion is a feature in IntelliJ to modify an expression by pressing TAB after a suffix text. A postfix completion provider is easy to add.

Concepts

PostfixTemplate
A single template with a UI label, a suffix text used for the completion and a short description which explains a transformation. The abstract class PostfixTemplate is the base of all available postfix template implementations.
PostfixTemplateProvider
A set of templates for a language. A PostfixTemplateProvider manages a list of custom postfix templates for a certain language.

Implementation

An implementation is done in a few steps:

  1. Implement your PostfixTemplates
  2. Implement a PostfixTemplateProvider
  3. Write a test case
  4. Setup your plugin.xml

Implement PostfixTemplate

A custom postfix template needs to pass a few properties to its parent constructor:

name
The presentable name to be displayed in the user interface. I couldn’t find a place where IntelliJ uses this, though.
key
The suffix which triggers your template. If you suffix is .key then your template’s isApplicable(...) and expand(...) methods will be called after the user typed .key<TAB>. The key has to start with a character supported by isTerminalSymbol(...) of your template provider (see below).
example
The description displayed in the code completion menu. Return a short explanation with a sample here.

Example of a PostfixTemplate implementation

This simple implementation for the JSON language wraps any JSON value in braces, e.g. 42.o is turned into {42}. Of course, the JSON is invalid after the expansion, but this is still helpful to quickly modify a JSON file.

 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
/**
 * Postfix template which wraps a JSON value in an array.
 * For example 123 becomes [123], [123] becomes [[123]]
 *
 * @author jansorg
 */
public class WrapInObjectPostfixTemplate extends PostfixTemplate {
    WrapInObjectPostfixTemplate() {
        super("o", "wrap with an object, e.g. {[42]}");
    }

    @Override
    public boolean isApplicable(@NotNull PsiElement context, @NotNull Document copyDocument, int newOffset) {
        return context instanceof JsonValue || context.getParent() instanceof JsonValue;
    }

    /**
     * expand is called in a write action, PSI modifications are ok.
     */
    @Override
    public void expand(@NotNull PsiElement context, @NotNull Editor editor) {
        //context might a leaf element
        JsonValue parent = PsiTreeUtil.getParentOfType(context, JsonValue.class);
        if (parent != null) {
            context = parent;
        }

        JsonElementGenerator generator = new JsonElementGenerator(context.getProject());
        JsonObject replacement = generator.createObject(context.getText());
        context.replace(replacement);
    }
}

Add documentation files

IntelliJ expects that each PostfixTemplate provides a few html files. They are used to display an example before and after a invocation along with a short description.

Create these files belong in a resource folder named postfixTemplates/yourClassName:

  1. description.html
  2. before.language.html
  3. after.language.html

yourClassName is a placeholder for the name of your new template class. language is probably a placeholder to define the content type of your sample, e.g. json or java. I couldn’t validate this, though.

Example of documentation files
postfixTemplates/WrapInObjectPostfixTemplate/description.html
A short file which is rendered in the settings dialog for your new template.
1
2
3
4
5
<html>
<body>
Wraps a JSON value into an object.
</body>
</html>
postfixTemplates/WrapInObjectPostfixTemplate/before.json.html
A freemarker template to show sample content before a postifx completion. $key is replaced with the key defined in your implementation:
1
<spot>42</spot>$key
postfixTemplates/WrapInObjectPostfixTemplate/after.json.html
A freemarker template to display the result of a postfix completion:
1
{42}

Implement PostfixTemplateProvider

Create a new subclass of PostfixTemplateProvider. Return a set of your custom templates in the method getTemplates().

public boolean isTerminalSymbol(char currentChar) is called by IntelliJ to find out whether a given separator is supported by your plugin. For example, . is the separator used for Java postfix completion and most other templates. Different templates of your provider are allowed to use different separators, just make sure that your provider supports all of them.

The shortest, meaningful implementation is

1
2
3
4
@Override
public boolean isTerminalSymbol(char currentChar) {
    return '.' == currentChar;
}

preCheck(...), preExpand(...) and afterExpand(...) can be used to patch your file, for example. The Java template provider adds a missing semicolon where necessary, for example.

Write a test case

Don’t forget to write a test case for your PostfixTemplates! You’ll be thankful in a few months.

Actually it’s pretty simple to do it. Here’s a short test of our implementation above:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/**
 * @author jansorg
 */
public class WrapInObjectPostfixTemplateTest extends LightPlatformCodeInsightFixtureTestCase {
    public void testWrapWithArray() throws Exception {
        assertWrapping("{123}", "123");
        assertWrapping("{null}", "null");
    }

    /**
     * Asserts that the expansion of {@code content} by .o equals {@code expected}.
     */
    private void assertWrapping(String expected, String content) {
        PsiFile file = myFixture.configureByText(JsonFileType.INSTANCE, content);
        myFixture.type(".o\t");

        Assert.assertEquals(expected, file.getText());
    }
}

Configuration in your plugin.xml

You need to add your new template provider in your plugin.xml file. Add one template provider for each language, each provider offers one ore more templates for a given language.

Example

1
2
3
4
5
<extensions defaultExtensionNs="com.intellij">
    <codeInsight.template.postfixTemplateProvider
        language="JSON"
        implementationClass="com.plugindev.json.editor.JsonPostfixTemplateProvider" />
</extensions>

Gotchas

An expansion key must be a valid Java identifier.
A limitation of IntelliJ’s implementation is that the key used for expansion must be valid Java identifier. An expansion key consisting like .[] is not possible. Choose a valid key like .a instead.
Definition of PostfixTemplateProvider
PostfixTemplateProvider (github.com)
Definition of PostfixTemplate
PostfixTemplate (github.com)
IntelliJ online help of the postfix completion feature
Postfix completion in the IntelliJ handbook (jetbrains.com)