技術文章>テキストエディタ実装技術その2

型を認識したメンバ名動的補完機能
Nobuhide Tsuda
21-Feb-2009
目次:
  1. はじめに
  2. テキストエディタにおける動的補完機能の現状
  3. 実装上の課題
  4. 解決方法
  5. 実装
  6. 評価と問題点
  7. 終わりに

はじめに

ViVi 2.08 に C/C++ ソースにおける変数の 型を認識したメンバ名動的補完機能(下図参照)を実装した。

本機能の完全な仕様をスクラッチから実装するには膨大な工数が必要と思われるが、 既存のプログラムを利用し、仕様を限定することで、実用的な機能を正味1週間程度という短期間で実装することに成功した。

本稿ではその実装について報告する。

テキストエディタにおける動的補完機能の現状

動的補完機能(dynamic-abbreviation, dabbrev または auto-completion)は、下図の様に、カーソル直前の文字列で始まる単語を編集中の文書から検索し、マッチするものをカーソル位置に挿入する機能である。 (文書がインクルードする文書内も検索する場合がある)

    int longLongAwfullVarName = 123;
    lo|  ← ここにカーソルがある状態で Ctrl + L で動的補完を行うと
            "longLongAwfullVarName" を参照入力できる

これに対して、あらかじめ用意された固定的なファイルから単語を補完する機能を静的補完機能(static-abbreviation, sabbrev)と呼ぶ。 (ちなみにオリジナルの vi には ab という静的補完機能がある。補完される文字列に Esc を含めておくと、それ以降が vi コマンドとして処理されるというマニアックなものである)

動的/静的補完機能にはタイプ時間の短縮やタイプミス削減の効果があり、テキストエディタには無くてはならない機能のひとつである。

しかし、C/C++ ソースでクラス/構造体メンバアクセス演算子である -> または . の直後で動的補完を行い、メンバ変数名/関数名を参照したい時でも、 単なる文字列マッチングを行うために、非メンバ名の文字列が補完候補に上がってしまう問題がある(下図参照)。

    class CHoge
    {
        int m_var;
    };
    void main()
    {
        CHoge *ptr = new CHoge;
        ptr->m|  ← ここで動的補完した場合、"main" も補完候補に上がってしまう
        ptr->|   ← この状態ではメンバ名を動的補完できない
    }

クラス/構造体メンバアクセス演算子の直後で動的補完した場合は、直前の式項の型を認識し、 適切なメンバ名(上図では“m_var”)だけを補完候補とするべきである。

Visual Studio や Eclipse 等のIDE であれば、このような機能(インテリセンス(TM)と呼ばれる)は標準的に搭載されているが、 汎用テキストエディタでこのような機能を搭載しているものはほとんど存在しない。
筆者の知る限りでは、スクリプトで実装した
Vim のオムニ補完 (2006年?〜、スクリーンショット)だけである。

そこで筆者は、ViVi を史上最強エディタにすべく、 「型を認識した動的補完機能」 を Version 2.08 に実装することにした。

実装上の課題

型を認識したメンバ名動的補完(インテリセンス/オムニ補完)を行うには、カーソル位置直前式項を識別し、 その型がクラス/構造体であれば、そのメンバ名情報を取得する必要がある。 しかし、これを実現するためには以下の実現方法を考えなくてならず、実は容易なことではない.

補完候補一覧をポップアップ表示し選択してもらうUIは 2.06 で既に実装済みであった。

型認識に関する課題

まず、C/C++ の関数はオーバーロード可能なため、引数の型が全て判別できないと、式の型を決定できない場合がある(厳密にはC言語ではオーバーロードはできないが多くのC言語処理系がオーバーロードをサポートしている)。

    CHoge *func(int);
    CFoo *func(char);
    func(x)->|       //  x の型が分からないと、func() の型は判別できない

当然ながら、全てのソースコードを厳密に構文解析すればこの問題は解決できるのだが、 C++ の文法はかなり複雑怪奇で、 テンプレートまでちゃんとサポートしようとしたら膨大な量の工数を要するのは火を見るよりも明らかである。

そして、通常の構文解析はソースコードを最初から最後まで解析するが、本件ではカーソル位置のみの解析で充分であり、このような解析は不要であると同時に無駄に処理能力と処理時間を消費してしまうと予想される。 よって、該当箇所のみを低負荷で解析できる、既存の構文解析器とは異なる構文解析方法を考え出す必要がある(と予想される)。

さらに、"UINT" など、特定のコンパイラが提供している型が使われている場合、 厳密に構文解析するにはそのヘッダも構文解析する必要があり、問題はさらに複雑になる可能性もある (テキストエディタはインクルードディレクトリに関する情報を知らない場合が普通である)。

型のメンバ名情報取得に関する課題

型が認識できたら、次はその型のメンバ名情報を取得しなくてはいけないが、 ユーザから動的補完が要求された時点で関連ソースファイル群を構文解析するのでは応答性が悪すぎてストレスになると予想される。 よって、あらかじめファイル群の構文解析を行い全ての型の情報を用意しておく必要があると考えられる。

