Craig Finseth は、 文字単位で現点を前後へ動かす関数を point_move としていました。 その関数は移動量を示す count を引数にとってました。 count が負のときは前方向へ現点を動かし、 正のときは後方向へ現点を動かしていました。 count の絶対値は移動量の文字数でした。 ところで、 文字とは何を指して文字とすれば良いのでしょうか。 自然な解釈では、 画面に表示されているグリフ 1 個になるのでしょう。 この場合、 表示されているグリフはバッファでは書記素クラスタに対応していると考えられます。 そのため、 point_move は count で指定した数の書記素クラスタの先頭座標分、 現点を前後に動かす関数だと考えれば良いのでしょう。
書記素クラスタの開始座標列は、 行の折り返し表示用に求めることができています。 それを point_move でも利用することにしましょう。 ところが、 行単位でしか座標列を求めることができないため、 行から行へ移動する手当てが必要です。 なお、 Finseth の point_move は現点を動かすのに対して、 この point_move は引数で指定したユーザ座標 loc から count 移動した先のユーザ座標を返します。
module CharInteractivable def point_move(loc, count) if count >= 0 grapheme_cluster_forward(loc, count) else grapheme_cluster_backward(loc, -count) end end #@<grapheme_cluster_forward@> #@<grapheme_cluster_backward@> end
grapheme_cluster_forward は、 ユーザ座標 loc から count 個後書記素クラスタのユーザ座標を求めて返します。 loc を含む行の書記素クラスタ開始座標列を line へ取り出して、 loc を探索します。 count 個後が line 中あるなら、 そこの座標に loc をセットします。 ないときは、 次の行へ移るために、 loc と count を更新します。
#@<grapheme_cluster_forward@>= def grapheme_cluster_forward(loc, count=1) while count > 0 && loc < current_buffer.size line = current_buffer.grapheme_clusters_of_line(loc) i = line.h.index {|x| loc <= line.first + x } if i + count < line.h.size loc = line.first + line.h[i + count] count = 0 else loc = line.last count -= line.h.size - i - 1 end end loc end
前方向版は grapheme_cluster_backward です。 loc を含む行の line を取り出し、 loc を探すところは同じです。 count 個前が line 中に見つかったときは探索は終わりです。 そうでないときは、 前の行へ移るために、 loc と count を更新します。 なお、 座標列 line の最後の要素は次の行の先頭座標にしてしまったのに対応して、 それのさらに一つ前へ移っておきます。
#@<grapheme_cluster_backward@>= def grapheme_cluster_backward(loc, count=1) while count > 0 && loc > 0 line = current_buffer.grapheme_clusters_of_line(loc) i = line.h.index {|x| loc <= line.first + x } if count <= i loc = line.first + line.h[i - count] count = 0 else loc = line.first count -= i if loc > 0 loc -= 1 count -= 1 end end end loc end
forward-char (C-f) コマンドを、 point_move を使って作ることができます。 手元のコードでは、 編集コマンドは関数オブジェクトの一種にしています。 ESC x でコマンドを指定するための名前がついており、 edit メソッドで、 編集を実行するようにしてあります。 edit の引数 arg へ数値引数が渡り、 引数で指定した分、 後方へ現点 window.dot を動かします。 count に負の数を指定したときは、 point_move のふるまいにより、 前方へ現点を動かすことになります。
class ForwardChar < Interactive include CharInteractivable def self.name() :forward_char end def edit(arg) count = digit_argument(arg) || 1 window.dot.point = point_move(window.dot.point, count) end end
backward-char (C-b) コマンドも同様に point_move を使って現点を前方へ動かします。
class BackwardChar < Interactive include CharInteractivable def self.name() :backward_char end def edit(arg) count = digit_argument(arg) || 1 window.dot.point = point_move(window.dot.point, -count) end end
文字削除にも、 point_move を使います。 現点の左側の文字を削除する delete-backward-char (DEL) コマンドでは、 point_move で数引数の個数分、 現点 window.dot から前方向にある座標を loc に求めて、 loc と 現点の間にある文字列をバッファから削除します。
class DeleteBackwardChar < Interactive include CharInteractivable def self.name() :delete_backward_char end def edit(arg) count = digit_argument(arg) || 1 loc = point_move(window.dot.point, -count) current_buffer.delete(window.dot, loc - window.dot.point) end end
現点の右側の文字を削除する delete-char (C-d) コマンドも、 point_move で数引数の個数分、 現点から後方向にある座標 loc を求め、 現点と loc の間にある文字列をバッファから削除します。
class DeleteChar < Interactive include CharInteractivable def self.name() :delete_char end def edit(arg) count = digit_argument(arg) || 1 loc = point_move(window.dot.point, count) current_buffer.delete(window.dot, loc - window.dot.point) end end
現点を後方の画面表示行へ動かす next-line (C-n) コマンドは、 Window の Layout を使って、 移動先を求めます。 Layout には move_line メソッドがあり、 表示行単位で移動できるようにしてあるので、 それを使います。 window の sticky_x 属性は、 ある桁からカーソル移動を始めたところ、 それよりも短い行やその桁に全角文字の右半分があるようなとき、 とりあえずカーソルを置ける位置へずらしますが、 またもや他の行に移ったときに同じ桁にカーソルを置けるなら、 その桁へカーソルを戻すために桁を控えておくための属性です。
class NextLine < StickyxInteractive def self.name() :next_line end def edit(arg) count = digit_argument(arg) || 1 count != 0 or return round_flag = current_buffer.mode.round_flag y, x = window.layout.move_line(window.dot.point, count).divmod(window.size.x) loc = y * window.size.x + window.retain_sticky_x(x) window.dot.point = window.layout.set_column(loc, round_flag) end end
逆に前方の画面表示行へ動かす previous-line (C-p) コマンドもほとんど同じになります。
class PreviousLine < StickyxInteractive def self.name() :previous_line end def edit(arg) count = digit_argument(arg) || 1 count != 0 or return round_flag = current_buffer.mode.round_flag y, x = window.layout.move_line(window.dot.point, -count).divmod(window.size.x) loc = y * window.size.x + window.retain_sticky_x(x) window.dot.point = window.layout.set_column(loc, round_flag) end end
折り返しを考慮しないバッファ内で行単位の移動をおこなうには、 forward-line コマンドを使います。 このコマンドは、 改行を行の区切り記号として count が正のときは後方へ、 負のときは前方へ現点を動かします。 動かした後、 行頭に現点を置きます。
class ForwardLine < Interactive include LineInteractivable def self.name() :forward_line end def edit(arg) count = digit_argument(arg) || 1 window.dot.point = forward_line(window.dot.point, count) end end
forward-line コマンドの本体の forward_line メソッドは、 Line 構造体を使わずに、 改行コードを探すやりかたで書いています。 forward_line の使い道は数行内の移動か、 画面表示高を越える大きな移動のどちらかで、 キャッシュしている Line をごっそり入れ替えてしまうことがあるためです。
module LineInteractivable def forward_line(loc, count) if count > 0 count.times { loc = [current_buffer.find_first_in_forward("\n", loc) + 1, current_buffer.size].min loc < current_buffer.size or break } else loc = current_buffer.find_first_in_backward("\n", loc) count.abs.times { loc > 0 or break loc = current_buffer.find_first_in_backward("\n", loc - 1) } end loc end def beginning_of_line(loc) current_buffer.find_first_in_backward("\n", loc) end def end_of_line(loc) current_buffer.find_first_in_forward("\n", loc) end end
beginning-of-line (C-a) コマンドで行の先頭へ現点を動かすには、 上の beginning_of_line を使います。
class BeginningOfLine < Interactive include LineInteractivable def self.name() :beginning_of_line end def edit(arg) window.dot.point = beginning_of_line(window.dot.point) end end
end-of-line (C-e) コマンドで行の末尾へ現点を動かすには、 上の end_of_line を使います。
class EndOfLine < Interactive include LineInteractivable def self.name() :end_of_line end def edit(arg) window.dot.point = end_of_line(window.dot.point) end end
scroll-up (C-v) コマンドで次画面へ画面開始点を動かすのにも、 layout の move_line を使います。 画面開始点が画面の左端になるように通算桁を選ぶのが next-line との違いです。 また、 画面移動に伴い現点が画面外に出たときは、 画面内に移動します。
class ScrollUp < StickyxInteractive def self.name() :scroll_up end def edit(arg) count = digit_argument(arg) count ||= window.size.y - current_buffer.mode.next_screen_context_lines - 1 count != 0 or return count > 0 or return editor.edit(:scroll_down, -count) x = window.layout.get_column(window.dot.point) % window.size.x window.layout.framer() y = window.layout.move_line(window.start.point, count) / window.size.x window.start.point = window.layout.set_column(y * window.size.x, false) if window.dot.point < window.start.point round_flag = current_buffer.mode.round_flag loc = y * window.size.x + window.retain_sticky_x(x) window.dot.point = window.layout.set_column(loc, round_flag) end end end
逆方向の scroll-down (ESC v) コマンドでは、 画面の外に現点が出てしまったとき、 画面の下の行へ現点を移動するので、 画面の最下行を求めなければなりません。
class ScrollDown < StickyxInteractive def self.name() :scroll_down end def edit(arg) count = digit_argument(arg) count ||= window.size.y - current_buffer.mode.next_screen_context_lines - 1 count != 0 or return count > 0 or return editor.edit(:scroll_up, -count) window.layout.framer() x = window.layout.get_column(window.dot.point) % window.size.x y = window.layout.move_line(window.start.point, -count) / window.size.x window.start.point = window.layout.set_column(y * window.size.x, false) y = window.layout.move_line(window.start.point, window.size.y - 2) / window.size.x round_flag = current_buffer.mode.round_flag loc = y * window.size.x + window.retain_sticky_x(x) bottom_point = window.layout.set_column(loc, round_flag) if window.dot.point > bottom_point window.dot.point = bottom_point end end end
表示中の端末画面内で、 上から何行目、 もしくは下から何行目へと、 表示行の先頭へ現点を動かすのが move-to-window-line (ESC r) コマンドです。 このコマンドは表示開始点を動かさずに、 現点を移動します。 これも Layout を使って書くことができます。 数引数の指定がないときは GNU Emacs にならって、 画面の中央の行の行頭へ現点を動かします。 数引数がゼロ以上のときは画面の上からの行位置を指定し、 画面上端の行をゼロとします。 負のときは画面の下からの行位置を指定子、 画面下端の行を -1 とします。 ただし、 バッファ末尾を表示しているときは、 画面下端ではなく、 バッファ末尾の行を -1 とします。
class MoveToWindowLine < Interactive def self.name() :move_to_window_line end def edit(arg) count = digit_argument(arg) count ||= (window.size.y - 1) / 2 if count < 0 count = window.size.y + count - 1 end count = count < 0 ? 0 : count > window.size.y - 2 ? window.size.y - 2 : count window.layout.framer() y = window.layout.move_line(window.start.point, count) / window.size.x window.dot.point = window.layout.set_column(y * window.size.x, false) end end
画面上での行位置を数引数で指定するのが同じでも、 move-to-window-line コマンドとは逆に、 現点を動かさずに表示開始行を動かすのが、 recenter (C-l) コマンドです。 なお、 手元のコードでは、 window.redisplay が、 画面内にバッファ末尾があるとき、 強制的に画面開始点をセットしてしまうため、 バッファ末尾を表示中のときは、 recenter は単なる画面の書き直しをするためだけのコマンドとしてふるまいます。
class Recenter < StickyxInteractive def self.name() :recenter end def edit(arg) arg or screen.clear count = [4] == arg ? nil : digit_argument(arg) count ||= (window.size.y - 1) / 2 if count < 0 count = window.size.y + count - 1 end count = count < 0 ? 0 : count > window.size.y - 2 ? window.size.y - 2 : count round_flag = current_buffer.mode.round_flag y = window.layout.move_line(window.dot.point, -count) / window.size.x window.start.point = window.layout.set_column(y * window.size.x, round_flag) end end