So What!?

カテゴリ

記事

連絡先

何か間違っている情報などがあればTwitterにてメッセージください。

ご注意

このBlogはアクセスデータをGoogle Analyticsおよび、Cloudflare Analyticsに送信しています。また研究開発用にSentryにパフォーマンスデータを送信しています。それ以外のデータは収集していません。

IntelliJのプラグインを作る-1日目

2023年11月7日

IntelliJというか、WebStormのプラグインを作りたくていろいろ調べながら進めているのでメモ。

※ この記事を書いた後にIntelliJ Platform Pluginのメジャーバージョンアップがあり、かなり大幅に変わっているので、もしかしたら参考にならないかもしれない。変更点はJetBrains Plugin Developer Conf 2024を見てもらえるとよいと思う。

追記ここから;

あれやりたいけどどうやってやればいいか分からない、とかが多発したけど、最初はどこをフックにして情報を探せばよいか検討がつかなかった。

プラグインの開発を始める前にExplore the IntelliJ Platform APIを読んでおくとよいかもしれない。

読んだとしても、APIの使い方が簡単になるわけではないけど、とっかかりを掴むのには役立ちそう。

あと、全部は見てないけど、テスト周りの応用的な話がYouTubeに上がってたりしたのでメモとして書いておく。

:追記ここまで

公式のテンプレートを使う

まずはJetBrainsのGitHubにintellij-platform-plugin-templateがあるので、まずはそのテンプレートを使ってプロジェクトを作りつつ、このGitHubリポジトリのREADME.mdにプロジェクトを作成した後の手順を見ながら変えないといけないところを変える。

テンプレートを使わない場合はPlugin DevKitっていうプラグイン開発用のプラグインがあるので、こちらも使うとシンプルな構成新規プロジェクトを作成できる。

plugin.xml

ここからは設定が済んでいる前提で、いくつかのコアな機能を深掘りしていく。今回はplugin.xmlについて深掘りする。

plugin.xmlはプラグインのメタ情報や、色々な処理のエントリーポイントを書くファイルなので、そのプラグインがどんな主要機能を提供しているかは、基本plugin.xmlを見ればなんとなくわかるようになる。たぶん。

plugin.xmlに記載する内容はConfiguration Structure Overviewを見る。

プラグインの名前などの基本的なメタ情報以外で、いくつか取り上げてみよう。

depends

このプラグインが依存しているモジュールを指定する。WebStorm向けのプラグインを作る場合はJavaScriptの指定が必須。

<idea-plugin>
    <depends>JavaScript</depends>
</idea-plugin>

resource-bundle

resource-bundleにはi18nを実現するためのファイルを指定する。アプリケーションのテキストメッセージやエラーメッセージなどの文字列を外部化しておいて、異なる言語に対応する。

言語ごとにファイルを作っておくと、ユーザーの使用言語にあったファイルを参照してくれる。はず。

Actions

アクションはIDEのツールバーに拡張機能ようのメニューを追加して、クリックされた時に何かをするタイプの拡張機能。拡張機能の中でも実行タイミングが最もわかりやすい。

データを保存する時の注意点とか、アクションをグループ化してツールバー内でネストするとか、ラベルとか、ショートカットキーとかいろいろ考える必要ありそう。

右クリックした時のメニューにアクションを追加する時のサンプル。

<actions>
    <group
        id="org.intellij.sdk.action.CustomDefaultActionGroup"
        class="org.intellij.sdk.action.CustomDefaultActionGroup"
        popup="true">
        <add-to-group group-id="EditorPopupMenu" anchor="first"/>
    </group>
</actions>

Listeners

リスナーというのは、プラグインが配信されるイベントをサブスクライブする仕組み。

リスナーはアプリケーションレベル、もしくはプロジェクトレベルで定義することができる。

イベント(IntelliJではトピックというっぽい)の一覧はExtension Point and Listener Listに載っている。

applicationListeners

applicationListenersはアプリケーションレベル、つまりIDE全体のイベント(IDEの起動や終了など)をサブスクライブする。

リスナーはプログラム内からも設定できるが、plugin.xmlを通して、宣言的に設定したほうがパフォーマンスがいいらしい。

<idea-plugin>
    <applicationListeners>
        <listener class="myPlugin.MyListenerClass" topic="BaseListenerInterface"/>
    </applicationListeners>
</idea-plugin>

projectListeners

projectListenersはプロジェクトレベルのトピック(プロジェクトのオープン、ファイルの変更など)をサブスクライブする。

<idea-plugin>
    <projectListeners>
        <listener class="myPlugin.MyListenerClass" topic="BaseListenerInterface"/>
    </projectListeners>
</idea-plugin>

Service

Servicesは複数箇所で呼び出されても、スコープごとにインスタンスが1つであることが保証されるコンポーネント。

