Personal tools
You are here: Home ブログ matsuyama imenu.el の使い方と応用
Document Actions

imenu.el の使い方と応用

imenu とは

本の読者は、目次(インデックス)を見ることにより瞬時に読みたいページを開くことができますが、 imenu は編集中のバッファに対してインデックスを作成することにより、編集したい場所に瞬時に移動するのを補助してくれます。 TAGS のように、インデックスの作成機能があらかじめハードコードで実装されているわけではなく、ユーザーが独自のインデックス作成機能を追加できるように汎用的に実装されています [1] 。例えば、 C の関数定義のインデックス作成機能を追加することもできますし、 HTML の見出し(H1 〜 H6)のインデックス作成機能を追加することもできます。 15 年以上前に作成された Emacs Lisp ですが、そのシンプルさと拡張性さゆえに今でも十分便利に利用できます [2]

[1]Emacs Lisp や C のやや雑な実装はあらかじめ用意されています
[2]Emacs (Lisp) が長寿なのを、メリット(洗練される)とみるかデメリット(時代遅れ)とみるか、意見が分かれますが、僕は前者を支持します

imenu を使う

まず以下のようなプログラムを書いてみましょう。

test.c:

void hello() {
  printf("Hello");
}

int main(int argc, char *argv[]) {
  hello();
  return 0;
}

次に、そのバッファで M-x imenu を実行してみましょう。 Index item とプロンプトされ、移動先の項目名を入力することができるようになります。ここで hello と入力して Enter を押すと hello 関数に移動することができます。関数が追加された場合などは、 M-x imenu RET *Rescan* RET を実行してインデックスを再構築する必要があります。基本的な使い方はこれだけです。シンプルですね。

http://dev.ariel-networks.com/Members/matsuyama/images/imenu1.png/image

imenu はデフォルトでは関数や変数っぽいものを探してきてインデックスを作成します。 Emacs Lisp や C や Java など、大体の言語には対応しているようですが、そのままでは多少不便かもしれません。

anything から使う

M-x imenu は使いづらいので anything [3] から imenu を使うようにしてみましょう。

[3]anything については http://dev.ariel-networks.com/Members/matsuyama/open-anything-emacs を参照してください

anything で imenu を使うには以下の設定を ~/.emacs に書きます。

~/.emacs:

