wasavi/0.6.580 released

リリースした。

Chrome: https://chrome.google.com/webstore/detail/wasavi/dgogifpkoilgiofhhhodbodcfgomelhe

Opera: https://addons.opera.com/ja/extensions/details/wasavi/

Firefox: https://github.com/akahuku/wasavi/raw/master/dist/wasavi.xpi

* * *

w/W/b/B コマンドがある。これらは vi 内の独自の文字の分類に従ってカーソルを移動させる。これ、Unicode における単語境界の仕様に準拠してもいいんじゃないかなあという気になりつつある。

a critical bug

wasavi をリリースしたばかりなのだが、Chrome 上のとある条件下でフリーズするという致命的なバグを直したので今月末辺りにまたリリースするかもしれない。フリーズと言ってもプロセッサのリソースを食いつぶすとかそういう類ではない。

これは 2 つのあまり関係なさそうな事柄が関係している。

まず、wasavi が保持するバッファは、各行の末尾に [cci]\n[/cci] の内容を持つテキストノードを保持している。これは、編集後に textarea に代入するために wasavi のバッファから単一の文字列を得る際、単にバッファの div 要素の textContent プロパティを参照するだけで済むようにするためだ。

次に、wasavi の input モードは div 要素の contentEditable 属性をオンにすることで実現している。

この 2 つの事柄が Chrome 上で絡みあうと不思議なことが起きる。IME を通した仮入力が最終的に空文字になり、かつカーソル行の div 要素の内容も空になった場合(たとえば空行で仮入力を開始したが、結局全部 backspace で消して仮入力を抜けた時…など)、前述の改行要素が Chrome によって勝手に削除されてしまうようなのだ。

wasavi のあらゆる編集機能はこの改行要素が存在することを前提としているので、ないとなるとあらゆるところでエラーが発生して満足に動かなくなる。

width of character

wasavi のステータス行にはカーソル位置の桁・行位置を表示しているのだけど、その桁位置とはなんぞやというと、これはカーソル位置の内部的な行頭からのオフセットをそのまま指しているわけではない。つまり内部的な桁位置と表示上の桁位置は違う。

まず内部で charWidth という変数があり、これは大元の textarea のフォント関連のスタイリングに基づく、[cci]0[/cci] の文字幅だ。技術的には span 要素に [cci]0[/cci] を入れた際の offsetWidth の値だ。

それを踏まえて表示上の桁位置を算出するには、span 要素に行頭からカーソルの物理位置までの文字列を入れ、その offsetWidth を出し、charWidth で割る。表示しているフォントが固定幅であればこれで特に問題ない。

ところが困ったことに、固定幅フォントでさえ、文字列全体の offsetWidth が charWidth の整数倍にならないことが多々あるのであった。つまりこれは、cssom では offsetWidth の型は long だ。しかし実際には、それは小数で保持されているのが原因なんだと思う。そういうわけで正しい表示上の桁位置が得られない。

うーんどうしよう。

 * * *

charWidth を算出する際に、100 個並べた [cci]0[/cci] の offsetWidth を 100 で割るようにした。つまり charWidth の精度を上げた。

Synchronizing the settings #3

とは言ってみたものの、本当に大丈夫なんだろうか。

とりあえず wasavi のバックエンドが localStorage に保存するデータのキーを列挙してみよう。

  • version: 現在インストールされている wasavi のバージョン文字列
  • targets, exrc, shortcut, shortcutCode, fontFamily, fstab, quickActivation, qaBlacklist, logMode, sounds, soundVolume: オプションページから編集することのできる項目群
  • memo-*: body 要素に対して wasavi を起動した場合その内容は localStorage に対して読み書きされる。これを Memorandum と呼んでいて、localStorage 上の memo-* キーは各 URL ごとの Memorandum の内容を保持している。* の部分は URL の SHA-1 値
  • filesystem.*.tokens: 各クラウドストレージに接続した際のクレデンシャル
  • wasavi_lineinput_histories: 行入力モードで入力された値の履歴
  • wasavi_registers: レジスタの内容

