プラグイン作って覚えたことを淡々と記録するよ(VFS編)

GUIばっかりも飽きるので,VFS(Virtual File System)についてちょっと。VFSとはIDEAが管理しているファイルシステムのことで,プロジェクトだとかモジュール配下のソースファイルとかは,こいつ経由で操作するのが望ましいらしい(java.ioとか使うのはルール違反)。


たぶん,これもいろいろできるんだろうけど,理解が低いので,どんだけできるかとか全然知らない。:-)


とりあえず,習作がてら試してみたのは,こんなこと。

特定のファイルをリネームしたり,移動したりすると,それに紐付いたファイルも連動する。
具体的には,*.txtファイルをリネーム・移動すると,同じファイル名の*.bakファイルも連動する。

イメージしやすい例だと,Wicketのページクラス(*.java)とHTMLテンプレート(*.html)を連動したいとか,ディレクトリ離れているけどClickのページクラス(*.java)とVelocityテンプレート(*.htm)を連動したいとかだ。


今回は,VirtualFileListenerを使い,ファイルの変更イベントをフックして実現してみたんだけど,たぶん良いやり方ではないと思う。できることはできたんだけど,なんか強引な気がする。


そんな強引なサンプルコードを晒す。まずは,VirtualFileListenerの登録部分。VirtualFileListenerの登録は,まあおまじないみたいなもんだから,特に説明することはない。

public class VfsSamplePlugin implements ProjectComponent {
    private Project project;
    private MyVfsListener myVfsListener;

    public VfsSamplePlugin(Project _project) {
        project = _project;
    }

    public void initComponent() { }
    public void disposeComponent() { }

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

    public void projectOpened() {
        myVfsListener = new MyVfsListener();
        // ここで登録
        VirtualFileManager.getInstance().addVirtualFileListener(myVfsListener);
    }

    public void projectClosed() {
        // ここで解除
        VirtualFileManager.getInstance().removeVirtualFileListener(myVfsListener);
    }
}

んで,肝心のVirtualFileListenerについて。

public class MyVfsListener extends VirtualFileAdapter {
    private static Logger LOGGER = Logger.getInstance("MyVfsListener");

    // *.txtをリネームしたら,連動して*.bakもリネームする.
    public void propertyChanged(VirtualFilePropertyEvent event) {
        VirtualFile file = event.getFile();
        if (!file.getExtension().equals("txt")) return;

        VirtualFile parent = event.getParent();
        String fileName = FileUtil.getNameWithoutExtension((String) event.getOldValue());
        final VirtualFile bundleFile = parent.findChild(fileName + ".bak");
        if (bundleFile == null) return;

        final Object requestor = event.getRequestor();
        final String newName =  FileUtil.getNameWithoutExtension((String) event.getNewValue()) + ".bak";
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
                    bundleFile.rename(requestor, newName);
                }
                catch (IOException e) {
                    LOGGER.info(e);
                }
            }
        });
    }

    public void contentsChanged(VirtualFileEvent event) { }

    // *.txtを移動したら,連動して*.bakも移動する.
    public void fileMoved(VirtualFileMoveEvent event) {
        VirtualFile file = event.getFile();
        if (!file.getExtension().equals("txt")) return;

        VirtualFile oldParent = event.getOldParent();
        final VirtualFile bundleFile = oldParent.findChild(file.getNameWithoutExtension() + ".bak");
        if (bundleFile == null) return;

        final Object requestor = event.getRequestor();
        final VirtualFile newParent = event.getNewParent();
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
                    bundleFile.move(requestor, newParent);
                }
                catch (IOException e) {
                    LOGGER.info(e);
                }
            }
        });
    }
}

VirtualFileListenerのイベントのうち,リネームは propertyChanged(),移動は fileMoved() が受け付ける。コピーや削除んときは調べてないから知らない。contentsChanged()は,多分ファイルを保存したら反応すんじゃないかと。
フックするイベントがわかりさえすれば,あとはVirtualFileを操作するだけなんだが,VirtualFileに対する操作は,好き勝手な時にやっちゃいかんと言われる。
エラーメッセージやJavadocのコメントみると,write-actionはApplication.runWriteAction(Runnbale)配下でやんなさいとあるが,だからといって,こんなコードを書けばいいってもんでもないらしい。

ApplicationManager.getApplication().runWriteAction(new Runnbale() {
    public void run() {
        try {
            bundleFile.move(requestor, newParent);
        }
        catch (IOException e) {
            LOGGER.info(e);
        }
    }
});

上記のコードを実際に動かすと,一応目的は達成するんだけど,IDEAログにエラー出てんで,正しいやり方じゃないんだろう。


要するにだ。やりたいことを整理すると,こんな感じになる。

  1. リネーム/移動といったコマンド発動→イベント発火
  2. イベント発火をリスナが受信
  3. リスナ内で,新たなコマンドを作成(←いまここ)
  4. 作ったコマンドをコマンドキューに詰めて,この処理(スレッド)の後に実行してほしい
    • 知りたいのはこれ!!


最後の処理をどうにかできると信じて,あれこれ調べた結果,とりあえずたどり着いたのが,EventQueueを使う方法(これはAWTのAPIよ)。

EventQueue.invokeLater(new Runnable() {
    public void run() {
        try {
            bundleFile.move(requestor, newParent);
        }
        catch (IOException e) {
            LOGGER.info(e);
        }
    }
});

これだとエラーは出ないんだけど,正解だといえる自身もない。:-(
余談だけど,Subversionと繋げてるとちゃんと連動してくれるよ。こっちはVCSちゅう別の機構が作用しているハズなので,気にせずVFSを操作すりゃ,勝手に連動してくれんだろうという思惑通りの結果でありました。


つうわけで,俺俺プラグインで使う分には良さそうだけど他の方法も模索したいという,煮え切らない結果でスマソ。


ps.
Open APIみるとRefactoringActionHandlerFactoryとかRefactoringListenerManagerとか,如何にもなコンポーネントがあるんだけど,まだ調べてもいないので何とも言えん。


(追記)Instant Calculatorのソース読んで,こんな方法もあることがわかった。

EventQueue.invokeLater(new Runnable() {
    public void run() {
        CommandProcessor.getInstance().executeCommand(project, new Runnable() {
            public void run() {
                ApplicationManager.getApplication().runWriteAction(new Runnable() {
                    public void run() {
                        try {
                            bundleFile.move(requestor, newParent);
                        }
                        catch (IOException e) {
                            LOGGER.info(e);
                        }
                    }
                });
            }
        }, "My VFS Sample", null);
    }
})

EventQueue使うのは変わりないんだけど,コマンドの実行方法がお作法に則っているっぽいんだが,どうだろう?
以前のCommandProcessorを経由しない方法でも,VCSの連携やUndoで両方とも元に戻るんで実質問題はないんだけどね。