ミニバッファは、 コマンド実行に必要な文字列を利用者との対話によって得たいときに使います。 普段、 キーボード入力は Window につながっています。 それを一時的にミニバッファが譲り受けて、 対話が終わると、 Window へ返却します。 キーボード入力のつなぎ先は Screen の activated_window 属性で指定します。 MiniBuffer が必要になったとき、 インスタンスをその都度作成して、 この属性にセットし、 不要になった時点でキーボード入力を返却してインスタンスを廃棄します。 そこは定型処理なので、 Screen の read_string と minibuffer_pop メソッドにまとめてあります。
class Screen def read_string(prompt, initial, completion:nil, confirm:nil, &blk) activated_window_push(MiniBuffer.new(activated_window)) activated_window.completion = completion activated_window.confirm = confirm activated_window.ok(&blk) if block_given? activated_window.restart(prompt, initial) end def minibuffer_pop() activated_window.completion = nil activated_window.completion_table = nil activated_window_pop() end end
文字列補完の例が典型的な read_string の使い方です。 文字列補完のためのリストを作成して completion に渡すとそれを使った補完が働きます。 confirm を "yn"
のように指定すると、 y か n の一文字入力で文字列入力を終了させるようになります。 read_string にブロックを渡すと ok コールバックにします。 ok コールバックは RET キーや confirm の文字で入力が終わる都度に呼び出されます。 そして、 プロンプトと文字列の初期設定値を、 restart に渡します。
なお、 手元のコードでは、 未だに GNU Emacs の interactive 特殊形式の機能を実装しておらず、 それぞれのコマンドが read_string でミニバッファを作った対話を進める書き方をしています。 interactive 特殊形式に倣うと、 コマンド実行に必要な文字列引数を Editor がコマンド実行前にミニバッファを使って読み取ってから、 Interactive に引数として渡すようになるのでしょう。 その方が、 コマンドと関数の差が埋まって都合が良いので、 いずれは interactive 特殊形式を取り入れて、 コマンドの書き方を変更する予定です。
MiniBufferBase クラスは、 これの派生クラスになる MiniBuffer と ISearchDialog に共通するメソッドを抜き出したものです。 lookup_key は、 @auto_exit_minibuffer が真のとき、 キー・シーケンスが元のウィンドウで解釈可能な場合に自動的にミニバッファを完了させる機能があります。 ISearchDialog が使います。 @inactive 属性は、 真のときカーソル表示を tail ウィンドウで、 偽のとき miniwindow でおこないます。 inactive 属性も ISearchDialog のための機能です。
class MiniBufferBase attr_reader :tail def initialize(window) @tail = window @inactive = false @auto_exit_minibuffer = false end def lookup_key(&blk) command = buffer.mode.keymap.match(screen.tty, &blk) if @auto_exit_minibuffer && command.name.nil? command.unmatch(screen.tty) command = window.lookup_key(&blk) if ! command.name.nil? exit_minibuffer() end end command end def activate() if @inactive tail.activate() else miniwindow.activate() end end def minibuffer?() true end def screen() tail.screen end def miniwindow() tail.screen.miniwindow end def last_command=(x) nil end def release_sticky_x() nil end def layout() window.layout end def dot() window.dot end def guess_key(command) nil end def print_key(command) nil end def unbound(command) nil end def before_execute_command(command) nil end end
MiniBuffer のメソッドの半数は文字列補完で説明が済んでいます。
class MiniBuffer < MiniBufferBase include Completable attr_accessor :control, :confirm attr_accessor :completion, :completion_table def initialize(window) super # 省略 @control = 1 @confirm = nil @ok_callback = lambda {|x| screen.activated_window_pop() } @cancel_callback = lambda { nil } end def ok(&blk) @ok_callback = blk; self end def cancel(&blk) @cancel_callback = blk; self end #@<buffer と window@> #@<restart@> #@<exit_minibuffer@> #@<abort_recursive_edit@> # 省略 end
MiniBuffer では miniwindow を window として扱います。 buffer も miniwindow のバッファとします。 こうすることで、 MiniBuffer のキーボード入力によって編集がおこなわれる箇所が miniwindow のバッファへ切り替わります。
#@<buffer と window@>= def buffer() miniwindow.buffer end def window() miniwindow end
Screen の read_string は MiniBuffer オブジェクトを作成し、 文字列補完用のリスト等を登録してから、 MiniBuffer に restart メッセージを送ります。 これによってプロンプトと文字列を MiniWindow にセットし、 control 属性を 1 にします。
#@<restart@>= def restart(prompt, initial) miniwindow.print(prompt, initial) @control = 1 end
MiniBuffer で利用可能なコマンドは miniwindow のバッファの mode に設定してあります。 それらのコマンドは特に設定していなくても、 tail のバッファから区別せず、 miniwindow のバッファの編集をおこないます。 ただし、 いくつかは特別扱いが必要なので、 外に切り出してあります。
切り出した筆頭は self-insert-command 等が使う self_insert メソッドです。 @confirm が nil だと、 バッファへ挿入します。 nil でないときは、 バッファに挿入してあから ok コールバックを呼びます。
#@<insert_char@>= def insert_char(ch, count) if @confirm.nil? buffer.insert_char(dot, ch, count) elsif @confirm.include?(ch) buffer.insert_char(dot, ch, count) @ok_callback.call(buffer.to_s) end end
@confirm が nil ときに RET キーを押すと、 ok コールバックを呼びます。 このコールバックは、 @confirm が nil でないときに登録されたキーを押しても呼び出します。
#@<exit_minibuffer@>= def exit_minibuffer() @ok_callback.call(buffer.to_s) end
どのようなときであっても、 ミニバッファで abort-recursive-edit (C-g) コマンドを実行すると、 現在の編集内容を捨てて、 元のウィンドウへ戻ります。 その途中、 cancel コールバックを呼びます。
#@<abort_recursive_edit@>= def abort_recursive_edit() @cancel_callback.call @completion = nil @completion_table = nil miniwindow.clear screen.activated_window_pop() end