appsweets akahuku labs.

Recently Posted Articles:

187 テスト、660 アサートになった。最終的に 200 弱のテストになるかと見積もっていたが、ぜんぜん足りなかった。あと c/y/>/< のオペレーション、undo/redo、ex コマンドのテストが残っている。これらを揃えると、400 テストくらいにはなるかもしれない。いつ終わるのだこれは。

テスト書いてる間に Opera も DOM3 composition event が実装されないかなあ……。Opera といえば、widget と unite はディスコンらしいですね。widget はともかく、unite はちょっともったいない。

ちなみに wasavi の開発は主に Opera 上で行っている。これは Opera の javascript デバッガである Dragonfly がなかなか使いやすくなっているからだ。具体的にはショートカットキーを自由に定義できるから。Firebug や Chrome の Developer Tools を含む大体の GUI 系のデバッガって VisualStudio の影響なのか step in/step out/step over がファンクションキーに割り当てられているが、step in を「i」、step outを「x」、step overを「n」、検索フィールドへのフォーカスを「/」、ソースのスクロールを「f」「b」とかにするだけでずいぶんと快適になるのである。ただこれだけのことだが、かなり違う。

ただ
  • 再定義したショートカット、どうも Opera を終了するときれいさっぱり忘れるっぽい
  • ときどきブレークポイントを張ってない行で一時停止することがある
  • ときどき一時停止する箇所とソースの対応が取れていない(}だけの行で止まるとか)ことがある

あたりは直してほしい。あとは満足。Dragonfly の開発チームに感謝したい。
前の記事で vim -C で起動するとより vi 互換な状態で起動するとか書いてしまったが、:set compatible? すると互換になってない(vim 7.3 on cygwin)。

な なんだってー! と素で言ってしまったぞ。vim -c ":set compatble" で起動させたほうが確実か。

vi 互換の vim は、だいたい nvi と同じような動作をするようだ(nvi のバグっぽいものを除いて)。

 * * *

d/foo<CR> と入力することを考える。つまり、カーソル位置から、前方にある "foo" の直前までの領域を削除する。この際、領域の左端と右端について、「それら端点から行頭までの間に、空白以外の文字があるか、またはないか」という条件を考える。したがって左端と右端との組み合わせで計 4 パターンが存在する。

== pattern 1 ==
    def
foo
baz

== pattern 2 ==
abc def
foo
baz

== pattern 3 ==
    def
ghi foo
baz

== pattern 4 ==
abc def
ghi foo
baz

いずれのパターンにおいても、1 行目の「d」にカーソルを置き、d/foo<CR> と入力する。

どうですか。それぞれのパターンでどうなるかわかりますか。 ...
vi/vim にはマークというものがある。

