rubyの単語補完を行うスクリプトとそれを呼び出すPeggyの拡張スクリプトを作りました

私はWindowsで動作するPeggyというエディタを使用していて、その拡張スクリプトとしてC++のインテリセンスや開いているファイル等からのdabbrevを行うmenucomplete.msという拡張スクリプトを作成しています。(スクリプトライブラリのページの84番です。)

menucomplete.msは、今のところインテリセンス的な機能はC++しかサポートしていないのですが、私がよく使うrubyでもインテリセンス的なことができたらいいなと常々考えていました。VimEmacsでは拡張機能で対応しているようですので、不可能ということはないと思うのですが、型推測をきちんとやるのは実際にはかなり難しいだろうと諦めていました。

ただ先日、難しい型推測をしなくても結構使い物になるのではないかと思いつき、作ってみた次第です。

rubyスクリプトとPeggyの拡張を行うためのMocaスクリプトはこちらです。
http://plk.sakura.ne.jp/store/complete.zip

使い方は今までのmenucomplete.msと全く同じです。ただし、同封されているsimple-complete.rbをmenucomplete.msと同じフォルダ(つまり、Anchor/share/scriptフォルダ)に置いてください。rubyスクリプトを編集しながらいろいろ試していただければ、今までとの違いがわかると思います。例えば以下のような補完が可能です。

ただ、後ほど解説しますが、このsimple-complete.rbというスクリプトは、補完対象のスクリプトがrequireしているファイルを自分もrequireしてから補完候補を探すという動作をします。つまり、requireとは言え、スクリプトを走らせることになりますので、requireが副作用を持つようなスクリプトを書いていないか、十分ご注意ください。

また、単語補完を行うsimple-complete.rbというスクリプトは、Peggyだけではなくエディタを問わずに利用できるようにしています。こちらについては、githubで公開しています。
http://github.com/ashel/ruby-simple-complete

以下、このsimple-complete.rbというスクリプトがどのような処理を行っているのか解説したいと思います。

コンセプト

simple-complete.rbはその名の通り、シンプルな単語補完を行うスクリプトです。なるべく難しいことをせずに、どういうスクリプトに対して使っても動くようにというコンセプトで作りました。270行くらいのスクリプトで、使う人がだいたい何をやっているか理解できるような規模のものになっていると思います。

また、実際の補完処理は、間違ってもいいから多めに候補を出すという方針でやっています。Peggyのようにインクリメンタルな補完ができるエディタであれば、候補が100以上になっても特に問題なく候補を選ぶ処理を行えるからです。

補完対象の語の分類

そもそも、rubyの単語補完とは、どのような語を対象とすべきなのでしょうか?simple-complete.rbでは、補完対象の語を幾つかのグループに分類しています。

クラス又はモジュールの定数

以下のような箇所で補完を行ったときは、ユーザはクラス又はモジュールの定数を補完しているものと考えることができます。(|はカーソル位置だと考えてください。)

  Math::|

Mathの定数はMath::PIMath::Eのどちらかしかありませんので、その二つが候補になればOKです。このような例は、rubyの単語補完の中で最も簡単な部類です。なぜかというと、型を推測する必要がなく、::の左側にあるクラス又はモジュールの定数を調べればよいためです。実際は、rubyでは::でモジュール関数やクラスメソッドを呼び出すこともできるのですが(例えばMath::sin(1)と書けます)、あまり一般的ではありませんので、simple-complete.rbでは::は定数を参照している、と見なしています。

クラスのクラスメソッド又はモジュールのモジュール関数

以下のような箇所で補完を行ったときは、ユーザはモジュール関数やクラスメソッドを補完しているものと考えることができます。

  File.r|

この場合は、File.readFile.readlinesが候補になればOKです。このような例も簡単な部類で、::の左側にあるクラス又はモジュールの特異メソッドを取得すればよいことになります。

インスタンスメソッド

以下のような箇所で補完を行ったときは、ユーザは何らかのクラスのインスタンスメソッドを補完しているものと考えることができます。

  obj.ma|

これは今までの例とは違って、objの型が何なのか推測するのが難しいです。従ってsimple-complete.rbでは、このような場合objの型が何であるかはを考えずに、何らかのクラスのインスタンスメソッドを呼び出しているのだろうと考えます。そして、定義されているクラス全てのインスタンスメソッドから、先頭がmaで始まるものを探して返します。

こんな適当な方法でまともに動作するのか、と思われる方もいるかもしれませんが、実は問題なく動作します。例えば、1.8.7の標準の状態では、maから始まるメソッドはmap,map!,match,max,max_byの5種類しかありません。これなら問題なく選ぶことができます。また、rubyのメソッド名を取得する機能はかなり高速に動作しますので、全てのクラスからインスタンスメソッドを取って来るという重そうな処理も現実的なスピードで完了します。

このように、間違ってもいいから多めに候補を出すという方針でやることによって、難しい処理を省くことができます。

グローバル変数

以下のような箇所で補完を行ったときは、ユーザは何らかのグローバル変数を補完しているものと考えることができます。

  $st|

この場合の処理は簡単です。グローバル変数を取得してその中からカーソルの右にマッチする文字列を返せばよいからです。候補は、$stderr,$stdout,$stdinとなるでしょう。

インスタンス変数

以下のような箇所で補完を行ったときは、ユーザは何らかのインスタンス変数を補完しているものと考えることができます。

  @a|