IntelliJでは、アプリケーションレベル、プロジェクトレベル、モジュールレベルの3タイプのサービスがある。(メモリの使用量が増えるので、モジュールレベルは使わないほうがいいらしい。)

Light Service

単純な機能の場合は、アノテーションをつけておくだけで、オンデマンドでロードされる。このタイプのServiceをLight Serviceという。

// アプリケーションレベルのService
@Service
class MyAppService {
  fun doSomething(param: String) {
    // ...
  }
}

// プロジェクトレベルのService
@Service(Service.Level.PROJECT)
class MyProjectService(private val project: Project) {
  fun doSomething(param: String) {
    val projectName = project.name
    // ...
  }
}

非Light Service

サービスをAPIとして外部に公開したりするときは、plugin.xmlに登録する必要がある。

interface MyAppService {
  fun doSomething(param: String)
}

class MyAppServiceImpl : MyAppService {
  override fun doSomething(param: String) {
    // ...
  }
}
<idea-plugin>
    <extensions defaultExtensionNs="com.intellij">
        <applicationService serviceInterface="myPlugin.MyAppService" serviceImplementation="myPlugin.MyAppServiceImpl"/>
    </extensions>
</idea-plugin>

Extensions

extensionsは比較的複雑な方法でIDEの機能を拡張する方法で、いくつかのパターンがある。

それはExtension Pointと呼ばれていて、1500種類以上もあるらしい。やばすぎ。

どうやら拡張機能自体が、Extension Pointを定義することができて、別の拡張機能がそのExtension Pointを使うことができるようになっていて、数が増えているよう。もちろんSDKがデフォルトで提供しているExtension Pointもある。

IntelliJ Platform Explorerを使うと、特定のExtension Pointsを使っているプラグインを探すこともできるし、プラグインが提供しているExtension Pointsを探すこともできるっぽい。

拡張機能をさらに拡張したくなった時にExtension Pointsがないかなーって感じで使うのかな、と思う。Extensionsだけでなく、Listenersも検索できるみたい。逆にExtension Pointsの名前がわかるなら、すでに使っているプラグインのコードを見にいけば使い方もなんとなく見えてくる。

Extension Point and Listener Listからいくつか紹介したいなーと思ったけど、とくにリファレンスっぽいものがあるわけではなく、Extension Pointsの実装へのリンクが貼ってあるだけっぽい。

リンク先のコードにエクステンションポイントのディスクリプションがコードコメントで書いてあるから、使いたいやつっぽいのを探して読んでいくしかなさそう。まぁまぁツラいな。

toolWindow

ToolWindowFactoryはいわゆるサイドバーに機能を置きたい拡張機能が使う。最近だとVSCodeのCopilot Chatとかが該当すると思う。

<idea-plugin>
    <extensions defaultExtensionNs="com.intellij">
        <toolWindow factoryClass="myPlugin.MyToolWindowFactory" id="MyToolWindow"/>
    </extensions>
</idea-plugin>

appStarter

ApplicationStarterはコマンドラインからプラグインを起動したいときにつかうエクステンションポイント。だと思う。

<idea-plugin>
    <extensions defaultExtensionNs="com.intellij">
        <appStarter implementation="myPlugin.MyAppStarter" />
    </extensions>
</idea-plugin>

MyAppStarterにpremainmainを実装しておくと、それぞれプラグインの起動前と起動時に呼び出される。

externalAnnotator

ExternalAnnotator今回作ろうと思っているプラグインのメインとなりそうなExtension Pointsの1つ。

外部のアノテーションツール(リンターなど)を使用して言語ファイルを処理するために使う。外部のアノテーションプラグインはIDEの通常のアノテーションが完了した後に実行されるので、完了するのが遅い。

アノテーションは3つのステップで行われる。

  • ファイルに関するデータを収集
  • ツールを実行してハイライトデータを収集
  • 最終的にハイライトデータをファイルに適用する
<idea-plugin>
    <extensions defaultExtensionNs="com.intellij">
        <externalAnnotator implementationClass="myPlugin.MyExternalAnnotator" language="HTML" />
    </extensions>
</idea-plugin>

おまけ:コアエレメント

plugin.xmlには機能的な情報意外にもプラグインのメタ情報を指定する。名前や説明文など、それぞれのプラクティスがまとまっているので読んでおくと良さそう。

  • プラグインの名前
  • プラグインの互換性情報
  • プラグインの説明文
  • プラグインのロゴ
  • プラグインのスクリーンショット、動画のURL
  • プラグインのタグ
  • プラグインのライセンス
  • プラグインの利用開始手順
  • プラグインの変更履歴
  • プラグインの問い合わせ先
  • ドネーションに関する設定