a から z、および A から Z までのマークがあり、任意のマークに行番号と桁番号のセットを覚えさせておくことができる。覚えさせるコマンドは m{mark}。覚えておいたマークにカーソルを移動させるのは `{mark} または '{mark}。

さて重要なことに、覚えたマークはその行番号と桁番号に依存するわけではない。

どういうことか。

foo
bar
baz

というテキストの 2 行目にマークをつけたとする。次に、テキスト先頭に新しい行を追加する。

FOO
foo
bar
baz

するとマークが記憶している行番号は自動的に補正されて 3 行目を指すようになる。

ということで、wasavi でもそういう風に実装してある。してあるのだが……勘違いしていた。この自動補正は桁位置に関しても行われると思っていた。つまり

foobar
   ^ここにマーク

という行(下線位置にマークがあるとする)に
fooFOObar
      ^ここにマーク

というふうに文字を挿入するとその分だけ桁位置も自動補正されると思ってたら、vi / vim とも桁位置に関してはそういうことはしないのね……。

でもまあとりあえず残しておく。

あと、ここまで書いて気付いたが、このネタは別に vim と vi 間の違いというわけではなかったがまあいいや。
barbaz と書いてある行の末尾にカーソルがあるとする。

vim 7.3 (-C オプション)
  1. Tb と入力: barbz
  2. Tb と入力: barbz(カーソル位置は変化しない)
  3. ; と入力: arbaz
  4. $Tbd; と入力: bz(すなわち、baz の下線部分が削除される)


nvi 1.79
  1. Tb と入力: barbz
  2. Tb と入力: barbz(カーソル位置は変化しない)
  3. ; と入力: barbz(カーソル位置は変化しない)
  4. $Tbd; と入力: bar(すなわち、barz の下線部分が削除される)


vim の場合、実質的に T である ; の結果(つまり、コマンド終了後のカーソルが置かれるべき位置。T の場合は見つかった文字の右となり)が、走査前のカーソル位置と同じであった場合は、それを無視する、というような特別扱いをしている、気がする。

nvi はそういうことはしない。さらに 4. は不思議な動作だ。削除される領域の左端、右端の両方について inclusive に動作している?

えーと……どうすればいいんだー。

 * * *

vim にあわせることにした。また、; だけでなく , についても上記の特別扱いを施すようにした。
たとえば 10 行のテキストがある状態で 100j とか 100k とか入力すると、vi はテキストの先頭・末尾行を超える移動はエラーになり、カーソル位置は変化しない(posix でそう定義されている。また nvi 1.79 はそう動作する)のに対し、vim はカウントをクリップし、先頭・末尾行へ移動する。

vim というのは今でこそ vi よりも遥かに多くの機能を持っているが、そもそもの起源は Amiga 用の vi クローンだった、そうだ。今でも、vim -C としたり :set compatible としたりするとより vi 互換になる。

が、それでも違うところは違うということだ。

さて wasavi はというと、本質的には posix の定義にできるだけ従う vi クローンである。vim クローンではない。これは、vim の機能の再現なんかしだしたら何年たっても実装が終わらないからだ。と言っても pure vi にこだわるわけでも vim の機能を無視するわけでもなく、複数の undo/redo、レジスタ「"」「:」「*」「/」、iskeyword/searchincr/smartcase/undolevels の設定なんかは vim から輸入している。

で、テキストの先頭・末尾行を超える移動をどちらに合わせるかなのだが、vim に合わせようと思う。たぶんそのほうが使いやすい。また、任意の桁に移動する「|」モーションとの対称性もとれる(「|」モーションは vi であっても、行末でクリップされる)。
iframe 化が形になったので 0.2 として公開した。
http://appsweets.net/wasavi/
wasavi は textarea 要素を vi っぽいテキストエディタに変化させる。

この手のエクステンションで実用に足るものは今のところあまりない(ブラウザ全体の操作性を特定のテキストエディタっぽくするエクステンションは別)わけであるが、html5 や DOM3+ とともに浸透する、かもしれない。

ところでこのような textarea の拡張はサーバ側で提供する可能性もある。そうすると、ブラウザ側のエクステンションとかちあってしまう可能性がある。textarea 要素自体が自分を拡張してよいか表明する必要がある。またブラウザ側のエクステンションはそれを尊重しなければならない。

そこで、対象の textarea 要素への data-texteditor-extension 属性および data-texteditor-extension-current 属性の導入を提案したい。

data-texteditor-extension 属性の値が none である場合は、ブラウザにインストールされたどのエクステンションも、textarea 要素に対する変更を行わない。

そうではなく、data-texteditor-extension が任意の文字列である場合は、その任意の文字列を自らの ID として認識する特定のエクステンションが textarea 要素への拡張を行う。つまり、<textarea data-texteditor-extension="wasavi"></textarea> とマークアップされていて、wasavi がブラウザにインストールされていれば、wasavi だけが textarea を拡張する。

そうではなく、data-texteditor-extension が auto である場合、もしくは data-texteditor-extension 属性自体が存在しない場合は、各エクステンションは、data-texteditor-extension-current 属性が存在しない限り、textarea 要素への拡張を行う。つまり、あんまりありえない状況ではあると思うが、ブラウザに wasavi と webemacs(というものがあるとして)エクステンションをインストールしてある場合、先に実行されたものが textarea を拡張し、textarea を拡張したエクステンションは、data-textedit-extension-current 属性に自らの ID をセットする。後続するすべてのエクステンションは拡張をあきらめる。

というのはどうか。




wasavi の iframe 化計画。最後に Firefox でも動かすようにした。Opera と Chrome 向けに変更した分でだいたいそのまま Firefox でも動いた。

1 つ困ったのは、page-mod の include パラメータの仕様。

現状、wasavi で content script が組み込まれる URL のルールは以下のようになる:
  • すべての URL(ただし wasavi を表示する iframe の内容となるページの URL は除く)へ次の javascript ファイルを組み込む: extension_wrapper.js、agent.js
  • wasavi を表示する iframe の内容となるページの URL へ次の javascript ファイルを組み込む: extension_wrapper.js、wasavi.js


Chrome の場合、manifest.json で content script を指定する際に include 節、exclude 節があるので上の通りに書く。Opera の場合、Greasemonkey 互換のヘッダを injected script に含めることができる。つまり @include と @exclude が使えるので上の通りに書く。

で、Firefox の page-mod。この API、include 節しかない。exclude がない。上の条件の「なんちゃらを除く」を適用できないわけだ。ふーむ。

PageMod に与える include 節は、match-pattern の MatchPattern を通して、実際の URL にマッチしているかを判定している。MatchPattern は
var mc = new MatchPattern('http://*');
var result = mc.test('http://www.example.com/');

こんな感じで使う。コンストラクタに渡すパターンは「*」を含めることができる。またパターンの型は RegExp インスタンスでもよい。ここで、match-pattern.js を見てみると、おもしろいことに気が付いた。
exports.MatchPattern = MatchPattern;
function MatchPattern(pattern) {
  if (typeof pattern.test == "function") {
    :
    :

RegExp かどうかを確かめるのに、instanceof じゃなくて単に test というメソッドを持っているかで判定している。MatchPattern#test で実際に pattern.test() を呼び出す段になっても、やはり RegExp かどうかにはこだわっていない。

あ、なるほどこれはダックタイピングおっけー宣言だ。つまり test、exec メソッドをもち、global、ignoreCase、multiline プロパティが true でないオブジェクトを渡しても動作する。そして test メソッド内では好きに中身を書いてよい……:
require('page-mod').PageMod({
  include: {
    test:function (url) {
      // url を見て、@include / @exclude に相当する処理を自分で書く
        ;

      return result;
    },
    exec:function (url) {
      return this.test(url) ? [url] : null;
    }
  },
  contentScriptFile:[self.data.url('extension_wrapper.js'), self.data.url('agent.js')],
    :
    :
});

こんな感じ。で、この通りに動く。

リファレンスにまったく書いてないちょっとしたハックなので、Add-on SDK のバージョンがあがると動かなくなる危険性をはらんでるわけだが、まあそのときはそのとき。宵越しの金はもたねえ! という奴だ(ぜんぜん違います)。
Opera 上で大体できてきたので、Chrome で動かしてみたらいきなり壁に当たった。

エージェントのコンテキストで、特定のキー入力がなされたら、iframe を生成し、iframe.contentWindow.postMessage() して必要なパラメータを送る。iframe の内容である html はそれをトリガーとして wasavi を実体化する。

が、Chrome の場合、通常の web ページの Window と content script の Window とは、相互に通信することが許されていないようだ。

つまり、content script から iframe.contentWindow を参照できない。undefined が返される。従って、postMessage() を呼び出すこともできない。Window を操作することが目的なのではなくてメッセージを送出するための手段が Window が持つ postMessage() なだけなのに、ひどい仕打ちだ。

どうするか。

エージェントと wasavi 本体の直接の通信を行わないようにする。これはつまり html5 由来のメッセージングを使わないようにするということだ。そのかわり、バックグラウンド部をプロクシとしてふるまわせる。

バックグラウンド部に対して
chrome.extension.sendRequest({type:'notify-to-child', payload:{type:'run', param:{...}}});

などと送る。バックグラウンド部は、notify-to-child コマンドの payload パラメータを wasavi 本体を内容に持つ iframe へ転送する。また、wasavi 本体からエージェントへの通信もやはりバックグラウンド部を中継する。
switch (req.type) {
case 'notify-to-child':
  var childTabId = 'childTabId' in req ? req.childTabId : extension.lastRegisteredTab;
  if (childTabId !== undefined && extension.isExistsTabId(childTabId)) {
    extension.sendRequest(childTabId, req.payload);
  }
  res();
  break;
}


1 つ問題がある。エージェントから iframe へメッセージを送信する場合、本来はあて先となる iframe のタブ ID が必要である。しかし、エージェントがその content script のコンテキストで iframe を生成し、直後に sendRequest() する時点では、生成した iframe のタブ ID はわからない。送る内容、誰に送るかは知っているが、そいつの住所がわからない。

バックグラウンド側では、Window が生成されるごとに(これは iframe の生成を含む)ユニークな ID を生成し管理している。そこで、最後に生成された ID を覚えておくようにし、あて先であるタブ ID が明示されていない type == 'notify-to-child' なメッセージについては最後に生成されたタブへ送るようにした。

この辺は、Opera で動かす分にはまったく必要ない仕組みなのだが、複数のブラウザのエクステンションで動かす以上、最小公倍数的な振る舞いをせざるを得ない。
いろいろと弄っている。

まず前回の記事の通り、エージェントは単に web ページ上のキー入力を監視し、必要ならば wasavi 本体を起動するだけにした。

バックグラウンドとの通信処理は、wasavi 本体へ移動した。正確には、別の javascript ファイルにした。通信処理というのは具体的には chrome.extension.sendRequest(data, callback) のラッパで、Opera と Firefox の場合にもそれと等価な処理を行うようになっている。特に Firefox+Add on SDK の場合は、バックグラウンドの通信に self.postMessage() と self.on('message', callback) を使うしかなく、各々のリクエストに直接対応するレスポンスコールバックを指定できないので、いろいろ回りくどい仕組みで同じことをできるようにしてある。

この状態では、あらゆる web ページで extension_wrapper.js と agent.js の 2 つの javascript ファイルがアタッチされることになる。また、wasavi 本体となる iframe の内容でも、やはり複数のファイル extension_wrapper.js と wasavi.js がアタッチされる。さてここで、それぞれの読み込まれる順番が気になる。agent.js や wasavi.js は extension_wrapper.js に依存しているので、必ず先に extension_wrapper.js が評価・実行されなければいけない。

いわゆる content script が複数あり、それらがとある web ページに対してアタッチされる場合、その順序はどうなっているのか?

まず chrome の場合、content script は manifest.json で指定するのだが、指定した順番で組み込まれることが文書化されている:

Each item in the content_scripts array can have the following properties:
js array of strings Optional. The list of JavaScript files to be injected into matching pages. These are injected in the order they appear in this array.



次に Firefox の場合は、まだ試してないが、リファレンスを読む限りではやはり指定した順番で組み込まれるようだ。

最後に Opera の場合。Opera では明示された文書は見つからなかった。さすが Opera だ細かいところはまったくやっつけだぜ! しかしこういった 記事 があるので、それを信じることにする。つまり、まずエクステンション名の逆順でループし、エクステンションごとに include ディレクトリ内のファイル名の昇順で組み込まれるとのことである。へー。

もう少し掘り下げてみる。

wasavi の構造は 3 つの部分に分けられる。
  • バックグラウンド部。エージェントと双方向通信できる必要がある
  • エージェント部。各タブの web ページごとにアタッチされ、wasavi 起動のためのキー入力が行われたかを監視する。キー入力されたら wasavi 本体を実体化する。バックグラウンドと双方向通信できる必要がある。各ページに必ずアタッチされる関係上、できるだけサイズを抑える必要がある
  • wasavi 本体。エージェントと同じコンテキストで実行される必要がある。これは、wasavi 本体もいわゆる content_script、injected script と呼ばれるものなので直接バックグラウンドとやりとりはできるが、3 つのブラウザのエクステンションで等しく動作させるためにバックグラウンドとの通信は抽象化され、通信を行う実際の処理はエージェントが持っている。なので、それを呼び出すために wasavi からエージェントが見えなければならないため


さて現在の実装では、エージェントと wasavi 本体は同じ web ページに対応する content_script として実行させているので、上記の条件に合っている。

これが、wasavi を iframe に出すようにすると、エージェントと wasavi 本体との関係が完全に分断されて個別のブラウジングコンテキストを持つようになる。これはかなりドラスティックな構造の変化になる。

やり取り自体は、iframe 上の wasavi から window.parent.postMessage() してエージェントでそれをキャッチとかでできると思うけど、これを使ったら web ページから横取りされうるわけで、分断させる意味がない。web ページからは見えないように、子要素である iframe 上の wasavi から親要素であるページにアタッチされたエージェントとの双方向通信を行う必要がある。

ということで、MessageChannel を使うことにする。

エージェント部
var iframe, iframeChannel;
  :
  :
  :
iframeChannel = new MessageChannel;
iframeChannel.port1.onmessage = function (e) {
    // wasavi からのメッセージを処理
};
iframeChannel.port1.start();
iframe.contentWindow.postMessage({'value':element.value}, origin, [iframeChannel.port2]);


wasavi 本体
var iframeChannel;
window.addEventListener('message', function (e) {
    window.removeEventListener('message', arguments.callee, false);

    if (e.ports && e.ports.length > 0) {
        iframeChannel = e.ports[0];
        iframeChannel.onmessage = function (e) {
            // エージェントからのメッセージを処理
        };
        iframeChannel.postMessage('hello!');
    }
}, false);


とこのように、MessagePort の片方の端点を渡す最初のメッセージだけは window.postMessage を使う必要があるが、それ以降はページからはアクセスすることができないチャンネルを通してやり取りできる。

ところで、そもそも wasavi.js とエージェント間のやり取りが必要なのかという話もある。エージェントの役割は上記の通りなのだが、もう少し内部的なところまで書き出すと
  1. 初期化時にバックグラウンドにメッセージを飛ばす。バックグラウンド側ではエージェントが実行されているタブを管理しているが、それに登録してもらうため
  2. キーボード入力を監視し、textarea 要素上で特定のキー入力があった場合 wasavi 本体を読み込んで実行する
  3. オプションページで設定が変化した場合、バックグラウンドでは管理しているすべてのエージェントへそれを通知する。エージェントはそれを受け取り、さらに wasavi 本体へ中継する
  4. wasavi 実行時に web ページから document/window へ登録されたキーボード系イベントリスナを無効にするためのちょっとした仕組みを組み込む

という感じなのだが、1 と 3 は wasavi 本体へ移しても差し支えない。4 は、wasavi 本体が iframe に乗っていて、かつフォーカスが iframe.contentWindow またはその子孫にあれば iframe のホスト側のキーボード入力は特に触る必要はない。つまり「ちょっとした仕組み」自体が必要ない

ということで、エージェントとしては単純に 2 の機能だけ受け持つようにする、かも知れない。とにかく弄るとなるとけっこう大掛かりになるのでなんとも言えない。

なお wasavi 本体が iframe 別ドメインの内容となったしても、その iframe に対する操作は可能なのでまったく安全というわけではない。このへんを強固にするにはやはりエクステンションの仕組みとして web ページに重ね合わせて表示される特別なレイヤーがあったほうがいいんだけどなあ。
現在、web ページで wasavi を起動させる場合、web ページの DOM を直接参照している。position:fixed または position:absolute な div を追加し、wasavi の構成要素はすべてその div の子要素として追加している。

しかしこれは、逆に言えば web ページから wasavi の構成要素もまた見えてしまうということだ。セキュリティ的にあまりよろしくない。もちろん、window.Wasavi であるとか window.WasaviAgent といった javascript のオブジェクトは web ページからは見えず、公開されるはあくまで DOM の要素だけなのだが、それでも勝手にイベントリスナを引っ掛けられたりすると、何が起こるか保証できない。

ということで、web ページの DOM のコンテキストから wasavi を隠さないといけないわけだが……どうしたものかなー。

extension のシステム側で、通常の web ページよりは手前に表示され、extension のコンテキストからしか操作・参照できない特殊なレイヤーみたいなのを提供してもらえればとても嬉しいのだが、そういったものはない。

他に web ページの一部として表示されるが、web ページから操作・参照できないものというとドメインの異なるリソースを与えられた iframe が該当するが……それをベースにしてみようかな?

つまり、extension のアーカイブ内に wasavi_frame.html みたいなのを用意しておいて、wasavi はその中に構築する。で、iframe の内容として表示させつつ、対象の web ページに置くのである。それで良さそうな感じがする。

で、とりあえず、extension アーカイブ内のファイルを iframe の内容として表示することってできたっけ? とまず Opera で試してみたのだが、これができないのである。エラーになる。ぐぬぬ……。Unite より退化してるじゃん!


116 テスト、427 アサート。

ところでよくわからないことがある。G したり / したりなど、大きくカーソルを移動させるコマンドを実行すると、移動前のカーソル位置がマーク「'」へ保存される。このマークは `` や '' で参照して飛ぶことができる……のだけど、どのコマンドでどの条件のときにマーク「'」を更新するのかの仕様がよくわからない。

とりあえず、vim のソースで setpcmark() を呼んでいるところがそれに該当するようなのだが……文書でまとまってないのかなー。
ちまちまとテストを書いている。現在 78 テスト、261 アサート。最終的には 200 弱のテストを通すことになると思う。

本当は、テスト関数 1 個につき 1 つのアサートが理想なんですけどねえ(test runner はアサートの個数までは管理してくれない)。

モーション単体のテストは書いたが、実際はオペレーション(d、y、c)との組み合わせをテストしないといけない。どう書いたものかな……。

あと、テストを書く際に OpenVim のそれも参考にしているのだけど、このページでは . コマンドのテストが間違ってると思う。w3. で w を 3 回繰り返すかどうかをテストしているが、w は . の対象にはならないはずなので(参考)。
そういうわけで Chrome 版の crx に加え、Opera 版の oex、Firefox 版の xpi を公開している。もちろん完成にはまだ程遠い。平行して弄ったり直したりするので、それに合わせて extension も随時更新されることになると思う。

更新されるのはいいのだけど、公開しているわけなのでバグが入り込むのはできるだけ避けたい。特に、おい以前発生したバグがまた入ってるぞー! とかね。

ちゃんとしたテストプロセスを通すことにしよう。できれば ant の自動ビルドにも組み込めると嬉しい。

といっても実は jsUnit によるテストは早い段階からできるようにはしてあるのだ。で最初のころはできるだけテストファーストを心がけている(いた)のだが、だんだん投げやりに……というとてもありがちな状態に陥っているのであった。

これはとても恥ずかしい。反省しつつちゃんとテストするようにする。そもそも vi の場合入力と出力ははっきりしているので、自動テストはしやすいのだ。

function testEdit1 () {
	Wasavi.run(document.getElementById('t1'));
	Wasavi.send(
		'ihello, world', 
		Wasavi.SPECIAL_KEYS.ENTER, 
		Wasavi.SPECIAL_KEYS.ESCAPE, 
		'ZZ'
	);
	assertFalse(Wasavi.running);
	assertEquals('hello, world\r\n', document.getElementById('t1').value);
}


それで、こんな風なテストをちょこちょこ書き始めたわけだが……ふと思ったのだけど、モノは vi である。30 年以上の歴史と山ほどのクローンがあるソフトウェアである。すでにまとまったテストスイートがどこかあるのではないのか?

しかし適当にぐぐった感じではそんなに見つからない。前の記事の openVim のページに 128 項目くらいのテストがある。それから vim のソース内にはテストが収められている(もちろん vim の機能は vi の何倍もあるわけで、あるからといってそのまま利用できるわけではないが)。kate というテキストエディタは vi の入力モードを備えているらしい。で、ソースも公開されていて、その中にテストが含まれている(kate/part/tests/vimode_test.cpp)。

見つけたのはこれくらい。

このほかに何かないかなー。

Advertisement