expand-abbrev を拡張する

背景

Emacs の便利機能のひとつ expand-abbrev (略語展開)。簡単に説明すると、たとえば「gei」と入力して ESC-/ を押すと「document.getElementById」に展開したり、「main」を「public static void main(String[] arg)」に展開できたりする。
展開する文字列はモードごとに変えることができて、たとえば「encoding」を ruby-mode では「# -*- coding: utf-8 -*-」に展開し、html-mode では「<meta http-equiv="Content-Type" content="text/html; charset=utf8" />」に展開する、ということができる。
もちろん展開する文字列はあらかじめ登録しておくする必要があるんだけど、この登録が C-x a i g や C-x a i l で簡単にできるのも、Emacs が便利な点のひとつ (いちいちメニュー操作をする必要がない)。

問題点

これはこれで便利なんだけど、Eclipse のテンプレート機能とかと比べると、Emacs の機能は弱い。特に、できることが「文字列を展開する」ことだけなのがつらい。せめて、展開後の文字列の指定可所にカーソルを移動するぐらいのことはできてほしい。

解決案

というわけで、自分なりに expand-abbrev を拡張してみた。これを使うと、展開後の文字列に「<|CURSOR|>」があればそこにカーソルを移動し、「<|INDENT|>」があれば適切にインデントしてくれる。

具体例

たとえば「it」を「it "<|CURSOR|>" do\n<|INDENT|>\n<|INDENT|>end"」に登録しておけば、「it」を入力して ESC-/ を押すと、

it "" do
  
end

に展開してくれ、カーソルが「"」と「"」の間に移動する。また 2 行目と 3 行目は適切にインデントされる。

ソース

閉じカッコの書き方が変なのは仕様だ。だってこうしないと S 式の読み書きができないんだもん!

;;; ------------------------------------
;;; my-expand-abbrev
;;; ------------------------------------
(defvar my-expand-abbrev-assoc
  '(
    ("CURSOR" . (let ()
                  nil
                  ))
    ("INDENT" . (let ()
                  (replace-match "")
                  (funcall (key-binding "\C-i"))
                  ))
    ("FILE"   . (let ((basename (file-name-nondirectory (buffer-file-name))))
                  (replace-match basename t)
                  ))
    ("LINE"   . (let ()
                  (replace-match (line-number-at-pos))
                  ))
    )
  );defvar

(defun my-expand-abbrev ()
  "Expand abbrev and '<|KEYWORD|>'."
  (interactive)
  (let (start-pos end-pos)
    ;; set start-pos and end-pos
    (setq end-pos (point))
    (backward-word)
    (setq start-pos (point))
    (goto-char end-pos)
    ;; expand abbrev
    (if (not (expand-abbrev))
        nil
      ;; expand <|KEYWORD|>
      (save-excursion
        (while (re-search-backward "<|\\([A-Z]+\\)|>" start-pos t)
          (let* ((keyword (match-string 1))
                 (pair (assoc keyword my-expand-abbrev-assoc)))
            (if (and pair
                     (listp (cdr pair)))
                (eval (cdr pair))
              );if
            );let*
          );while
        );save-excursion
      ;; go to <|CURSOR|> position if it exists
      (setq end-pos (point))
      (goto-char start-pos)
      (if (search-forward "<|CURSOR|>" end-pos t)
          (replace-match "")
        (goto-char end-pos)
        );if
      );if
    );let
  );defun

;; ESC-/ ってちょっと打ちにくいよね?
(global-set-key "\C-l" 'my-expand-abbrev)

EmacsLisp の添削は大歓迎。

考察

似たようなライブラリは他にもあるだろう。知っている人は教えてちょーだい!