テキスト・エディタの interactive 引数指定の正規表現

GNU Emacs の編集コマンドは interactive 特殊形式を持つだけでなく、 この特殊形式で対話時に引数をどのように得るかを指定します。 この仕掛けは、 関数と編集コマンドの差異をなくす働きをします。 エディタの read-eval-print ループからは、 特殊形式を基に引数を作ってコマンドを呼び出します。 一方、 関数やコマンドからは、 この特殊形式を無視し、 コマンドをあたかも通常の関数であるかのように摘要に利用できます。

引数指定は文字列になっています。 その中で、 1 つの引数を 1 文字で指定します。 例えば、 コマンド・プレフィックスから数を得たいときは (interactive "p") と記述します。 ミニバッファを使って文字列を得たいときは s にプロンプトを続けて (interactive "sString ?") と記述します。 ミニバッファから値を得る引数指定のプロンプトを省略することはできません。 また、 プロンプト付き引数指定の後に他の引数指定を続けるときは、 1 つの改行で区切ります。

  (interactive "sString 1 ?\nsString 2 ?")

プロンプトが付かない引数指定も改行で区切ります。 現点 (d) とマーカー (m) の値を引数に受け取る指定は次のようにします。

  (interactive "d\nm")

ここでのローカルな規則の緩和として、 末尾に改行を付けても良いことにしましょう。

  (interactive "sString 1 ?\nsString 2 ?\n")

さらに、 パターンを簡単にするため、 プロンプトなしの引数指定文字 p と、 プロンプト付きの引数指定文字 s の 2 つだけが使えるものとしましょう。

  1. 空行を許す
  2. p にプロンプトを指定しない
  3. p の後に ps を続けるときは改行を 1 つ挟まなければならない
  4. s にプロンプトを指定しなければならない
  5. sプロンプト の後に ps を続けるときは改行を 1 つ挟まなければならない
  6. 末尾に改行があっても良い
  7. 改行を 2 つ以上並べることは禁止

このような規則にマッチする ruby正規表現は簡単なものです。

  INTERACTIVE_PATTERN = %r{
    \A (?: [p] | [s][^\n]+ ) (?: \n (?: [p] | [s][^\n]+ ) )* \n? \z
  }msx

テキスト・エディタの行の折り返し表示 その 4 - キャッシュ更新の改善

行の折り返し表示 その 3」 でテキスト・エディタで長い文書ファイルを開いても、 表示速度が極端に落ちないようにと、 行の折り返し表示のための書記素クラスタ情報をキャッシュすることにしました。 ところが、 キャッシュは良いことばかりではないようで、 キー入力による文字挿入・削除の応答性を改善するために速度を落としている箇所を探してみたところ、 キャッシュ追加時の each ループが原因になっていました。

#@<unshift@>=
  def unshift(value)
    # 問題部分
    each do |e|                                                     # !
      if e.value.last >= value.first && value.last > e.value.first  # !
        delete(e)                                                   # !
      end                                                           # !
    end                                                             # !
    super(value)
  end

このループは、 バッファが改行で終わるとき、 バッファ末尾の書記素クラスタ情報で line.first == line.last になっておりキャッシュに残っていた分を削除するのに必要でした。 他の場合は、 行内の場所 loc に対して、 line.first <= loc < line.last がなりたつので文字挿入・削除時にキャッシュから削除できていました。

