Up and Down Quickly

github 上の wasavi の issue で、[cci]j[/cci] [cci]k[/cci] が重い というものがあるのだが、いくらなんでもそれらのキーの機能が重かったら即気が付くわけで、従ってまったく手元で再現せず手を付けられないでいた。

しかし、ふともしかして [cci]:set jkdenotative[/cci] してるんじゃないの? と聞いてみたところその通りであるらしい。なるほど。すべての謎が解けた。

本来 [cci]j[/cci] と [cci]k[/cci] は、改行までの 1 行(物理行)を単位としてカーソルを上下に移動させる。一方、[cci]gj[/cci] と [cci]gk[/cci] は、レンダリングされた結果における折り返し行を単位とする。ここで、もしユーザーが改行までの 1 行が十分に長いテキストを頻繁に編集するのなら、[cci]j[/cci] [cci]k[/cci] は折り返し行単位で移動したほうがカーソルの移動は圧倒的に楽である。

そういうわけで、[cci]jkdenotative[/cci] オプションは [cci]j[/cci] [cci]k[/cci] と [cci]gj[/cci] [cci]gk[/cci] の機能を交換するか否かを指定する。まあ [cci]:map[/cci] でもいいんだけど。

で。この折り返し行単位の移動が確かに重いのである。従って [cci]:set jkdenotative[/cci] とすると、[cci]j[/cci] [cci]k[/cci] でのカーソルの移動がめちゃ重くなるわけだ。

よしわかった。やってやろうじゃん。100 倍速くしてやる。

折り返し行単位でのカーソルの移動を行うためには、折り返し行のそれぞれの先頭位置が、物理行のどの位置に対応するかというマッピング情報が必要になる。wasavi における物理行はすなわち div 要素なのだけど、しかし DOM にはそのマッピング情報を得る手段がない。そういうわけで、かなり力技を使わざるを得ない。物理行のある位置 n が属する次の折り返し行の先頭の物理位置を得るには、n から物理行の末尾に向かってループする。ループ内では 1 文字ずつカーソル位置を span でくくり、getBoundingClientRect() を呼び出す。これが前回の呼び出し位置と top プロパティが異なっていればそれが折り返し行の区切りになる。従来のやり方はこれであり、そして、すんごく重い。getBoundingClientRect() が重い。そういうわけでできるだけこのメソッドの使用回数を抑えるか、あわよくばゼロにする必要がある。

getBoundingClientRect() が重いのは、それがページ全体における要素の位置を計算するからだと思う。display:static な要素の場合、その要素の前のすべてを再計算しないといけない(レンダリングの結果でそれぞれの要素がそれを保持していないんだろうか? という気もするが)。

それはそれとして、とにかく別の方法を使わなければならない。ループを用いるのは同じだが、先立ってカーソル位置をまず span でくくり、これを left とする。また、カーソル位置から物理行末尾までの文字列を別の span でくくり、これを right とする。

ループでは right の先頭を left の末尾へ追加するという処理を行い、その都度 left の offsetHeight をチェックし、ループ直前のそれよりも増えていれば、折り返し行の区切りに達したことになる。offsetHeight はその要素だけで再計算が完了するはずなので、getBoundingClientRect() よりは速い。

また、ループは 2 つのステージで構成するようにする。第 1 ステージでは、ループカウンタの増分は 1 ではなくループごとに 2 倍して、ガンガン先に進める。この場合、「次の」折り返し行を超えた先へ到達してしまう可能性があるので、そのようである場合(初期 offsetHeight + 1 行分の lineHeight よりも offsetHeight が大きければ)増分を 1/2 してやり直したりする微調整は必要。

いずれにしてもカーソル位置の次の折り返し行に達した場合は第 2 ステージへ移り、増分を 1 に固定して正確な折り返し行の区切り位置を見つけ出す。これによりオーダーは O(n) から O(log2 n) になる、はず。

やってみたところ、修正前は [cci]10j[/cci] するのに 1600ms ほどかかっていたものが、160ms 程度まで短くすることができた。うーんさすがに 100 倍速は無理だった。また、折り返し行の区切り位置を見つけ出すループに関しては、たとえば単純ループが 38 回かかるところを 14 回で済んでいる。これはちょっと多いが、第 1 ステージは 6 回程度で抜けているのでオーダーの計算としては合ってる。第 2 ステージは更に最適化できるかもしれない。

というわけで折り返し行単位のカーソル上下移動が実用的になったと思う。最初からそう組んどけよ! と言われればはいその通りですと言わざるを得ないのだが。

Leave a Reply

Your email address will not be published. Required fields are marked *