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すればこんな画面が出てくるはず。
あとはプロジェクト名とか決めて終了。 プラグインのプロジェクト構成はこんな感じ。
よく見かけるやつなので特にコメントもないかな。 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に説明文を入れる必要がある。書かないとエラーになった記憶。
この画面の説明文は"postfixTemplates/テンプレートのクラス名"パッケージに配置する。 before.java.templateにはBeforeに表示されるコードをそのまま記述。 after.java.templateにはAfterに表示されるコードをそのまま記述。 description.htmlにはDescriptionに表示する説明文をhtmlで記述する。 これでおしまい。動作確認後ビルドしてPluginRepositoryで配布しようね。
明日はhiromikai_green氏がなんか書いてくれると思います。期待。