改善前

  バッファ          書記素クラスタ情報
  "quick brown\n"   Lines.new(0, 12, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
  "fox jumps\n"     Lines.new(12, 22, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
  ""                Lines.new(22, 22, [0])

バッファ末尾を含めて、 すべての書記素クラスタ情報で、 line.first <= loc < line.last がなりたてば、 挿入・削除時にキャッシュから削除できる理屈で、 キャッシュ追加時の余計な重い each ループが不要になります。 そのためには、 擬似的にバッファ末尾を表す制御文字があるかのように書記素クラスタ情報を作れば良いわけです。 つまり、 バッファの正味の文字数は 22 文字なのですが、 擬似末尾制御文字を含めて書記素クラスタを 23 個とします。

改善後

  バッファ          書記素クラスタ情報
  "quick brown\n"   Lines.new(0, 12, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
  "fox jumps\n"     Lines.new(12, 22, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
  ""                Lines.new(22, 23, [0, 1])

改善後では、 バッファ末尾が改行で終わってないときも同様に考えて、 擬似的なバッファ末尾文字があるかのように書記素クラスタ情報を作ることにします。 この場合、 バッファの正味の文字数は 21 文字なのですが、 擬似末尾制御文字を含めて書記素クラスタを 22 個とします。

改善後

  バッファ          書記素クラスタ情報
  "quick brown\n"   Lines.new(0, 12, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
  "fox jumps"       Lines.new(12, 22, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

ギャップ・バッファの作る書記素クラスタ情報 Line は行の折り返しを考慮しません。 求めたいユーザ座標 loc を含む改行文字と改行文字の間を行として、 その中に含まれている書記素クラスタの開始ユーザ座標の相対値をリストアップします。 前のコードではギャップを挟むとき、 文字列に切り出してから書記素クラスタの開始位置を求めていたのですが、 今回はそこも改善し、 文字列切り出しなしで書記素クラスタをたどっていきます。 その場合、 書記素クラスタの途中にギャップが開いている可能性があるので、 ギャップの直前の書記素クラスタを、 ギャップ直後へ移動してから書記素クラスタをもう一度、 同じ文字からたどらせています。 ただし、 この改善による応答性への寄与は小さく、 上の each ループの除去ほどの効果はありません。

Line = Struct.new(:first, :last, :h)

class GapEditor
  GRAPHEME_CLUSTER = %r/\G\X/

  def grapheme_clusters_of_line(loc)
    left = find_first_in_backward("\n", loc, false)
    dol = find_first_in_forward("\n", loc, false)
    line = Line.new(left, dol + 1, [])
    right = (dol < size) ? dol + 1 : dol
    data = @data
    i = left
    if left < @gap_start && @gap_start < right
      gs = @gap_start
      while i < gs
        line.h << i - left
        i = GRAPHEME_CLUSTER.match(data, i).end(0)
      end
      gap_move(left + line.h.last)
      line.h.pop
      i = @gap_start
    end
    gap = right <= @gap_start ? 0 : @gap_end - @gap_start
    i, left_gap, right_gap = i + gap, left + gap, right + gap
    while i < right_gap
      line.h << i - left_gap
      i = GRAPHEME_CLUSTER.match(data, i).end(0)
    end
    while true
      line.h << right - left
      (line.h.last + line.first < line.last) or break
      right += 1
    end
    line
  end

  # 省略
end

末尾の特別扱いが不要になったので、 キャッシュへの追加時の削除が不要になり、 unshift は Cache クラスのものをそのまま使えるようになります。 キャッシュからの検索に使う条件式も素直になります。

class RangeCache < Cache
  def lookup(loc)
    each do |e|
      r = e.value
      if r.first <= loc && loc < r.last
        touch(e)
        return r
      end
    end
    nil
  end
end

Buffer クラスで、 書記素クラスタ情報 Line をキャッシュするやりかたは、 前のままで変化はありません。 キャッシュにあるときは、 それを使い、 ないときはギャップ・バッファに Line オブジェクトを作ってもらってキャッシュに追加します。

class Buffer
  def grapheme_clusters_of_line(loc)
    line = @line_cache.lookup(loc)
    if line.nil?
      line = content.grapheme_clusters_of_line(loc)
      @line_cache.unshift line
    end
    line
  end

#@<content_insert, content_delete@>

  # 省略
end

文字挿入・削除で、 以前はバッファの内容を更新してからキャッシュとマークを更新していたのですけど、 更新の順番が逆の方が、 デバッグ時の挙動の追跡がわかりやすいことに気がついたので、 逆にしています。

#@<content_insert, content_delete@>=
  def content_insert(dot, str)
    count = str.size
    screen.window_of_buffer_owner(self).each do |win|
      update_mark_insert(win.mark_list, dot, count)
      update_cache(win.layout.cache, dot, 0, count)
    end
    update_mark_insert(@mark_list, dot, count)
    update_cache(@line_cache, dot, 0, count)
    content.insert(dot.point, str)
    dot.point += count
    @revision += 1
    self
  end

  def content_delete(dot, count)
    count_abs = count.abs
    screen.window_of_buffer_owner(self).each do |win|
      update_mark_delete(win.mark_list, dot, count_abs)
      update_cache(win.layout.cache, dot, count_abs, -count_abs)
    end
    update_mark_delete(@mark_list, dot, count_abs)
    update_cache(@line_cache, dot, count_abs, -count_abs)
    content.delete(dot.point, count)
    @revision += 1
    self
  end

#@<update_cache@>

キャッシュの更新は前のままです。 そのままでもバッファ末尾のキャッシュを扱えるようになりました。 次のコードは dot.point をくくりだしただけで、 本質は前と同じです。

#@<update_cache@>=
  def update_cache(cache, dot, count1, count2)
    loc = dot.point
    cache.each do |e|
      if e.value.first <= loc + count1 && loc < e.value.last
        cache.delete(e)
      elsif e.value.first > loc
        e.value.first += count2
        e.value.last += count2
      end
    end
  end

Layout クラスが、 書記素クラスタ情報 Line から行を折り返して、 Grid を作ります。 Line の変更によって、 影響を受けるのは、 現在の Grid がバッファ末尾を含むことをチェックする箇所です。 今度は、 grid.last がバッファ・サイズを越えていることを調べるようにします。

class Layout
  def eos?()
    @grid.last > window.buffer.size
  end

#@<layout@>

  # 省略
end

書記素クラスタ開始位置配列から行の折り返し位置を求める layout クラスは前と同じで良いのですが、 バッファ末尾でも Line の開始位置配列が必ず 2 個以上になったことから、 末尾が 1 個のときを特別扱いしなくて済むようになります。

#@<layout@>=
  def layout(loc)
    tty = window.screen.tty
    buffer = window.buffer
    @grid = @cache.lookup(loc)
    if @grid.nil?
      tab_width = buffer.mode.tab_width
      width = window.size.x
      line = buffer.grapheme_clusters_of_line(loc)
      @grid = Grid.new(line.first, line.last, [[]])
      x = 0
      line.h.each_index do |j|
        loc = line.h[j] + line.first
        len = line.h[j + 1] - line.h[j]
        ch = loc < buffer.size ? buffer[loc] : END_BUFFER
        span = END_BUFFER == ch ? 0 \
             : "\n" == ch ? 0 \
             : "\t" == ch ? tab_width - (x % width) % tab_width
             : ch.ord < 0x20 ? 2
             : tty.wcwidth(ch.ord)
        if x % width + span > width
          x += width - x % width
          @grid.v << []
        end
        @grid.v[-1] << HBox.new(line.h[j], len, span)
        if span > 1
          (span - 1).times {|d| @grid.v[-1] << HBox.new(line.h[j], 0, -(d + 1)) }
        end
        break if END_BUFFER == ch || "\n" == ch
        if x % width + span >= width
          @grid.v << []
        end
        x += span
      end
      @cache.unshift @grid
    end
    self
  end

以上で、 キャッシュの更新を挿入・削除時に完了させることができるようになり、 キャッシュ追加時のループを削除することができました。 しないでも良い無駄な処理をしないで済ますようにデータを構成する大切さを体感できる事例でした。

テキスト・エディタのマルチ・バッファ

ここまでで、 マルチ・バッファにする下地は整っているので、 まずは、 落穂拾いから始めます。

テキスト挿入もしくは削除に伴い、 マークと行折り返しレイアウト・キャッシュを更新する目的で、 Window に buffer 属性を、 Buffer に window 属性を設けて相互リンクしていました。 Window に buffer 属性があるのは当然としても、 Buffer に window 属性が必要な理由は、 Buffer が更新するマークとキャッシュに Window に所属するものがあるためです。

ところが、 この方式では Window と Buffer の関連付けが変わるごとに、 その都度、 相互リンクのつじつま合わせをおこなわねばなりません。 例えば、 switch-to-buffer (C-x b) コマンドで Window が表示しているバッファを変更すると、 それまで表示していた Buffer の window 属性から Window 自身を取り除かなければなりません。 続いてこれから表示する Buffer の window 属性に window 自身を追加します。 これらは暗黙のうちにおこなわれるようにコーディングしてあるものの、 delete-window (C-x 0) コマンド等では、 メソッドを明示的に実行することで、 Window と Buffer の関連付けを切っていました。

予防処置として、 双方向リンクのつじつま合わせを明示的にコードに書き込むのは避けておきたいものです。 なぜならば、 メモリ・リークの原因の一つが双方向リンクの切断ミスだからです。 多少のオーバー・ヘッドで済むなら、 Window と Buffer の双方向リンクなしでマークとキャッシュの更新ができるように、 修正しておきましょう。

都合の良いことに、 今や、 すべての Window を Screen が保持しているので、 Buffer から Screen へリンクすることで、 循環リンクを作ることができます。 これを使うことで、 Buffer が更新したいマークとキャッシュを所有する Window を探すことが可能です。 また、 screen から window を削除するだけで、 自動的に screen → window → buffer → screen の循環リンクを切ることができ、 ゴミ集めが回収対象の window オブジェクトを検出できるようになります。

まず、 Screen クラスの initialize をブロックで初期化をおこなえるように変更し、 今度は、 最初の画面表示用の buffer と、 miniwindow 用の buffer をブロックで作るようにしました。 これにより、 buffer を作るには screen が必要で、 screen を初期化するのには buffer が欲しいという事情に対応しています。

class Screen < ScreenBase
  def initialize(tty)
    super(tty)
    @window_min_height = 4
    @tile = []
    buffer, miniwin_buffer = yield self
    @activated_window = @tile[0] = Window.new(self, buffer)
    @miniwindow = MiniWindow.new(self, miniwin_buffer)
  end

  def window_of_buffer_owner(buffer)
    each_window.select {|win| win.buffer.equal?(buffer) }
  end

  def each_window
    block_given? or return to_enum(:each_window)
    @tile.each {|win| yield win }
    @miniwindow.nil? or yield @miniwindow
    self
  end

#@<delete_window@>
end

Screen の delete_window メソッドで、 もはや相互リンクを切るメッセージを window オブジェクトへ送る必要がなくなりました。

#@<delete_window@>=
  def delete_window(window=nil)
    not one_window?() or return
    i = find_index_window(window) or return
    window = tile[i]                          #+
    # window = tile[i].buffer_release         #-
    if selected_window.equal?(window)
      select_window(next_window)
    end
    if i == tile.size - 1
      tile[i - 1].size.y += window.size.y
    else
      tile[i + 1].topleft.y = window.topleft.y
      tile[i + 1].size.y += window.size.y
    end
    tile.delete_at(i)
    self
  end

今度は Buffer から window 属性を削除して screen 属性を追加します。 window_raise メソッドが不要になったので撤去します。

class Buffer
  attr_reader :screen
  # attr_reader :window       #- 削除

  def initialize(screen, name)
    @name = name.dup
    @screen = screen
    # 省略
  end

#@<content_insert@>
#@<content_delete@>

  # def window_raise(win) ... end         #-
end

content_insert で window 属性の each を使っていた箇所を、 screen 属性を使って書き改めます。 修正箇所は 1 行だけです。 content_delete も同様に修正すれば良いので、 省略します。

#@<content_insert@>=
  def content_insert(dot, str)
    content.insert(dot.point, str)
    count = str.size
    screen.window_of_buffer_owner(self).each do |win|     #!
      update_mark_insert(win.mark_list, dot, count)
      update_cache(win.layout.cache, dot, 0, count)
    end
    update_mark_insert(@mark_list, dot, count)
    update_cache(@line_cache, dot, 0, count)
    dot.point += count
    @revision += 1
    self
  end

#@<content_delete@>=
  # 同上につき省略

WindowBase から Buffer との相互リンクを作る箇所を削除します。

class WindowBase
  attr_reader :screen, :buffer, :topleft, :size, :cursor
  attr_reader :dot, :start, :mark_list
  attr_reader :layout

  def initialize(screen, buffer)
    @screen = screen
    @buffer = buffer
    # @buffer.window << self                  #-
    # 省略
  end

  # 省略
end

Window から buffer_release を削除します。 現点と表示開始点をセーブして、 レイアウト・キャッシュを空にする部分は buffer= へ引き継ぎました。 バッファが表示されなくなってから、 再度表示されるとき、 以前の現点と表示開始点を覚えておくために、 Buffer にも dot 属性と start 属性があります。 なお、 他の window に表示中のバッファへに変えるときは、 一番上の window から現点と表示開始点を受け継ぎます。

class Window
  def buffer=(buf)
    a = (buf.nil?) ? [] : screen.window_of_buffer_owner(buf)
    if @buffer
      @buffer.dot.point = dot.point
      @buffer.start.point = start.point
      @buffer.start.revision = start.revision
    end
    layout.cache.clear
    @buffer = buf or return buf
    if a.empty?
      dot.point = @buffer.dot.point
      dot.revision = 0
      start.point = @buffer.start.point
      start.revision = @buffer.start.revision
    else
      dot.point = a.first.dot.point
      dot.revision = 0
      start.point = a.first.start.point
      start.revision = a.first.start.revision
    end
    buf
  end
end

予防処置はここまでです。 ここから、 マルチ・バッファのための機能追加に入ります。

表示中の window を screen が管理しているように、 開いている buffer を editor が管理しています。 Editor に buffer_list 属性があり、 表示・未表示を問わず、 buffer はここに入れます。 例外は screen.miniwindow の buffer です。 混乱を避けるため、 これだけは buffer_list に入れてません。

class OptionError < RuntimeError
end

class Editor
  attr_reader :screen, :buffer_list
  # 省略

  def initialize()
    @screen = nil
    @buffer_list = []
    # 省略
  end

#@<getopt@>
#@<run@>
#@<get_buffer@>
#@<get_buffer_create@>
#@<get_buffer_create_from_path@>
#@<kill_buffer@>
end

get_buffer は buffer_list から、 名前が name である buffer を探します。 同時に見つけた buffer を先頭に移動し、 バッファ操作コマンドが最近使った順で buffer を探すのに備えます。

#@<get_buffer@>=
  def get_buffer(name)
    i = buffer_list.find_index {|x| x.name == name } or return nil
    buffer = buffer_list[i]
    buffer_list.delete_at(i)
    buffer_list.unshift buffer
    buffer
  end

上の探索で見つからなかったとき、 名前が name の buffer を新しく作って buffer_list に追加するのが、 get_buffer_create です。 新しく作った buffer の filename 属性は空文字列にしてあります。

#@<get_buffer_create@>=
  def get_buffer_create(name)
    buffer = get_buffer(name)
    if buffer.nil?
      buffer = Buffer.new(screen, name)
      buffer.mode = mode
      buffer.isearch.mode = search_local
      buffer_list.unshift buffer
    end
    buffer
  end

switch-to-buffer (C-x b) コマンドはこれを使って記述します。 buffer_list にあるバッファ名を補完に使う前準備があるものの、 ミニバッファでの対話結果で得た名前のバッファを得て、 ウィンドウのバッファを切り替えます。

class SwitchToBuffer < Interactive
  def self.name() :switch_to_buffer end

  def edit(arg=nil)
    name_default, list = take_buffer_completion()
    prompt = (name_default == '') ? 'Switch to buffer : ' \
           : 'Switch to buffer (default %s): ' % [name_default]
    screen.read_string(prompt, '', completion: list) do |str|
      screen.minibuffer_pop()
      if str == ''
        str = name_default
      end
      if str != '' && str[0] != ' '
        buffer = editor.get_buffer_create(str)
        if ! window.buffer.equal?(buffer)
          window.buffer = buffer
        end
      end
    end
  end

#@<take_buffer_completion@>
end

補完用リストは、 Editor の buffer_list から buffer.name を抜き出してリストにしたものです。 その際、 空文字列の名前と空白で始まる名前を除外します。 さらに、 その buffer が未表示のとき、 最近使ったものを優先してバッファのデフォルト名として選択します。

#@<take_buffer_completion@>=
  def take_buffer_list()
    name_default = nil
    list = []
    editor.buffer_list.each do |buf|
      if buf.name && buf.name[0] != ' '
        list << buf.name
        if screen.window_of_buffer_owner(buf).empty?
          name_default ||= buf.name
        end
      end
    end
    [name_default || '', list.sort]
  end

get_buffer_create と同じですが、 名前をパス名から作るのが get_buffer_create_from_path です。 このメソッドは、 単に名前が付いている空の buffer を作るだけで、 ファイルの読み込みをおこないません。

#@<get_buffer_create_from_path@>=
  def get_buffer_create_from_path(path)
    filename = File.expand_path(path)
    name = Buffer.filename_friendly(filename)
    get_buffer_create(name)
  end

find-file (C-x C-f) コマンドはこれを使って記述します。 ミニバッファでファイル名を受け取ってから、 バッファを求め、 ファイルを読み込みます。 既に開いているときは、 GNU Emacs とは異なり、 既にあるバッファに切り替えます。

class FindFile < Interactive
  include FileInteractivable

  def self.name() :find_file end

  def edit(arg=nil)
    return read_file(arg) if String === arg
    read_file_name(1, 'Find file: ') do |str|
      screen.minibuffer_pop()
      buffer = editor.get_buffer_create_from_path(str)
      window.buffer = buffer
      if buffer.filename.empty?
        read_file(str)
      end
    end
  end
end

Buffer の特異メソッド filename_friendly は、 絶対パスからディレクトリを適度に取り除いて、 名前を作る関数です。

require 'pathname'
require 'fileutils'

class Buffer
  def self.filename_friendly(filename)
    abspath = Pathname.new(filename)
    curdir = Pathname.pwd
    homedir = Pathname.new(Dir.home)
    if abspath.ascend.any? {|x| x == curdir }
      abspath.relative_path_from(curdir).to_s
    elsif abspath.descend.any? {|x| x == homedir } \
        && curdir.descend.any? {|x| x == homedir }
      abspath.relative_path_from(curdir).to_s
    else
      filename
    end
  end
end

kill_buffer は buffer_list から名前が name である buffer を取り除きます。 取り除いた後、 buffer_list が空になってしまったときは、 UNTITLED の名称で get_buffer_create します。 さらに、 削除対象 buffer を表示中の window を削除していきます。 window が 1 つだけ残り、 それも buffer を表示しているときは、 buffer_list の先頭か UNTITLED の表示に切り替えます。 最後に、 バッファの内容を掃除して、 バッファの削除は終わりです。

#@<kill_buffer@>=
  def kill_buffer(name)
    i = buffer_list.find_index {|x| x.name == name } or return
    buffer = buffer_list[i]
    buffer_list.delete_at(i)
    if buffer_list.empty?
      get_buffer_create('UNTITLED')
    end
    screen.window_of_buffer_owner(buffer).each do |window|
      not screen.one_window? or break
      screen.delete_window(window)
    end
    if screen.one_window? && screen.activated_window.buffer.equal?(buffer)
      if buffer_list.empty?
        screen.activated_window.buffer = get_buffer_create('UNTITLED')
      else
        screen.activated_window.buffer = buffer_list.first
      end
    end
    buffer.kill!
    nil
  end

kill-buffer (C-x 0) コマンドはこれを使って、 バッファを削除します。 現窓のバッファ名をデフォルトにして、 buffer_list から補完してミニバッファからバッファ名を得るところは一緒です。 もしバッファが変更されていないときは、 上の kill_buffer を使って削除して終わりです。 変更されているときは、 引き続き、 y か n の入力を待ち、 y のときに、 上のメソッドを使って削除します。

class KillBuffer < SwitchToBuffer
  def self.name() :kill_buffer end

  def edit(arg=nil)
    _, list = take_buffer_completion()
    name_default = window.buffer.name
    prompt = 'Kill buffer (default %s): ' % [name_default]
    name = ''
    screen.read_string(prompt, '', completion: list) do |str|
      case minibuffer.control
      when 1
        name = ('' == str) ? name_default : str
        buf = editor.get_buffer(name)
        if buf.nil?
          screen.minibuffer_pop()
          screen.miniwindow.print('', 'Kill buffer not found %s.' % [name])
        elsif ! buf.changed?
          screen.minibuffer_pop()
          screen.miniwindow.print('', 'Kill buffer %s.' % [name])
          editor.kill_buffer(name)
        else
          minibuffer.control = 2
          minibuffer.miniwindow.print(
            'Kill buffer %s changed. y or n ? (default n): ' % [name], '')
          minibuffer.completion = nil
          minibuffer.confirm = 'yn'
        end
      when 2
        if str == ''
          str = 'n'
        end
        screen.minibuffer_pop()
        if 'y' == str
          editor.kill_buffer(name)
        end
      end
    end
  end
end

これでエディタの導入部を記述することができるようになりました。 まず、 argv を getopt で解釈して、 パス名とプラス記号オプションによる行番号指定の組を files 配列に並べます。 続いて、 tty を初期し、 screen を作ります。 Screen の new のブロックで、 @screen に設定して get_buffer_create で screen 属性を使えるようにしてから、 files 配列から buffer を作り、 ファイルを読み込みます。 この導入部ではコマンドラインに同じファイル名が重複していてもエラーとはせず、 2 番目以降を無視します。 buffer_list の先頭と、 miniwindow 用バッファをブロックの戻り値にして、 screen の初期化をおこなわせます。 その後は、 画面表示・キーボード入力・コマンド実行を繰り返します。

#@<run>=
  def run(argv)
    opt = getopt(argv)
    if ! opt[:error].empty?
      raise OptionError, "invalid option " + opt[:error].join(" ")
    elsif opt[:version] || opt[:help]
      puts "version %s" % [VERSION] if opt[:version]
      puts "craft of text editing on ruby-lang." if opt[:help]
      return 0
    end
    # @kill_ring = KillRing.new(kill_ring_max)
    XTerm.open('/dev/tty') do |tty|
      tty.raw do
        tty.alternate_buffer do
          Screen.new(tty) do |scr|
            @screen = scr
            opt[:files].reverse_each do |path, line_number|
              buffer = get_buffer_create_from_path(path)
              if buffer.filename.empty?
                buffer.read_file(path)
                if line_number > 0
                  buffer.dot.point = buffer.forward_line(0, line_number - 1)
                end
              end
            end
            if buffer_list.empty?
              get_buffer_create('UNTITLED')
            end
            [buffer_list.first,
             Buffer.new(screen, ' *miniwindow*').flush_undo]
          end
          screen.miniwindow.buffer.mode = minibuffer_local
          read_evaluate_print_loop(tty)
        end
      end
    end
    0
  end

現在、 コマンド・ライン引数のオプションはプラス記号による行番号指定だけです。 コメントの例のように、 ファイルごとに行番号を指定することができます。

#@<getopt@>=
  # $ ruby SCRIPT +100 foo bar +120 baz
  #=> {:files => [["foo", 100], ["bar", 0], ["baz", 120]]}
  def getopt(argv, opt={})
    opt[:help] = opt[:version] = false
    opt[:error] = []
    opt[:files] = []
    opt_plus = 0
    i = 0
    while i < argv.size
      if %r/^[+](\d+)$/ =~ argv[i]
        opt_plus = $1.to_i
      elsif '--help' == argv[i]
        opt[:help] = true
      elsif '--version' == argv[i]
        opt[:version] = true
      elsif %r/^[+-]/ =~ argv[i]
        opt[:error] << argv[i]
      else
        opt[:files] << [argv[i], opt_plus]
        opt_plus = 0
      end
      i += 1
    end
    opt
  end