IntelliJ IDEAのPostfix補完プラグインを作る

JetBrains IDE Advent Calendar2日目の記事です。前日はlaco0416氏のWebStormのTypeScript統合機能でした。

ところでPostfix補完使ってますか?当然使ってますよね? Postfix補完をキメると気持ちいいですよね?Postfix補完でガシガシコードがかけると何でもできちゃいそうな気分になりますよね。

けどPostfix補完標準のテンプレートだけだと足りないですよね・・・。たとえばOptional.ofNullable(obj)とかobj.optから出したい・・・。というので作ったのがJava8Postfixってプラグイン

IntelliJのSettings→PluginsでPostfixとかで検索してインストールできるので、Java8使ってる方は良ければ入れてください。


宣伝はここまでにして、本題に入ってPostfixプラグインの作り方。 まず開発環境としてIntelliJプラグイン開発用プラグインを入れておきます。その後NewProjectすればこんな画面が出てくるはず。 plugindev1

あとはプロジェクト名とか決めて終了。 プラグインのプロジェクト構成はこんな感じ。 dev2

よく見かけるやつなので特にコメントもないかな。 plugin.xmlプラグインの情報を記述する所。識別用のIDとか説明文とか。 この辺は調べるといくつか資料出てくるので飛ばします。

ここからPostfix補完のプラグインの話。 新しいPostfixのテンプレートを登録するためにJavaPostfixTemplateProviderを継承した新しいクラスを作成します。 JavaPostfixTemplateProvider#getTemplatesの戻り値には新しくプラグインで追加するPostfixTemplateのSetを返すようにしておきます。 Java8Postfixではこんな感じ。

public class Java8Postfix
        extends JavaPostfixTemplateProvider {
    private final HashSet<PostfixTemplate> templates;
 
    public Java8Postfix() {
        this.templates = ContainerUtil.newHashSet(new PostfixTemplate[] { new NullableOptionalPostfixTemplate(), new OptionalPostfixTemplate(), new ArrayToStreamPostfixTemplate(), new MethodToLambdaPostfixTemplate() });
    }
 
    @NotNull
    public Set<PostfixTemplate> getTemplates()
    {
        return templates;
    }
}

このままでは当然何も動作しないので、plugin.xmlにProviderを読み込んでもらうように指定します。

<extensions defaultExtensionNs="com.intellij">
  <codeInsight.template.postfixTemplateProvider language="JAVA" implementationClass="net.orekyuu.postfix.Java8Postfix"/>
</extensions>

次はPostfixTemplateを実装する。 Java8PostfixではPostfixTemplateWithExpressionSelectorを継承しています。 今回の例では.optのコードを例に出します。

public class OptionalPostfixTemplate
        extends PostfixTemplateBase
{
    public OptionalPostfixTemplate()
    {
        //(1)
        super("opt", "Optional.of(Object)", ConditionMerger.or(JavaPostfixTemplatesUtils.IS_NOT_PRIMITIVE, new Condition[] { MyConditions.IS_DOUBLE, MyConditions.IS_INT, MyConditions.IS_LONG }));
    }
 
    //(2)
    protected void expandForChooseExpression(@NotNull PsiElement context, @NotNull Editor editor)
    {
        //(3)
        PsiExpression expression = JavaPostfixTemplatesUtils.getTopmostExpression(context);
        if (expression == null) {
            return;
        }
        PsiType type = expression.getType();
        String optionalClass = "Optional";
        if (PsiType.DOUBLE.equals(type)) {
            optionalClass = "OptionalDouble";
        } else if (PsiType.LONG.equals(type)) {
            optionalClass = "OptionalLong";
        } else if (PsiType.INT.equals(type)) {
            optionalClass = "OptionalInt";
        }
        Project project = context.getProject();
        Document document = editor.getDocument();
 
        TextRange range = expression.getTextRange();
        document.deleteString(range.getStartOffset(), range.getEndOffset());
 
        TemplateManager manager = TemplateManager.getInstance(project);
        //(4)
        Template template = manager.createTemplate("", "");
        template.setToReformat(true);
        template.addTextSegment(optionalClass + ".of(");
        template.addTextSegment(expression.getText());
        template.addTextSegment(")");
 
        manager.startTemplate(editor, template);
    }
}
 
