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 同様、ステータスラインのファイル名のおしりに、差異があれば “[+]” と表示するようにした。

Clipboard API

wasavi からクリップボードを操作する場合、vim と同様レジスタ “* を使って y/d/p する。Chrome と Firefox はエクステンションがクリップボードにアクセスする API を提供しているので、問題ない。

問題は Opera なのだ。例によって。

一方、Opera は 12.50 で Clipboard API をサポートするという。これは何かといえば、ユーザがブラウザの提供するカット、コピー、ペースト機能を呼び出した時に、その本来の働きが行われる前にイベントとして取れるようにする、またそのイベントハンドラの中でクリップボードにアクセスできるようにする、その辺の仕様を規定したものだ。

重要なことに、これはカット、コピー、ペースト機能自体をスクリプタブルにするものではない。そういうことはできないと仕様に明示してある。ユーザが ctrl-c を押した時にセレクションの内容にかかわらずクリップボードには “ほげほげ” とコピーする的ないたずらはできるが、ユーザの知らないところでクリップボードを読み書きすることはできない。そういうあくどいことはともかく、たとえばブログでよくあるコードサンプルでの「ボタンを押すとコードをコピー」的なものもできない。

というわけで、Composition Events 同様迂回策を考える必要があるのだが……。まずクリップボードへの書き込みに関しては、かつては Flash を経由してできたかもしれないが、今は無理。読み出しに関しては、input モードで ctrl+v が押されたことを前述の Clipboard API を通じてキャッチし、デフォルトの動作をキャンセルした上で wasavi の動作として貼り付けることはできると思う。使い勝手は端末から vim を使ってる時に input モードで端末の機能を使ってペーストする感じに似ている。

ctrl+v はもちろん wasavi では vim 同様の特殊な文字入力のプリフィクスなので、やはり vim 同様 ctrl+q もプリフィクスとして認めるようにして ctrl+v は開けておく必要がある。

しかしこうしてみるとととても中途半端なものにしかならないなあ。ダメだなこれは。はやく Opera もエクステンションでクリップボードの読み書きできるようにしてください。

wasavi, come here

wasavi を呼び出すのに、textarea 内で特定のショートカットを押す。このときの手法は、コンテントスクリプトで document に capture フェーズの keydown イベントハンドラを引っ掛けるという極々ふつーのものだ。ふつー過ぎて、ページによっては時々この keydown イベントの監視による wasavi の起動がなされないことがある。

コンテントスクリプトはページ自体が持つスクリプトよりも早い段階で実行され、keydown イベントハンドラの登録もページスクリプトより先だ。イベントハンドラの呼び出しは登録順なので、つまりキー入力に対して wasavi の起動監視が最も早く行われるはずなのだが、なんで起動がなされないのかちょっと不思議。いろいろ追いかけてみる必要がある。あるのだけど、とりあえずのワークアラウンドとして textarea 上のコンテキストメニューに「Run wasavi」的なものを追加して、そこからも起動できるようにした。Opera にも 12.10 からエクステンションによるコンテキストメニューのアクセスが可能なのだ。ってすごい最近ですね。

ちなみにエクステンションによっては、hotkey 的な API を提供しているものもある。たぶんそっちを使ったほうが強力なのだろう。でもざっと各エクステンションの仕様を見る限り Chrome の commands API では動的にキーバインドを定義できない? manifest に記述してそれだけなんだよね。というわけでまだそういうものは使っていない。Opera にそういう API ないし。

Opera と言えば、こうやって wasavi で書いてる間もなんか起動したりしなかったりする。これはどうも wasavi の問題ではなくて Opera のキャッシュがぶっ壊れている(iframe に読み込んだ http://wasavi.appsweets.net/ に対して、その injected script である wasavi.js が実行されない、ときがある)っぽいのが困る。設定からキャッシュを全削除して Opera を再起動すると直る。直らなかったりする。やめてよねそういうの!

wasavi 0.5.280 released

リリースした。

変更点
  • キー入力がバッファリングされた後再生されなかったのを修正
  • 入力した行が textwidth を超えてしまうことがあるのを修正
  • 実行中に上書きした設定がページリロード後に再生される機能を実装
  • Opera で実験的・擬似的に Composition Events が動くようにした
  • 入力モード中のパフォーマンスを修正
  • J コマンドが失敗するのはカーソル行が最下行にあるときのみにした
  • Google 日本語入力使用時、不要なコントロールコードが入力されることがあるのに対処
  • Firefox 18 で ctrl+[a-z] を押下した時のキーコードが変更されているようなので対処
ダウンロード

 * * *

Chrome、Opera、Firefox の各エクステンションのリポジトリに更新を申請した。

Spring is approaching

ぼちぼち Opera でも安定して input モードが動作する頃になってきたような気がする。そろそろ公開するかな。ただ *nix で Composition Events のエミュレーション自体が不可能なのは対処のしようがないのが残念。*nix と書いたのはたぶん BSD とかでも同じだろうということで(試してはいない)。

ただまだ undo ログが不正になる時があるんだよなー。それを直したら公開しよう。

undo はほんとうに難しい。もしかしたら根本的に書きなおさないとダメかもしれない。

Similarity and Edit distance

ページのリロード後も設定を持ち越す、という動作を実装する。先に書いたとおり、難しいのはリロード前に wasavi を起動させ設定を追加した textarea が、リロード後に必ずしも同じノードパスとは限らない、という点だ。これは、ノードパスを文字列化したもののレーベンシュタイン距離をとり、適当な閾値以内かどうか、で判断すれば良いと思う。

一方、ページのリロードではなくフォームの送信のように URL が変化する場合もある。また、URL のパスは同一でも、クエリ文字列やフラグメントが微妙に変化する場合もある。つまり URL についても類似性を見る必要があるかもしれない。例えば example.com/?foo=bar 上の textarea に対する設定は、example.com/?foo=baz をブラウズしたときも同様に再現されてほしい。ただしこれは微妙かもしれない。クエリ文字列やフラグメントが 1 文字変化しただけでまったく異なる内容を提供される場合もないとは言えないからだ。しかし、類似性を見て得る情報はしょせんエディタの設定だ。間違えたところで、今のところはそんなに危険というわけでもないので、まあいいだろう。

URL については構成部品ごとに分けて考える必要があると思う。パス部分は曖昧な類似性は関係ない。同じか否かで見る。一方クエリ文字列は難しい。& で連結される key=value の部分は順不同だからだ。?foo=1&bar=2&baz=3 と ?baz=3&foo=1&bar=2 は実質同じクエリ文字列だが、レーベンシュタイン距離は結構な値になる。そうすると、クエリ文字列の類似性はレーベンシュタイン距離ではなく、trigram による similarity measure を見たほうがいいかもしれない。

 * * *

そんな感じで組んでみた。覚えておく URL は直近の 30 件、類似性の閾値は 0.8 とした。