Emacsのabbrev-mode

emacs には abbrev-modeなる モードがあります。

日本語に訳すとabbrev(iation)は略語、略称といった意味らしいですが、emacsユーザーからは「静的略称展開」というごっつい名前で呼ばれてることが多いですね。名前の割に機能は単純で 、例えば「abb」 と入力したら その文字列を「abbreviation」 に展開する という入力補助のためのシンプルな補完機能です。

abbrev-modeを利用するには メジャーモードに応じたabbrev-table を登録する必要があります。cperl-modeだったら cperl-mode-abbrev-table、fundamental-modeだったら fundamental-mode-abbrev-tableといった具合です。どんなabbrev-tableがあるかは ?M-x edit-abbrevsとするとテーブル一覧を参照できます。

さてテーブルの定義の仕方ですが、 cperl-modeを例にとって書くと以下のようになります ( ! 設定ファイルに直に書き込む場合 ) テーブルには略称と展開されたあとの文字列、フック関数等を書きます。

;; cperl-modeの時に、fwを $c->forward() と展開する定義を書く
 
(define-abbrev-table
  'cperl-mode-abbrev-table 
  '(
    ("fw"   "$c->forward( )" )
    ))

この設定であれば 「fw」と入力すると「$c->forward( )」に展開されるようになります。Catalystを使う時に便利そうですね。なお静的略称展開をONにしたいときは ?M-x addbrev-mode してマイナーモードとして起動して置く必要があります。

( 上記のコードを*scratch*バッファにコピペして、最下行の一番末尾の 「)」の後ろにカーソルを置いてから C-xC-e すると 、elispコードして実行され略称テーブルが再定義されます。)

まぁ、この略称展開でも使えないことはないんですが、fw $c->forward('')へと展開した後のカーソルの位置が微妙です。実際に動かしてみると分かりますが、この展開だとカーソルの位置が以下のようになります

;; fw を展開後、 * の位置にカーソルが来てしまう !
fw => $c->forward( ) * 

どうせなら 展開後のカーソルの位置が () の間、つまり $c->forward( * ) になってるのがいいですよね。forwardの引数を入力するのにカーソル移動する手間が減って楽です。さてじゃあ、どうやったら実現できるでしょうか?

こういう時はフック関数を使います。さきほどのテーブルに少し手を加えます。

(define-abbrev-table
  'cperl-mode-abbrev-table 
  '(
    ("fw"   "$c->forward"  expand-forward) 
    ))

forwardから (' ')の部分を削り、expand-forwad というフック関数を新たに定義しました。このフック関数は略称「fw」を展開した後に呼び出されます。

さて肝心のexpand-forwardはどのように定義すればいいかというと、以下のような感じでどうでしょう

(defun expand-forward ()
  (insert "(")
  (save-excursion
    (insert ")")))

insert関数は 引数の文字列をバッファに挿入するだけの関数です。ここでは '(' と ')' を挿入するだけです。

ここでのキモはsave-excursionという関数です。save-excursionは「カーソルの位置をセーブし、関数の 本体を実行し、そしてその際にカーソルの位置が変わったなら、それを 元の位置に戻す *1」 という不思議な機能の関数です。下に逐次的にどう動くのかを書いたので参照してください... ちょっと分かりにくいかな。

;; 0. * がカーソルの位置
fw*

;; 1.スペースを押すと fw が展開される 
$c->forward *

;; 2. フックのexpand-forwardが呼ばれ、一つ目のinsert が呼ばれる
$c->forward( *

;; 3. save-excursionが呼び出されているので、カーソルの位置が一時的に記録される
$c->forward( *

;; 4. save-excursion内にあるinsertが呼ばれる
$c->forward( )*

;;5. insertはsave-excursion内での実行だったので、関数実行後カーソルは記録していた位置に戻る
$c->forward( *)


;;; 実際にこう動いてるのかどうかはちょと分からないんですけど...結果として こうなります :)


という感じなのです。分かっていただけたでしょうか? 実際に自分好みの略称を作って *scratch*バッファで試してみるといいでしょう。save-excursionが必要な理由がすぐにご理解いただけるかと。

この静的略称展開とフック関数の組み合わせはcperl-modeであれば for や while , if , 等のブロックを使う構文を展開する時に使用されています。if と入力すると if ( ) { } が出てくるあの機能です。


静的略称展開だけでは物足りないなぁ感が強いのですが、フック関数を付け足すとかなり柔軟な補完機能を実現することができます 。ちょっとした補完機能ならinsertやsave-excursion , newline 関数等の基本的な関数の組み合わせてで すぐにつくれます。


まぁそんなこんなで、自分で補完機能まで実装できてしまうEmacsの懐の広さが僕はとても好きです。elispは覚えるべき関数も多いし少し厄介な仕様の言語なのですが、まぁそれでも*scratch*バッファでバキバキ書いたそばから実行して使えるのがとても楽しいですね。

  • バックスラッシュが ? マークに文字化けてます。読み替えてください

より引用した文章に筆者による変更を加えた
「これは、ポイントとマークの位置をセーブし、関数の 本体を実行し、そしてその際にポイントとマークの位置が変わったなら、それを 元の位置に戻す」 

  • cperl-modeのコールバック関数はエラい複雑な実装になっている。ソース追っても分からない。Perl側でいろんな書き方ができてしまうので、それに対応するためだと思います。