(require 'anything-config)
(setq anything-sources
     (list ...
           anything-c-source-imenu
           ...))
http://dev.ariel-networks.com/Members/matsuyama/images/imenu2.png/image

デフォルトで定義されている anything-c-source-imenu は深いインデックスに対応してなかったりするので EmacsWiki の anything-c-source-imenu を使用することをお勧めします。

http://www.emacswiki.org/cgi-bin/wiki/AnythingSources

which-func でさらに便利に

which-func は imenu のインデックスを用いて、現在どの関数を編集しているか(Which Function)をモードラインに表示してくれる機能です。 which-func という名前は、単に関数のインデックスがよく作られるからそのように名付けられたのですが、一般的には which-func はポイント付近にあるインデックスの項目名を表示する機能と言えます。

which-func を使うには以下の設定を ~/.emacs に書きます。

~/.emacs:

(require 'which-func)
; java-mode と javascript-mode でも which-func を使う
(setq which-func-modes (append which-func-modes '(java-mode javascript-mode)))
(which-func-mode t)
http://dev.ariel-networks.com/Members/matsuyama/images/imenu3.png/image

CEDET / Semantic との連携について

CEDET [4] / Semantic を使うと、標準のインデックスより詳細なインデックスを利用できるようになります [5]

[4]http://cedet.sourceforge.net/
[5]Semantic のパーザーが詳細なインデックスを作成してくれます

~/.emacs:

(require 'semantic-imenu)

標準の設定だと anything などから使った場合に読みにくい表示になるので以下のような設定を書いておくと良いかもしれません。

~/.emacs:

(setq semantic-imenu-summary-function
      (lambda (tag)
        (semantic-format-tag-summarize tag nil t)))

imenu を使いこなす

先述したように、 imenu はユーザーが独自のインデックス作成機能を追加できるように汎用的に実装されています。ここでは、その機能をフルに活用して作業効率を高める方法を紹介します。

ちなみに以下のコードが実行されていることが前提です。

(require 'cl)
(require 'imenu)

独自のインデックス作成機能を作る

imenu はインデックスを作成するために、バッファローカル変数にバウンドされた関数を呼びだそうとします。

最初に呼びだそうとするのは imenu-create-index-function バッファローカル変数の関数です。この関数は内部でインデックスを作成して返す関数である必要があります。インデックスの形式は (項目名 . ポイント) あるいは (項目名 . 子供) のシンプルなツリー構造です。

簡単な例を示します。まず以下のようなバッファを作ってみましょう。

test.txt:

* 見出し1
内容1
* 見出し2
内容2

次に *scratch* バッファなどで以下の関数を評価しておきましょう。この関数はバッファの先頭から * で初まる行を項目名としたインデックスを作成する関数です。

(defun outline-imenu-create-index ()
  (let (index)
    (goto-char (point-min))
    (while (re-search-forward "^\*\s*\\(.+\\)" (point-max) t)
      (push (cons (match-string 1) (match-beginning 1)) index))
    (nreverse index)))

そして test.txt のバッファで M-: (setq imenu-create-index-function 'outline-imenu-create-index) とやって、 imenu からこの関数が呼ばれるようにします。最後に M-x imenu RET *Rescan* RET とすれば独自のインデックス作成機能が無事追加されました。

http://dev.ariel-networks.com/Members/matsuyama/images/imenu4.png/image

text-mode の場合に、このインデックス作成機能を使いたい場合は以下の設定を ~/.emacs に書いておくと良いでしょう。

~/.emacs:

(add-hook 'text-mode (lambda () (setq imenu-create-index-function 'outline-imenu-create-index)))

さて、 imenu が imenu-create-index-function バッファローカル変数に関数がバウンドされていないと判断すると、今度は imenu-prev-index-position-function バッファローカル変数と imenu-extract-index-name-function バッファローカル変数を使ってインデックスを作成しようとします。これらにバウンドされた関数は次の手順で呼び出されます。

  1. バッファの最後にポイントを移動する
  2. imenu-prev-index-position-function バッファローカル変数の関数を呼び出し、一つ前の項目に移動する
  3. imenu-extract-index-name-function バッファローカル変数の関数を呼び出し、項目名を取得する
  4. 2 が nil を返すまで 2 〜 3 を繰り返す

この手順にそって imenu がインデックスを構築していきます。前の例をこの方法で実装してみましょう。

まず * の前に移動する関数を定義します。

(defun outline-imenu-prev-index-position ()
  (re-search-backward "^\*\s*.+" (point-min) t))

次に項目名を取得する関数を定義します。

(defun outline-imenu-extract-index-name ()
  (if (looking-at "^\*\s*\\(.+\\)")
      (match-string 1)))

最後に test.txt のバッファで M-: (setq imenu-create-index-function 'imenu-default-create-index-function imenu-prev-index-position-function 'outline-imenu-prev-index-position imenu-extract-index-name-function 'outline-imenu-extract-index-name) とやって、 imenu からこれらの関数が呼ばれるようにします。 M-x imenu RET *Rescan* RET すればちゃんとインデックスが作成されていると思います。

http://dev.ariel-networks.com/Members/matsuyama/images/imenu5.png/image

どちらの方法が使いやすいかはケースバイケースだと思いますが、基本的には後者でインデックスを作成し、複雑な処理を行う場合は前者でインデックスを作成すれば良いでしょう。

以上を踏まえて実用できる応用例を二つあげておきます。是非参考にしてください。

応用1. HTML の見出しのインデックス作成機能を作る

HTML の見出しでインデックスを作成できるようにしてみましょう。これは上の例で作ったものを少し変更にしただけです。

(defun html-imenu-create-index ()
  (let (index)
    (goto-char (point-min))
    (while (re-search-forward "<h[1-6]>\\(.+?\\)</h[1-6]>" (point-max) t)
      (push (cons (match-string 1) (match-beginning 1)) index))
    (nreverse index)))

以下のような設定を書いておくと良いでしょう。

~/.emacs:

(add-hook 'html-mode (lambda () (setq imenu-create-index-function 'html-imenu-create-index)))
(setq which-func-modes (append which-func-modes '(html-mode)))

which-func を有効にしておくと、編集中のセクションがすぐに分かって便利です。

http://dev.ariel-networks.com/Members/matsuyama/images/imenu6.png/image

応用2. JavaScript のメソッドのインデックス作成機能を作る

さらに実用的なものを紹介しましょう。一般的に JavaScript でのメソッドへの移動は TAGS を使ってもなかなかうまく実現できません [6] 。なぜなら

function foo() {
  ...
}

という定義はうまく処理できても、 prototype.js でのクラス定義やオブジェクト記法でのメソッドはうまく処理できないからです [7]

// prototype.js のクラス定義
var Foo = Class.create({
  bar: function() {
    ...
  }
});

// オブジェクト記法での関数定義
// 名前空間として使ったりする
var Foo = {
  bar: function() {
    ...
  }
};
[6]http://d.hatena.ne.jp/secondlife/20060904/1157354851 という方法もありますが
[7]もちろんパーザーが対応すればうまく処理できますが、言語仕様でもないものをパーズすべきかというと微妙な話です

さて、上のような一見面倒くさそうな記述をうまくパーズして、メソッドのインデックスを作成できるようにしてみようと思いますが、もちろん厳密にパーズするわけではありません。このような処理は適当にやるのが効果的なのです。今回は以下のような手順でやってみましょう。

  1. Class.createObject.extend 、オブジェクト記法を検出して、定義される対象を項目名とする
  2. そこから { を検索し、対の } までが、その対象でのメソッド定義可能範囲と前提する
  3. { から } までで、 name: function () となっている name をメソッド名としてインデックスに追加する

以上のことをやれば、それなりにちゃんとした JavaScript のメソッドのインデックスを作ることができます。

実際のコードは以下のようになります。

;; 識別子の正規表現
(defvar javascript-identifier-regexp "[a-zA-Z0-9.$_]+")

;; } までの class のメソッドを列挙する関数
(defun javascript-imenu-create-method-index-1 (class bound)
  (let (result)
    (while (re-search-forward (format "^ +\\(\%s\\): *function" javascript-identifier-regexp) bound t)
      (push (cons (format "%s.%s" class (match-string 1)) (match-beginning 1)) result))
    (nreverse result)))

;; メソッドのインデックスを作成する関数
(defun javascript-imenu-create-method-index ()
  (cons "Methods"
        (let (result)
          ;; $name = Class.create
          ;; $name = Object.extend
          ;; Object.extend($name,
          ;; $name = {
          ;; をクラスあるいはオブジェクトとする
          (dolist (pattern (list (format "\\b\\(%s\\) *= *Class\.create" javascript-identifier-regexp)
                                 (format "\\b\\([A-Z]%s\\) *= *Object.extend(%s"
                                         javascript-identifier-regexp
                                         javascript-identifier-regexp)
                                 (format "^ *Object.extend(\\([A-Z]%s\\)" javascript-identifier-regexp)
                                 (format "\\b\\([A-Z]%s\\) *= *{" javascript-identifier-regexp)))
            (goto-char (point-min))
            (while (re-search-forward pattern (point-max) t)
              (save-excursion
                (condition-case nil
                    ;; { を探す
                    (let ((class (replace-regexp-in-string "\.prototype$" "" (match-string 1))) ;; .prototype はとっておく
                          (try 3))
                      (if (eq (char-after) ?\()
                          (down-list))
                      (if (eq (char-before) ?{)
                          (backward-up-list))
                      (forward-list)
                      (while (and (> try 0) (not (eq (char-before) ?})))
                        (forward-list)
                        (decf try))
                      (if (eq (char-before) ?}) ;; } を見つけたら
                          (let ((bound (point)))
                            (backward-list)
                            ;; メソッドを抽出してインデックスに追加
                            (setq result (append result (javascript-imenu-create-method-index-1 class bound))))))
                  (error nil)))))
          ;; 重複を削除しておく
          (delete-duplicates result :test (lambda (a b) (= (cdr a) (cdr b)))))))

ついでに単純な関数のインデックスも作る関数も定義しておきます。

(defun javascript-imenu-create-function-index ()
  (cons "Functions"
         (let (result)
           (dolist (pattern (list
                             (format "\\b\\(%s\\) *= *function" "function \\(%s\\)"
                                     javascript-identifier-regexp
                                     javascript-identifier-regexp)))
             (goto-char (point-min))
             (while (re-search-forward pattern (point-max) t)
               (push (cons (match-string 1) (match-beginning 1)) result)))
           (nreverse result))))

あとは以下の関数を定義して、

(defun javascript-imenu-create-index ()
  (list
   (javascript-imenu-create-function-index)
   (javascript-imenu-create-method-index)))

適当な JavaScript ファイルで M-: (setq imenu-create-index-function 'javascript-imenu-create-index) とやります。

test.js:

var Foo = Class.create({
  bar1: function() {},
  bar2: function() {}
});
http://dev.ariel-networks.com/Members/matsuyama/images/imenu9.png/image

ちゃんとパーズできてるみたいです。

ちなみに prototype.js 1.6.0.2 だとこんな感じになります。

http://dev.ariel-networks.com/Members/matsuyama/images/imenu7.png/image

anything から使うともっと便利になります [8]

[8]EmacsWiki の anything-c-source-imenu を使っています
http://dev.ariel-networks.com/Members/matsuyama/images/imenu8.png/image

最後は以下のような設定を ~/.emacs に書いておけば良いでしょう。

~/.emacs:

(add-hook 'javascript-mode-hook (lambda () (setq imenu-create-index-function 'javascript-imenu-create-index)))

これで prototype.js のクラスやオブジェクト記法のメソッドにも簡単に移動できるようになりました。すばらしい。

最後に

imenu はシンプルですが非常にパワフルな Emacs Lisp です。独自の構文を持つファイルや、複雑な階層構造を持つファイルを編集するときに、移動に手間をとられるようだったら是非 imenu を使うのを検討してみましょう。一度、インデックス作成機能を作ってしまえば、 which-func で現在編集中の場所が瞬時に分かりますし、 anything などと組み合わせると移動に手間をとられるということはほとんどなくなると思います。

Category(s)
emacs
The URL to Trackback this entry is:
http://dev.ariel-networks.com/Members/matsuyama/imenu/tbping
Add comment

You can add a comment by filling out the form below. Plain text formatting.

(Required)
(Required)
(Required)
(Required)
(Required)
This helps us prevent automated spamming.
Captcha Image


Copyright(C) 2001 - 2006 Ariel Networks, Inc. All rights reserved.