JetBrains IDEのLive Templateを使い倒す

はじめに

この記事はピクシブ株式会社 Advent Calendar 2017の15日目の記事です。
17新卒で、普段はpixiv PAYのサーバーサイドの開発でRailsを書いています。
入社まではJavaをメインに書いていました。昔からIntelliJ IDEAが好きなので、JetBrains IDEのLive Template機能について書きます。

Live Templateとは

よくあるコードのテンプレート機能です。 この機能の面白いところはテンプレート内に変数を置いて、入力を促したり式を埋め込むことができるというところです。
うまく使えば効率的にコードを書くことができるのではないでしょうか!?

新しいテンプレートを追加する

Preferences > Editor > Live Templatesから設定できます。 右上の+ボタンからLive Templateを選択すると新しいテンプレートを作成できます。

設定項目 説明
Abbreviation テンプレートを展開するための文字列
Description 補完候補ポップアップに表示される説明文
Template Text テンプレート
Applicate in ... Live Templateを使える場所
Edit variables テンプレート内の変数を編集するウィンドウを開く

1. 単純なテンプレートを展開する

まずは固定の文字列を展開するテンプレートを作ってみます。

Abbreviation: hello
Description: Hello Worldを出力
Applicate in ...: Java > Statement
Template Text:

System.out.println("Hello World!");

f:id:orekyuu:20171213222508g:plain

2. 変数を使ってn回任意の文字列を標準出力

次はn回の部分と出力文字列を変数にしてみます。テンプレート内で $VARIABLE_NAME$ と言った形式で変数を定義できます。また、同じ変数名を使えば同時に同じ文字列が入力されます。
$END$は特殊な変数で、すべての変数の入力が終わったあとにキャレットが$END$の位置に移動します。次に入力したくなるであろう場所に$END$を書いておくとキャレットの移動が減って便利です。

Abbreviation: each_sout
Description: n回任意の文字列を出力
Applicate in ...: Java > Statement
Template Text:

for (int $VARIABLE_NAME$ = 0; $VARIABLE_NAME$ < $LOOP_COUNT$; $VARIABLE_NAME$++) {
    System.out.println("$TEXT$");
    $END$
}

f:id:orekyuu:20171213223541g:plain

3. 関数を使って補完をサジェストを出す

Edit variablesからダイアログを開くとテンプレート内の変数に対しての設定をするためのダイアログが開きます。ここでIntelliJが用意してくれている関数を使って決められた候補内からサジェストを出したり、変数名を提案させることができます。
どのような関数があるかは公式ドキュメントに説明があるのでそちらを確認してください。 Springで使われるAutowiredを使ったフィールドを作るテンプレートを作ってみます。

Abbreviation: autowired
Description:
Applicate in ...: Java > Decration
Template Text:

@Autowired
private $TYPE$ $VARIABLE$;

Edit variables:

Name Expression Default value Skip if denfied
$TYPE$ className() false
$VARIABLE$ suggestVariableName() false

f:id:orekyuu:20171213225801g:plain

4. enum関数を使ってサジェストする

Javaのような静的型付けの言語ではIntelliJのサポートが手厚くサジェスト系の関数が豊富ですが、RubyMineではサジェストはあまり効きませんし、サジェスト系の関数もほぼありません。しかし、FactoryBotで作ったfactoryやtraitの名前を覚えるのは大変です。
そこでtraitのバリエーションをLive Templateに書いておいて良い感じに補完してくれるようにしました。決められた候補から選択させる場合、enum関数を使うと便利です。JetBrainsのIDEは中間マッチングをしてくれるので「たぶんhogeという文字列含んでたよな・・・」のような状態でもサジェストを出してくれるので、人間が考えることを減らしてくれます。

Abbreviation: create_user
Description: ユーザーを作成
Applicate in ...: Ruby
Template Text:

FactoryBot.create(:user, $trait$)

Edit variables:

Name Expression Default value Skip if denfied
$trait$ enum(":admin", ":anonymous") false

f:id:orekyuu:20171213231638g:plain

5. groovyスクリプトを使う

より高度な補完を行いたくなったときにgroovyScript関数を使うことでgroovyのコードを実行することができます。
第一引数にgroovyスクリプトもしくはスクリプトファイルのパスを指定し、スクリプトに渡したい値があれば第二引数以降に渡します。第二引数以降は$1、$2のような変数に順に割り当てられます。また_editorという変数にcom.intellij.openapi.editor.Editorインスタンスが入っているので、ここから編集中のファイルの情報やプロジェクトの情報が取得できます。
サンプルとして標準関数ではとれない現在編集中のファイルのプロジェクト内でのパスを展開するテンプレートを作ってみました。雑スクリプトなのですが、きちんと書けばrequest specのコメントをうまく補完することができそうです。

