UTF-8 エンコーディングのままでバッファ・ギャップ その2

前回のギャップ・バッファのギャップ移動を使って、 挿入・削除・置換を試してみます。

挿入では、 現点にギャップを移動します。 ギャップのバイト数が挿入したい文字列のバイト数よりも大きくなるように適宜バッファを grow メソッドで長くします。 そして、 ギャップ開始位置から挿入したい文字列と同じバイト数分を、 置換します。 バッファは文字数による座標なので、 バイト数分を置換すると、 見掛け上、 ギャップの大きさとバッファの大きさが変化することがあります。 この見掛けの変化分は、 置換した文字列のバイト数と文字数の差になるので、 それでギャップの終了位置とバッファの終了位置の両方を更新しておきます。

挿入後に、 現点は挿入した文字数分、 後ろへ進みます。 同じようにマークも後ろへ進むため、 現点を進める前にマークの点を進めておきます。 マークは画面の表示開始位置等を記録しているものもあります。 例えば、 画面の表示開始位置が折り返した長い行の途中で、 その行の表示開始前の位置に文字列が挿入されたときに、 マークに記録している点が行頭からずれることがあります。 そのため、 マークの位置が示す位置が信頼できなくなっていることを示すため、 マークにリビジョン番号をつけておき、 リビジョン番号が大きくなっていたら、 位置が信用できなくなっていることを判別できるようにしておきます。

ところで、 insert メソッドの第 1 引数 dot を現点であるマーク・オブジェクトへ束縛します。 現点はバッファ・オブジェクトではなく、 テキスト編集ウィンドウが所持します。 こうすることで、 一つのバッファを複数のウィンドウが共用し、 テキストの別の場所を同時に表示しながら編集できるようにします。 ウィンドウが所持する現点は、 バッファ・オブジェクトが管理しているマークでもあり、 あるウィンドウごしにユーザが insert メソッドで文字列をバッファに挿入すると、 他のウィンドウの現点を挿入に合わせて移動します。

Mark = Struct.new(:point, :revision)

  #  str.size==3 str.bytesize==7
  #          S                                 E         $
  #  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6
  #  |a|b|c|d| | | | | | | | | | | | | | | | | |h|i|j|k|l| | | | |
  #  |a|b|c|d|e e e|f f f|g| | | | | | | | | | |h|i|j|k|l| | | | |
  #  0 1 2 3 4     5     6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2
  #                        S                   E         $
  def insert(dot, str)  # dot: Mark, str: String -> Buffer
    if ! str.empty?
      gap_move(dot.point)
      grow(str.bytesize)
      @data[@gap_start, str.bytesize] = str
      @gap_start += str.size
      @gap_end += str.size - str.bytesize
      @data_end += str.size - str.bytesize
      @mark.each_value do |e|
        if e.point > dot.point
          e.point += str.size
          e.revision += 1
        end
      end
      dot.point += str.size
    end
    self
  end

grow メソッドは、 バッファのバイト数を倍々で増やしていき、 ギャップのバイト数が挿入したい文字列のバイト数より大きくします。 バッファを伸ばすために、 ここではギャップの終わりに空白列を挿入することにします。

  GAP_LOWER_LIMIT = 16

  def grow(delta)
    if @gap_end - @gap_start - delta < GAP_LOWER_LIMIT
      capacity = @data.bytesize
      gs, ge = @gap_start, @gap_end
      while ge - gs - delta < GAP_LOWER_LIMIT
        ge += capacity
        capacity += capacity
      end
      if capacity > @data.bytesize
        count = capacity - @data.bytesize
        @data.insert(@gap_end, ' ' * count)
        @gap_end += count
        @data_end += count
      end
    end
    self
  end

削除の引数 count の絶対値の文字数分を削除します。 count が正のときは現点から末尾側を削除し、 負のときは現点より先頭側を削除することにします。 末尾側の削除では現点は動かず、 先頭側の削除では削除した文字数分を現点が前へ移動します。 そこで、 count が負のときは、 原点をあらかじめ前へ動かし、 count の符号を正にしておきます。

削除では、 ギャップの終了位置を後へ動かす場合と、 開始位置を前へ動かす場合があります。

後ろへ動かす場合、 文字数で指定した分に相当するバイト数分終了位置を移動します。 そして、 削除した分と同じバイト数の空白列を挿入することで、 文字数の削除をおこないます。 そのとき、 データ終了位置もつじつま合わせのために書き直しておきます。

前へ動かす場合、 前へ動かすのは指定した count 個の文字数です。 この文字数分のバイト数に当たる分を空白で埋めておきます。 さらに、 ギャップの終了位置とバッファの終了位置の両方とも、 つじつま合わせで補正しておきます。

前へ向かっての削除と、 後ろの方の削除の両方によって、 見掛け上の文字数が変化します。 変化するのはマークの点も同じなので、 マークに記録している座標の補正をおこなっておきます。

  def delete(dot, count)
    if count > 0
      count = [count, self.size - dot.point].min
    elsif count < 0
      count = [-count, dot.point].min
      dot.point -= count
    end
    if count != 0
      if dot.point + count != @gap_start && dot.point != @gap_start
        gap_move(dot.point)
      end
      if dot.point == @gap_start
        #  count==3 t.bytesize==7
        #          S                   E                       $
        #  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4     5     6 7 8 9 0 1 2
        #  |a|b|c|d| | | | | | | | | | |e e e|f f f|g|h|i|j|k|l| | | | |
        #  |a|b|c|d| | | | | | | | | | | | | | | | | |h|i|j|k|l| | | | |
        #  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6
        #          S                                 E         $
        t = @data[@gap_end, count]
        @data[@gap_end, count] = ' ' * t.bytesize
        @gap_end += t.bytesize
        @data_end += t.bytesize - count
      else
        #  count==3 t.bytesize==7
        #                        S                   E         $
        #  0 1 2 3 4     5     6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2
        #  |a|b|c|d|e e e|f f f|g| | | | | | | | | | |h|i|j|k|l| | | | |
        #  |a|b|c|d| | | | | | | | | | | | | | | | | |h|i|j|k|l| | | | |
        #  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6
        #          S                                 E         $
        @gap_start -= count
        t = @data[@gap_start, count]
        @data[@gap_start, count] = ' ' * t.bytesize
        @gap_end += t.bytesize - count
        @data_end += t.bytesize - count
      end
      @mark.each_value do |e|
        if dot.point < e.point && e.point < dot.point + count
          e.point = dot.point
        elsif dot.point + count <= e.point
          e.point -= count
        end
      end
    end
    self
  end