こうしてみるとキャメルケースだったり [cci]-[/cci] で繋いでいたり [cci].[/cci] で繋いでいたり [cci]_[/cci] で繋いでいたりとてもアレだがまあそれはそれとして。ユーザーをスイッチした時、リセットされるべきものと触るべきでないものを明確にしたい。大体の区別は前者は Chrome にログインするアカウントに属する情報、後者はローカルマシンに属する情報ということだ。

まず version は明らかにローカルマシンに属する。これはいい。したがって同期されるべきではなく、かつユーザーがスイッチされたとしても変更されるべきでない。

次にオプションページの項目は当然ながら同期される対象であり、ユーザーがスイッチされた場合は新しいユーザが所持するデータのものに差し替えられるべきである。

ここまでは簡単だ。これ以降が難しい。

Memorandum はまだその仕様自体が固まっていない。とりあえず version と同様の扱いにしておこう。

クレデンシャル。クレデンシャルの情報はクリティカルであり同期で使いまわすのは怖い。一方でユーザースイッチの場合はどうなんだろう。クラウドストレージに対して受けた認証は Chrome のアカウントでもローカルマシンでもなく各ストレージのアカウントだ。ということは Chrome のアカウントがスイッチされたとしても触らないほうが良いのだろうか。なんとも判断できない。

行入力履歴とレジスタはまた異なる疑問点がある。使い勝手で考えると、これらが Chrome のデバイス間で共有されるのはまあまあ便利だと思う。しかしこれらの情報ってかなり頻繁に更新されるのだ。デバイス A で wasavi を起動し履歴とレジスタの内容を更新する。それを同期に載せる。デバイス B でも wasavi を起動する。同期イベントがやってくる。ローカルで更新した内容が上書きされる。そんな感じで不整合が簡単に起こりうる気がする。ということは実装の都合上同期しないほうがいいのかもしれない。ユーザースイッチの場合はどうか。これもなんとも判断できない。

…ということなのだが、いまいち確固たる理由付けによる区分けができない。

Synchronizing the settings #3

よくリファレンスを読んでみたところ Identify API に getProfileUserInfo() と onSignInChanged イベントがあり、もちろん前者は Chrome にログインしているユーザーの情報を得るために使い、後者はユーザーがスイッチされる時に発生するようだ。

よかったよかった。これを使えば大丈夫。

Synchronizing the settings #2

とりあえず Chrome の場合のみ、[cci]chrome.storage.sync[/cci] を通して同期された設定を参照するようにした。Presto Opera、Firefox では設定は同期されない。また Blink Opera では前述の API 自体は使えるのだが、今のところ同期しないので実質的に現状と変わらない。

ところで組んでいて気がついたのだけれど。

この同期されるデータというのが何に属しているのか。もちろん Chrome に入力する Google アカウントだ。一方で Chrome には任意のタイミングでこのアカウントをログアウトしたり、別のアカウントにスイッチするようなインターフェースが用意されている。

ということは、エクステンションからアカウントに属するストレージを参照できる以上、現在のアカウントのある程度の情報やアカウントがスイッチされたというイベントも受け取れることができないと辻褄が合わなくなる。

しかし探してみてもそういうものが見つからない。[cci]chrome.storage.onChanged.addListener[/cci] で内容の変化に対するイベントをリスンすることができるが、例えばアカウントをスイッチした時に何かイベントが発生するということはないようだ。現在のログイン状態を知ることができればまあまあなんとかできるかもしれないが、そういう API もない感じ。いっそ [cci]chrome://sync[/cci] をスクレイピングとか…? いやまさか。

これ、Wasavi に限らず storage API を使っているエクステンションに共通する問題だと思うんだけど。つまり Chrome にログインしたあと、各エクステンションのバックグラウンドをリロードするか、あるいは単に Chrome 全体を再起動しないと辻褄が合わなくなると思うんだけど。

しかし google 様がそんな仕様にするのだろうか。何か勘違いしているのかもしれない。もう少し調べてみよう。

Alternative NTW

Chrome で Ctrl-[ntw] を使うことができない件。

その代わりに Alt-[ntw] でも同じ機能を呼び出すようにした。Selenium でのテストもそちらを使用する。

