wasavi for Firefox is available

1月19日に AMO へアップデートを申請した wasavi 0.5.281 が今日フルレビューを通った。まるまる3週間かかったことになる。AMO からの素敵なお便りメールを受信する設定にすると、

Most updates are being reviewed within 2 weeks.

などと高らかに謳っている自画自賛メールがやたら届くわけだが、それはあくまで平均の数値であって実際にはなかなかそう上手くは行っていない。

別に Mozilla のそういうスタンスがダメだというわけではない。Chrome のように機械的にチェックするだけとか、Opera のようにおめーそれ体裁しかみてねーだろ! といったスタンスに比べたら、時間がかかってもひと通りソースコードを見てもらえるというのは良いことだ。とても良いことだ。

それにしてもちょっとレビューに時間がかかり過ぎじゃないですかね……ということなのだ。Mozilla って別に爪に火灯すような財政状況でもないと思うんだけど、なんでレビュアーを増やさないのかな。

Scripting #7

とりあえずの取っ掛かりとしてスクリプトを動かす基本的な部分は作った。が、このアーティクルではスクリプティングについて特に書くことはない。github で要望や pull request が来ていたという話なのだ。

  • スペルチェッカがほしい
  • コンテキストメニューから wasavi を起動できるようにしてほしい

こんなところである。また、pull request の方はメッセージや readme を自然な英語に書き換えてもらった。これは嬉しい。

さてこの中で興味深いのは、スペルチェッカだと思う。ピュアな vi にはそんな機能はない。また、nvi にもない。vim はある。ちなみに elvis にもあるらしい。

といっても、同等の機能を javascript で実装するのはなかなか面倒だ。実装というか、辞書を用意するのが大変だ。そこで、単にブラウザの機能で入力用の textarea の spellcheck 属性を true にすることにする。とりあえず input モード時、間違った綴りの単語を区別することはできるようになる。Opera では間違った綴りの単語に赤線が引かれる。
wasavi_spell_checker
ただ、たとえば Opera の場合、間違った綴りの単語の上でコンテキストメニューを出すと正しい綴りの単語が提示されて、単語を置き換えたりできるのだが、wasavi は個々の要素に対するコンテキストメニューを封じているので、置換はできないのであった。本当にただ、綴りが間違っているかどうかを知ることができるだけである。また、input モードを抜けると赤線は消去される。

spellcheck 属性は、html5 で規定されている。したがって html5 対応と謳うブラウザはだいたいスペルチェック機能を持っていると考えてよい(必須の条件ではない)のだが、どうせブラウザがスペルチェック機能を内包するのなら、javascript の API も規定してくれればいいのになー。

Scripting #6

すでに書いていると思うけれど、:script コマンドには 2 種類の書き方がある。:script の引数としてスクリプトコードを与える方法と、:scriptend までの間に記述する方法。前者を single-lined script、後者を multi-lined script としよう。

vim では、たとえば :lua と打つとその場で(つまり、コマンドライン入力モードのまま)スクリプトコード本体を打ち込めるようになっている。非常に賢い。でもそれを実装するのは面倒だなあ……。

というわけでとても乱暴なことに、wasavi 実行中に対話的に ex コマンドを入力するシーンでは :script は single-lined script のみ認めることにする。multi-lined script は exrc の実行、および :source でのみ有効となる(:source はまだ実装していないけど)。

 * * *

とりあえず、:script を含んだ exrc をエラーなく実行できるようにした。Chrome や Firefox でも同様に実行できるようにした。

それにしても新しく書かなければならないコードが多すぎてさっぱり進んでいる感じがしない。

Scripting #5

特に Chrome に対して重要な変更を施した。

wasavi が属する文書とスクリプトフレームが属する文書は、同一のオリジンである必要がある。これは、wasavi 側から scriptframe.contentWindow.dispatchEvent(ev) としたり、あるいは逆にスクリプトフレーム側から window.parent.dispatchEvent(ev); とするためだ。同一オリジンじゃないと dispatchEvent() は拒否される。dispatchEvent() を用いるのは、それが同期的に実行されるからで、postMessage() を使うといろいろと話がややこしくなる。

