電通総研 テックブログ

電通総研が運営する技術ブログ

IntelliJプラグイン開発の始め方~コード補完編~

こんにちは。電通総研ITの寺尾です。 今回はIntelliJコード補完機能の実装方法についてご紹介します。

前回はこちら:IntelliJプラグイン開発の始め方~コード検査編~

コード補完とは

Java予約語や変数名などを記述する時、「Ctrl + Space」でIDEAから提示される候補から選択するという操作はよくされると思います。
近年では、AIによるサポートで入力候補が提示される機能もよく見かけるようになりました。

そのように提案(サジェスト)されるリストを、プラグインコード補完機能として実装することができます。

Completion

実装前に知っておくこと

前回と同様に、本記事でもKotlinでの実装例を提示します。

Kotlinプロジェクトに関する内容は、アクション機能編の実装前に知っておくことをご参照ください。

実装

ゴール

今回は、Domaのディレクティブ内でDAOメソッドで定義されたパラメータをサジェストする機能を実装してみます。

事前準備

IntelliJに指定の言語に対するコード補完機能として認識させるための設定と、実装クラスを用意します。
Doma Tools」では、SQLをカスタム言語として扱っているため、独自の言語IDとコード補完クラスを紐づけています。

まずは以下の準備から始めていきましょう。

  • CompletionContributorのサブクラス
  • CompletionProviderのサブクラス
  • プラグイン設定登録

コード補完機能ではコントリビュータクラスを登録し、カーソル位置が特定の条件に一致する場合にプロバイダークラスを呼び出す処理を行います。

plugin.xmlには、以下のように<completion.contributor>タグを使ってコントリビュータクラスを登録します。
コントリビュータクラスはIntelliJの拡張ポイントとして登録するため、<extensions defaultExtensionNs="com.intellij">タグの中に記述します。

<completion.contributor language="DomaSql"
      implementationClass="org.domaframework.doma.intellij.contributor.sql.SqlCompletionContributor"
      order="first" />

各実装のポイントを以下で解説します。最後に「Doma Tools」リポジトリの実装コードのリンクも添付していますのでご参考ください。

コントリビュータクラスの実装

コード補完を呼び出した時のカーソル位置が、条件に合う場合にのみコード補完を呼び出すように制御します。
このコントリビュータクラスでは、extend()で条件判定の処理を呼び出し、最後に条件に一致する場合のプロバイダークラスを設定します。

以下の例では、独自に要素チェックを行うPsiPatternUtilを実装し、各メソッド内で判定処理を実装しています。

open class SqlCompletionContributor : CompletionContributor() {
    init {
        extend(
            CompletionType.BASIC, // コード補完タイプ。今回は通常のコード補完呼び出しで実行するためBASICを指定
            // コード補完を呼び出す条件を定義
            PsiPatternUtil
                .createPattern(PsiComment::class.java)
                .andOr( 
                    // 独自の処理で要素のパターンをチェック
                    PsiPatternUtil
                        .createPattern(PsiComment::class.java)
                        .inFile(
                            PlatformPatterns
                                .psiFile()
                                .withLanguage(SqlLanguage.INSTANCE),
                        ),
                    PsiPatternUtil
                        .createPattern(PsiComment::class.java)
                        .and(PsiPatternUtil.isMatchFileExtension("java")),
                )
                ),
            // パターンと一致する場合に呼び出すプロバイダークラスインスタンス
            SqlParameterCompletionProvider(),
        )
    }
}

プロバイダークラスの実装

コード補完の本体処理を担当するCompletionProviderを実装します。
プロバイダークラスの役割は、サジェスト情報を収集してリストをセットすることになります。

オーバーライドメソッドaddCompletions()を実装し、カーソル位置の要素をチェックしてサジェストリストを生成します。

override fun addCompletions(
        parameters: CompletionParameters, // カーソル位置の情報など
        context: ProcessingContext,
        result: CompletionResultSet, // サジェストを登録するリスト
    ) {
      // サジェスト情報を収集するロジックを実装
      // resultにサジェストオブジェクトを追加
    }

サジェストオブジェクトの生成と登録

サジェストしたい情報の型によって、resultにセットする際に工夫が必要になります。
例えば、クラスのフィールド(PsiField)オブジェクトは以下のようにVariableLookupItemへの簡単な変換で追加ができます。

// フィールドをリストに登録
// VariableLookupItemにPsiFieldを渡したオブジェクトを生成しリストに追加
result.addAllElements(filterFields.map { param -> VariableLookupItem(param) })

対して、メソッド(PsiMethod)や独自の型からサジェストオブジェクトを生成する場合は、以下のように各パラメータをセットしたビルダーを使用します。

// LookupElementBuilderでサジェスト情報を設定したオブジェクトを生成する
filterMethods.forEach { method ->
    val lookupElm =
        LookupElementBuilder
            .create(method.name) // 候補名を設定
            .withPresentableText(method.name)  // 補完決定時に入力する文字列を設定
            .withTailText(method.parameterList.text, true) // 補足情報にメソッドのパラメータ情報を設定
            .withTypeText(method.returnType?.presentableText ?: "") // 候補の型情報を設定
            .withAutoCompletionPolicy(AutoCompletionPolicy.ALWAYS_AUTOCOMPLETE) // ALWAYS_AUTOCOMPLETE:候補が1つだけの場合自動で補完する
    // 生成したLookupElementBuilderを登録
    result.addElement(lookupElm)
}

result.addElement()によって、サジェストリストへの登録が完了します。

動かしてみる

実際にコード補完を呼び出し、取得した情報がサジェストされることを確認してみましょう。

デバッグ起動方法は前回と同じく、デバッグ起動するで実行してください。

参考: 「Doma Tools」実装コード

Doma Tools」のコード補完機能は、以下コードで実装しています。

まとめ

コード補完機能の実装ポイントは以下3点になります。

  • コントリビュータクラスで、補完処理の呼び出し条件を適切に設定する
  • プロバイダークラスで、サジェストリストを登録する
  • 不完全な状態の構文を考慮した実装を行う

コード補完機能では、入力途中の要素に対して考慮するポイントが多く存在します。
PSI要素の親子関係や要素タイプだけではカバーできないケースにも対応する必要があるため、実装が複雑になりがちな機能だと思います。

そのため、複数のケースで繰り返し使える処理の共通化や要素の判定条件の整理が、正確なサジェストには重要になります。

さいごに

Domaのように複数のファイルをまたいだ実装を行うケースでは、コード補完によるコーディングサポートによって開発者の負担を大きく軽減できるようになります。
カスタム言語を扱う予定がありましたら、構文やプロパティを適切に記述するための機能として併せて実装してみてください!

Doma Toolsへのレビューも投稿していただけますと大変励みになります🙇‍♀️

Doma Tools マーケットプレースページ

プラグインOSSとしてDomaコミュニティへ寄贈されています。
不具合修正や機能要望、ディスカッションにもぜひご参加ください。

採用ページ

執筆:@terao.haruka
レビュー:@nakamura.toshihiro
Shodoで執筆されました