Abbreviation: relative_file_path
Description: ファイルのパスを展開
Applicate in ...: Ruby
Template Text:

"$relative_file_path$"

Edit variables:

Name Expression Default value Skip if denfied
$relative_file_path $ groovyScript("長いので下に書きました") true
com.intellij.psi.PsiDocumentManager.getInstance(_editor.getProject())
  .getPsiFile(_editor.getDocument())
  .getVirtualFile()
  .getCanonicalFile()
  .getPath()
  .minus(_editor.getProject().getBasePath())

f:id:orekyuu:20171214004530g:plain

育てたLive Templateを別PCと共有する

実際にコーディングで育てたテンプレートは自宅PCと職場PCの両方で使いたくなります。JetBrainsが出しているプラグインIDE Settings Syncを使うことで同じアカウントに紐付けされた複数のIDEで設定を共有できます。

おわりに

Live Templateは書いて終わりではなく、使いながら不満を感じたら手を加えていくといった運用をすると使い勝手の良いものになっていくかと思います。
明日は@tadsanさんがPHPEmacsの話をしてくれます。2日連続のエディタ記事ですね!お楽しみに!


ピクシブ株式会社ではではエディタにこだわりのあるエンジニアを募集しています!

痛IntelliJプラグインを作った話

この記事はJetBrains Advent Calendar 2017 11日目の記事です。
前日はKotlinでプラグインを作る話でまさかの2日連続プラグインの話…!

