number of users

Chrome 版の wasavi についてはユーザー数をストア上のページで知ることができる。

いままでたまに調べる程度ではだいたいユーザー数は 300 人とかそんなオーダーだったのだが。

今見てみたら 3,581 人いるというのである。なにそれこわい。前にも書いた気がするがユーザー数が増えたところで特に嬉しいことはない。かえって、アップデートの際のテストを真面目にしないといけないなぁ的なプレッシャーがのしかかってくる。いや今でも真面目にやってますけどね!

Surround the world #2

surround.vim の正確な動作仕様が知りたいのだけれど、それっぽい文書が見つからないのでソースと動作を見ながら目コピーせざるを得ない。

先立って用語がある。

  • 包囲領域: カーソル位置を含む選択範囲。前方に開始文字列、後方に終端文字列がある
  • 開始文字列: 包囲領域の先頭にある文字列。”(” “‘” のようにたいていは 1 文字だがタグの場合は複数の文字になりうる
  • 終端文字列: 包囲領域の末尾にある文字列。”)” “‘” のようにたいていは 1 文字だがタグの場合は複数の文字になりうる
  • 包囲識別子: 開始文字列、終端文字列へマッピングされる 1 文字の識別子:
    識別子 開始文字列 終端文字列 備考
    / /* */ C コメントを対象にする
    次のうちのいずれか: !#$%&*+,\-.:;=?@^_|~ 識別子そのもの
    次のうちのいずれか以外のテキストオブジェクトの末尾の1文字: pswW それぞれのテキストオブジェクトの先頭文字及び末尾文字。さらに、識別子が以下のいずれかである場合は開始文字列の末尾および終端文字列の先頭に U+0020 が付加される: ([{
    a < >
    r [ ]
  • 包囲識別文字列: 開始文字列、終端文字列へマッピングされる 1 文字以上の文字列。以下の例外を除き、包囲識別子と同じく 1 文字の入力をユーザーに求める。以下の場合はモードは line_input へ遷移する:
    • 最初の文字が [cci]tT^T<,[/cci] のいずれかであった場合、[cci]>[/cci] が入力されるまでは通常の line_input と同じ動作。初期値は [cci]<[/cci]。[cci]>[/cci] が入力されたら自動的に line_input モードを抜ける(これは一時的な cnomap により実現されている)
    • 最初の文字が [cci]l[/cci] または [cci]\[/cci] であった場合、…
    • 最初の文字が [cci]f[/cci] または [cci]F[/cci] であった場合、…
    • 最初の文字が [cci]^F[/cci] であった場合、…
    • 最初の文字が上記以外であった場合、その文字が入力されたら自動的に line_input モードを抜ける。
      以下の文字に関しては特別なマッピングが施される:

      文字 開始文字列 終端文字列
      b ( )
      B { および U+0020 U+0020 および }
      r [ ]
      a < >
      p \n \n\n
      s U+0020 (空文字)
      : : (空文字)
      上記以外の英字 (空文字) (空文字)

      またこのとき、U+0020 を前置することができる。U+0020 を前置した場合、包囲を新しく行う場合に開始文字列の直後、終端文字列の直前に U+0020 が追加される

* * *

surround.vimは大きく分けて normal モード、input モード、visual モードのそれぞれに特定のマップを定義する。まず normal モードから見ていく。normal モードに定義されるマップは

  • [cci]ds[/cci]: ds + 包囲識別子 を入力する。包囲領域から開始文字列・終端文字列を削除する
  • [cci]cs[/cci]: cs + 包囲識別子(from) + 包囲識別文字列(to) を入力する。領域を囲んでいる from を to に変更する
  • [cci]cS[/cci]: cs と似ているが、開始文字列の直後、終端文字列の直前に改行が挿入され、また包囲領域の内容は適切にインデントされる
  • [cci]ys[/cci]: ys + (モーション | テキストオブジェクト) + 包囲識別文字列を入力する。モーションによって生成された範囲の先頭に開始文字列、末尾に終端文字列を追加する
  • [cci]yS[/cci]: ys と似ているが、開始文字列の直後、終端文字列の直前に改行が挿入され、また包囲領域の内容は適切にインデントされる
  • [cci]yss[/cci]: これは ys の Operation Alias に相当する。つまり暗黙的に [cci]_[/cci] モーションが指定されたことになる。従ってカーソル行全体が対象になるのだが、開始文字列が追加されるのは対象文字列の最初の非空白文字の直前、終端文字列が追加されるのは対象文字列の最後の非空白文字の直後である。なお、この機能はカウント n を前置することができ、その場合はカーソル行から n 行分が操作対象になる
  • [cci]ySs[/cci]: yss と似ているが、開始文字列の直後、終端文字列の直前に改行が挿入され、また包囲領域の内容は適切にインデントされる
  • [cci]ySS[/cci]: ySs と同じ

visual/select モードに定義されるマップは

  • [cci]S[/cci]: (v|V|^V) + S + 包囲識別文字列を入力する。normal モードの ys に相当
  • [cci]gS[/cci]: (v|V|^V) + gS + 包囲識別文字列を入力する。normal モードの yS に相当

いくつか気になる点がある。

  • d/c オペレーションが認識する包囲領域は複数行に対応していないようだ。いいのかな?
  • [cci]ys[/cci] はちょっと動作とコマンドが乖離しすぎて違和感がある。なぜ y に割り当てたんだろう
  • [cci]ys[/cci] が包囲識別文字列を最後に入力させるのは実装もめんどうそう。ys + 包囲識別文字列 + (モーション | テキストオブジェクト) ならちょっとだけ楽だったんだけど
  • 言うまでもなく、vi コマンドのオペレーションはオペレータとモーションの組み合わせで構成され、そこには直交性がある。が、surround.vim の場合決め打ちで最低限のマップ定義しかしてないので直交性が崩れている。いいのかな? ただし、たとえば [cci]>s”[/cci] とかできてもあんまり意味はないのは確かではある

しかし surround.vim 自体がすでにほぼ10年の歴史のあるプラグインであるので、世の中のユーザーはこの仕様にすっかり馴染んでいると思われる。なのでできるだけ尊重してそのまんま移植するのが正解であろう。

Surround the world

issue の中に、surround.vim をサポートしてほしいというものがある。

最初に告白しておくと、個人的には surround.vim 以前に vim のこの手のプラグインはまったく使っていない(別に使わない主義とかそういうわけではない)。なので、便利であれば実装することにやぶさかではないのだけれど、プラグインってなに? というところから調査しないといけない。

vim のプラグインは、本質的には vimscript のソースで、これは indent や syntax や colors で使うファイルも同様だ。つまり結局のところ全部 vimscript なのだが、それぞれで何を行うかが暗黙の了解的に区分けられていたり読み込まれるタイミングが違ったりはする。

まあそれはそれとして、とりあえず surround.vim を読んでみる。このファイルを ~/.vim/plugin/ に置くと、テキストオブジェクトが結果的に拡張されて i なんちゃらとか a なんちゃらの他に s なんちゃらと S なんちゃら、および ss/Ss/SS なんちゃらが使えるようになる。また visual モードでもいくつかの拡張が行われる。

結果的にというのは、本来テキストオブジェクトを拡張するとしたらコマンドディスパッチャへのフックとかそういう話になると思うのだが、surround.vim はそれを単に map で処理しているからだ。これはこれでなるほどという感じはする。ただ、これを wasavi に組み込むとなると、やはりコマンドディスパッチャに融合させる形になると思う。map で処理してもいいけどタイムアウト処理とかを意識する必要が出てくる。

0.6.410 released

各エクステンションストアにリリースした。ただし例によって Firefox 版は beta 扱い。

それはさておき。

wasavi の設定って、エクステンションの localStorage もしくは SimpleStorage に保存してある。つまりローカルに持っている。一方で、例えば Chrome には、chrome.storage.sync のように自動的に複数のデバイス間で同期が行われるストレージもある。もしかして、wasavi の設定もそういうストレージに置いたほうが便利なのではなかろうか?

と思って実装してみようとしたのだが、やっぱりやめた。同期が行われるということは wasavi の設定が google 先生のサーバとやりとりされるということだ。もし exrc にムフフな情報を書いていた場合、それが漏れないという保証ができない。いやまあそうそう漏れることはないんでしょうけど。

実装するとしたら自動的ではなくてユーザーのアクションをトリガーにするような半自動的な物のほうがよいのかもしれない。よくないかもしれない。

Some essential changes

wasavi 0.6.387 を github に上げた。このビルドには、いくつかの重要な変更がある。

まず、Firefox 版が Github 上で起動しない問題。前述の通り、これは wasavi をページ上に出す際の iframe に割り当てる src のドメインの問題だ。Github の CSP が data: スキームを許可していない。

そこで、iframe の src へは [cci]about:blank?wasavi-frame-source[/cci] というアドレスを割り当てることにした。当然中身は空っぽなので PageMod を経由して中身を差し込む。ちなみに about: スキームも CSP は許可していないのだが、結局 CSP の記述を実際にどう利用するかはブラウザが判断することであり、いろいろな例外があるわけで、すなわち about: 系は許されるのであり。このへんは実際に試してみないとわからないのだった。

実は Github で起動しない理由は他に、Hotkey モジュールに定義したショートカットよりもページスクリプトが消費するキー入力イベントが優先されるというのもある。しかし Firefox 版で Hotkey モジュールを使った理由というのは、それがページスクリプトよりも優先されるはずだからなのであり、実際その当時はその通り動作した。しかし現在はそうではないのである。しかし、まあ、Firefox というものは、大体において、そういうものなのでしょう、きっと。これは単に Chrome や Presto Opera と同様、keydown のフック機構を経由してキー入力を監視する方式に切り替えればいい。このくらいでは泣かないぞ。

* * *

insert モードと replace モードを総称して input モードと呼ぶ。従来はそれを実現するために、カーソル位置に重ね合わせる形で透明な textarea を配置し、それが受けたイベントを wasavi へ転送するというわけのわからない複雑さを備えた謎の機構を経由していた。なんでこんなめんどくさいことをしているのかといえば、すなわち Presto Opera のためだ。あの苦労してこさえた擬似 composition イベントは、textarea がフォーカスを持っている状態じゃないと正しく生成できないのである。

しかし、まあ、Presto Opera でありますよ。そろそろ引退させたほうがいろいろな方面が幸せになるのですよ。

というわけで input モードでは div 要素の contenteditalbe 属性を利用するようにした。これにより結構な量のコードをばっさり削除できるのである。おまけに入力のオーバーヘッドも減り、Selenium のテストも速くなり、word wrapping なども CSS での指定をそのまま使用でき、悪いものはないのである。強いて言えば前述の通り Presto Opera で IME を通した入力ができなくなるが、Presto Opera 上で wasavi を使っている人間なんて地球上にただ数人いるかいないかであり特に問題ではない(とはいえ最低限おかしくならない程度の処理は入れたが、まあ完全ではない)。

Some topics

  • [cci]cfx run[/cci] で wasavi のデバッグ用 Firefox を起動する。そうすると console への出力がそのまま端末にリダイレクトされるのだが。Firefox 自身が持っている chrome:// なんちゃらドメインの各種 javascript ソースから発生するログがあまりに多すぎる。たいていは strict モードにまつわるエラーで、中には本気でこれバグってるんじゃないの? 系のエラーメッセージもあったりする。オブジェクトの存在しないプロパティにアクセスしてたり、null のオブジェクトに触ってたり。

    ちょっと、ひどい。直したほうがいいんじゃないですか。

  • Blink Opera の wasavi では、ページフックスクリプトをいうものを有効にしていたのだが。すべて除去した。

  • Firefox 版の wasavi では、たとえば github 上で動作しない。これはなぜかといえば github のサーバから送られてくるレスポンスヘッダ内の CSP が原因だ。Firefox 版の wasavi は data スキームの内容を src にした iframe だ。しかし github のページの CSP は data スキームの iframe を許可しない。

    これ、どうしたものかなー。もしかしたら解決の糸口になるかもしれないものを一応メモ。

    Firefox Addon SDK: Loading addon file into iframe
    http://stackoverflow.com/questions/21082162/firefox-addon-sdk-loading-addon-file-into-iframe

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 ステージは更に最適化できるかもしれない。

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

change a IME state

あらゆるところですでに怨嗟の声が書き込まれまくっているので、特に新しく書くこともないのだけれど、iBus 1.5 で IME の状態を切り替えるという話。

つまるところ、IME のオン・オフを切り替えるために従来は IME 内の状態(”直接入力” と “ひらがな”)を操作していた。それが、システムに登録されたキーボードレイアウト(たとえば、US キーボードと mozc が有効なキーボード)自体を切り替えるようになったのである。”IME が有効なキーボードレイアウト” という概念が不自然で、この辺の移行が混乱しているのが文句の原因らしいのである。

個人的には、方式が変わること自体は別に文句はない。ちょこちょこっと設定しなおして、右 Alt キーで IME の状態をトグルできればそれで良いのです。ただ困るのはいずれの方式にしても微妙にストレスフルな不具合というか何と言うかがあるという点で:

まず iBus 1.5 デフォルトの、キーボードレイアウト自体の切り替え方式にした場合。この場合、レイアウトの切り替えに使うキーは iBus の設定ウィンドウ中に登録する。

  • 良い点: レイアウトの状態(≒ IME の状態)をシステムアイコンに反映させられる
  • 悪い点: iBus の設定の中の “すべてのアプリケーション間で同じインプットメソッドを共有する” をオンにすると、このようにブラウザ上で文章を書きつつ端末上の vim に切り替えた場合など、明らかに IME がオフであるべきアプリケーションでも IME がオンになっている状況が多発してとてもストレスが貯まる。
    そこで前述の設定をオフにすれば、レイアウトの状態はアプリケーションごとに独立するのでそれは解決するのだが、そのかわり IME をオンにした時の最初のプリエディットの最初の文字を取りこぼす現象が多発してこれはこれでものすごくストレスが貯まる。

そんなわけで、じゃあ “すべてのアプリケーション間で同じインプットメソッドを共有する” はオフにした上で、IME の状態はやっぱり IME 自身が管理したほうがいいんじゃないの? と思い、右 Alt キーは mozc が反応するように設定しなおしたのだが。

そうするとシステムアイコン上のキーボードレイアウトは常に単に mozc のアイコンが表示されるだけになり、現在の入力モードがわからない。いや入力モード自体は iBus のプロパティパネルとやらに表示されるのだが。これが表示される位置がなかなかめっちゃくちゃで、また表示を自動にすると表示から数秒後に自動的に消えてしまうし、常に表示にすれば常に邪魔くさいわけで、つまりフロート式のプロパティパネルという仕様自体が間違っていると思うのです。

iBus のプロパティパネルのシステムアイコン版が、システムアイコンのキーボードレイアウトアイコンの隣に表示されればそれが最も現実的な解だと思うのだけれど。それをするためにどこの設定をどーすればいいのかわからないわけでストレスが貯まる。

ストレスをいくら貯めたところでポイントには変換できないのである。どうにかならないのかしらん。

Input mode revised

キーボード周りで Qeema を利用することにしたが、それにより若干従来のキーボードマネージャからインターフェースが変わった部分がある。この際なので、input モード周りのキー入力のハンドリングのおさらいをしたい。

モードが insert / replace へ遷移することにより、カーソル位置にまず I ビームを表示させる必要がある。この I ビームは、textarea 要素のそれを流用する。ちなみに input モード時に textarea 要素を流用するのは I ビームの見た目がほしいからではなく、composition events は、activeElement が編集可能な要素じゃないと発生しないからである。

textarea 要素のその位置は、折り返し行単位のカーソル行の上にぴったり重ね合わせるようにする。また、ここでカーソル行の前後に 2 つの span を生成する。

1 つは、折り返し行の先頭から I ビームまでの文字列で、これを leading と呼んでいる。leading span は visibility:hidden にする。その代わり、textarea の value 要素を leading で初期化する。これにより、カーソル行のカーソル位置と input 用 textarea の I ビームの位置が(だいたい)一致する。だいたいというのは…理屈の上では、margin やら padding やらを合わせれば wasavi の行の div と textarea で揃えればぴったり重ね合わさるはずが、ブラウザによってはそうならないものもあるのである。ただまあ、ずれるとしても数ピクセルなので大したことではない。ただ I ビームの先頭位置を を textarea の内容で調節するのはもう 1 つの問題がある。つまり色分けできないのである。現状では wasavi は色分けしないのでいいのだけど、将来必ず困ってしまうのであるのであるが、それはさておき。

もう 1 つの span は I ビーム位置に挿入する空の span で、compositionupdate や compositionend イベントで送られてくるプリエディット文字列を入れる場所だ。このようにして入力用 textarea とプリエディットの状態を同期させることによりインライン入力っぽいような動作をさせている。実際に IME から一気に送出された文字列を挿入する処理は、IME 経由以外の方法で入力された文字と共通する。この仕掛けは qeema の恩恵による。

composition events を正しくリスンし正しく相応しい処理を行うのは、アジア圏の言語のためだけではない。Compose キーを併用した文字の入力は、たとえば Firefox では composition events を経由するので(それを踏まえて qeema でもそうしてある)、より広範囲の言語に対応するために必須の処理だ。

ところで Project Spartan なる新生 IE は、javascript ベースの拡張をサポートするそうだ。Presto Opera のサポートを漸次的に終了すると同時に、Spartan の動向をウォッチする必要があるかもしれない。