ついでと言っては何だが、[cci]map[/cci] の左辺に指定するキーストロークで Alt を併用したものを受け付けるようにした。ただし完全ではなく、保証できるのは Alt-[a-z] だけ。この辺は DOM3 KeyboardEvent がフルに実装されればもうちょっとましになると思う(つまり、キーボードレイアウトに依らずにすべてのキーについて何が押されたかを正確に判断できる)のだけど、それはまあ 1 年後くらいかな…。このへんは何故か Chrome のほうが W3C の規格からずれている。

Presto Opera is gone

Selenium を 2.46.0 に更新したところ、2 点。

まず付属の jar から OperaDriver が消えていた。

つまり、そういうことなのだろう。

wasavi はまだ、とりあえずは Presto Opera でも動作するようにする。これはつまりできるだけ Chrome/Firefox/Opera とで共通して動くコードを書くのを心がけるということだ。一方で段階的に Presto Opera の専用コードは削除していくし、新規に書くこともない。たとえば、qeema から既に Presto Opera 対応の部分はざっくり消した。

ただし、前述の通り Selenium は OperaDriver を deprecated にしたわけなので、通しての機能テストを Presto Opera に施すことはできない。従って wasavi が Presto Opera 上でひと通り動作することを保証することはできなくなる。

ところで Presto Opera が脱落すると wasavi が動作すると公式に謳えるブラウザが Chrome と Firefox だけになってしまい若干寂しい。聞く所によると Microsoft Edge は Chrome のそれをパク、いや非常に高い互換性を持つ拡張の仕組みを持つらしいが…。

ただ同時に、どういうわけか Firefox の拡張とも高い互換性を持つ、という噂があったりよく分からない。そんなこと可能なんだろうか。とにかくいよいよ Windows 10 のリリースが近いが、Windows 10 に同梱される Edge にはまだ拡張の仕組みは組み入れられておらず、大体今年中に形になればいいかな程度の完成度だという。

次に。

Chrome はいくつかのキーボード・ショートカットを予約している。たとえば [cci]Ctrl+T[/cci] とか [cci]Ctrl+W[/cci] とか [cci]Ctrl+P[/cci] とか、つまりブラウジングするにあたってとても基本のもの。予約しているというのはどういうことか。スクリプトからそれらのキー入力を得ることができないのだ。これらのキー入力の場合 keydown イベント等自体が発生しないのでどうしようもない。google 神に歯向かう手段がまったく用意されていない。

とはいえ wasavi ではこれらの特別なショートカットのいくつかにも機能を割り当てている。たとえば行入力中の [cci]Ctrl+W[/cci] はカーソルの前の単語を削除する。

従来、それらの機能をテストする際は、当然ながら Ctrl と W を同時に押したような擬似的なキーストロークを生成するとテストできないので、そのかわり U+0017 を生成していた。[cci]Ctrl+W[/cci] は wasavi においては U+0017 とみなされるのでまあこれでよかろーということなのだが。ところが数日前に Chrome が version 44 に上がったせいかこの手法が通じなくなってしまった。U+0017 を生成しても Chrome に [cci]Ctrl+W[/cci] とみなされるようになってしまったようなのだ。

そんなわけで多くのテストが失敗するようになってしまった。うーんどうしたものかな。

be a developer #2

例の Firefox の署名周り。公式のドキュメントとしては https://dev.mozilla.jp/2015/02/extension-signing-safer-experience/ あたり。

このドキュメントには書いていないが、ものすごく重要なことがある。それは AMO に提出した拡張がレビューされるまで、月単位で待たなければならないということだ。レビューされるのが早いか、弥勒菩薩が降臨するのが早いか…という勢いである。枯れた安定した拡張ならともかく、新しく次々と更新されるホットなそれにとっては AMO はまったく頼りない場所であるということだ。

それを補うために、従来はベータチャネルというものがあって、1.0.0beta みたいに名づけたバージョンはベータ版扱いになり、アップした際の機械的なコードバリデータでのチェックでエラーさえ出なければとりあえず即 AMO 上に乗る仕組みがあった(ただし、当然 AMO がレビュー済みというお墨付きは得られない。また事前に full レビューを済ましたものでなければならない)。