ところで Chrome では、wasavi が属する文書はエクステンション内のものにしていた。つまり chrome-extension://なんちゃら、という URL の文書。ところが、この URL に属する文書に付随するスクリプトは content script と同じ扱いになるのだった。この環境でスクリプトを動かすには、権限が高すぎる。

Opera や Firefox(Add-on SDK) では、エクステンション内の文書を通常のページの iframe の内容として表示することはできない(Firefox ではなにか方法はあると思うが調べてない)ので、appsweets.net 上の文書を参照している。これなら、スクリプトを動かす際に一般の web ページの権限が適用される。

ということで、Chrome でもその流儀に従うようにした。これは manifest の変更を意味する。ただしより強い権限を要求するのではなくその逆で、web_accessible_resources が不要になる。しかし一方でコンテキストメニューのために permissions に contextMenus を追加したので、次のリリースをインストールする際にはなにがしかのダイアログは出るはず。

ちなみに Chrome の不思議な特性として、コンテントスクリプトから、コンテントスクリプトの影響下にない素の Window オブジェクトにアクセスすることができない。つまり scriptframe.contentWindow を触れない。そういうわけなので、wasavi 側の文書で script 要素を生成し、以下のコードを text 属性に突っ込む:


document.addEventListener('WasaviScriptRequest', function (e) {
var ev = document.createEvent('CustomEvent');
ev.initCustomEvent('WasaviScriptRequest', false, false, e.detail);
document.getElementById('wasavi_script').contentWindow.dispatchEvent(ev);
}, false);

で、wasavi 側からスクリプトを呼び出す際は自らの文書に対してイベント起こす:


var ev = document.createEvent('CustomEvent');
ev.initCustomEvent('WasaviScriptRequest', false, false, {code:"console.log('hello, world')"});
document.dispatchEvent(ev);

Scripting #4

スクリプトの環境では、window.wasavi オブジェクトが事前に定義されていて、それを通して wasavi を操作する。たとえば wasavi.run(‘Ihello, world\u001b’); のような。

ここで面倒なのは、wasavi のコマンドの中には非同期に実行されるものがいくつかあるということだ。たとえばクリップボードレジスタを参照する場合や、:read、:edit など。スクリプトから呼び出す際は、これらのコマンドも同期的に動作させなければならない。そうしないと

wasavi.run(':r foo.txt\n');
wasavi.run(':Iwow!\u001b');

と言ったスクリプトが正しく動作しない。

とは言ったもののこれはかなり難しい。非同期的な処理を同期的に扱うのは、やはり ES6 の generators が欲しい。しかし generators を現時点で実装しているブラウザはないのだった。正確に言えば、Firefox は javascript 1.8 としての generators は実装しているが微妙に仕様が違う。Chrome は canary と dev で chrome://flags を操作することで試験的に使うことができる。Opera は……まあ Opera だからね!

ということで、スクリプトの機構は作っておき、さらにシンタックスハイライティングの定義をするインターフェースも作るが、wasavi に対してコマンド列を送る機能は各ブラウザに generators が実装されるまでおあずけということになると思う。

ES6 っていつ策定完了するのかなあ?

Scripting #3

ぼちぼち実装し始める。

とりあえずスクリプトフレームを起動時に生成するようにした。ついでに、appsweets.net ドメインのコンテンツを Application Cache に入れるようにした。

スクリプトフレームで動く injected script を新設した。中味は後で書く。

ex コマンドのパーサに手を入れた。script コマンドが入力された場合、「リテラルモード」に入るようにした。ただし、script コマンドの引数が与えられていた場合はリテラルモードには入らない。つまり

:script foo();



:script
foo();
:scriptend