残念ながら、このようなケースに対処するのは非常に難しいです。rubyでは、インスタンス変数のリストを取得するには実際にそのクラスを作ってみなければいけないからです(具体的には、instance_variablesはObjectクラスのインスタンスメソッドになっている)。クラスを作ることによってどんな副作用が起こるかわかりませんので、simple-complete.rbではこのような場合は何もしていません。従来通りdabbrevに頼ることになります。

クラス変数

以下のような箇所で補完を行ったときは、ユーザは何らかのクラス変数を補完しているものと考えることができます。

  @@a|

インスタンス変数と異なり、クラス変数はclass_variablesメソッドを使ってクラスを作らなくても取得できます。simple-complete.rbでは、「現在定義されている全てのクラスのクラス変数を取得して、マッチする文字列を返す」という処理を行います。

トップレベルの語

以下のような箇所で補完を行ったときは、様々なケースが考えられます。

  w|

ユーザが補完したいのは、組み込み関数のwarnかもしれませんし、キーワードのwhile,whenかもしれません。もしくは、クラス内で補完を行った場合は、そのクラスのメソッドである可能性もあります。従って、simple-complete.rbではそれら全ての中からwで始まる語を抜き出して候補として返します。

候補の取得

補完対象の語がどのようなものかわかったら、実際に候補を取得する必要があります。候補の取得は、Module.constants等のrubyのリフレクション機能を用いて行えるのですが、問題なのは補完対象のスクリプトでどのようなクラスやメソッドが定義されているかわからないという点です。

クラスやメソッドの定義が増えるのは、そのスクリプト内で定義されたものを除けば、基本的には何らかの外部スクリプトをrequireしたときです。例えば

  require 'fileutils'

とすることによって、FileUtilsモジュールが追加されます。

従ってsimple-complete.rbは、補完対象のスクリプトのrequireの行を抜き出し、そこでrequireされているファイルを実際に自分でrequireするという方法を用いて、自分自身の状態が補完対象のスクリプトと同じになるようにします。

また、補完対象のスクリプト

class SampleTest < Test::Unit::TestCase

といった行があるとき、このスクリプトではTest::Unit::TestCaseを継承したクラスを定義していることになります。このようなクラスでは、Test::Unit::TestCaseやそれがインクルードしているモジュールのインスタンスメソッドがトップレベルで出てきます。(例えばこの場合、assert_equalなどが出てくる可能性があります。)そのため、simple-complete.rbはこのような行があった場合、定義しているクラスや継承しているクラスのメソッドをトップレベルの補完候補として追加します。

また、補完対象のスクリプト

  include FileUtils

のような行があった場合もトップレベルの挙動が変化します(FileUtilsの特異メソッドがトップレベルの候補になる)。simple-complete.rbはこのような行があった場合、自分自身でトップレベルにFileUtilsをincludeした状態で補完候補を探します。

dabbrevを行う

上のような候補の取得を行っても、補完対象のファイルで定義されているクラスや関数を拾うことはできません。そもそも編集中のファイルはコンパイルが通らないことが多いですし、実行するとファイルアクセスをしてしまったり、危険なことが起こりうるためです。

従ってsimple-complete.rbでは、補完対象のファイル内でdabbrev(動的略称展開)を行って、補完元の文字から始まる語を全て抜き出し、候補として加えます。この機能はエディタに組み込まれていることも多いかと思いますが、simple-complete.rbはそれだけで完結した機能を持つようにdabbrevを行っています。実際menucomplete.msでは、simple-complete.rbから受け取った結果に、現在開いている全てのファイルからのdabbrevも加えて候補として表示しています。

欠点

simple-complete.rbはある程度実用的に使うことができるのですが、幾つか欠点があります。

requireするだけで何かをしてしまう外部ライブラリに対応できない

requireして使うスクリプトは、それだけではクラス等を定義するだけで何もしないのが普通ですが、一部のライブラリはrequireするだけで処理を始めてしまいます。残念ながらtest/unitがこのような例にあたるため、現在のところsimple-complete.rbはtest/unitのrequireを特別扱いし、test/unit/testcaseのrequireに置き換えます。このような特殊対応を行うべき外部ライブラリが他にあるかもしれません。

rubygemsを使っているとスクリプトの動作が遅くなる

Windowsでrubygemsを使うとスクリプトの起動が遅くなるという問題があるのですが、simple-complete.rbはこの問題に思い切り引っかかってしまいます。もちろんマシンにもよると思うのですが、残念ながら私が使用してるパソコンでは、rubygemsをrequireするだけで0.5秒ほどかかってしまいます。rubygemsをrequireしていなければプログラム全体が0.05秒で終了しますので、requireにかかる時間が大きいことがわかります。

このため、残念ながら私は現在、simple-complete.rbの108行目と109行目の間に

  next if item == 'rubygems'

という一文を入れてrubygemsのrequireを避けています。rubygemsが必要なファイルのrequireはこれで全てエラーになり、エラーは無視されますので、プログラムとしては問題なく進みます。(ただし、もちろんrubygemsでrequireしているモジュールの補完はできなくなります。)環境が違えば、このようにrubygemsを避けなくても問題なく動くかもしれませんので、使用する環境に合わせて修正していただけたらと思います。

今後のこと

今のところ新しいmenucomplete.msはPeggyのスクリプトライブラリには登録していませんが、もう少し自分で使ってみて、問題がなさそうであれば登録したいと思います。また、RSenseというrubyの開発補助ツールが先日リリースされましたので、将来的にはこちらにも対応したいと思っています。(実はsimple-complete.rb自体が、RSenseを使ってみて、もっと賢くなくていいからシンプルな方法はないだろうか、と考えて作ったところがあります。)

こうした方がよい等の意見がありましたら、お気軽に@asheltwやコメント欄までご連絡ください。