IntelliJ IDEAのプラグインを作ろう!(拡張ポイント)


まえがき

この記事シリーズのソースコードは以下のリポジトリで管理されています。
Qiita Artifact Plugin

目次

  1. プロジェクト作成と設定まで
  2. アクションの追加, 通知, ダイアログ
  3. 拡張ポイント(ここ)

はじめに

最初の記事で独自言語作りましょうとか言いつつここまでまだ何もしてません。
基礎って大事なんです。てことで今回はExtension Pointについてやります。

拡張ポイントとは

Extension PointはIDEが様々な動作をする時呼び出されるものです。
引数を用いて結果を返すことでIDEの表示が変更されたり,ユーザーの操作の結果を受け取って裏で処理することなどができます。

拡張ポイントを利用するには

  1. それぞれの拡張ポイントに設定されている[クラス|インターフェース]を実装したクラスを作る。
  2. plugin.xmlに拡張ポイントを使うことを宣言する。

という手順が必要です。

また、拡張ポイントを作成するには

  1. 呼び出したい[メソッド|フィールド]を定義した[クラス|インターフェース]を作る
  2. plugin.xmlに拡張ポイントを登録する。
  3. 他のプラグイン等で宣言された拡張ポイントを取得し呼び出す。

という手順が必要です。

拡張ポイントを利用する

ここでは例としてeditorTabColorProviderという拡張ポイントを利用してみます。
前に書いた手順に従って進みます。

クラスを作る。

editorTabColorProviderではEditorTabColorProviderというインターフェースを実装するように指定されているので実装します。
TabColorExtensionImplクラスを作成し以下のように記述しましょう。

TabColorExtensionImpl.java
public class TabColorExtensionImpl implements EditorTabColorProvider {
    @Nullable
    @Override
    public Color getEditorTabColor(@NotNull Project project, @NotNull VirtualFile virtualFile) {
        switch (virtualFile.getFileType().getDefaultExtension()) {
            case "txt":
                return JBColor.GREEN;
            case "xml":
                return JBColor.BLUE;
            case "java":
                return JBColor.YELLOW;
            default:
                return null;
        }
    }
}

この例では開いたファイルの拡張子に従ってタブの色を変更します。

plugin.xmlに宣言する。

以下のようにplugin.xmlに追記してください。

plugin.xml
<idea-plugin>
  ...
  <extensions defaultExtensionNs="com.intellij">
    <editorTabColorProvider implementation="<パッケージ名>.TabColorExtensionImpl" />
  </extensions>
</idea-plugin>

<extensions>に宣言されているdefaultExtensionNsはプラグインのidを指定することでそのプラグインで指定されている拡張ポイントを利用することができます。
例えば自分が作成しているEmojiPrefixの拡張ポイントであるemojiPanelFactoryを利用するにはdefaultExtensionNscom.github.syuchan1005.emojiprefixを指定します。

拡張ポイントの探し方(おまけ)

IntelliJ Platformで使用できる拡張ポイントは機能ごとに下の3つのXMLファイルにまとめられています。

基本はその名前で判断しますが,できないときはブレークポイントを設定しデバッグをして探すなどしましょう。
(そのうちこの辺もリストにしたいけどめんどくさいよねぇ)

拡張ポイントを作成する

今回は単純に何かが起きたらfireEventメソッドが呼び出されるといったものを作ります。
拡張ポイントの名前はfireEventListenerとします。

クラスの場合

(インターフェースの場合と混同しないよう,すべての項目にCと接頭辞を浸けます)
(クラスの方どう作ればいいかよくわからなくて若干ゴリ押ししてます)

クラスの作成

CFireEventListener.java
public class CFireEventListener {
    public static final ExtensionPointName<CFireEventListener> EP_NAME =
            ExtensionPointName.create("<パッケージ名>.cFireEventListener");

    @Attribute("implementationClass")
    private String implementationClass;

    public CFireEventListener() {}

    public void fireEvent() {}

    protected CFireEventListener getInstance() {
        try {
            Class<?> clazz = Class.forName(implementationClass);
            return (CFireEventListener) clazz.getConstructor().newInstance();
        } catch (ReflectiveOperationException e) {
            return null;
        }
    }
}

plugin.xmlに拡張ポイントを登録

plugin.xmlに以下のように記述します

plugin.xml
<idea-plugin>
    ...
    <extensionPoints>
        <extensionPoint name="cFireEventListener" beanClass="<パッケージ名>.CFireEventListener">
            <with attribute="implementationClass" implements="<パッケージ名>.CFireEventListener"/>
        </extensionPoint>
    </extensionPoints>
</idea-plugin>

インターフェースの場合

(クラスの場合と混同しないよう,すべての項目にIと接頭辞を浸けます)

インターフェースの作成

IFireEventListener.java
public interface IFireEventListener {
    ExtensionPointName<IFireEventListener> EP_NAME =
            ExtensionPointName.create("<パッケージ名>.iFireEventListener");

    void fireEvent();
}

plugin.xmlに拡張ポイントを登録

plugin.xmlに以下のように記述します

plugin.xml
<idea-plugin>
    ...
    <extensionPoints>
        <extensionPoint name="iFireEventListener" interface="<パッケージ名>.IFireEventListener"/>
    </extensionPoints>
</idea-plugin>

拡張ポイントのテスト

テストのために拡張ポイントを呼び出すFireEventActionクラスを作成し,Menuに追加します。

FireEventAction.java
public class FireEventAction extends AnAction {
    @Override
    public void actionPerformed(AnActionEvent anActionEvent) {
        for (CFireEventListener listener : CFireEventListener.EP_NAME.getExtensions()) {
            listener.getInstance().fireEvent();
        }
        for (IFireEventListener listener : IFireEventListener.EP_NAME.getExtensions()) {
            listener.fireEvent();
        }
    }
}
plugin.xml
<action class="<パッケージ名>.FireEventAction" id="<プラグイン名>.FireEventAction"
                    text="Fire Action" icon="AllIcons.RunConfigurations.Scroll_down"/>

このままでは呼び出すことはできますが拡張ポイントが使われていないので,クラスを作成しplugin.xmlに登録します

CFireEventListenerImpl.java
public class CFireEventListenerImpl extends CFireEventListener {
    @Override
    public void fireEvent() {
        Notifications.Bus.notify(new Notification("<プラグイン名>", "Fire Event", "Class", NotificationType.INFORMATION));
    }
}
IFireEventListenerImpl.java
public class IFireEventListenerImpl implements IFireEventListener {
    @Override
    public void fireEvent() {
        Notifications.Bus.notify(new Notification("<プラグイン名>", "Fire Event", "Interface", NotificationType.INFORMATION));
    }
}
plugin.xml
<idea-plugin>
    ...
    <extensions defaultExtensionNs="<プラグインID>">
        <cFireEventListener implementationClass="<パッケージ名>.CFireEventListenerImpl" />
        <iFireEventListener implementation="<パッケージ名>.IFireEventListenerImpl" />
    </extensions>
</idea-plugin>

これでメニューに追加されたFire EventをクリックすることでExtension Pointが動いていれば通知が出ます。