行の折り返し表示 その 2 - Line を求める

昨日の Layout は対象行の書記素クラスタの開始位置の配列 Line を使いました。 これを求めるのは Buffer の役目です。

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

# Line[first=200, last=211, h=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11]]
#                              |H |e |l |l |o |こ|ん|に|ち|わ|\n|
class Buffer
  def initialize()
    @content = GapEditor.new
    @revision = 0
    @window = []
    @line_cache = RangeLocationCache.new(64)
  end

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

  def slice(loc, *argv) @content.slice(loc, *argv) end
  alias :[] :slice

  def find_first_in_forward(str, loc)
    @content.find_first_in_forward(str, loc, false)
  end

  def find_first_not_in_forward(str, loc)
    @content.find_first_in_forward(str, loc, true)
  end

  def find_first_in_backward(str, loc)
    @content.find_first_in_backward(str, loc, false)
  end

  def find_first_not_in_backward(str, loc)
    @content.find_first_in_backward(str, loc, true)
  end
end

上記のように、 Buffer のメソッドはキャッシュをおこなうラッパーで、 Line を求めているのは GapEditor です。 ユニコードの書記素クラスタを確実に追いかけていくには行頭から始めるのがてっとり早いため、 ユーザ座標 loc を含む行の行頭 left と、 次の行の行頭 right を求めます。 行の途中にギャップが位置するときは行を別の文字列にとりだし、 そうでないときは GapEditor のバッファそのものを使って、 正規表現を使って書記素クラスタの開始位置を求めていきます。

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

  def grapheme_clusters_of_line(loc)
    left = find_first_in_backward("\n", loc, false)
    right = find_first_in_forward("\n", loc, false)
    right += 1 if right < size
    if right <= @gap_start || @gap_start <= left
      str = @data
      left1, right1 = left, right
      gap = right <= @gap_start ? 0 : @gap_end - @gap_start
    else
      str = slice(left, right - left)
      left1, right1 = 0, str.size
      gap = 0
    end
    line = Line.new(left, right, [])
    i = left1 + gap
    while i < right1 + gap
      line.h << i - left1 - gap
      i = GRAPHEME_CLUSTER.match(str, i).end(0)
    end
    line.h << right - left
    line
  end

#@<find_first_in_forward@>
#@<find_first_in_backward@>
#@<slice@>
end

find_first_in_forward は、 str に含まれる文字をユーザ座標 loc 以降から探して、 最初に見つかった文字の左側の境界のユーザ座標を返します。 ここでは、 C. Finseth の流儀にならってユーザ座標を文字間の境界にあてています。 もしも、 ユーザ座標 loc の右側の文字が str に含まれているときは、 loc は動きません。 同じ座標を返します。 もしも、 loc 以降に探したい文字が見つからなかったときは、 バッファ末尾のユーザ座標を返します。 これは inverse フラグが偽のときの動作で、 inverse を真にすると、 str に含まれない文字のユーザ座標を求めます。

#@<find_first_in_forward@>=
  #  find_first_in_forward("\n", x, false) => y
  #                 x                             y
  #     |q|u|i|c|k| |b|r|o|w|n| |f|o|x| |j|u|m|p|s|\n|o|v|e|r| |t|h|e|
  def find_first_in_forward(str, loc, inverse)
    not str.empty? or return loc
    gs, gap = @gap_start, @gap_end - @gap_start
    n = self.size
    while true
      if loc >= n
        return n
      elsif loc < gs
        return loc if str.include?(@data[loc]) ^ inverse
      else
        return loc if str.include?(@data[loc + gap]) ^ inverse
      end
      loc += 1
    end
  end

find_first_in_backward は上の逆方向版で、 str に含まれる最初に見つかった文字の右側の境界のユーザ座標を返します。 もしも、 ユーザ座標 loc の左側の文字が str に含まれているときは、 loc は動きません。 同じ座標を返します。 もしも、 loc 以前に探したい文字が見つからなかったときは、 バッファ先頭のユーザ座標ゼロを返します。 改行文字で検索をすると行頭を求めることができます。 inverse は上と同じ意味で、 真なら str に含まれない文字を探します。

#@<find_first_in_backward@>=
  #  find_first_in_backward("\n", x, false) => y
  #                  y                             x
  #     |q|u|i|c|k|\n|b|r|o|w|n| |f|o|x| |j|u|m|p|s|\n|o|v|e|r| |t|h|e|
  def find_first_in_backward(str, loc, inverse)
    not str.empty? or return loc
    gs, gap = @gap_start, @gap_end - @gap_start
    while true
      loc -= 1
      if loc < 0
        return 0
      elsif loc < gs
        return loc + 1 if str.include?(@data[loc]) ^ inverse
      else
        return loc + 1 if str.include?(@data[loc + gap]) ^ inverse
      end
    end
  end

slice は、ギャップ・バッファから部分文字列を取得します。 String クラスの同名のメソッドとは異なり、 負の値で位置を指定することができず、 ゼロ以上の値でユーザ座標を指定しなければなりません。 バッファ末尾を指定すると空文字列を返します。 バッファ末尾より大きな開始座標を指定すると nil を返します。 引数が 1 個の整数のときは、 そのユーザ座標の右側のコード・ポイントを 1 つ返します。

#@<slice@>=
  def slice(loc, *argv)
    if Integer === loc && argv.empty?
      b, d = loc, loc + 1
    elsif Integer === loc && 1 == argv.size && Integer === argv[0] && argv[0] >= 0
      b, d = loc, loc + argv[0]
    elsif Range === loc && argv.empty? && Integer === loc.first && Integer === loc.last
      b, d = loc.first, loc.last
      d += 1 if ! loc.exclude_end?
    else
      raise ArgumentError
    end
    0 <= b or raise ArgumentError
    b <= self.size or return nil
    d = self.size if d > self.size
    (b < self.size && b < d) or return END_BUFFER
    gap = @gap_end - @gap_start
    if d <= @gap_start
      @data[b ... d]
    elsif @gap_start <= b
      @data[b + gap ... d + gap]
    else
      @data[b ... @gap_start] + @data[@gap_end ... d + gap]
    end    
  end

  alias :[] :slice

その 3 - キャッシュに続く。