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

# まえがき
この記事シリーズのソースコードは以下のリポジトリで管理されています。
[Qiita Artifact Plugin](https://github.com/syuchan1005/QiitaArtifactPlugin)

# 目次
1. [プロジェクト作成と設定まで](https://qiita.com/syu_chan_1005/items/74a834bb353f6b7df4de)
2. [アクションの追加, 通知, ダイアログ](https://qiita.com/syu_chan_1005/items/a13a1ba48f5cfe862dd8)
3. 拡張ポイント(ここ)

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

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

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

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

という手順が必要です。

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

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

という手順が必要です。

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

## クラスを作る。

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

```java: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`に追記してください。

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

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

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

- [LangExtensionPoints.xml](https://upsource.jetbrains.com/idea-ce/file/idea-ce-d00d8b4ae3ed33097972b8a4286b336bf4ffcfab/platform/platform-resources/src/META-INF/LangExtensionPoints.xml)
- [PlatformExtensionPoints.xml](https://upsource.jetbrains.com/idea-ce/file/idea-ce-d00d8b4ae3ed33097972b8a4286b336bf4ffcfab/platform/platform-resources/src/META-INF/PlatformExtensionPoints.xml)
- [VcsExtensionPoints.xml](https://upsource.jetbrains.com/idea-ce/file/idea-ce-d00d8b4ae3ed33097972b8a4286b336bf4ffcfab/platform/platform-resources/src/META-INF/VcsExtensionPoints.xml)

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


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

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

### クラスの作成
```java: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`に以下のように記述します

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

## インターフェースの場合
(クラスの場合と混同しないよう，すべての項目に`I`と接頭辞を浸けます)

### インターフェースの作成
```java:IFireEventListener.java
public interface IFireEventListener {
	ExtensionPointName<IFireEventListener> EP_NAME =
			ExtensionPointName.create("<パッケージ名>.iFireEventListener");

	void fireEvent();
}
```

### plugin.xmlに拡張ポイントを登録
`plugin.xml`に以下のように記述します

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

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

```java: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();
		}
	}
}
```

```xml:plugin.xml
<action class="<パッケージ名>.FireEventAction" id="<プラグイン名>.FireEventAction"
                    text="Fire Action" icon="AllIcons.RunConfigurations.Scroll_down"/>
```

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

```java:CFireEventListenerImpl.java
public class CFireEventListenerImpl extends CFireEventListener {
	@Override
	public void fireEvent() {
		Notifications.Bus.notify(new Notification("<プラグイン名>", "Fire Event", "Class", NotificationType.INFORMATION));
	}
}
```

```java:IFireEventListenerImpl.java
public class IFireEventListenerImpl implements IFireEventListener {
	@Override
	public void fireEvent() {
		Notifications.Bus.notify(new Notification("<プラグイン名>", "Fire Event", "Interface", NotificationType.INFORMATION));
	}
}
```

```xml:plugin.xml
<idea-plugin>
    ...
    <extensions defaultExtensionNs="<プラグインID>">
        <cFireEventListener implementationClass="<パッケージ名>.CFireEventListenerImpl" />
        <iFireEventListener implementation="<パッケージ名>.IFireEventListenerImpl" />
    </extensions>
</idea-plugin>
```

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