auto-completeの基本的な使い方については前回説明しました。今回はさらに踏み込んたauto-completeの拡張方法について説明したのちに、auto-completeの将来やその周辺について説明したいと思います。

auto-completeの拡張方法
=======================

情報源の基本
————

拡張可能と称しているソフトウェアの多くがプラグイン手法を採用しているように、auto-completeも一種のプラグイン手法によって拡張可能性を実現しています。それが情報源と呼ばれるauto-completeを拡張するためのコアの概念になります。情報源という表現はもともとanythingという拡張で使用されていたものですが(*1)、拡張性に富む形式であること、概念の再利用によって学習コストを低く保てることから、auto-completeでも同様の形式を採用しました(*2)。

(*1) anythingにおいてはプラグインという表現は語弊があります
(*2) 形式に完全な互換性があるわけではありません

情報源の実体は連想リストで表される簡単なデータ構造です。連想リストの各々の属性には候補生成式・候補選択時アクション・補完開始条件などの補完に関する挙動が記述されます。次に情報源の基本的な形式を示します。

(defvar ac-source-sample
‘((init . 初期化式)
(candidates . 候補生成式)
(action . 候補選択時アクション)
(prefix . 補完開始条件)))

抽象的な説明を続けていても仕方がないので、具体例に従って説明を進めましょう。まず情報源を定義するに際して、どのような補完を実現したいかを明確にする必要があります。例えばプログラミング言語のキーワードであったり、ビルトイン関数であったり、ファイル名であったりです。ここでは例としてFoo, Bar, Bazの三つのキーワードを補完できるようにしてみます。

情報源の連想リストにはさまざまな属性を指定できますが、一番重要で必須な属性が情報源と候補生成式を対応させるcandidatesプロパティです。候補生成式には引数なしで呼び出される関数や任意のコード断片を記述し、その評価結果は文字列のリストでなければなりません。Foo, Bar, Bazを生成する情報源は、candidatesプロパティのみを使って次のように定義されます(*3)。

(*3) 情報源の名前は慣習的にac-source-という接頭辞を持つことになっています

(defvar ac-source-my-keywords
‘((candidates . (list “Foo” “Bar” “Baz”))))

定義した情報源を利用するにはac-sources変数に追加します。ac-sources変数には複数の情報源を含めることができますが、本稿では分かりやすさのために次のように情報源を設定することにします。

;; ac-source-my-keywords情報源のみを利用する
(setq ac-sources ‘(ac-source-my-keywords))

ac-sources変数を設定したら実際に補完してみましょう。

うまく補完されましたか。以上のように数行のコードを書くだけで簡単に拡張できるのがauto-completeの強みです。これ以降の節では細かい挙動について説明しますが、candidatesプロパティさえおさえておけば7割の補完機能は簡単に実現できることを覚えておいてください。

パフォーマンス
————–

候補生成式は補完が継続されるたび(文字が入力されるたび)に評価されます。候補生成式の評価が遅延すれば、それだけユーザーへのレスポンス性を損なうことになります。そのため候補生成式にはできるだけ軽い処理を記述するべきです。例えばEmacs Lispのシンボルを補完する情報源を次のように定義したとしましょう。

;; 全てのシンボルを文字列のリストとして返す関数
(defun my-symbols ()
(let (syms)
(mapatoms (lambda (s) (push (symbol-name s) syms)))
syms))
;; 全てのシンボルを補完する情報源
(defvar ac-source-my-symbols
‘((candidates . my-symbols)))

この情報源を利用して補完してみると、よほどハイスペックなマシンでない限り、かなりのもたつきを感じると思います。この問題を解決するには主に三つの方法があります。

一つはcandidatesで返される値をあらかじめ計算しておく方法です。候補数が滅多に増減しない場合に有効です。

;; あらかじめ計算
(defvar my-symbols-cache (my-symbols))
(defvar ac-source-my-symbols
‘((candidates . my-symbols-cache))) ; キャッシュを返すだけ

上記の例はシンボル数が一定であるという前提を立てていますが、新しいシンボルがinternされたりするとその限りではないため、この前提はあまり適しているとは言えません。

そのような場合にはinit属性を利用して補完開始時にあらかじめ計算してしまう方法がベターです。

;; あらかじめ計算
(defvar my-symbols-cache)
(defvar ac-source-my-symbols
‘((init . (setq my-symbols-cache (my-symbols)))
(candidates . my-symbols-cache))) ; キャッシュを返すだけ

