anything.elの新機能「バッファによる候補作成」の予告

anything.elは通常、文字列リストをstring-matchで絞り込んでいる。しかし、前にも書いたようにこのやり方は遅い。そこでバッファにinitで全候補を書き出してからre-search-forwardで拾っていくのが速いしEmacsらしいやり方だ。
現にanything-c-moccur.elanything-c-mx.elではそのテクニックが使われている。

せっかくいいやり方なのだから普及させなくては。普及させるには、書きやすくないとだめだ。そこで、anything-sourcesに使える属性に新しく「candidates-in-buffer」を加えることにした。ぶっちゃけcandidates-in-bufferはショートカットで、

(candidates-in-buffer)

(candidates . anything-candidates-in-buffer)
(volatile)
(match identity)

と等価である。人間うっかりミスになかなか気付かないもので新属性を導入すればvolatileを入れるのを忘れる心配がなくなる。anything-get-sourcesで前者から後者に展開するので、互換性は保たれる。

anything-candidates-in-bufferは「候補バッファ」からre-search-forwardで候補を拾う関数だ。この関数を直接使うことはあんまりないと思う。(candidates-in-buffer)で間接的に使うのがほとんどだろう。

候補バッファは1行に1候補並べているバッファだ。候補バッファは、新しく作成することも、既存のバッファを登録することもできる。作成・登録はinitでやる。候補バッファの作成・登録はanything-candidate-buffer関数を使う。この関数を用意した理由は、defvarで無駄なグローバル変数をばら撒きたくないからだ。いちいち定義するのめんどうし。そこは関数を使って抽象化しよう。

  • (anything-candidate-buffer 'local) か (anything-candidate-buffer t) でバッファローカルな空の候補バッファを作成する。バッファ名は「 *anything candidates:ソース名*バッファ名」となる。メソッド名一覧など、バッファの内容を抜き出すのに便利。
  • (anything-candidate-buffer 'global) でグローバルな空の候補バッファを作成する。バッファ名は「 *anything candidates:ソース名*」となる。Emacs関数リストなどの作成に便利。
  • (anything-candidate-buffer BUFFER) で既存のBUFFERを候補バッファに登録する。たとえばファイルリストを保存しているファイルを開き、その中から候補を選ぶ場合に便利。


candidates-in-buffer属性を使うと、実際にプログラミングするのはinit関数のみになる。雛型はこんな感じだ。

(defvar anything-c-source-ほにゃらら
  '((name . "なまえ")
    (init
     . (lambda ()
         (when 候補作成の条件
           (with-current-buffer (anything-candidate-buffer 'local)
             候補を書き出す))))
    (candidates-in-buffer)
    (action あくしょーん)))

候補作成の条件に (anything-current-buffer-is-modified) を入れておくと、anything-current-bufferの内容が変更されない限り、前回作成した候補(候補バッファの内容)が再利用される。
with-current-buffer内ではカレントバッファが変わってしまうため、buffer-file-nameはanything-buffer-file-nameを使う必要がある。これはanything-current-bufferのbuffer-file-nameだ。うーん、他のバッファローカル変数にアクセスできるようなラッパー関数を用意すべきか…


たとえば、Rubyのソースコード(C言語)解析支援ツール list-call-seq.rb - http://rubikitch.com/に移転しましたのソースを書き換えてみよう。オリジナルではshell-command-to-stringをsplit-stringしている。split-stringは信じられないことにEmacs Lispで書かれているので遅い。

(defvar anything-c-source-list-call-seq
  '((name . "Ruby Source (call-seq)")
    (candidates
     . (lambda ()
         (with-current-buffer anything-current-buffer
           (when (and buffer-file-name (eq major-mode 'c-mode))
             (split-string (shell-command-to-string
                            (format "list-call-seq.rb -n %s"  buffer-file-name))
                           "\n")))))
    (invariant)            ;; *
    (action 略)))

新機能を使うとこうなる。

(defvar anything-c-source-list-call-seq
  '((name . "Ruby Source (call-seq)")
    (init
     . (lambda ()
         (when (and buffer-file-name
                    (anything-current-buffer-is-modified)
                    (eq major-mode 'c-mode))
           (with-current-buffer (anything-candidate-buffer 'local)
             (call-process-shell-command
              (format "list-call-seq.rb -n %s" buffer-file-name)
              nil (current-buffer))))))
    (candidates-in-buffer)
    (action 略)))


前回はinvariant属性を使っているので行数は少し増えているが、with-current-bufferの中はcall-process-shell-commandだけなので、見通しは良くなったのではないだろうか。invariantを使わないと、buffer-chars-modified-tickを使ってごにょごにょ書く必要がある。
「(with-current-buffer (anything-candidate-buffer 'local)」の部分はバッファローカルな候補バッファを作成して、そのバッファをカレントにする。無駄なグローバル変数がない分書きやすくなっている。
候補バッファの参照は1度だけなのでさらに短く書くことができる。

(defvar anything-c-source-list-call-seq
  '((name . "Ruby Source (call-seq)")
    (init
     . (lambda ()
         (when (and buffer-file-name
                    (anything-current-buffer-is-modified)
                    (eq major-mode 'c-mode))
           (call-process-shell-command
            (format "list-call-seq.rb -n %s" buffer-file-name)
            nil (anything-candidate-buffer 'local)))))
    (candidates-in-buffer)
    (action 略)))

invariant属性は取り込もうかどうか迷っている。(anything-current-buffer-is-modified)を使えばいいので…

どうだろうか? コメント、トラックバック等求む。

追記

さっそくid:IMAKADOさんから。

・ 2008年08月08日 IM IMAKADO 5, anything, rubikitch, tmp バッファを使った候補の絞り込 み後で詳しく読む (display . real)の形式はどう実装されるのかテキストプロパティを使う ?

candidates-in-bufferを使うのは大量の候補を扱うことを念頭にしているから、 (display . real) 形式は扱わないようにする予定。実際call-process-shell-command等の外部コマンドの出力結果や既存のバッファを候補にすることがほとんどだろう。たった1つの候補しか選択しないのに、バッファ先頭から1行1行text propertyを付加していくのは無駄だ。

その代わり、display-to-real属性を新設して、「選択された候補→actionに渡す引数」の変換機構を設ける。candidates-in-bufferとの併用はもちろんのこと、従来の方法でもcandidate-transformerでmapcarとかで無理に (display . real) を作成しなくてもよくなるから、コードがコンパクトになるだろう。たとえば、

(let ((anything-sources
        '(((name . "TEST")
           (candidates "foo")
           (display-to-real . upcase)
           (action ("identity" . identity))))))
  (anything))

を実行したら、FOOが返ってくるようになる。こちらでは、一応ユニットテストも含めて実装済

display-to-realという名前はどうだろう。しばらく考えてみて、もっといい名前が思い付かなかったら仕様として確定し、リリースするつもりだ。

anything.elは重い」という声を黙らせないといけない。