という 2 種類の記述方法が使えることになる。vim では、:lua や :ruby はヒアドキュメントっぽい記述方法になっているが、それは別に踏襲しない。リテラルモードを終了させるのは行がまさに “scriptend” である場合だ。なお、scriptend が現れないままソースコードの最終行までパースした場合、自動的に scriptend が補完されたような動作をする。つまり ex コマンド列の最後のコマンドが script であった場合は、scriptend は書かなくてもよい。

ex コマンドに script を新設。ちなみに、スクリプト関連としてはこの他に

  • source: スクリプトファイルを読み込み、実行
  • listen: イベントにスクリプトを登録する。vim で言うところの autocmd
  • unlisten: イベントからスクリプトを削除する。vim で言うところの autocmd!
  • command: 任意のスクリプトコード塊に名前を付け、ex コマンドとして認識させる。vim の同名コマンドと同じ

あたりが必要だと思う。:source のスクリプトファイルというのは、エクステンションの localStorage に擬似的なファイルシステムを構築することになるだろうか。このへんは Opera Unite みたいにローカルファイルシステムの特定の場所にマップする仕組みがあるととてもいいのだけど。

あとは、script コマンドのハンドラ内で実際にスクリプトフレームへスクリプトコードを投げつけ実行させる処理を書く。

Scripting #2

一方で、スクリプト側から wasavi を制御する場面を考えると、まずスクリプトを実行させる場所になる iframe(以下スクリプトフレーム)で最初に wasavi オブジェクトを定義し、それを通してアクセスさせる。

アクセスの手段は

  • run: コマンド列の送信
  • rows: 任意の行の読み書き
  • options: オプションの読み書き
  • registers: レジスタの読み書き
  • marks: マークの読み書き

あたりがあればいいと思う。

ところで面倒なことがある: スクリプトフレームは wasavi 本体とは異なるドメイン上の文書として存在させたい。これはスクリプトから wasavi の DOM とかを直接いじられたくないからなのだが、そうするとスクリプトと wasavi とのやりとりはクロスドメインメッセージングを通して行うことになる。つまり非同期ということになる。うーん。これはちょっといまいちなんじゃないか。複数回コマンドを発行するだけでも


wasavi.run('Ihello, world\u001b', function () {
wasavi.run('0cwbye\u001b');
});

みたいなネストしまくりコールバック地獄になってしまう。Deferred 的なライブラリを用意するのもちょっと根本的な解決とはいえない。それよりも同期式のほうがずっと単純でわかりやすい。generators が各ブラウザに実装されればなんとかなるんだけど。harmony に generators って入ってるんだっけ? 調べたら function* foo () {…} みたいな形で作るようになってるのか。早く実装されないかなー。

で、それを待ってもいられないので、仕方ないのでスクリプトフレームも wasavi と同じドメインの文書ということにして、もう window.parent 使って直接読み書きするしかないかもしれない。とりあえず元の window.parent はスクリプト上の wasavi オブジェクト内に閉じ込めて、スクリプトからは隠す的なことはすると思う。他に親フレームにアクセスする手段ってあったかな。多分なんかある。

Scripting