abstract class PostfixTemplateBase
        extends PostfixTemplateWithExpressionSelector
{
    public PostfixTemplateBase(String postfix, String desc, PostfixTemplateExpressionSelector selector)
    {
        super(postfix, desc, selector);
    }
 
    public PostfixTemplateBase(String postfix, String desc, Condition<PsiElement> condition)
    {
        this(postfix, desc, JavaPostfixTemplatesUtils.selectorAllExpressionsWithCurrentOffset(condition));
    }
}
 
public final class ConditionMerger
{
    private ConditionMerger()
    {
        throw new UnsupportedOperationException();
    }
 
    public static Condition<PsiElement> or(final Condition<PsiElement> first, final Condition<PsiElement>... option)
    {
        if (first == null) {
            throw new NullPointerException("first condition is null");
        }
        return new Condition<PsiElement>() {
            public boolean value(PsiElement element) {
                if (first.value(element)) {
                    return true;
                }
                for (Condition<PsiElement> condition : option) {
                    if (condition.value(element)) {
                        return true;
                    }
                }
                return false;
            }
        };
    }
}
 
public enum MyConditions
        implements Condition<PsiElement>
{
    IS_ARRAY {
        @Override
        public boolean value(PsiElement element) {
            if(!(element instanceof PsiExpression)) {
                return false;
            } else {
                PsiType type = ((PsiExpression)element).getType();
                return JavaPostfixTemplatesUtils.isArray(type);
            }
        }
    },
    IS_LAMBDA {
        @Override
        public boolean value(PsiElement element) {
            return element instanceof PsiLambdaExpression;
        }
    },
    IS_METHOD_CALL {
        @Override
        public boolean value(PsiElement element) {
            return element instanceof PsiMethodCallExpression;
        }
    },
    IS_DOUBLE {
        @Override
        public boolean value(PsiElement element) {
            if(!(element instanceof PsiExpression)) {
                return false;
            } else {
                PsiType type = ((PsiExpression)element).getType();
                return PsiType.DOUBLE.equals(type);
            }
        }
    },
    IS_LONG {
        @Override
        public boolean value(PsiElement element) {
            if(!(element instanceof PsiExpression)) {
                return false;
            } else {
                PsiType type = ((PsiExpression)element).getType();
                return PsiType.LONG.equals(type);
            }
        }
    },
    IS_INT {
        @Override
        public boolean value(PsiElement element) {
            if(!(element instanceof PsiExpression)) {
                return false;
            } else {
                PsiType type = ((PsiExpression)element).getType();
                return PsiType.INT.equals(type);
            }
        }
    };
}

(1) 第一引数に補完を書ける時に使用する文字列。optを渡しているので.optで補完できるようになる。 第二引数は説明文。 第三引数は補完をかけるための条件。今回の場合はプリミティブでないオブジェクト || double || int || longのみ補完できる。

(2) 補完をかける時のイベント。ここでテンプレートを展開する。

(3) PsiExpressionを取得してくる。これで対象の式の戻り値とか解析しながら補完をかけれる。

(4) Templateを使って変換後の文字列を作る。文字列の操作が楽だったりsetToReformat(true)としておけばフォーマット整えてくれたりするので基本はこれを使うんじゃないかなー? 最後にTemplateManager#startTemplateを呼び出さないといけないので注意。

Postfix補完プラグインではPsiHogehogeの扱いが難しいので色々調べないといけない感じある。 調べるとIntelliJ IDEA PSI Cookbookってサイトがあったので参考になると思う。

最後にEditor>General>Postfix Completionに説明文を入れる必要がある。書かないとエラーになった記憶。 dev3

この画面の説明文は"postfixTemplates/テンプレートのクラス名"パッケージに配置する。 dev4 before.java.templateにはBeforeに表示されるコードをそのまま記述。 after.java.templateにはAfterに表示されるコードをそのまま記述。 description.htmlにはDescriptionに表示する説明文をhtmlで記述する。 これでおしまい。動作確認後ビルドしてPluginRepositoryで配布しようね。

明日はhiromikai_green氏がなんか書いてくれると思います。期待。