僕は萌え系のイラストが好きなオタクエンジニアなんですが、オタクエンジニアをやっているとエディタやIDEを痛くしたくなることってありますよね…!?
EclipseにはMoeclipseという選択肢があったりします。JetBrains IDEの場合数年前までSexy Editorというプラグインでエディタの背景を変える必要がありました。
その後、本体にSet Background Imageというアクションが追加され、そこからプラグインを入れることなく背景を変更することができるようになりました。(Shift+Cmd+Aのアクション検索からしか設定に飛べないです

背景を痛くすることは簡単にできる時代になりましたが、痛IDEを名乗るにはまだ早いですよね。もっと痛くしましょう。

IDEプラグインを作ってみた

このプラグインはエディタの背景にキャラクターを表示して操作によっていろいろなリアクションをしてくれます。
ということでJetBrainsPluginRepositoryに公開しようと思ったんですが審査でリジェクト…。頑張って絵も描いたのにどうやら公開はできなさそう…。しかし、せっかく作ったので使ってもらいたい!うちの娘を可愛がってもらいたい!

プラグインリポジトリを作る

Plugin Repositoryを各自で用意することができるようになっているので自分で用意することにしました。
ドキュメントに設定が書かれていたりするので簡単にサッっと立てることができました。

おわりに

これで最高の痛IDE環境になりました。痛IDEを作るならもうJetBrains IDEしか選択肢はないのではないでしょうか…!?
実装に関しての話は別記事に書いているので興味のある方はそちらも読んで頂けると!

orekyuu.hatenadiary.jp

いますぐダウンロード

github.com

IntelliJ IDEAのプラグインの作り方

はじめに

とりあえずブログを作ってみてからかなり期間が空きましたがorekyuuです。まだこのブログの事忘れてないですよ…?
一つ前の記事を作った頃はまだ学生だったんですよね(遠い目

さて、前置きはこのへんで最近作った莉穂ちゃんプラグインを題材にIntelliJプラグインの作り方について解説してみようと思います。

開発環境の作り方

IntelliJプラグインのプロジェクトはざっくり分けて以下の二種類があります。

  1. Gradleプロジェクト
  2. ideaプロジェクト

GradleプロジェクトはGradle Intellij pluginを使う方法です。
こちらはGradleを使っているのでライブラリの管理が楽だったりしますが、プロジェクトをImportしてもIntelliJの実行構成ができなかったり、RubyMineやPhpStormで試そうとしても分からなかったので今回は見送り。そもそもライブラリ追加しないですし。

ideaプロジェクトはIntelliJのプロジェクトテンプレートで作った構成そのままの形です。
特にライブラリを追加したりしなければこちらで良いかと思います。莉穂ちゃんプラグインはこちらの形になっています。
ideaプロジェクトにする場合は.idea以下をちゃんとgit管理下においてあげてくださいね。

依存関係の設定

プラグインを作るときに一番初めに触ることになるのが/META-INF/plugin.xmlです。
これはプラグインの情報やComponent、Extensionの設定を書くファイルです。idやnameを設定したあとはdependsの設定をします。
この設定を書かなければデフォルトではIntelliJでしか使えないプラグインになってしまいます。

莉穂ちゃんプラグインでは以下のような設定になっています。

<!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/plugin_compatibility.html
     on how to target different products -->
<depends>com.intellij.modules.lang</depends>
<depends>com.intellij.modules.platform</depends>
<depends>com.intellij.modules.xml</depends>
<depends>com.intellij.modules.vcs</depends>
<depends>com.intellij.modules.xdebugger</depends>

このあたりのモジュールはすべてのJetBrains製品で利用できるモジュールです。
IDEに依存するモジュールはコメントアウトされているリンクを参照してください。

キャラクタの表示

プラグインの基本的な部品のことをComponentとよんでいます。Componentには以下の3種類があります。

  1. Application Level Component
  2. Project Level Component
  3. Module Level Component

ApplicationはIDE起動時に初期化、ProjectはIDEインスタンス作成時に(Projectを開いてなくても作られる!)と言った具合にだいたい名前と同じスコープで作られます。
参考: http://www.jetbrains.org/intellij/sdk/docs/basics/plugin_structure/plugin_components.html

今回はProject LevelでComponentを作りました。 plugin.xml

<project-components>
    <component>
        <implementation-class>net.orekyuu.riho.RihoPlugin</implementation-class>
    </component>
</project-components>

RihoPlugin.java

public class RihoPlugin implements ProjectComponent {

    private final Project project;
    private IdeActionListener ideActionListener;

    public RihoPlugin(Project project) {
        this.project = project;
    }

    @Override
    public void initComponent() {
        EditorFactory.getInstance().addEditorFactoryListener(new EditorFactoryListener() {

            private MessageBusConnection connect;
            private CharacterBorder character = null;
            private HashMap<Editor, CharacterBorder> characterBorders = new HashMap<>();

            @Override
            public void editorCreated(@NotNull EditorFactoryEvent editorFactoryEvent) {
                Editor editor = editorFactoryEvent.getEditor();
                Project project = editor.getProject();
                if (project == null) {
                    return;
                }
                JComponent component = editor.getContentComponent();
                try {
                    Riho riho = new Riho();
                    CharacterBorder character = new CharacterBorder(component, new CharacterRenderer(riho), riho);
                    characterBorders.put(editor, character);
                    component.setBorder(character);
                    connect = project.getMessageBus().connect();
                    connect.subscribe(RihoReactionNotifier.REACTION_NOTIFIER, riho);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void editorReleased(@NotNull EditorFactoryEvent editorFactoryEvent) {
                CharacterBorder characterBorder = characterBorders.get(editorFactoryEvent.getEditor());
                if (characterBorder != null) {
                    characterBorders.remove(editorFactoryEvent.getEditor());
                    characterBorder.dispose();
                }
            }
        }, () -> {
        });

        MessageBusConnection connect = project.getMessageBus().connect();
        connect.subscribe(Notifications.TOPIC, new NotificationListener(project));
        connect.subscribe(RefactoringEventListener.REFACTORING_EVENT_TOPIC, new RefactoringListener(project));
        connect.subscribe(CompilerTopics.COMPILATION_STATUS, new CompilerListener(project));
        ideActionListener = new IdeActionListener(project);
        ActionManager.getInstance().addAnActionListener(ideActionListener);
    }

    @Override
    public void disposeComponent() {
        ActionManager.getInstance().removeAnActionListener(ideActionListener);
    }

    @Override
    @NotNull
    public String getComponentName() {
        return "RihoPlugin";
    }

    @Override
    public void projectOpened() {
    }

    @Override
    public void projectClosed() {
    }
}

Component側では初期化時にEditorFactoryにListenerを登録して、EditorのSwingコンポーネントの背景にキャラを描画するようにしています。

テストのイベントを拾う

莉穂ちゃんプラグインユニットテストが成功すると笑ったり、失敗すると悲しんだりします。
このイベントを拾うためにExtensionsを書きます。
Extensionsは他のプラグインapiが用意したExtensionPointがあればそれに対して拡張を書くことができます。IntelliJの拡張であればPlatformExtensionPoints.xmlから拡張したい場所を探すと良いと思います。
今回はtestStatusListenerを拡張しました。 plugin.xml

<extensions defaultExtensionNs="com.intellij">
    <!-- Add your extensions here -->
    <testStatusListener implementation="net.orekyuu.riho.events.TestListener"/>
</extensions>

TestListener.java

public class TestListener extends TestStatusListener {

    @Override
    public void testSuiteFinished(@Nullable AbstractTestProxy abstractTestProxy) {

    }

    @Override
    public void testSuiteFinished(@Nullable AbstractTestProxy abstractTestProxy, Project project) {
        // some code...
    }
}

その他のイベントの購読、キャラクターにリアクションさせる

テスト以外に、タイピングやリファクタリングなどでも莉穂ちゃんはリアクションしてくれます。しかしExtensionPointにはそのイベントが取れそうな物がありません。
IntelliJはイベントの送信にMessagingの仕組みを用意しています。莉穂ちゃんプラグインではMessageを購読してイベントが来たときにリアクションさせています。また、リアクションさせるためのメッセージもこの仕組みを使っています。
この仕組みに出てくる登場人物は以下のとおりです。
Topic: メッセージを送るためのチャンネル
Subscriber: メッセージを受け取る人
Publisher: メッセージを送信する人

リファクタリング時にキャラクターにリアクションさせるシナリオを考えてみます。

1 リファクタリングのメッセージを購読する
RihoPlugin.java(Application Level Component)

MessageBusConnection bus = project.getMessageBus().connect();
bus.subscribe(RefactoringEventListener.REFACTORING_EVENT_TOPIC, new RefactoringListener(project));

2 メッセージを受け取ってキャラクターにメッセージを送る
RefactoringListener.java

public class RefactoringListener implements RefactoringEventListener {

    private final Project project;

    public RefactoringListener(Project project) {
        this.project = project;
    }

    @Override
    public void refactoringStarted(@NotNull String s, @Nullable RefactoringEventData refactoringEventData) {
    }

    @Override
    public void refactoringDone(@NotNull String s, @Nullable RefactoringEventData refactoringEventData) {
        RihoReactionNotifier publisher = project.getMessageBus().syncPublisher(RihoReactionNotifier.REACTION_NOTIFIER);
        publisher.reaction(Reaction.of(FacePattern.SMILE1, Duration.ofSeconds(3)));
    }

    @Override
    public void conflictsDetected(@NotNull String s, @NotNull RefactoringEventData refactoringEventData) {
        RihoReactionNotifier publisher = project.getMessageBus().syncPublisher(RihoReactionNotifier.REACTION_NOTIFIER);
        publisher.reaction(Reaction.of(FacePattern.JITO, Duration.ofSeconds(3), Emotion.MOJYA, Loop.once()));
    }

    @Override
    public void undoRefactoring(@NotNull String s) {
        RihoReactionNotifier publisher = project.getMessageBus().syncPublisher(RihoReactionNotifier.REACTION_NOTIFIER);
        publisher.reaction(Reaction.of(FacePattern.SYUN, Duration.ofSeconds(3)));
    }
}

3 RihoReactionNotifier用のTopicを作る
RihoReactionNotifier.java

public interface RihoReactionNotifier {

    Topic<RihoReactionNotifier> REACTION_NOTIFIER = Topic.create("riho reaction", RihoReactionNotifier.class);

    void reaction(Reaction reaction);
}

4 REACTION_NOTIFIER Topicを受け取ってリアクションさせる

public class Riho implements RihoReactionNotifier {
    //some code...
    @Override
    public void reaction(Reaction reaction) {
        reactionStartTime = Instant.now();
        this.reaction = reaction;
    }
}

プラグインの公開

JetBrains Plugin Repositoryにアップロードすれば完了です!
…なにごともなければ!

莉穂ちゃんプラグインはリジェクトされました!!!

とはいえせっかく作ったので公開したいです。ということで急遽個人Plugin Repositoryを作ることにしました。
インストール方法が面倒くさいのはこのせいなんです…。

PluginRepositoryを作る

オリジナルのPluginRepositoryを作る方法を解説したEnterprise Plugin Repositoryという記事が公式ブログにありました。
こちらを参考にupdatePlugins.xml を書きました。

<plugins>
  <plugin id="net.orekyuu.riho" url="http://uploader.orekyuu.net/plugin/riho/riho-1.0.3.jar" version="1.0.3"/>
  <plugin id="net.orekyuu.riho" url="http://uploader.orekyuu.net/plugin/riho/riho-1.0.2.jar" version="1.0.2"/>
</plugins>

これのファイルとjarファイルを誰でもアクセスできるようにしてサーバーにデプロイすれば完了です!やった!

さいごに

IntelliJプラグイン解説周りの情報って少ないんですよね。誰かの役に立てばと思い書いてみた次第です。 よければ下のツイートを参考にぜひ僕の看板娘を表示する莉穂ちゃんプラグインを使ってやって下さい!

また、開発に興味があればこちらのリポジトリもよろしくお願いします!ではでは〜

github.com