テキスト・エディタのミニバッファ

ミニバッファは、 コマンド実行に必要な文字列を利用者との対話によって得たいときに使います。 普段、 キーボード入力は 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