あるいはcache属性を利用する方法もあります。cache属性を指定しておくと初回のcandidates属性の評価結果がキャッシュされ、二回目以降はcandidates属性が呼び出されなくなります。

(defvar ac-source-my-symbols
‘((candidates . my-symbols)
(cache)))

オムニ補完
———-

文脈に応じて最適な情報源で補完する機能をオムニ補完と呼びます。もともとはVimで使われていた表現で、簡単に言えば補完機能が空気を読んで高精度な補完を実現してくれるのです。現状のauto-completeではファイル名補完のみオムニ補完に対応しています。実際にファイル名補完を試すには(push ‘ac-source-filename ac-sources)で情報源を追加して、auto-complete-modeが有効な適当なバッファでファイル名らしき文字列を入力します。

前置きはこれぐらいにして、ある情報源をオムニ補完に対応させる方法を説明します。今回は例としてC言語のバッファで#include < と入力すると、システムヘッダファイルをオムニ補完するようにしてみたいと思います。

まず、システムヘッダファイルを補完する情報源を定義する必要がありますが、ここでは簡単のために/usr/include直下のヘッダファイルのみを対象とします(*4)。

(*4) まじめに対応すると結構大変だと思われます

では/usr/include直下のヘッダファイルを列挙する関数を定義しましょう。

(defun list-system-header-files ()
(directory-files “/usr/include” nil “^.*\\.h$”))

次にこの関数を用いて情報源を定義します。

(defvar system-header-files-cache (list-system-header-files))
(defvar ac-source-system-header-files
‘((candidates . system-header-files-cache)))

この情報源をac-sources変数に設定して補完してみましょう。

ここまでは今までのおさらいです。情報源をオムニ補完に対応させるにはprefix属性を利用します。prefix属性には正規表現あるいは関数を指定し、その評価結果を補完対象の開始位置とします。prefix属性に正規表現を指定した場合は、現在のカーソルから逆向きに検索をかけ、マッチしたグループ1の開始位置あるいはグループ0(全体)の開始位置を補完対象の開始位置とします。prefix属性に関数を指定した場合は、その関数の返り値を補完対象の開始位置とします。取得された開始位置はac-point変数に格納され、補完対象であるac-prefix変数には、この開始位置と現在のポイントの部分文字列が入ります。ポップアップメニューはこのとき取得されたac-point変数の位置に表示されます。

ある情報源にprefix属性が指定されており、その評価結果が非nilであれば、auto-completeはその情報源を利用してオムニ補完を開始します。オムニ補完に至るまでの道程をもう少し詳しく書けば次のようになります。まずac-sources変数内の情報源を同じprefix属性を持つグループに分けます。次にグループの登場順にそのグループのprefix属性を評価してゆき、はじめに非nilを返したグループを一時的にac-sources変数に設定して通常の補完を開始します。prefix属性には暗黙のデフォルト値があるため、prefix属性のない情報源での通常の補完も本質的にはオムニ補完と言えます。

では、先ほど定義した情報源をオムニ補完に対応させてみましょう。上記したように、#include < と入力されたときにこの情報源を利用してオムニ補完したいので、prefix属性に#include <にマッチする正規表現を書けばよいことが分かります。

