テキスト・エディタのウィンドウ

Window は WindowBase の派生クラスです。 同じ WindowBase の派生クラス MiniWindow との違い、 MiniWindow が画面出力専用であるのに対し、 Window はコマンド実行による編集作業を補助するメソッドをいくつか備えています。 また、 モード行をもっており、 redisplay でモード行の描画をおこないます。

class Window < WindowBase
  attr_accessor :last_command

  def initialize(screen, buffer)
    super
    @sticky_x = nil
    @last_command = false
  end

  def modeline_height() 1 end

#@<redisplay@>
#@<display_mode_line@>
#@<activate@>
#@<hilite_matching_parenthesis@>
#@<activate_cursor@>
#@<lookup_key@>
#@<guess_key@>
#@<print_key@>
#@<before_execute_command@>
#@<unbound@>
#@<insert_char@>
#@<keyboard_quit@>
#@<copy@>
#@<buffer_release@>
#@<buffer=@>
#@<stichy_x@>
end

メソッドは大きく 4 つに分かれます。

  1. Screen からの redisplay メッセージによって再描画をおこなうもの
  2. Editor からの activate メッセージによってカーソルを置くもの
  3. Editor からの lookup_key メッセージによって Command を読みこむもの
  4. Interactive からのコマンド実行過程で呼ばれるもの

これらはすべて Editor のコマンド実行ループの中から順に実行されていきます。 キーボード入力につながるウィンドウは 1 度に 1 つに限り、 Screen の activated_window 属性で示しています。 再描画はすべてのウィンドウに対しておこないますが、 その後のカーソル表示・コマンド入力までは activated_window が担当します。

class Editor
  def read_evaluate_print_loop(tty)
    screen.clear
    while live?
      if ! tty.ready?
        screen.redisplay                                        # 再描画
        screen.refresh
        screen.activated_window.activate                        # カーソル表示
      end
      @command = screen.activated_window.lookup_key {|x|        # キー入力
        tty.ready? or screen.activated_window.guess_key(x)      # 途中表示
      }
      tty.ready? or screen.activated_window.print_key(command)  # 完了時表示
      edit(command.name, command.arg)                           # コマンド実行
    end
    nil
  end
end

バッファからの再描画は Layout オブジェクトにまかせます。 その途中で、 ウィンドウ表示域にバッファ末尾を表示しているかどうを示す bottom_flag が求まります。 モード行の表示でそれを使います。

#@<redisplay@>=
  def redisplay()
    layout.framer()
    bottom_flag = layout.frame_end_of_buffer()
    layout.display_page()
    display_mode_line(bottom_flag)
  end

モード行は、 バッファが変更されているかどうか、 バッファの名前、 バッファのどこを表示しているかを整形して表示します。 モード行を反転表示させたくなったので、 暫定的に modes を Screen に新設し、 SGR のコードを指定できるようにしています。 バッファ名は文字列なので、 何桁で表示可能かを書記素クラスタに分解し、 wcwidth で書記素クラスタの幅を求めています。

#@<display_mode_line@>=
  def display_mode_line(bottom_flag)
    pos = (start.point.to_f / (buffer.size + 1).to_f * 100.0).to_i
    t1 = (buffer.changed?) ? '--**- ' : '----- '
    t3 = '(%s) ' % [buffer.mode.name]
    t3 += (0 == start.point && bottom_flag) ? '----ALL-------' \
       : 0 == start.point ? '----TOP-------' \
       : bottom_flag ? '----BOT-------' \
       : '----%2d%%-------' % [pos]
    width = size.x - t1.size - t3.size
    a = buffer.name.grapheme_clusters
    i, w = 0, 0
    t2 = ''
    while i < a.size
      span = screen.tty.wcwidth(a[i].ord)
      w + span <= width or break
      t2 << a[i]
      w += span
      i += 1
    end
    screen.lines[topleft.y + size.y - 1] = t1 + t2 + ' ' * (width - w) + t3
    screen.modes[topleft.y + size.y - 1] = 7
  end

Layout で display_page すると cursor 属性に現点の右側の文字を置いた位置が入ります。

#@<activate@>=
  def activate()
    hilite_matching_parenthesis()
    activate_cursor()
  end

そこに移動する前に、 現点の左側が閉じ括弧なら、 対応する開き括弧が画面に表示されている場合に限り、 そこへカーソルを短時間動かし、 括弧の対応を示します。

#@<hilite_matching_parenthesis@>=
  def hilite_matching_parenthesis()
    loc = buffer.find_left_parenthesis(start.point, dot.point) or return
    left = buffer[loc]
    y, x = layout.get_window_yx(loc)
    screen.tty.set_cursor(y, x).notice.print(left).flush
    sleep(0.3)
    screen.tty.set_cursor(y, x).normal.print(left).flush
  end
end

それから、 現点に対応する位置にカーソルを置くエスケープ・シーケンスを出力します。

#@<activate_cursor@>=
  def activate_cursor()
    screen.tty.set_cursor(cursor.y + topleft.y, cursor.x + topleft.x).flush
  end

カーソルを移動したら、 今度はキーボードから入力があるまで待ち、 コマンドが決まるまでキーを入力していきます。 コマンドはバッファの mode に登録済みの keymap を使って決定します。

#@<lookup_key@>=
  def lookup_key(&blk)
    buffer.mode.keymap.match(screen.tty, &blk)
  end

