読者です 読者をやめる 読者になる 読者になる

アラタナエンジニアブログ

aratana Engineer's Blog

IntelliJ IDEAのプラグインをScalaで作ろう

Scala

 こんばんは、アラタナグループのエンジニアの松下です。
2月からゲヒルン株式会社の正職員として東京で働いています。

仕事ではIntelliJ IDEAを使いながらScalaのコードを書いています。
IntelliJ IDEAは機能もプラグインも豊富なので、大抵のことは標準の機能や公開されているプラグイン
導入することで実現できます。
機能やプラグインが見つからない場合でも、自分でプラグインを書いて使うことができます。
プラグインJavaJVM上で動く言語で書くことができます。

今回はこのIntelliJ IDEAのプラグインScalaJavaScalaのビルドツールであるsbtを使って書いてみようと思います。


題材として、仕事中でもTwitterのタイムラインをこっそり確認できるプラグインを作ります。
(生産性を上げる統合開発環境で生産性を下げるプラグインなのは気にしない…)
設定画面でアクセストークンとシークレットを入力して保存して、ショートカットを押すと右上に通知として、タイムラインの直近のツイートを表示したいと思います。

完成版はこちら

satoshi-m8a/idea-plugin_sbt_sample · GitHub

プラグイン作成用のプロジェクトの作成

 はじめに、IntelliJ IDEAで File → New ProjectからIntelliJ Platform Plugin を選択し、Scalaにチェックを入れ、Next。
適当なプロジェクト名と保存先を入力しプロジェクトを作成します。

今回は twitter_ideaという名前のプロジェクトにしました。

f:id:satoshi_m8a:20150215152730p:plain

sbtの設定

 次に、build.sbtファイルをプロジェクトルート直下に置きます。
内容は以下のようにしました。

name := """twitter_idea"""

version := "1.0"

scalaVersion := "2.11.5"

libraryDependencies ++= Seq(
  "org.scalatest" %% "scalatest" % "2.2.4" % "test",
  "org.twitter4j" % "twitter4j-core" % "[4,)"
)

scalacOptions += "-target:jvm-1.6"

Twitter4Jを使っています。

関連するプロジェクトをインポートしますかという問い合わせがあるので、
import projectを選択します。

プラグイン設定ファイル(plugin.xml)の移動

 プロジェクトルートにあるMETA-INFフォルダを
src/main/resourceフォルダに移動します。

アクションの作成

 次に、src/main/scalaフォルダを作り、com.exampleパッケージを作ります。

com.exampleパッケージを右クリック New → Action からアクションを追加します。
ダイアログが出てくるので、入力内容は以下のようにしました。

f:id:satoshi_m8a:20150215142754p:plain

この設定では、ToolsメニューにShow Timelineという項目が増えます。
この項目を選択するか、Ctrl+Shift+T を二回押すとプラグインのアクションが実行されます。

Scalaでアクションを記述する

 TwitterIdeaPlugin.java という名前のファイルができるので、拡張子を.javaから.scalaに変更します。(重要)

内容は以下のようにしました。

package com.example

import com.intellij.notification.{Notification, NotificationType, Notifications}
import com.intellij.openapi.actionSystem.{AnAction, AnActionEvent}
import com.intellij.openapi.components.ServiceManager
import twitter4j.TwitterFactory
import twitter4j.conf.ConfigurationBuilder

import scala.collection.JavaConversions._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

class TwitterIdeaPlugin extends AnAction {

  def buildClient(config: TwitterIdeaConfig) = {
    val cb = new ConfigurationBuilder()
    cb.setDebugEnabled(true)
      .setOAuthConsumerKey(config.consumerKey)
      .setOAuthConsumerSecret(config.consumerSecret)
      .setOAuthAccessToken(config.accessToken)
      .setOAuthAccessTokenSecret(config.accessTokenSecret)
    new TwitterFactory(cb.build()).getInstance()
  }

