PhpStorm Plugin（オートコンプリート）の作成方法について


今回はオートコンプリートプラグイン作成方法について簡単に説明します

基本的な部分はRubyMineやAndroid Studioでも変わらないと思います（試してないです）

![plugin1.gif](https://qiita-image-store.s3.amazonaws.com/0/4393/a4d4f27a-7017-2847-7684-aaf9378aee7c.gif "plugin1.gif")


## 今回作成するプラグイン

今回は`Sample::get()`の引数として渡されるクラス名を補完するプラグインを作成します

```php
/**
 * 何か処理する
 *
 * @param string $className クラス名
 */
public static function get($className)
{
	// 何か処理
}
```

## 試したバージョン

- `IntelliJ IDEA` - 13.1.2
- `PhpStorm` - 8.0.3
- `IntelliJ Platform Plugin SDK` - IDEA IU-135.690


## 作成方法

### プロジェクトの作成

プラグインプロジェクト（IntelliJ Platform Plugin）を新規作成します

今回はSampleStormという名前にしてます


### プロジェクトの設定

以下に従いライブラリ（php-openapi.jar、php.jar）とplugin.xmlを編集（ライブラリの依存関係を記入）をしてください

- [Setting-up environment for PhpStorm plugin development](https://confluence.jetbrains.com/display/PhpStorm/Setting-up+environment+for+PhpStorm+plugin+development)

#### Project Language Level

デフォルトでPhpStormは6.0で起動する為、7.0以降でビルドされたプラグインをインストールするとエラー（`Unsupported major.minor version 51.0`みたいなやつ）になります。

自分しか使わない、環境が固定されている等であれば特に6.0にする必要はないと思いますが、不特定多数に使うような場合は6.0にしておく方がよいかもしれません。

### META-INF/plugin.xml

プラグインの情報としてid, name, version, vendorを編集します。

ここの情報を編集しないとプラグインは動作しないので注意してください。


```xml
<idea-plugin version="2">
  <id>com.samplestorm</id>
  <name>SampleStorm</name>
  <version>1.0</version>
  <vendor email="aaa@example.com" url="http://www.example.com">Example</vendor>
```

### SampleCompletionContributorクラス

[ドキュメント](https://confluence.jetbrains.com/display/IntelliJIDEA/Completion+Contributor)によるとオートコンプリートを提供するのに一番簡単な方法はCompletion Contributorを利用する事だそうなのでそれを利用します。

CompletionContributorクラスを継承したSampleCompletionContributorクラスを作成します。

```java
package samplestorm.completion.contributor;

import com.intellij.codeInsight.completion.CompletionContributor;

public class SampleCompletionContributor extends CompletionContributor {

    public SampleCompletionContributor() {
        extend(CompletionType.BASIC, PlatformPatterns.psiElement(PsiElement.class), new CompletionProvider<CompletionParameters>() {
            @Override
            protected void addCompletions(@NotNull CompletionParameters completionParameters,
                                          ProcessingContext processingContext,
                                          @NotNull CompletionResultSet completionResultSet) {
                completionResultSet.addElement(LookupElementBuilder.create("Hello"));
            }
        });
    }

}
```

コンストラクタ内でextendメソッドを利用してSampleCompletionContributorクラスが提供するオートコンプリートを設定します

### META-INF/plugin.xml

作成したSimpleCompletionContributorクラスを追加します

言語がPHPの場合に実施するようになります

```xml
<extensions defaultExtensionNs="com.intellij">
    <!-- Add your extensions here -->
    <completion.contributor language="PHP" implementationClass="samplestorm.completion.contributor.SampleCompletionContributor"/>
</extensions>
```

これで一旦デバッグを起動するとHelloという文字列に補完が入るようになります

![ss 2015-04-09 13.43.39.png](https://qiita-image-store.s3.amazonaws.com/0/4393/ec8454f5-5b21-d7ee-3329-7296316f8ca1.png "ss 2015-04-09 13.43.39.png")


### CompletionContributor.extendメソッド

以下の引数を指定する事でオートコンプリートを設定できます

1. `CompletionType` - オートコンプリートのタイプを指定します
1. `ElementPattern` - オートコンプリートする対象を指定します
1. `CompletionProvider` - オートコンプリートを提供するクラスを指定します

今回作成するプラグインでは2のElementPatternでSample::get()を対象として、3のCompletionProviderでPHPクラス名を提供するように実装します。

### オートコンプリートする対象を指定する

上記サンプルでは最低限の指定しかしていないので別にSample::get()以外（aaa:bbb())でも補完がかかります。

![ss 2015-04-09 13.58.05.png](https://qiita-image-store.s3.amazonaws.com/0/4393/50631ff7-e5b9-99a7-ae83-6fc32dada5e5.png "ss 2015-04-09 13.58.05.png")

これを修正してSample::get()にだけ補完が入るように修正します。

指定するにはPsi Elementの基本知識を必要になります。

#### PsiElement

PhpStorm（IntelliJ）ではプロジェクト配下のファイルはPSI（Program Structure Interface） Elementによりツリー上で表現されています

例えば、Sample::get("className")のget("className")だけをすごく簡単に書くと以下のような感じなります（感じなので正確じゃないです）

```
// ツリーはもっとあると
+ Method reference (get)
	+ Class reference (Sample)
	+ Parameter list
		+ String ("className")
			+ PsiElement (")
			+ PsiElement (className)
			+ PsiElement (")
```

このようなツリー上の場合にのみ補完が入ると指定する事でSample::get()の時だけ補完が入るようになります。

指定ルールはリーフから書くので今回でいうとPsiElement (className)から指定します ※もっと綺麗な指定方法があると思います、力業ですみません！

1. 自分の親（`withParent`）は`String`
2. 1の親は`Paramter List`
3. 2の親は`Method Reference`
4. 3はクラスメソッド
5. メソッド名はget
6. 3の親の名前はSample

指定ルール返すsamplePatternというメソッドを作成します

```java
    public static PsiElementPattern.Capture samplePattern() {
        return PlatformPatterns.psiElement(PsiElement.class)
                .withParent(
                        PlatformPatterns.psiElement(StringLiteralExpression.class)
                                .withParent(
                                        PlatformPatterns.psiElement(PhpElementTypes.PARAMETER_LIST)
                                                .withParent(
                                                        PlatformPatterns.psiElement(PhpElementTypes.METHOD_REFERENCE)
                                                                .referencing(
                                                                        PhpPatterns.psiElement().withElementType(
                                                                                PhpElementTypes.CLASS_METHOD
                                                                        ).withName(StandardPatterns.string().oneOf("get"))
                                                                                .withParent(
                                                                                        PhpPatterns.psiElement().withName("Sample")
                                                                                )
                                                                )
                                                )
                                )
                );
    }
```

extend()を修正してsamplePatternを呼び出すようにします

```java
extend(CompletionType.BASIC, samplePattern(), new CompletionProvider<CompletionParameters>() {
    @Override
    protected void addCompletions(@NotNull CompletionParameters completionParameters,
                                  ProcessingContext processingContext,
                                  @NotNull CompletionResultSet completionResultSet) {
        completionResultSet.addElement(LookupElementBuilder.create("Hello"));
    }
});
```

デバッグを起動します

これで`Sample::get()`では補完が入り、`aaa::bbb()`では補完が入らないようになります

![ss 2015-04-09 14.29.02.png](https://qiita-image-store.s3.amazonaws.com/0/4393/a9c3c52d-39e9-e4b6-bb02-4b5c4e9dc476.png "ss 2015-04-09 14.29.02.png")

### 補完時にPHPクラスを提供する

CompletionProviderを継承したSampleProviderを作成します

```java
public class SampleProvider<CompletionParameters>  extends CompletionProvider {

    @Override
    protected void addCompletions(@NotNull com.intellij.codeInsight.completion.CompletionParameters completionParameters,
                                  ProcessingContext processingContext,
                                  @NotNull CompletionResultSet completionResultSet) {

        completionResultSet.addElement(LookupElementBuilder.create("Hello"));
}
```

extend()を修正してSampleProviderを指定します

```java
extend(CompletionType.BASIC, samplePattern(), new SampleProvider<CompletionParameters>());
```

デバッグを起動して補完がうまく動くのを確認します

addCompletions()メソッドの引数で渡されるCompletionResultSetにLookupElmentに補完候補を追加するだけで後はいい感じで動作してくれます

Helloを追加している行をコピーすると他の文字列も補完が入るようになるのが確認できると思います

#### LookupElement

補完が入った時の1行が1LookupElementのイメージです、CompletionResultSetが全体

![ss_2015-04-09_11_05_52 2.png](https://qiita-image-store.s3.amazonaws.com/0/4393/62aa8c9f-0bb4-b53b-f944-cb673a75c401.png "ss_2015-04-09_11_05_52 2.png")

今回は既に用意されているものを利用しますが、これも継承する事で独自のものを作れます

アイコンやテキスト、装飾（文字色、Boldとか）を設定できるようになります（時間があったらどこかで書きます）

今回はプロジェクト内のPHPクラスが補完されればいいのでプロジェクト内のPHPファイルを取得して追加していくようにします

プロジェクト内のPHPをインデックス化して検索機能を提供しているクラスPhpIndexが提供されているのでこれを利用します、またLookupElementも既に用意されているPhpLookupElementを利用します

```java
@Override
protected void addCompletions(@NotNull com.intellij.codeInsight.completion.CompletionParameters completionParameters,
                              ProcessingContext processingContext,
                              @NotNull CompletionResultSet completionResultSet) {

    Project project = completionParameters.getPosition().getContainingFile().getProject();
    PhpIndex phpIndex = PhpIndex.getInstance(project);
    for (String className : phpIndex.getAllClassNames(completionResultSet.getPrefixMatcher())) {
        PhpClass phpClass = phpIndex.getClassByName(className);
        if (phpClass != null) {
            completionResultSet.addElement(new PhpLookupElement(phpClass));
        }
    }
}
```

デバッグでPHPクラスが補完されるか確認してみてください

### パッケージ化する

プロジェクトを選択して右メニューから以下を選択するとパッケージ（SampleStorm.jar）が作成できます

![ss 2015-04-09 15.21.50.png](https://qiita-image-store.s3.amazonaws.com/0/4393/0588860c-5551-2f91-ecdd-9042fd4d3f2d.png "ss 2015-04-09 15.21.50.png")

これをPhpStormのPluginをディスクからインストールしてみると動くはずです

![ss 2015-04-09 15.19.17.png](https://qiita-image-store.s3.amazonaws.com/0/4393/844d1635-0c30-f63b-c599-59d6cb6d4438.png "ss 2015-04-09 15.19.17.png")


## おまけ

### PsiViewer

Psi Elementのツリー上を確認すた為に便利なプラグインが[PsiViewer](https://plugins.jetbrains.com/plugin/227)があります

これを利用して`Sample::get()`のツリーがどのようになっているを確認します

![ss 2015-04-09 14.07.35.png](https://qiita-image-store.s3.amazonaws.com/0/4393/c7f6315f-7fc7-a025-7c69-50bd4716cda4.png "ss 2015-04-09 14.07.35.png")

## Links

参考にしたサイトです

- [IntelliJ IDEA Plugin Development](https://confluence.jetbrains.com/display/IntelliJIDEA/Plugin+Development)
- [PhpStorm Plugin Development](https://confluence.jetbrains.com/display/PhpStorm/Plugin+Development)
- [PhpStorm: Symfony2 Plugin](http://www.slideshare.net/Haehnchen/phpstorm-symfony2-plugin)
- [IntelliJ IDEAのプラグインを作ろう！](http://qiita.com/Vexus2/items/e04a21f00e467b7ac8ad)

以上