さらに、編集中の C/C++ ソースファイルをいつどのように構文解析するかも重要な課題である。 解析時に待たされるのは嫌なのでマルチスレッドで裏でこっそり解析して欲しいところである。 また、1文字編集のたびに全体を再解析するのはCPUパワーの無駄遣い(そもそも解析が追いつかないと思われる)なので、 効果的なタイミングでの、再解析が必要な差分箇所のみの再解析を実現したいところである。 本件も、それほど容易な課題では無いように思われる。

解決方法

以上をふまえ ViVi 2.08 では以下の方法で問題を解決した。

型認識

式の全ての項の型を認識しようとすると難しいが、最初から欲張らずに目標を上手に絞れば難易度は格段に低くできる。

ViVi 2.08 では、シンボル-> または シンボル. 直後にカーソルがあり、シンボルが ローカル変数、仮引数、クラスメンバ変数、グローバル変数の場合のみ処理対象とすることにした。 シンボルとは「変数宣言があり名前がついている変数」のことである。型情報は変数宣言から容易に得ることができる。 シンボル以外の場合としては(「型認識に関する課題」で述べた)関数の戻り値に->.がついたfunc(x)->の形があり得るが、こちらは今回はサポート外として除外した。

シンボルの型だけを認識するのであれば、処理は比較的簡単で高速である。

教訓:困難なことはとりあえずやらない。出来ることからやる。明日できることを今日やるな。

型のメンバ名情報取得

まずは「解析のタイミング」についてだが、クラス/構造体のメンバ変数/関数宣言はそう頻繁に変更されるものではないので、編集のたびに情報を更新する必要はなく、文書保存時に構文解析すれば十分だと考えた。
次に「メンバ名情報の取得方法」だが、実は、
ctags.exe 等で生成される tags ファイルには、 クラス/構造体とメンバ名の情報が含まれている。それを参照して型のメンバ名情報を取得することができる。

ソースファイル:

"test1.h"
class CHoge
{
    int m_var;
    int func();
};

"test1.cpp"
int CHoge::func()
{
    return 0;
}

tags ファイルの内容:

CHoge   .\test1.h   /^class CHoge$/;"   c
func    .\test1.cpp /^int CHoge::func()$/;" f   class:CHoge
m_var   .\test1.h   /^  int m_var;$/;"  m   class:CHoge

上図の様に tags ファイルの各行は各シンボルの情報を記述するレコードになっている。

全レコードをスキャンし、末尾が“class:CHoge”のものを探せば、CHoge のメンバを発見できる。 (tags の全レコードを毎回スキャンするのは O(N) で非効率的だが、ViVi 程度の規模であれば、今日のCPUでは負荷にはならないようだ。 ViVi ソースは約18万行、tags は 14,784行、1,222,921バイト @2009年2月時点)

MFCなどのクラスライブラリもあらかじめ tags を作っておけばよい。

tags ファイルを参照する場合、編集中のソース群に対して陽に ctags を実行するのは面倒で、しばしば忘れがちだ。 しかし、2.08 ではプロジェクトのメンバファイルを保存した時などに tags ファイルを自動更新 する機能があるので、 面倒が無い。

教訓:面倒でむずいことは他人に任せる。

実装

本機能の処理手順は以下の通り

  1. カーソル位置直前の変数取得
  2. 簡易構文解析を行い、変数の宣言位置を取得
  3. 宣言部分を簡易構文解析し、変数の型を取得
  4. 取得した型を tags ファイルから検索し、マッチするメンバ変数名/関数名文字列取得
  5. 取得した補完候補文字列群をポップアップウィンドウに渡して、一覧表示
  6. 選択された文字列をカーソル位置に入力

非常に簡易な構文解析を行うアウトライン解析部分に手を入れて、変数を含むブロックを取得
そのブロックのみをもう少し詳しく構文解析
ローカル変数/仮引数だった場合は、その宣言部分を構文解析し、型をゲット
メンバ変数、グローバル変数だった場合は、変数を tags ファイルから検索し、型情報をゲット。

その型を tags ファイルから検索(レコードの最後が class:クラス名 or struct:構造体名)して メンバ変数名、メンバ関数名をゲット

このようにして実装したメンバ名動的補完機能実行時のスクリーンショットを下図に示す

図1. Ctrl + L で、"m" で始まるメンバ名を動的補完した場合

図2. Ctrl + L で、全てのメンバ名を動的補完した場合

図3. Ctrl + Alt + L で、型を考慮しない従来の動的補完を実行した場合

評価と問題点

終わりに

現状は実装を開始したばかり。2.10 以降で完璧なものに仕上げていく予定。

本件により局所的な構文解析結果やソース群のクラス/構造体情報を利用することが可能になったので、 それらの情報を利用したコード自動生成やリファクタリング機能なども順次実装していきたいと考えている。

謝辞

本稿の添削、レビュー、駄目だし、リライトは、某友人の工学院大学 山口実靖准教授にやっていただいた。
氏のおかげで箇条書きに毛の生えたような文章がすっかり格調高く正確で理解しやすいものになった。
卒論指導でお疲れにもかかわらず、大変にありがたい。 この場を借りて御礼申し上げる。今後もよろしく〜

更新履歴

09/01/30 Created
09/02/21 第1版