今回署名周りの変更に従って、どうもこのベータチャネルのフローも変化している感じがする。コードバリデータでエラーも警告も一切でない 100 点満点の結果を出せば、おそらく従来通り即 AMO に乗るのだろう。しかし警告を出すと人の手によるコードレビューに送るフローしか選択できなくなってしまう。そして人の手によるレビューは前述の通り月単位で時間がかかるのである。したがって、「不安定かもしれないけど即公開される」というベータチャネルの意義を AMO 自身が完全にぶっ壊しているということになる。

ここで、「警告を出さないクリーンなコードにしないお前が悪いんじゃん」と思われるかもしれない。本質的にはそうだろう。しかし現実的にはコードバリデータがどうも信用できないのだ。警告ではないものにまで言いがかりをつけてくるポンコツに思えて仕方がないのだ。

たとえば以下のように、setTimeout() や setInterval() の第 1 引数に関数リテラルではないものを渡すとバリデータは警告を出す。しかも、reject severity: high という扱いで。

handler = function () { ... };
:
:
setTimeout(handler, 100);

これが何故警告の対象になるのかというとおそらく、コールバック関数を変数経由にすると、その変数の中身を外部から書き換えられてしまい、任意の関数を送り込まれる危険性がある……的なものだと思う。だからコールバックは関数リテラルで書けよとバリデータは言ってくる。

しかしこの場合、問題の本質はコールバックを変数経由にした場合にその変数が外部から操作可能なスコープにあるかどうかであって、関数リテラルで書いているかどうかではないと思うのです。そういう視点ではなく、バリデータは単に字面しか見ていない感じがする。信用できない。

ちなみにこれを

handler = function () { ... };
:
:
setTimeout(function(){handler()}, 100);

と書き換えると警告は出ないのだが、これじゃあバリデータくんが危険視するらしき危険性はまったく除去されてない。これを見逃すというのは、やはりバリデータは単に字面しか見ていない感じがする。信用できない。

Mozilla は自分のところで javascript エンジンを作っているのだから、自慢のなんとかモンキーをベースにもっとちゃんとしたバリデータを作れると思うのです。それをやっていない。バカでも作れる、grep に毛が生えたようなツールしか作っていない。

他にもたくさん「なんでこれが警告扱いになるのか意味わかんねーです…」というものをたくさんお出しされてとってもげんなり来る。

もっと真面目にやってほしい。

Longterm goal

中長期的な目標のうち大きめのものを一応メモしておこう。

まずはモデルとビューを分離したい。現状では wasavi が保持しているテキストの実体は iframe に表示されている html 文書そのものなのだ。それにより再表示の部分は完全にブラウザ任せにできるメリットはあるのだけど。しかしせいぜい 100 行程度の小さな文書であればいいのかもしれないが、それ以上になるとメモリ消費量や再表示の速度などなかなかきつくなってくると思う。

なので、テキスト自体は単なる String の配列か、あるいは XMLDocument にしておいて、表示の部分はまさにビューに表示すべき領域だけを描画するようにしたい。スクロール処理も含めてそのためのプロトコル的なものを考える必要がある。

それからモデルとビューの分離は、もうひとつ重要な変化をもたらす。現状 wasavi のエディタ部分のコードはすべて iframe 側にある。つまり wasavi のエディタとしての機能をもたらすコードは原則的には iframe の生成のたびに評価・実行されるということだ。コードは現状一万行以上あるわけで、これは大いにリソースの無駄だと思う。もちろん、ほとんどの場面でブラウザが持つコードキャッシュがそれをカバーしてくれるわけだけど、常にそうとは限らない。

これが、モデルとビューを分離するとどうなるか。エディタとしてのコードをバックエンド側に常住させることができるのだ。機能の区分けとしてはあくまでフロントエンドなので位置づけが難しい(フロントエンドのバックエンド側?)が wasavi が起動するたびにバックエンド側で

instances.push(new WasaviFrontend)

的に常駐しているコードからインスタンスを生成して割り当てれば、起動のたびに巨大なコード群を評価させる必要はなくなる。iframe 側が担当するのはキー入力の受付と、バックエンドへの送出、バックエンドから送られてくる再表示コマンド列の評価実行だけということになる。