もうちょっとスクリプティングについて整理してみよう。

  • なぜスクリプティングが必要なのか?
    ユーザに wasavi の動作を好きにさせるため
  • どのような動作をスクリプタブルにするのか?
    バッファへの読み込みの前後、書き込みの前後、モード変更の前後、オプションの変更時などでイベントを発生させ、各タイミングでユーザ定義のスクリプトを実行させる。スクリプト内で、wasavi に対してなにがしかの動作を行わせるようにする。また、ex コマンドに :script(スクリプトの定義と実行)、および vim で言うところの :command を追加する。
  • スクリプトの言語は何か?
    javascript。javascript でやるとすると、なんでもやり放題というふうにはさせないようにしなければならないのと、同期・非同期のことを考える必要がある。前者は sandbox な iframe 内で実行させるようにすればいいかもしれない。例によって Opera だけ sandbox にまだ対応していないが、まあこれは html5 の仕様なのでいずれ実装される。されたらいいな。

    後者は難しい。スクリプトの呼び出し自体を同期的にするか、非同期的にするか? また、スクリプトに対しては wasavi を操作するインターフェースを公開することになるのだが、スクリプトからの wasavi の呼び出しもまた同期的なのか、非同期的なのかを考える必要がある。

    まずスクリプト自体の呼び出しだが、これは両方サポートする必要があると思う。バッファを編集するコマンドとして定義したスクリプトは、シーケンシャルに実行されないと辻褄が合わなくなる。一方、たとえば XHR を利用して何処かに何かを通知するようなスクリプトは、別にサーバの応答をその場で待つ必要もないだろう。そういうわけで両方必要。

    呼び出し方。:script コマンドにスクリプトのソースを引数として与えて

    :script foo();

    あるいは :script と :scriptend コマンドのペアの間に記述して

    :script async
    function foo () {
    wasavi.run('Ihello, world\u001b');
    }
    foo();
    :scriptend

    みたいな感じか。デフォルトは同期呼び出しで、:script の最初の引数が “async” だったら非同期スクリプトとして呼び出す……みたいな。いやダメだな。非同期として設計されたスクリプトを同期設計のつもりで呼び出したり、その逆が起こりうる。スクリプト自身が動作を表明した方がいい。

    :script
    wasavi.async = true;
    function foo () {
    ;
    }
    foo();
    :scriptend

    こんな感じか。

だいたいイメージが固まってきたかな!

Next work

次に実装すべきものの中で大きなものを考えると、シンタックスハイライトとスクリプティングだと思う。

さてこの 2 つのうちではどちらに手を付けたものだろうか?

シンタックスハイライトについては、2 つの考える必要がある点があると思う。どのように色分けを実装するか、そしてシンタックスをどう動的に定義できるようにするか。前者は、それで閉じている問題だが、後者はスクリプティングの仕様と絡んでくる。

と、いうことは、スクリプティングを先に考えたほうがいいのかもしれない。イメージしているのは、vimscript のように ex コマンドを盛大に拡張する感じのものなのだけど、ただ vimscript はあんまり好きじゃない。編集のための ex コマンドと vimscript で呼ぶための関数及び if とか try とかの制御構文が、何もかもごちゃまぜなのが好きじゃない。ex コマンドの仕組みを間借りしてスクリプトを実行するとしても、両者ははっきり区別したほうがいいんじゃないかしらん。まぜこぜになってるメリットがよくわからない。

html ファイル内の script 要素のように、スクリプト開始コマンド・終了コマンドを設けて、その間にスクリプトを書くようにしてはどうか?


:script
function foo () {
;
}
:scriptend

みたいな感じで、scriptend コマンドによってスクリプトが評価・実行される。ところで思わずそのスクリプトを javascript 的な言語で書いてしまったが、どうするか。javascript が無難かなあ。オレオレ言語をぶちあげてコンパイラと VM を書く元気はない。しかし素の javascript の実行を許すと、なんでも出来すぎるのがまずい気もする。

うーん悩むなあ。

Dirty or Clean

編集済みフラグというものがある。何かバッファに編集を施すとオンになる。:write で保存するとフラグが降りる。それから、undo/redo するとフラグが適宜変更される。

そういった機能をつける。前 2 者は全くめんどくさくないのだが、難しいのは undo/redo との絡みだ。

編集済みフラグとはつまり、ディスク(wasavi では対象の textarea)の内容と差異があるかどうかということだ。保存をした時点での undo バッファの最後のアイテムを参照する変数を用意する(saveAt 変数)。undo/redo すると、内部的な undo バッファのインデックスが変化する。saveAt と undo バッファのインデックスが指すアイテムが同じであれば、差異はない状態。同じでなければ保存前の状態に戻っている・あるいは保存後編集を加えている、どちらも含めて差異がある状態と判断できる。

というわけでそういうふうに組んだ。vim 同様、ステータスラインのファイル名のおしりに、差異があれば “[+]” と表示するようにした。