(defvar ac-source-system-header-files
‘((candidates . system-header-files-cache)
(prefix . “#include *<\\([^>]*\\)”)))

単純に#include *< という正規表現では#が開始位置となってしまうため正しく補完できません。そこで\(\)でグループ1を設定して、その開始位置を補完対象の開始位置になるようにします。つまり<>で囲まれた文字列が補完対象になります。この情報源を再定義して適当なバッファで#include < と入力して補完してみましょう。文字列を入力していけばシステムヘッダファイルの補完が行えます。

最後の仕上げとしてaction属性を使って補完確定時に>と改行を入力するようにしてみましょう。action属性には補完確定時(RETによってac-completeが呼び出された場合)に呼び出される関数を指定します。すでに>が入力されている場合にそなえて、現在のポイントが行末であるときに>と改行を入力することにします。

(defvar ac-source-system-header-files
‘((candidates . system-header-files-cache)
(prefix . “#include *< \\([^>]*\\)”)
(action . (lambda ()
(when (eolp)
(insert “>\n”))))))

この情報源を定義して補完中にRETすると#include のように入力され改行されます。

辞書
—-

auto-completeで独自の辞書を用いて補完を行いたいという要望が思いのほか多かったので、ここで補足しておきたいと思います。

前述したように辞書を情報源の定義に埋め込んでしまう方法が一番簡単で分かりやすいと思います。

(defvar ac-source-my-dict
‘((candidates . (list “word1” “word2” “word3”))))

あるいは行単位で単語が定義された辞書ファイルをあらかじめ用意し、次のようなコードで読み込むという方法もあります。

(defun load-dictionary (file)
(let (dict)
(with-temp-buffer ;一時バッファを作成
(insert-file-contents file) ;fileの内容を挿入
(goto-char (point-min)) ;先頭に移動
(while (not (eobp)) ;バッファの最後でない間
(push (buffer-substring ;現在行をdictに追加
(line-beginning-position)
(line-end-position))
dict)
(forward-line 1))) ;一行下に移動
(nreverse dict))) ;dictを反転して返す
(defvar my-dict (load-dictionary “~/.emacs.d/dict”))
(defvar ac-source-my-dict
‘((candidates . my-dict)))

今のところ上記のようにローテクで辞書情報源を定義する必要がありますが、将来的にはインタラクティブに辞書を操作し、auto-completeからシームレスに使えるような機構を作ろうと考えています。

auto-completeの将来とその周辺
=============================

競合
—-

auto-completeが生まれて一年が経ちましたが、私の不肖および怠惰により、仕様はあやふやでアーキテクチャもいまいちという有様でありながら、そこそこ多くのユーザーが使ってくださっているようです。そのことは本当に励みになるのですが、やはりコードの拙さというのは隠しきれない面があります。思うに文章の技術とコーディングの技術には相関関係があると思うのですが、まあそれはいいとして、とにかく言いたいのは、auto-completeはソフトウェアとしてはまだまだ未熟だということです。ユーザーにはソフトウェアを選択する自由があり、隠す意味も全くないのであえて紹介しますが、auto-completeに似た機能を持つものとしてcompany-mode(*5)という拡張があります。この拡張は機能の充実性および安定性でauto-completeより優れていると思います。ライセンスも同じで機能も同じソフトウェアが存在する意味はないので、どちらか一方が消滅すべきだと最近まで考えていましたが、よくよく考えてみるとそこには微妙なコンセプトの違いがあることを発見しました。

(*5) http://nschum.de/src/emacs/company-mode/

company-modeは良くも悪くもEmacs的です。Emacsになじみの深い人にはcompany-modeはしっくりくるものでしょう。一方、auto-completeにはEmacsの悪しき面を改めるという私の精神が反映されているため、かなり前衛的なことにも取り組んでいます。詳しくは私のEmacsブログ(*6)を参照してもらえば分かると思いますが、とにかく私はそのような見地にたっているため、ものすごく優れたインターフェースを実現するかもしれませんし、あまりにEmacsから逸脱した馬鹿なこともやりだしかねません。その点を念頭に置いてauto-completeの今後を見守っていただければと思います。

(*6) http://d.hatena.ne.jp/m2ym/

将来
—-

基礎もまだ固まっていないのに図々しく将来の話をすると次のようになります。

1. 多数のプログラミング言語でオムニ補完の実現
2. ヘルプ機能との連携の実現
3. 空気のように使えるインターフェースの実現

(1)を実現すれば、例えばC++やJavaなどの静的型付言語に加えてRubyやPythonなどの動的型付言語でもメソッド補完などを行えるようになります。これはEclipseのCodeAssistやVisual StudioのIntelliSenseをイメージしてもらうと分かりやすいと思います。これの延長線上にあるものですが、(2)により補完中にメソッドなどのヘルプをシームレスに閲覧できるようになります。わざわざWebで調べる手間が省けて作業効率が向上すると考えています。(3)はかなり抽象的ですが、要するに特に意識せずに自然に補完が行えるようなインターフェースを実現するということです。脳へのオーバーヘッドをなくすことはコーディングの本質に没頭でき作業効率のさらなる向上が期待できます。

まとめ
======

二回に渡り拙作のauto-completeの基本的な使い方から拡張方法、将来について説明しました。auto-completeにはまだまだ至らない点が多数ありますが、将来的には社会に貢献できるようなソフトウェアにしたいと考えています。これからもあたたかい目で見守っていただければと思います。