こんにちは。電通総研ITの寺尾です。
前回ご紹介したプラグイン開発環境構築に続き、今回から実際に機能実装についてお話していきます。
本記事でご紹介する機能はアクション機能です。
前回はこちら:IntelliJプラグイン開発の始め方~環境構築編~
アクション機能とは
IntelliJにおけるアクションとは、メニューやショートカットで呼び出せる機能全般を指します。
普段IntelliJを使う際に、ショートカットキーによるファイル検索、Gitウィンドウでの操作、Runメニューからのデバッグ起動などをすることはないでしょうか。
実はこれらも、IntelliJ上ではアクションとして実装された機能となっています。
「Doma Tools」で実装したアクションを例に、任意のメニューに独自のアクションを追加する方法についてご説明します。
実装前に知っておくこと
実装言語
公式ドキュメントではJavaコードのサンプルが提示されていますが、「Doma Tools」ではテンプレートをベースにKotlinで実装しています。
KotlinはJavaと非常によく似た実装が可能な言語ですが、以下の特徴を持つKotlinを採用しています。
Kotlinでプラグインを開発する利点
- null安全性:変数宣言時に、Null許容かを明示的に指定できます
- 型安全ビルダー:静的に必須パラメータをチェックし、コンパイルが成功する状態でのみ、ビルド処理が呼び出せるようにします
- コールバック:IntelliJプラットフォームが多用するコールバックを、Kotlinはラムダ式によって容易に実装できます
他にも保守性やパフォーマンスに効果のある機能を持ち、IntelliJプラグイン開発において非常に相性の良い言語となっています。個人的にも、null安全性は開発において特に便利な点だと感じています。
これからプラグイン開発をされる方も、Kotlinで実装してみてください!
Kotlinプロジェクトに関する詳細は、Kotlinをご参照ください。
【補足】
簡単なKotlinコードをウェブ上で実行することもできますので、この機会にKotlinにも触れてもらえればと思います!
Play Kotlin
PsiElement
IntelliJプラグインの実装ではそのほとんどでPsiElement
のオブジェクトを扱います。
これはファイルやディレクトリ、ファイル内のメソッド名やパラメータなどあらゆる要素を表す基本的な型になっており、
IntelliJ上のあらゆる要素はこの型によるツリー構造を持っています。
サブタイプも多く実際には複雑なツリー構造を持つ場合もあるため、開発を通して扱い方を覚えていきましょう。
公式ドキュメント
参考:PSIツリー構造の確認方法
デバッグ環境では、ツール > View Psi Structure of Current File
から、PSIツリー構造を確認できます。
またPsiViewerプラグインを導入することで、デバッグ環境以外でもPSIツリー構造を確認できるようになります。
実装
ゴール
今回例に挙げるアクションは、DAOファイルからSQLファイルにジャンプする機能です。
アクションが有効なカーソル位置の判定や、ファイルジャンプまでの実装方法についても見ていきます。
事前準備
アクション機能の実装に必須なものは以下の2つになります。
AnAction
のサブクラス- プラグイン設定登録
プラグイン設定でアクションとして登録できるクラスはAnAction
のサブクラスとなっており、
アクション機能の登録はplugin.xml
に
前回ご紹介したアクション機能の登録例と同じ内容ですが再度掲載します。
- actions: アクションメニューをグループ化するタグ
- group: アクショングループの設定。グループ名やどのメニューにアクションを追加するなどを設定
- action: アクション情報を記載するタグ
- id: アクション機能の識別ID(任意のID)
- class: 実装したAnActionのサブクラスの完全修飾クラス名
- text: アクション表示名
- description: アクションの説明
- keyboard-shortcut: デフォルトのショートカットキーを登録するタグ
<actions> <group id="org.domaframework.doma.intellij.DomaToolGroupActions" text="Doma Tools" popup="true" icon="AllIcons.Nodes.AbstractClass"> <add-to-group group-id="EditorPopupMenu" anchor="last" /> <action id="org.domaframework.doma.intellij.action.JumpToSQLFromDao" class="org.domaframework.doma.intellij.action.dao.JumpToSQLFromDaoAction" text="Jump to SQL" description="Jump from DAO file to SQL file"> <keyboard-shortcut keymap="$default" first-keystroke="alt D"/> </action> </group> </actions>
準備が整いましたら、実装を始めていきましょう!
参考:Plugin DevKitを使う
Plugin DevKitを使うことで、簡単に上記の下準備ができます。
ファイル > 新規 > Plugin DevKit > Action
表示制御の実装
AnAction
クラスで主に使用するオーバーライドメソッドについて説明します。
紹介するメソッドの役割は、それぞれアクション表示制御、アクション実行スレッドの設定、処理本体です。
まずは表示制御から見ていきます。
update
メニュー表示時やショートカット実行時、まずはこのメソッドが呼び出され、アクションの条件を満たしているかを制御します。
引数で渡されるAnActionEvent
型が持つpresentation.isEnabledAndVisible
を、最終的にtrue
とすることがアクション実行条件を満たすことになります。
例えば、以下の条件をチェックするロジックを実装します。
以下は実装例です。ファイル情報やカーソル位置の取得は、他機能の実装でもよく使うロジックになります。
ファイルタイプの判定
アクションの呼び出しがJavaファイル上であるかを判定するため、ファイル情報を取得してファイルタイプをチェックします。
// eはupdateに渡されるAnActionEvent型引数 // アクションを呼び出したファイル情報をPsiFileオブジェクトで取得 val currentFile = e.getData(CommonDataKeys.PSI_FILE) ?: return val file: PsiFile = currentFile ?: return // PsiFileからVirtualFile型の要素を取得し、FileTypeManagerによってファイルタイプを取得 val fileType = FileTypeManager.getInstance().getFileTypeByFile(file.virtualFile) val isDaoFileType = when (fileType.name) { "JAVA", "Kotlin", "CLASS" -> true else -> false }
カーソル位置の取得
DAOメソッドを対象にアクションを実行するため、カーソル位置の要素をチェックします。
// editorにエディタ画面情報を格納 val editor = e.getData(CommonDataKeys.EDITOR) ?: return // editor.caretModel.offsetで、エディタ画面上のカーソル位置の要素をPsiElement型オブジェクトとして取得 val element = file.findElementAt(editor.caretModel.offset) ?: return
SQLファイルへのジャンプが必要なDAOメソッドかは、実際にはメソッドに付与されたアノテーションタイプなどを細かくチェックする必要がありますが、
今回は簡単にカーソル位置の要素がDAOメソッドかのチェック方法を例にします。
// 取得したelementが、メソッド要素であることをチェック val isDaoMethod = element is PsiMethod
最後に、チェックした結果をAnActionEvent
のプロパティに設定して表示制御のロジックは完成です。
e.presentation.isEnabledAndVisible = isDaoMethod
アクションスレッドの設定
update()
をバックグラウンド(BGT)とイベントディスパッチ(EDT)のどちらのスレッドで呼び出すかを指定します。 書き込み処理など編集を加える処理はEDT、その他は基本的にBGTを設定します。
※6/20 getActionUpdateThread()
がupdate()
に影響するメソッドであることをご指摘いただいたため、明記させていただきました。
詳細は公式ドキュメントをご参照ください。
getActionUpdateThread
今回はファイルジャンプのみを行うためBGTで設定します。
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
アクション処理の実装
アクションが呼び出された時の本体処理を実装します。
actionPerformed
必要な情報を再度取得してファイルジャンプ処理を実装します。
// カーソル位置のDAOメソッド要素を取得 val editor = e.getData(CommonDataKeys.EDITOR) ?: return val element = currentFile?.findElementAt(editor.caretModel.offset) ?: return // PsiMethodの子要素が取得される可能性もあるため、念のため親のPsiMethodを取得 val method = PsiTreeUtil.getParentOfType(element, PsiMethod::class.java) ?: return val daoFile = method.containingFile.virtualFile // 以下はプロジェクトの構成情報を扱うために必要なオブジェクト val project = e.project ?: return val module = project.getModule(daoFile) // ジャンプ先のファイル情報を取得し、ファイルジャンプ
ファイルジャンプのロジック
ここからは少し複雑なロジックになるため、ファイル情報を取得するために必要なコアな実装例のみを記載します。
ファイルパスを取得
Domaのルール上、DAOメソッドに関連するSQLファイルは、リソースディレクトリ内の以下に配置されていることが前提となります。
【SQLファイルの配置先】
{リソースディレクトリ}/META-INF/{DAOパッケージパス}/{DAO名}/{DAOメソッド名}.sql
まずは基となるDAOメソッドのファイルパスを取得します。
カーソル位置から取得した要素オブジェクトから、以下のようにファイル情報を取得できます。
// DAOメソッド名 val methodName = element.name // プロジェクトのルートパス。「{プロジェクトディレクトリ}/src/main」のようなパスを取得 val contentRoot = this.psiProject.getContentRoot(daoFile)?.path // contentRootが取得できていれば、収集した情報でSQLファイル用の相対パスを生成 val sqlFilePath = contentRoot.let { // DAOファイルの絶対パス val daoFilePath = daoFile.path // DAOファイルのパスを文字列変換し、SQLファイル用の相対パスを生成 // 例)「src/main/java/example/dao/ExampleDao.java」を変換 daoFilePath .replace("java","resources/META-INF") .replace(".java","/") .plus("$methodName.sql") // src/main/resources/META-INF/example/dao/ExampleDao/exampleMethodName.sql }
SQLファイルを取得
リソースディレクトリ内から相対パスでファイルを取得する場合、ResourceFileUtil
を使用します。
以下の処理では、DAOファイルと同じモジュール内のリソースディレクトリから、相対パスに一致するSQLファイルをVirtualFile
オブジェクトとして返します。
第3引数は検索スコープを設定します。getModuleScope
には、テストリソースディレクトリとしてマークされたディレクトリを検索対象とするかのフラグを設定します。この例では対象外とするためfalse
で設定します。
sqlFilePath
はMETA-INF/
から始まる相対パスです。
val targetSql = ResourceFileUtil.findResourceFileInScope( sqlFilePath.replace("//", "/"), project, module.getModuleScope(false), )
SQLファイルへジャンプ
最後はファイルジャンプの実装です。
対象ファイルにジャンプする処理自体は非常にシンプルで、FileEditorManager
を使用して以下のように実装します。
FileEditorManager.getInstance(project).openFile(targetSql, true)
これでDAOメソッドからSQLファイルをエディタ画面上で開くことができるようになりました!
動かしてみる
デバッグ起動して、想定通りにファイルジャンプアクションが動作するか試してみましょう。
前回ご紹介したデバッグ起動するで、IntelliJのデバッグ画面を起動してください。
画面が起動したら、以下手順で動作確認してみましょう。
もしアクションが表示されない場合や、アクションを実行してもファイルジャンプできない場合
開発画面側でブレークポイントを設定し、DAOやSQLファイル情報が正しく取得できているか等を確認してください。
参考: 「Doma Tools」の実装コード
「Doma Tools」の同様のジャンプアクションは以下コードで実装しています。
JumpToSQLFromDaoAction
さいごに
今回はIntelliJプラグインのアクション機能実装に関するお話でした。
アクション機能は「Doma Tools」に実装した中では個人的に比較的簡単な機能となっており、最初に実装するにはちょうど良い難易度でした。
この記事ではファイルジャンプ機能を例に挙げましたが、SQLファイルの生成など様々なアクションも実装できますので、「Doma Tools」や他のプラグインも参考にして好みの機能を追加してみてください!
今後も他機能の実装に関する記事を投稿していきますので、ぜひご覧ください。
レビューも投稿していただけますと大変励みになります🙇♀️
Doma Tools マーケットプレイスページ
本プラグインはOSSとしてDomaコミュニティへ寄贈されています。
不具合修正や機能要望、ディスカッションにもぜひご参加ください。
採用ページ
執筆:@terao.haruka、レビュー:@nakamura.toshihiro
(Shodoで執筆されました)