今回は自分でEmacsLisp(以下、elisp)を書いてEmacsを拡張する際のTipsについて紹介します。
拡張する際に気に留めておくこと
Emacsを拡張する上で覚えておくべきなのはEmacs上で行える対話的な動作は「M-x 関数名」で実現できるということです。例えば、C-bはカーソルを1文字分左へ戻しますが、これは「M-x backward-char」とタイプすることと同じです。もしC-bが押しにくい(多分私だけです)のであれば、以下のようなelispを評価してキーバインドを変更することができます。
(global-set-key "\C-l" 'backward-char)
Emacsではこのようなキーバインドに限らず、あらゆる操作をelispを使って変更したり、新たに定義することができます。関数名もしくはキーバインドの一方しかわからない場合は、「help-for-help f」(C-h f)とタイプして関数名を入力したり、「help-for-help k」(C-h k)とタイプしてキーを入力すると、それに対応する関数の説明やキーバインドが表示されますので、これで確かめましょう。
実際の拡張例
次は、最近私が実際に拡張した例について紹介します。(もしかしたら既に同じようなものがあるかもしれません)
find-tagとelscreenの連携
elscreenは簡単に言うとEmacsをタブエディタっぽく使えるようにするものです。人によってはGNU SCREENのEmacs版と言えばしっくりくるかもしれません。ここではタブではなく、スクリーンと呼ぶようにします。find-tagはetagsで作成した関数のインデックスを利用して、指定した関数が定義されているファイルのポイントへジャンプする関数です。しかし、find-tagはデフォルトでは指定した関数が定義されているファイルを開く際、単に今開いているバッファを切り替えるだけです。elscreenを使っている場合、どうせならあたらしいスクリーンで開いてほしいものです。さらに言うと既にそのバッファを開いているスクリーンがあるなら、そっちのスクリーンにジャンプするとか、今開いているバッファが*scratch*バッファなら普通にswitch-to-bufferでバッファを切り替えるようにしたいところです。というわけで、find-tagの関数の中身を見てみます。(コメントは省いています)
(defun find-tag (tagname &optional next-p regexp-p) (interactive (find-tag-interactive "Find tag: ")) (let ((buf (find-tag-noselect tagname next-p regexp-p))) (condition-case nil (switch-to-buffer buf) (error (pop-to-buffer buf)))))
これを見るとswitch-to-bufferという関数にbufという変数を渡しています。プログラマならbufというのはバッファのことだと想像できると思います。switch-to-bufferは現在編集しているバッファを切り替える関数です。ここでしたいのは単にバッファを切り替えるのでなく、新しいスクリーンを作成してそのスクリーン上でバッファを開くことですので、この部分を別の関数に置き換えてやるとよさそうです。しかし、実際には以下のような手順を踏む必要があります。
- バッファ(buf)が既にどこかのスクリーン上に存在するか調べる
- 既に存在する場合はそのスクリーン上のバッファにジャンプし、ない場合は新たにスクリーンを作成してその上でバッファを開く
この手順を踏むにはバッファからスクリーン、もしくはスクリーンからバッファを特定したり、指定したスクリーンにジャンプする操作とスクリーンを新規作成する操作が必要です。ここで最初に言ったことを思い出してください。Emacs上で行える対話的な動作は「M-x 関数名」で実現できます。ということはこれらの操作を行うための関数を組み合わせて目的の操作を定義することができそうです。
スクリーンを新規作成する
elscreenはデフォルトではC-z C-cでスクリーンを新規作成できます。このキーバインドが割り当てられている関数を「help-for-help k」(C-h k)で調べてみると、この操作にはelscreen-createという関数が割り当てられています。実際に(elscreen-create)と書かれた行の末尾にカーソルを持っていってC-x C-eとタイプしてみると、新しいスクリーンが作成されるのがわかると思います。(elispコードの末尾にカーソルがある状態でC-x C-eとするとそのS式を評価して、その結果がミニバッファに表示されます)
指定したスクリーンにジャンプする
elscreenを使っていると画面上部に番号とファイル名がセットになったスクリーンのリストが表示されます。別のスクリーンにジャンプするには「C-z 番号」とタイプします。このキーバインドを「help-for-help k」(C-h k)で調べてみると、elscreen-jumpという関数があるのがわかります。試しに以下の関数を実行します。
(elscreen-jump 1)
これで番号1のスクリーンに移動できればいいのですが、エラーが出ます。なので、この関数をもう少し深い追いしてみましょう。elscreen-jumpの中身は以下のようになっています。
(defun elscreen-jump () "Switch to specified screen." (interactive) (let ((next-screen (string-to-number (string last-command-char)))) (if (and (<= 0 next-screen) (<= next-screen 9)) (elscreen-goto next-screen))))
変数名などから推測するに最後にタイプしたキーが0から9の間であればelscreen-gotoという関数が呼ばれるようです。実際に使ってみます。
(elscreen-goto 1)
上記のelispを実行すると、番号1のスクリーンにジャンプすることができます。
バッファからスクリーン、もしくはスクリーンからバッファを特定する
さきほどのelscreen-gotoの引数は数字でした。ということはスクリーンの番号からバッファを特定することができそうです。また、その逆のことをする関数もありそうです。こればかりはhelp-for-helpで探すわけにはいかないので、実際のコードを追ってみるしかありません。そんなわけでelscreenのソースを眺めてみると、elscreen-find-screen-by-bufferという関数が見付かりました。番号が2のスクリーンで開いているファイルがetags.elの場合、以下のelispを実行すると結果は以下の通りになります。
(elscreen-find-screen-by-buffer "etags.el") 2
また、さらに追っていくとelscreen-get-window-configurationという逆のことをする関数も見付かりました。
(elscreen-get-window-configuration 2) (#<window-configuration> #<marker at 12071 in etags.el>)
結果を見る限り、etags.elのバッファに関する情報と見てよさそうです。あとはスクリーンの一覧が取得できれば、スクリーンからバッファを特定する処理が書けそうです。さらにelscreenのコードを見ていくとelscreen-get-screen-listという関数が見つかります。スクリーンが3つある状態でこの関数を評価すると、
(elscreen-get-screen-list) (2 1 0)
という風にスクリーン番号のリストが取得できます。
これらの関数を組み合わせて出来上がったのが以下の関数です。
(defun switch-to-elscreen-create (buf) (defun create-new-buf (buf) (if (equal elscreen-default-buffer-name (buffer-name (window-buffer))) (switch-to-buffer buf) (elscreen-create) (switch-to-buffer buf))) (defun switch-to-elscreen-create-inner (screen-list buf) (cond ((null (elscreen-get-window-configuration (car screen-list))) nil) ((equal (car screen-list) (elscreen-find-screen-by-buffer (buffer-name buf))) (elscreen-goto (car screen-list))) (t (switch-to-elscreen-create-inner (cdr screen-list) buf)))) (if (null (switch-to-elscreen-create-inner (elscreen-get-screen-list) buf)) (create-new-buf buf) t))
上記の処理ではさらに、今開いているバッファが*scratch*バッファなら単純にswitch-to-bufferでバッファを切り替えるようにしています。あとはfind-tag関数内のswitch-to-bufferをこの関数に置き換えてやれば完成です。もしくは、find-tag-elscreenという風に別の関数を定義した方がいいかもしれません。
(defun find-tag (tagname &optional next-p regexp-p) (interactive (find-tag-interactive "Find tag: ")) (let ((buf (find-tag-noselect tagname next-p regexp-p))) (condition-case nil (switch-to-elscreen-create buf) (error (pop-to-buffer buf)))))
TAGSファイルをプロジェクト名で指定できるようにする
TAGSファイルを作成しておくと、ソースコードの検索が楽になります。
しかし、TAGSファイルを読み込むにはいちいちそのファイルがあるディレクトリを指定しなければなりません。また、ソースコードの規模が余程大きい場合を除けば、TAGSファイルは一つのプロジェクトに一つあれば十分です。なので、私はTAGSファイルのあるディレクトリを指定するのではなく、そのTAGSディレクトリが関連づけられているキーワードを指定するようにしています。抱えているプロジェクトが一つだけならEmacsを起動すると同時にそのTAGSファイルを読み込んでもよさそうですが、ここでは複数のプロジェクトを抱えていると仮定して話を進めます。
例えば、eigaとphotoというプロジェクトを抱えているとします。それぞれのプロジェクトのTAGSファイルを元にインデックスを参照するには、M-x visit-tags-tableでTAGSのあるディレクトリを指定する必要があります。emacsを起動したディレクトリにTAGSファイルがあれば簡単なのですが、そうとは限りません。そこで、visit-tags-table-keyという関数を作成して、この関数を呼び出した際にeigaとタイプすればeigaプロジェクトのTAGSファイルを読み込み、photoとタイプすればphotoプロジェクトのTAGファイルを読み込むようにしてみました。
まず、visit-tags-tableの中身を見てみましょう。(find-tagと同じく、コメントは省いています)
(defun visit-tags-table (file &optional local) (interactive (list (read-file-name "Visit tags table: (default TAGS) " default-directory (expand-file-name "TAGS" default-directory) t) current-prefix-arg)) (or (stringp file) (signal 'wrong-type-argument (list 'stringp file))) (let ((tags-file-name file)) (save-excursion (or (visit-tags-table-buffer file) (signal 'file-error (list "Visiting tags table" "file does not exist" file))) (setq file tags-file-name))) (if local (set (make-local-variable 'tags-file-name) file) (setq-default tags-file-name file)))
visit-tags-tableを呼び出すとミニバッファにVisit tags table: (default TAGS)と表示されます。ここでTAGSファイルが置かれているディレクトリを指定するわけですが、指定したディレクトリの文字列を関数に渡してやる必要があります。Emacsでこの役目を務めるのはinteractiveという関数です。interactiveはその関数が対話的な動作であるということを宣言するための関数ですが、これにある形で引数を渡すと、関数に渡す引数をユーザが入力することができるようになります。ここでは、ディレクトリではなくプロジェクト名を入力してTAGSファイルを読み込むようにしたいので、visit-tags-tableに渡す引数の部分、つまりinteractiveの中を変えてみると良さそうです。
まず、プロジェクト名から各プロジェクトのTAGSファイルを見つけられるようにする必要があります。elispでは連想配列(のようなもの?)が使えるので、プロジェクト名をインデックスに使います。
(defvar tags-file-list '( ("eiga" . "/home/bokko/eiga/") ("photo" . "/home/bokko/photo/") ))
次に、interactive関数の中身に注目します。
(interactive (list (read-file-name "Visit tags table: (default TAGS) " default-directory (expand-file-name "TAGS" default-directory) t) current-prefix-arg))
パッと見ても何をやってるのかよくわかりませんが、default-directoryとexpand-file-nameが何なのかわかれば大体の見当は付きそうです。この2つを評価してみます。
default-directory "~/" (expand-file-name "TAGS" default-directory) "/home/bokko/TAGS"
~/はEmacsを起動したディレクトリです。また、expand-file-nameは相対パスを絶対パスに変換してくれるようです。以上のことからディレクトリを入力するのではなく、プロジェクト名を入力するようにして、そのプロジェクトに対応するディレクトリにあるTAGSファイルを読み込む関数は以下のように定義できます。
(defun visit-tags-table-key (file &optional local) (interactive "sTags-key: ") (defun find-tags-directory (tags-key file-key-list) (if (equal tags-key (car (assoc tags-key file-key-list))) (cdr (assoc tags-key file-key-list)) nil)) (setq file (expand-file-name "TAGS" (find-tags-directory file tags-file-list))) (or (stringp file) (signal 'wrong-type-argument (list 'stringp file))) (let ((tags-file-name file)) (save-excursion (or (visit-tags-table-buffer file) (signal 'file-error (list "Visiting tags table" "file does not exist" file))) (setq file tags-file-name))) (if local (set (make-local-variable 'tags-file-name) file) (setq-default tags-file-name file)))
プログラミングしながら環境構築
このようにEmacsではelispで自分好みの開発環境をプログラミングしながら整えることができるので、非常にかゆいようなところでも手が届くようになります。例えば、WebアプリケーションをMVCなフレームワークを利用して開発していると、アクションに相当するファイルを開いている時にビューのファイルを別のスクリーンで開くような仕組みが欲しくなります。最初からそういう機能をエディタ側で持っていることはまずないですが、その気になれば自分で作ることができます。MVCなフレームワークではディレクトリ階層が深くなりがちでアクションとビューのファイルを開くのにいちいち下り坂と上り坂を行き来するようなことをしないといけないことが多々あるのでこれは是非欲しい機能です(実際、rails.elのようにそれに似たような支援を行うものも存在します)。私の場合、社内で使っているフレームワークで似たような状況に遭遇したので、つい最近、簡単なelispを書いてアクションとビューをコマンド1つで(elscreen上の)別々のスクリーンで開けるようにしました。
参考文献
Emacsの基礎を骨の随まで叩き込みたい人は前者を読むのがいいと思います。また、後者は実際にEmacsを拡張する際にリファレンスとして手元に置いておくと幸せになれるかもしれません。