コマンドが決まるまで途中でキー入力待ちをするときは、 その都度、 入力済みのキー・シーケンスをエコー領域へ書いてして端末画面へ同期します。 そして、 カーソルをウィンドウの所定の位置へ戻します。

#@<guess_key@>=
  def guess_key(command)
    print_key(command)
    screen.redisplay()
    screen.refresh()
    activate_cursor()
  end

さらにコマンド決定後にも、 エコー領域にキー・シーケンスを書きます。 端末画面への同期はコマンド実行後におこなわれるので、 ここではバッファへ書き込むだけです。 ただし、 一文字挿入する文字をいちいちエコー領域に表示していくと見た目がうるさく感じたので、 数引数をつけてないときは表示しないようにしています。

#@<print_key@>=
  def print_key(command)
    if command.name == :self_insert_command && command.arg.nil?
      screen.miniwindow.clear()
    else
      screen.miniwindow.print('', command.to_s)
    end
  end

ここまでは Editor がコマンドを決定するまでの補助機能です。 ここからは、 コマンドを実行する Interactive が利用する機能に変わります。 すべての Interactive はコマンド実行前にキーボード入力がつながっているウィンドウもしくはミニバッファのコマンド実行前のフック・メソッドを呼びます。 Window ではコマンド実行前におこなうことはありません。

#@<before_execute_command@>=
  def before_execute_command(command)
    nil
  end

バッファの mode にキーが割り当てられていないとき、 もしくはキーが割り当ててあるコマンド名がつけてある Interactive オブジェクトが mode に登録されていないときは、 未登録を表すコマンド unbound を実行します。

class Unbound < Interactive
  def self.name() :unbound end

  def edit(arg)
    window.unbound(editor.command)
  end
end

unbound コマンドは activated_window の unbound メソッドを呼びます。 Window では、 エコー領域に未登録のメッセージを表示します。

#@<unbound@>=
  def unbound(command)
    screen.miniwindow.print('', '(Unbound %s)' % [command])
  end

コマンド自身になっている印字可能文字をバッファに挿入するのは self-insert-command です。

class SelfInsertCommand < Interactive
  def self.name() :self_insert_command end

  def edit(arg)
    count = digit_argument(arg) || 1
    ch = editor.command.string
    window.insert_char(ch, count)
  end
end

このコマンドはキーボード入力をつないでいるウィンドウの insert_char メソッドを呼びます。 Window では、 このメソッドにより、 buffer への文字挿入をおこないます。

#@<insert_char@>=
  def insert_char(ch, count)
    buffer.insert_char(dot, ch, count)
    screen.miniwindow.clear()
  end

keyboard-quit (C-g) コマンドは、 エコー領域をクリアするだけで、 他には何もしません。

#@<keyboard_quit@>=
  def keyboard_quit()
    screen.miniwindow.clear()
  end

split-window-below コマンドで上下にウィンドウを分割するとき、 下になる Window をコピー処理で作成します。

#@<copy@>=
  def copy()
    win = Window.new(screen, buffer)
    win.dot.point = dot.point
    win.start.point = start.point
    win
  end

kill-window コマンド等でウィンドウを削除する前にバッファとのつながりを断ちます。 バッファはウィンドウ・オブジェクトを覚えているので、 バッファにウィンドウを忘れさせます。 また、 バッファがどこにも表示されなくなった段階の現点と表示開始点を記録しておきます。

#@<buffer_release@>=
  def buffer_release()
    layout.cache.clear
    buffer.window_delete(self)
    if buffer.window.empty?
      buffer.dot.point = dot.point
      buffer.start.point = start.point
    end
    @buffer = nil
    self
  end

ウィンドウとバッファは循環参照しているので、 バッファからウィンドウへの参照も削除しておきます。

class Buffer
  def window_delete(win)
    window.delete_if {|item| item.equal?(win) }
    self
  end
end

switch-to-buffer コマンドや find-file コマンド等はバッファを置き換えます。 そのとき、 前のバッファは buffer_release でつながりを断つ必要があります。 新しくつなぐバッファがどこにも表示されていなかったときは window 属性が空になっています。 その場合、 バッファに記録しておいた以前にウィンドウに表示されていたときの現点と表示開始点に戻します。 新しくつなぐバッファが既に表示しているときは、 選択中のウィンドウの現点と表示開始点を受け継ぎます。

#@<buffer=@>=
  def buffer=(buffer)
    buffer_release()
    @buffer = buffer
    if buffer.window.empty?
      dot.point = buffer.dot.point
      dot.revision = 0
      start.point = buffer.start.point
      start.revision = buffer.start.revision
    else
      dot.point = buffer.window.first.dot.point
      dot.revision = 0
      start.point = buffer.window.first.start.point
      start.revision = buffer.window.first.start.revision
    end
    buffer.window << self
    buffer
  end

next-line コマンドや previous-line コマンドは、 移動先の行の長さが足りなかったり、 全角文字の右半分に移動しようとしたりするときに、 カーソルを横にずらします。 ずれているときからさらに先に進み、 移動を開始したときと同じ桁にカーソルを置くことができるときは、 元の桁へ戻ります。 このように移動開始時の桁を覚えておくのが stichy_x 属性です。 この属性は桁を覚えておく必要がないときは nil になっていて、 覚えているときは Numeric になっています。 そのように属性値をセットする用途に、 2 つのメソッドを Interactive クラスへ提供します。

#@<stichy_x@>=
  def retain_sticky_x(x)
    @sticky_x ||= x % size.x
  end

  def release_sticky_x()
    @sticky_x = nil
  end