Emacs Lisp(実践編)
著者 菅原泰樹
elisp(lisp) については何となくイメージが掴めたでしょうか?この章ではもう少し Emacs固有の概念であるバッファ,ウィンドウ,ポイントについてまずは説明します.その後,実用的な elisp の例として Java ソースファイルのメソッドの一覧を表示・選択・ジャンプできるコマンドを作っていきます.
■■■ バッファとウィンドウ
バッファは Emacs で編集するテキストを持っているオブジェクトです.簡単に言うと文字列みたいなものだと思って下さい.MVC の Model に相当します.バッファは elisp の文字列を処理するときにとっても重要です.実際文字列を操作する関数よりもバッファを操作する関数の方が多いくらいです.バッファとリストを自由自在に扱えるようになれば,あなたはもう elisper です.
ウィンドウはバッファを表示する領域です.MVC の View に相当します.Emacs を使っている方なら C-x 2 で2つのウィンドウに同じバッファを表示した事があるかと思います.この事から分かるように複数のウィンドウに同じバッファを表示する事ができます.
今編集してるテキストに何かしたり,ファイルを開いて何かするプログラムを書くにはバッファの扱い方を知る必要があります.
■■ 少しだけ使ってみる
まずはウィンドウとバッファに軽く触れてみます. <リスト1> を評価してみて下さい."*Messages*" バッファを今アクティブになってるウィンドウに表示します.
----------------------
<リスト1> ウィンドウとバッファを使う
(let ((buffer (get-buffer "*Messages*"))) ;*Messages* バッファを取得して
(switch-to-buffer buffer)) ;そのバッファを今のウィンドウに表示する
----------------------
■■ カレントバッファ
各種関数が処理対象とするデフォルトのバッファをカレントバッファと言います.カレントバッファを設定するには `set-buffer' 関数を使います.カレントバッファを取得するには`current-buffer' 関数を使います.何もしていない場合,アクティブなウィンドウが表示しているバッファがカレントバッファです.`set-buffer' を使ってもウィンドウにそのバッファが設定されない事に注意して下さい. <リスト2> を評価すると何となくイメージが掴めるかと思います.
----------------------
<リスト2> カレントバッファを使う
(let ((buffer (get-buffer-create "*カレントバッファテスト*")))
;; デフォルトのバッファに文字列を書き込む
(insert "バッファを設定する前\n")
;; "*カレントバッファテスト*" バッファをカレントバッファにする
(set-buffer buffer)
;; カレントバッファに文字列を書き込む
(insert "新しいバッファ\n")
;; カレントバッファを表示する
(display-buffer (current-buffer)))
----------------------
----------------------
<表1> バッファとウィンドウの基本関数
(set-buffer BUFFER) カレントバッファを BUFFER に設定する.
(current-buffer) カレントバッファを返す.
(get-buffer NAME) NAME という名前のバッファを返す.
(get-buffer-create NAME) NAME という名前のバッファを生成する.存在する場合は生成せずにそれを返す.
(kill-buffer BUFFER) BUFFER を削除する.
(switch-to-buffer BUFFER) 現在のウィンドウに BUFFER を表示する.
(display-buffer BUFFER) BUFFER をポップアップして表示する.
(selected-window) 現在のウィンドウを返す.
(window-buffer WINDOW) WINDOW が表示しているバッファを返す.
----------------------
■■■ ポイントとカーソル
カーソルが指している位置の事をポイントと言います.ポイントは文字の上ではなく,文字と文字の間にあります.ポイントの値はバッファの先頭を1として,1文字進むごとに1増える整数です.全角文字1文字でもポイントの値は1しか増えません(バイト数ではありまえん).文字の位置とポイントの関係を <リスト3> に載せておきます.
カーソルが`a'の上にある場合,そのカーソル位置
のポイントは1です.
----------------------
<リスト3> 文字の位置とポイントの関係
;; 以下がバッファの先頭にあった場合
abcあいう
;; 各文字前後のポイントは以下のようになる
a b c あ い う
1 2 3 4 5 6 7
----------------------
<リスト4> に簡単なポイントの使い方を載せておきます.
----------------------
<リスト4> ポイントを使う
(let ((pos (point))) ;元の位置を覚えておく
(goto-char (point-min)) ;先頭に移動
(sit-for 1) ;ちょっと待つ
(goto-char (point-max)) ;末尾に移動
(sit-for 1) ;ちょっと待つ
(goto-char pos)) ;元の位置に戻る
----------------------
----------------------
<表2> ポイントとカーソルの基本関数
(point) カレントバッファのポイントを返す.
(goto-char POS) カレントバッファのポイントを POS に移動する.
(point-min) カレントバッファの先頭の位置を返す.
(point-max) カレントバッファの最後の位置を返す.
(forward-line N) N行先に移動する.マイナスの場合は前の行に移動する.
----------------------
■■■ プログラムを書いてみる
さて,ここからは実際に動くプログラムを作っていきます.最初に書いた通り java のソースからメソッド一覧を表示・選択・ジャンプするプログラムを書いてみます.まず <リスト5> というソースファイルを準備します.このファイルを Emacs で開いておいて下さい.
----------------------
<リスト5> Test.java
public class Test {
private privateMethod1() {
}
private privateMethod2() {
}
protected protectedMethod1() {
}
protected protectedMethod2() {
}
public publicMethod1() {
}
public publicMethod2() {
}
}
----------------------
■■ まずは Test.java のバッファを選択する
覚える関数: set-buffer
まずは Test.java バッファを選択する事から始めます.<リスト6> を評価すると Test.java バッファを選択できます(見ためは何も変わりません).
----------------------
<リスト6> Test.java バッファを選択
(set-buffer "Test.java")
;; 実行結果
=> #<buffer Test.java>
----------------------
- `set-buffer' でバッファを選択できます.選択したバッファは表示はされません.
■■ Test.java からメソッドを探して,リストに詰める
覚える関数: require, save-excursion, re-search-forward, match-string, goto-char, point-min, push
覚える変数: case-fold-search
バッファが選択できたので,そのバッファからメソッド一覧を取得します.<リスト7>を実行するとメソッドの一覧が取得できます.
----------------------
<リスト7> メソッド一覧を取得
(require 'cl) ;push の為に cl パッケージをロード
(save-excursion ;ポイントとカレントバッファを保存
(set-buffer "Test.java") ;バッファを移動して
(goto-char (point-min)) ;先頭へ移動
(let ((case-fold-search nil) ;大文字・小文字を無視
;; メソッドを検索する正規表現
(regexp "\\(private\\|protected\\|public\\) +\\([a-zA-Z0-9_]+\\)(")
methods)
(while (re-search-forward regexp nil t) ;メソッドを検索して
(push (match-string 2) methods)) ;リストに詰める
methods))
;; 実行結果
=> ("publicMethod2" "publicMethod2" "protectedMethod2" "protectedMethod1" "privateMethod2" "privateMethod1")
----------------------
- まず `require' 関数で cl パッケージをロードしています。`require' は `load' とは違い一度ロードしたら2回目以降はロードしません<脚注1>
- `case-fold-search' を nil にしておくと検索する際に大文字と小文字の違いを無視します.
- `re-search-forward' 関数を使うと正規表現で文字列を探す事ができます.2番目と3番目の引数はおまじないだと思ってください.文字列が見つかるとその位置に移動するので,while で回すとマッチする全ての文字列を順々に検索します.
- `re-search-forward' の結果は `match-string' 関数で取得し,引数には何番目のカッコの値が欲しいのかを指定します.0の場合はマッチした文字列全体です.
- `goto-char' で指定したポイントに移動します.`(goto-char (point-min))' でバッファの先頭へ移動します<脚注3>.
- `push' はリストの先頭に値を追加します.
- `save-excursion' はカーソル位置とバッファを保存する為のスペシャルフォームです.これで囲っておくと,`save-excursion' を抜けたときにカーソル位置とバッファが元の位置に戻ります.
----------------------
<脚注2> cl パッケージは何故か嫌われている事が多いようです。本来は eval-when-compile で囲むべきすがここでは省略します。
----------------------
----------------------
<脚注3> narrowing(C-x n n) している場合はバッファの先頭ではなく narrowing している範囲の先頭になります.
----------------------
■■ リストを正しい順番に並び換える
覚える関数: nreverse
<リスト7>ではメソッドの順番が逆順になってしまいました.`push' がリストの先頭に値を追加する為です<脚注4>.<リスト8>の変更で正しい順番に並び変える事ができます.
<リスト8> 正しい順番に並び換える
----------------------
(save-excursion
(set-buffer "Test.java")
(goto-char (point-min))
(let ((case-fold-search nil)
(regexp "\\(private\\|protected\\|public\\) +\\([a-zA-Z0-9_]+\\)(")
methods)
(while (re-search-forward regexp nil t)
(push (match-string 2) methods))
(nreverse methods))) ;逆順にするように変更
;; 結果
=> ("privateMethod1" "privateMethod2" "protectedMethod1" "protectedMethod2" "publicMethod1" "publicMethod2")
----------------------
- `nreverse' を使うとリストを逆順に並びかえる事ができます.この関数を使うと,もとのリストの値が変更されるので注意して下さい.
----------------------
<脚注4> lisp のリストが (cons 値 (cons 値 nil)) となっている為です.(cons 新しい値 元のリスト) とするだけで済むので,先頭に追加する方がコストが低いし楽なのです.
----------------------
■■ 結果を別のウィンドウに表示する
覚える関数: interactive, get-buffer-create, erase-buffer, display-buffer, insert
ここからはコマンドを作っていきます.まずは今選択してるバッファのメソッド一覧を別ウィンドウに表示する関数を作ってみます.<リスト9>のように変更してから,Java のソースを開いたバッファ上で M-x list-methods として下さい.メソッドの一覧が表示されます.
----------------------
<リスト9> 結果を別のウィンドウに表示する
;; 今まで作ってきたものを関数にしておく
(defun list-methods-get-methods (buffer)
(save-excursion
(set-buffer buffer)
(goto-char (point-min))
(let ((case-fold-search nil)
(regexp "\\(private\\|protected\\|public\\) +\\([a-zA-Z0-9_]+\\)(")
methods)
(while (re-search-forward regexp nil t)
(push (match-string 2) methods))
(nreverse methods))))
;; 新しいコマンド
(defun list-methods ()
(interactive) ;コマンドであると宣言する
(let ((methods (list-methods-get-methods (current-buffer))) ;メソッド一覧を取得
(buffer (get-buffer-create "*list-methods*"))) ;*list-methods*バッファを取得して
(set-buffer buffer) ;そこに移動
(erase-buffer) ;バッファの中身をクリアして
(dolist (method methods) ;メソッド一覧でループ
(insert method "\n")) ;メソッド名を挿入
(display-buffer buffer))) ;最後に表示する
----------------------
- 関数の先頭に `interactive' を書いておくと,その関数はコマンドになります.
- `get-buffer-create' は新しいバッファを作る関数です.すでに存在する場合は,もとのバッファを返します.
- `erase-buffer' はバッファの中の文字列を全て削除します.
- `display-buffer' はそのバッファを新しいウィンドウにポップアップします.このウィンドウは選択されません.
- `insert' は文字列をバッファに挿入します.
■■ メソッドを選んだらソースに飛ぶようにする
覚える関数: put-text-property, get-text-property
表示するだけではつまらないので,今カーソルがある位置のメソッドにジャンプできるようにします.ジャンプする為に,一覧表示のメソッド名にジャンプ先のバッファとポイントをテキストプロパティとして設定します.テキストプロパティは名前と値の対で,これを使うと文字列に好きな情報を持たせる事ができます.
<リスト10>のように変更してから,先程のように M-x list-methods をして下さい.その後 *list-methods* バッファで M-x list-methods-select-method とすると,選択したメソッドの位置にジャンプします.
----------------------
<リスト10> メソッドを選んだらソースに飛ぶ
;; 既存の関数を少し変更
(defun list-methods-get-methods (buffer)
...省略
(while (re-search-forward regexp nil t)
(push (list (match-string 2) buffer (point)) ;(メソッド名, バッファ, 位置)を返すように変更
methods))
(nreverse methods))))
;; 既存の関数を少し変更
(defun list-methods ()
...省略
(dolist (method methods)
(let ((pos (point)))
(insert (car method) "\n") ;メソッド名は返り値の car になった
;; テキストプロパティを設定するように変更
(put-text-property pos (point)
'list-methods-method-buffer (nth 1 method))
(put-text-property pos (point)
'list-methods-method-position (nth 2 method))))
(pop-to-buffer buffer) ;表示と同時にそのウィンドウに移動するように変更
(goto-char (point-min)))) ;行の先頭へ
;; 新しいコマンド
(defun list-methods-select-method ()
(interactive)
;; バッファと位置をポイント位置のテキストプロパティから取得
(let ((buffer (get-text-property (point) 'list-methods-method-buffer))
(pos (get-text-property (point) 'list-methods-method-position)))
(when (and buffer pos) ;その位置にテキストプロパティが設定されていれば
(pop-to-buffer buffer) ;バッファを表示して
(goto-char pos)))) ;ソース位置に移動
----------------------
- まず `list-methods-get-methods' を (メソッド名, バッファ, 位置) のリストを返すように変更します。
- `put-text-property' はテキストプロパティを文字列に設定します.ここでは list-methods-method-position, list-methods-method-buffer に元のメソッドの位置とバッファを設定しています.
- `get-text-property' は指定した位置の文字列からテキストプロパティを取得します.
■■ 一覧表示をメジャーモードにする
覚える関数: make-sparse-keymap, define-key, use-local-map, run-hooks
覚える変数: major-mode, mode-name
M-x list-methods-select-method で選択はできるようになりましたが,これでは使い辛いです.そこで RET で選択等をできるようにする為,メソッドの一覧表示をメジャーモードにします.
メジャーモードを作るときの手順は以下のようになります.
1. そのモード用のキーマップを作る.
2. major-mode 用のコマンドを作る.
3. そのコマンドでは 変数 `major-mode' にそのモードを表すシンボルを設定.
4. 変数 `mode-name' にそのモードの名前を設定.
5. `use-local-map' でモード用のキーマップを設定.
6. `run-hooks' でそのモード用のフックを実行
さっそくやってみましょう.<リスト11>のように変更して下さい.これで M-x list-methods で表示されたバッファが list-methods-mode になりました.p, n で上下に移動,RET で選択,qで終了します.
----------------------
<リスト11> メジャーモードに変更
;; list-methods-mode 用のキーマップ
(defvar list-methods-mode-map nil)
(unless list-methods-mode-map
(let ((map (make-sparse-keymap))) ;キーマップを生成
;; キーマップにキーとコマンドの対を設定する
(define-key map "n" 'next-line)
(define-key map "p" 'previous-line)
(define-key map "q" 'bury-buffer)
(define-key map "\r" 'list-methods-select-method)
(setq list-methods-mode-map map))) ;list-methods-mode-map に生成したキーマップを設定する
;; major-mode 用の関数を追加
(defun list-methods-mode ()
(interactive)
(use-local-map list-methods-mode-map) ;list-methods-mode-mapをキーマップとして使う
(setq major-mode 'list-methods-mode) ;major-mode を設定
(setq mode-name "List Methods") ;モード名を設定
(run-hooks 'list-methods-mode-hook)) ;このモード用のフックを実行
;; 既存の関数を少し変更
(defun list-methods ()
...省略
(pop-to-buffer buffer)
(goto-char (point-min))
(list-methods-mode))) ;モード設定を追加
----------------------
- `make-sparse-keymap' は空のキーマップを生成します.
- define-key はキーとコマンドの対を設定します.この関数はキーマップを引数に取る以外は `global-set-key' と同じです.
- `major-mode' には普通そのメジャーモードの関数と同じ名前のシンボルを設定します.
- `mode-name' には判りやすい好きな名前を設定します.この値がモードラインのメジャーモード欄に表示されます.
- `run-hooks' は引数のシンボルのフックを実行します.
- list-methods の最後で list-methods-mode に入るようにします.
■■ 最後に
今回は elisp でプログラムを書いていく様子を記事にしてみました.意外と簡単に実装できる事に驚いた方もいるのではないでしょうか.実際に開発するときには *scratch* バッファがもっと活躍しているのですが,記事の都合上その様子が紹介できないのが残念です.この記事を元に「elisp で何かやろう!」という方が増えてくれれば幸いです.
最後に<リスト12> に全てのソースを載せておきます.
----------------------
<リスト12> 完成
(require 'cl)
(defvar list-methods-mode-map nil)
(unless list-methods-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "n" 'next-line)
(define-key map "p" 'previous-line)
(define-key map "q" 'bury-buffer)
(define-key map "\r" 'list-methods-select-method)
(setq list-methods-mode-map map)))
(defun list-methods-mode ()
(interactive)
(use-local-map list-methods-mode-map)
(setq major-mode 'list-methods-mode)
(setq mode-name "List Methods")
(run-hooks 'list-methods-mode-hook))
(defun list-methods-get-methods (buffer)
(save-excursion
(set-buffer buffer)
(goto-char (point-min))
(let ((case-fold-search nil)
(regexp "\\(private\\|protected\\|public\\) +\\([a-zA-Z0-9_]+\\)(")
methods)
(while (re-search-forward regexp nil t)
(push (list (match-string 2) buffer (point))
methods))
(nreverse methods))))
(defun list-methods ()
(interactive)
(let ((methods (list-methods-get-methods (current-buffer)))
(buffer (get-buffer-create "*list-methods*")))
(set-buffer buffer)
(erase-buffer)
(dolist (method methods)
(let ((pos (point)))
(insert (car method) "\n")
(put-text-property pos (point)
'list-methods-method-buffer (nth 1 method))
(put-text-property pos (point)
'list-methods-method-position (nth 2 method)))
(display-buffer buffer)
(list-methods-mode))))
(defun list-methods-select-method ()
(interactive)
(let ((buffer (get-text-property (point) 'list-methods-method-buffer))
(pos (get-text-property (point) 'list-methods-method-position)))
(when (and buffer pos)
(pop-to-buffer buffer)
(goto-char pos))))
----------------------