  def actionPerformed(e: AnActionEvent) = {
    val config: TwitterIdeaConfig = ServiceManager.getService(classOf[TwitterIdeaConfig])

    if (!config.isConfigure) {
      Notifications.Bus.notify(new Notification("twitter for idea", "Config Error", "Please Config Twitter Key and Secret", NotificationType.INFORMATION))
    } else {
      val twitter = buildClient(config)
      val notifications = twitter.getHomeTimeline.toList.take(10).map(s => {
        new Notification("twitter for idea", s.getUser.getName, s.getText, NotificationType.INFORMATION)
      })

      notifications.foreach(Notifications.Bus.notify)

      Future {
        Thread.sleep(8000)
        notifications.map(_.expire)
      }
    }
  }
}

後述するTwitterIdeaConfigから設定を読みだしてTwitter4Jに渡しています。
タイムラインから直近10ツイートを読み出したら、Notifications.Bus.notify で 右上に通知を出しています。

設定保存用のクラスをJavaで書く

 PersistentStateComponentを継承してTwitterIdeaConfigクラスを書きます。
src/main/java/com/example直下にTwitterIdeaConfig.javaファイルを作成し、内容は以下のようにします。

package com.example;

import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.util.xmlb.XmlSerializerUtil;

@State(
        name = "TwitterIdeaConfig",
        storages = {
                @Storage(
                        id = "other",
                        file = "$APP_CONFIG$/twitteridea.xml")
        })
class TwitterIdeaConfig implements PersistentStateComponent<TwitterIdeaConfig> {
    public String consumerKey = "";
    public String consumerSecret = "";
    public String accessToken = "";
    public String accessTokenSecret = "";

    public boolean isConfigure() {
        return !(consumerKey.isEmpty() || consumerSecret.isEmpty() || accessToken.isEmpty() || accessTokenSecret.isEmpty());
    }

    public TwitterIdeaConfig getState() {
        return this;
    }

    public void loadState(TwitterIdeaConfig state) {
        XmlSerializerUtil.copyBean(state, this);
    }
}

@Storageアノテーションの内容を変更することで保存先や保存方法を変更することができます。
今回はtwitteridea.xmlという設定ファイルを作ります。
Twitter アプリのコンシューマーキーとシークレット、アクセストークンとシークレットを保存しています。
アプリ登録は自身で行ってください。

設定画面を作る。

以下の画像のような設定画面をJava+ Swingで書きます。

f:id:satoshi_m8a:20150215144906p:plain

ここから、TwitterIdeaConfigurable.javaとTwitterIdeaForm.formをダウンロードすれば良いと思います(適当)
https://github.com/satoshi-m8a/idea-plugin_sbt_sample/tree/master/src/main/java/com/example

プラグイン設定(plugin.xml)ファイルを書く

 src/main/resource/META-INFの中に、plugin.xmlファイルの

<extensions defaultExtensionNs="com.intellij">

の中に

<extensions defaultExtensionNs="com.intellij">
  <!-- Add your extensions here -->
    <applicationService serviceImplementation="com.example.TwitterIdeaConfig"/>
    <applicationConfigurable instance="com.example.TwitterIdeaConfigurable"></applicationConfigurable>
</extensions>

を挿入します。

実行してみる

 Run → Run 'Plugin'を選択して、実行します。
初期設定のIntelliJが起動するので、適当に設定して、適当なプロジェクトを開きます。
Preferencesを開くと先ほど作成した設定画面が存在しているので、そこに自分で作ったTwitter APIのトークンとシークレットを入力します。 ApplyやOKを押すと設定が保存されます。
プロジェクトの画面で、Ctrl+Shift+Tを二回押すと…

f:id:satoshi_m8a:20150215150905p:plain

やりました!!遠目から見たら、IntelliJの通知が大量にでているだけの様に見えますね!!誰もTwitterのタイムラインを見ているなんて思わないはず!これでタイムラインの監視業務が捗ります!!?
(職場でも気にせずサブディスプレイ一杯にTweetDeckを広げて見ていますが…)

まとめ

 sbtを使いながら、ScalaJavaのコードを混在させた、設定画面付きのIntelliJ IDEAのプラグインの作成方法をご紹介させていただきました。
今回の様にGUIの部分はJava+Swing で書いて、その他の部分をScalaで書くことも出来ます。
自分はGUIが絡むコードをScalaで書くのはまだ難しく感じるので、Javaで書いたりしています。適宜Javaで書けるのもScalaの良い所ですね。