file i/o #6: write

結局のところ、HTTP PUT で保存するようにした。

jsOAuth はリクエストを生成するためのメソッドとして、より高位な順に

  • fetchRequestToken()、fetchAccessToken()
  • get()、post()、postJSON()
  • request()

を提供している。PUT はないので新しく put() を書くか、request() を直接呼び出す必要がある。が request 自体が PUT に微妙に対応し忘れている箇所があるのでそこはちょこちょこっと修正する必要はある。

さて jsOAuth を使ったとしても、dropbox にアクセスするトランスポートはつまるところ XMLHttpRequest なのだが、xhr で PUT する場合、つまり open() 時に ‘PUT’ を指定して send() 時に文字列を引数にした場合、その文字列は UTF-8 のバイト列に変換され、リクエスト自体が Content-Type: text/plain;charset=UTF-8 とみなされる(仕様)。これでは、UTF-8 以外のエンコーディングに対応できない。

send() の引数として文字列ではなく、任意のエンコーディングのバイト列に変換した ArrayBuffer にすれば、この制限は迂回できる。最新の API の実装度に関しては Chrome と Firefox に対してだいたい 2 周遅れであるところの Opera もかろうじて ArrayBuffer くらいは対応している。しかし javascript では自前のテーブルを用意するほかに UTF-16 から任意のエンコーディングへ変更する手段がない。どのブラウザも内部では UTF-16 でテキストを保持し、自身がさまざまなエンコーディングを UTF-16 へ変換するための機構を備えているのだから、それを利用したインターフェースを javascript に公開してくれてもいいのに、ないのだ(Firefox はたぶんある)。

これを解決するには、dropbox とのやり取り自体を、ブラウザと dropbox 間で直接ではなく、ここのサーバを経由させ、サーバ上で適宜適当なエンコーディングへ変換させつつ行う必要がある。ブラウザと dropbox とのやりとりを代理で行うというのはつまり、OAuth を用いたアクセスを代理で行うということだ。サーバ上で OAuth アクセスを行うには単にこれを利用させていただくだけでいい。そして実はこの方が、dropbox から割り当てられたコンシューマ キーとコンシューマ シークレットをエクステンションのソース内に埋め込まなくてもいい(そのあたりの質問)のでむしろ筋がよかったりする。

でもなー dropbox とのやり取り自体速いわけでもないのに、ここのサーバを経由させるもっともっさり感漂うことになってしまうなー……。

file i/o #5

edit コマンドも作った。+command もある。でもこれ、ワンライナーで vi コマンド実行するときくらいしか使わないよね。vim 独自の ++command はエンコーディングを変えて読み込みなおす際に結構使うけど。エンコーディングに関してはまだちょっと分からない点があるので保留。

なおファイル名だけど、[scheme]/[path] という形にする。dropbox 上のファイルの場合は dropbox://foobar.txt みたいな感じになる。複数のオンラインストレージに対応した場合に、スキームを省略した場合はどうするか? たぶん wasavi のオプションページで「スキームを省略した場合の規定のストレージ」みたいな感じで適当なものを設定することになると思う。また、ex コマンドで

:changescheme dropbox

のように動的に切り替えられるようにする、かもしれない。

ということで次は保存だ。REST api で dropbox へ保存するには URL /files/path へ POST するか、/files_put/path[?options] へ PUT するかの 2 通りの方法がある。汎用的なのが前者で、簡略化されているのが後者という扱いだ。いずれにしても、保存するのだからバッファの内容を送信するわけなのだけど、前者は Content-Type:multipart/form-data で送る必要があり、また後者はもちろん PUT リクエストを行える必要がある。

が、jsOAuth の場合、ファイルの送信は POST する場合は FormData に依存する、つまり <input type=”file”> を経由してユーザがローカルファイルシステム上のファイルを指定する必要がある。従って javascript のみで完結させることができない? PUT については、そもそも jsOAuth が PUT で送る手段自体を提供していない。結局のところこの辺は jsOAuth 自体をいろいろ弄る必要があるようだ。

file i/o #4

ex コマンド read コマンドは動くようになった。
内部の動作ではなく、操作の流れを主体にすると以下のようになる:

  1. :r file nameと入力
  2. 別タブで dropbox 上のページが開き、api へのアクセスの許可を求められる
  3. 許可する。ページは自動的に閉じ、wasavi を開いているタブが再度アクティブになる
  4. ファイルが読み込まれるまで待つ。認可のフェーズが進むごと、あるいは読み込みが適当に進むごとにステータスライン上の進捗メッセージが更新される
  5. 読み込まれる

もちろん、一度認可を済ませていればあとはまったくローカルで vi を動かしているかのように read コマンドは振る舞う。まあさすがに dropbox からの読み込みは一瞬とは行かないが……。

さて read コマンドは、ファイルアクセス系のコマンドの中でもかなり単純な部類で、本質的にはファイルシステムからのペーストみたいなものだ。ただ以下のことを考える必要はある:

  • 改行コード。今更言うまでもないが、\r と \n と \r\n 系がある。\r は OS X より前の Mac OS だろうから対応する意味はないかもしれない。行ごとの読み込み時の改行コードを覚えておいて書き出し時にはそれを再利用するのと、新規に行を挿入する場合は読み込み時に最も多かった改行コードを用いる、あたりがポイントか? もっと単純に単体のファイルには 1 種類の改行コードのみが使われると決め打ちしてもいいかな
  • エンコーディング。世の趨勢、特に web 系は UTF-8 固定でいいじゃんという感じになりつつあるようなないような感じだが、まだ他のエンコーディングを一切考えなくてもよいというわけでもない。でも javascript でエンコード変換って重いしテーブルはでかいしどうもなー。appsweets.net 上に適当なプロクシサービスを置いてそれを利用するか? しかしそういったプロクシを通すなら、そもそも OAuth の処理自体もサーバでやったほうがいいじゃんという気も……

それはそれとして、読み込みの次の段階は edit コマンドだ。こちらは read に比べるとなかなか手ごわい。

e[dit][!][+command][file]
コマンド名に「!」が追加されておらず、かつバッファが変更済みの場合、エラーになる。

file が指定されている場合、バッファの内容は file の内容で置き換えられ、file のパス名がバッファに関連付けられる。file が指定されていない場合、バッファの内容はバッファに関連付けられたパス名の最新の内容で置き換えられる。何らかの理由で file の内容にアクセスできない場合、バッファは空になる。

+command オプションは空白で区切られる。+command 内の空白文字はバックスラッシュを前置することでエスケープできる。+command はバッファの内容が置き換えられ、現在行と現在桁がセットされた直後に ex コマンドとして実行される。

現在行と現在桁は以下のようになる:

  • バッファの内容が空である場合:
    現在行は 0 になる
    現在桁は 1 になる
  • そうではなく、ex コマンドモードで実行されたか、もしくは +command 引数が指定されている場合:
    現在行はバッファの最終行になる
    現在桁は最初の非空白文字になる
  • そうではなく、file が省略され、現在のパス名が用いられた場合:
    現在行はバッファの先頭行になる
    現在桁は最初の非空白文字になる
  • そうではなく、file が以前に編集されたファイル名の場合:
    現在行は最後に編集した位置を覚えていればその位置になる。覚えていないかその値がバッファの新しい内容において不正である場合は、バッファの先頭行になる
    現在桁は最後に編集した位置を覚えていればその位置になる。覚えていないかその値がバッファの新しい内容において不正である場合は、最初の非空白文字になる
  • そうではない場合:
    現在行はバッファの先頭行になる
    現在桁は最初の非空白文字になる

file i/o #3: hey, poll

OAuth を用いた api の使用認可を行うわけなので、途中で dropbox.com のページ上で wasavi アプリケーションに対してアクセスを許可していいか? と聞かれる。ページには「許可」ボタンと「拒否」ボタンがある。また、拒否の意味を込めてページを閉じる場合もありえる。あるいは件の認可に関係のない別のページへ遷移する場合もある。

  1. 許可ボタンをアクセスした場合はその情報がページから dropbox へ送信される。サーバからのレスポンスで oauth_callback で指定した url へリダイレクトされる
  2. 拒否ボタンをアクセスした場合はその情報がページから dropbox へ送信される。ページは dropbox.com/home へリダイレクトされる
  3. ページを閉じた場合は、当然だがそのタブは消滅する
  4. 他のページへ遷移した場合はそのタブが指すアドレスが変化する

そういったユーザのアクションをエクステンションのバックグラウンド側で受け取りたい。wasavi の実行はユーザのアクションを待って止まっている状態なので、許可であれ拒否であれ全てのアクションを認識する必要がある。

コールバックで指定した url に対してコンテントスクリプトを仕掛けておく単純な方法では、1. 以外を受け取れない。どうするかなー。

 * * *

ということで表題の通り、ポーリングすることにする。とりあえず Opera に限って考えると、タブに新しい文書が読み込まれるごとにバックグラウンド側の opera.extension.onconnect でイベントを取れる。この中で、https://www.dropbox.com/1/oauth/authorize を指す文書が生成されたら opera.extension.tabs.getAll() でタブの管理情報(BrowserTab オブジェクト)みたいなものを得る:

opera.extension.onconnect = function (e) {
var targets = [];
opera.extension.tabs.getAll().some(function (tab) {
if (tab.port == e.source) {
targets.push({tab:tab, startUrl:tab.url, callback:function () { ... }});
return true;
}
});
};

この管理情報は、スナップショットではなくライブなので、これを監視すればナビゲーションの変化を追跡できる。適当な間隔(1秒くらい)でタイマを動かし、タブが指す url が変わっていたら認可処理を続行する。特に、コールバック先の url になっていたら api の使用を許可されたとみなすことができる(もちろんリクエストトークンを受け取ってユーザの認可待ちという oauth の内部状態とも合致する必要がある)。

file i/o #2

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

  1. fetchRequestToken()、fetchAccessToken() というメソッドが用意されていて、その名のとおりの働きをするのだが、しかしリクエストが GET 固定なのだ。dropbox では該当する api は POST で投げることになっている(といいつつ、GET で投げても受け付けてはくれる……今のところは)のでちょっと困る。

    そういうわけでこれらのメソッドを使わず、下位のメソッド post() を呼ぶしかない。上記の fetch*() でリクエストメソッドを指定できたら嬉しいなー

  2. リクエストを投げた際にエラーが帰ってきた場合、http のステータスで何のエラーか判断したいのだが、request() は xhr.status を返してくれない。返してくれたら嬉しいなー
  3. まだ Firefox では動かしていないが、ソースを見る限り、XMLHttpRequest を生成する箇所が

    } else if (typeof require !== 'undefined') {
    // CommonJS require
    try {
    XHR = new require("xhr").XMLHttpRequest();
    } catch (e) {
    // module didn't expose correct API or doesn't exists
    if (typeof global.XMHttpRequest !== "undefined") {
    XHR = new global.XMLHttpRequest();
    } else {
    throw "No valid request transport found.";
    return null;
    }
    }
    }

    というような感じなので、たぶん Add on SDK 下ではたぶんエラーになる。SDK では

    require("api-utils/xhr");

    なのだ。Add on SDK に対応してもらえるか、xhr のファクトリを外に出してくれると嬉しいなー

file i/o

ひとまず dropbox から任意のファイルを読み出すことを考える。

dropbox の REST api を呼び出すには、OAuth が必要だ。そこでまず javascript 版の OAuth ライブラリをぐぐってみる。

あたりが有名? コードをざっとみた感じ、jsOAuth の方がよさそうなのでこっちを使ってみる。

OAuth に手をつける前に、wasavi 側の都合を考える。やはり同期・非同期の仕組みがここでも必要になる。読み込む処理はバックグラウンド側で行うためだ。そこで、コマンドを非同期で行うかどうかをレジスタ * を参照しているかだけではなく、それぞれのコマンドごとにフラグを持たせる。今回の場合は ex コマンド read を新設し、multiAsync フラグをつけておく。

読み込む処理の流れは以下のようになる:

  1. ex コマンド read が入力される
  2. コマンド エグゼキュータが非同期モードで開始する
  3. read コマンド本体が実行される
  4. バックグラウンドへ入力されたファイルパスを送信する。ex コマンドの実行はいったんここで中断する。しかしながら、依然として ex コマンドの実行中という状態には変わらない。この間 vi コマンドのキー入力がされても無視する
  5. ここからバックグラウンド側の処理: dropbox に対してリクエストトークンを要求する
  6. リクエストトークンを得たら、ブラウザの新しいタブで dropbox 上のapi 使用の認可ページを表示する。ユーザは wasavi に対して、dropbox 内のファイルのアクセスを許可する
  7. ページは指定したコールバックページにリダイレクトされる。コールバックページが表示された = ユーザが許可したことをバックグラウンド側で認識する
  8. dropbox に対してアクセストークンを要求する
  9. アクセストークンを得たら、dropbox に対してアカウント情報を要求する
  10. アカウント情報を得たら、確かに wasavi が使用する dropbox アカウントであることを確認する

  11. 確認が取れたら、dropbox に対して入力されたファイルパスの内容を要求する
  12. 内容を得たら、wasavi へ送信する
  13. ここから wasavi 側の処理: wasavi 側で内容を受信したら、バッファへ書き込む。これで read コマンドが完了
  14. 後続する ex コマンドを実行するため、コマンド エグゼキュータの実行を再開する
  15. 全てのコマンドを実行し終えたら、コマンド実行中のフラグを降ろす。通常の vi コマンドモードへ戻る

という感じ。

このほか、

  • バックグラウンド側で何らかのエラーが発生した場合も、wasavi 側への送信は行う。エラーメッセージを送信する
  • 取得したアクセストークンはバックグラウンドが保持するローカルストレージかセッションストレージに保存しておく。セッションのほうがいいかなあ?
  • アクセストークンを取得済みであれば、1. ~ 8. はスキップしてよい
  • アカウント情報の確認をどうするか? 各ユーザが dropbox へログインする際のアカウント名を wasavi のオプションで登録しておき、それと比較するということになるのだが、dropbox の場合送られてくるアカウント ID は謎の数字っぽいんだよなー。

なお javascript 版の dropbox api ラッパーというものもあるのだけど、これは使わない。最終的には dropbox だけでなく SkyDrive や Google Drive あたりにも対応させたいので、抽象化したクラスを自分で書いたほうがいいと思う。

stand alone

ロードマップのうち、まず stand alone form に手をつけてみる。

通常 wasavi が動作する場合は、任意の web ページに iframe を生成し、http://wasavi.appsweets.net/ の内容を表示する。このページ自体は単にプレースホルダで、内容はほとんど空っぽだが、この URL に対してエクステンション内のスクリプトが注入されて wasavi を構築する。

上記のアドレスを、iframe の内容ではなくブラウザのトップレベルとして表示した場合は、stand alone form として動作するようにする。

とまあ、そういう風に動作させること自体は別に難しくはないのだが、今の状態では編集したテキストを読み込んだりも書き込んだりもどうすることもできないので(ただしクリップボード経由のやりとりは可能)これでは実用にならない。

というわけで、オンラインストレージとの接続部分を作ることになる。作るというと嘘になるかもしれない。この辺ってすでにいろいろライブラリありそうだからそれらを比較研究した上で使わせてもらおうかなーという魂胆だ。

clipboard access #2

とりあえずこんな形にした:

  • キーボードからの入力によって vi/ex コマンドが直接実行される際に
  • レジスタを示すプリフィクス “* が入力されているか、レジスタに * を指定した ex コマンドである場合

コマンドの実行にさきがけてクリップボードの値を読み込み、そのコールバック内でコマンドを非同期的に実行する。コマンドの実行は setTimeout(command, 0) で独立したイベントキューに登録され、個別に行われる。ex コマンドは | で連結できるが、それもはやりコマンドごとに個別に実行される。

ということで、普通に使う分にはクリップボードを参照しながらの編集ができるようになる。

普通ではない使い方というのは vi コマンド @ や、ex コマンド @、または global/v で指定したサブコマンド、および外部インターフェースから vi コマンドを実行する場合で、これらはキーボードからの入力によって直接実行されるわけではない(つまり、実行のコンテキストがネストされている)ので、クリップボードの読み込みはできない。ネストされたコマンドは常に同期的に実行される。

バックグラウンドとの通信を同期的に行う仕組みがあれば別にこんなことはしなくて済むのだがなー。

clipboard access

前置き: vi では、削除やヤンク(コピー)した内容はレジスタと呼ばれる領域に格納される。これは vi 内で完結しているので、他のアプリケーションで参照することはできない。だが vim の場合はレジスタ名に「*」を用いるとクリップボードに対する読み書きになり、他のアプリケーションとの連携が可能になる。

で、ブラウザのエクステンションでクリップボードへのアクセスが可能かどうか。そういう API が提供されているかどうか。

  • Opera
    Opera では、クリップボードへのアクセスはできない。まあ Opera だからね!
  • Firefox
    clipboard モジュールが提供されている。読み書きともにできる。ただし後述するような Chrome と同じ課題はある。
  • Chrome
    いつも 3 つのブラウザの機能を比較するときは Chrome を最初に持ってくるのを、今回は一番最後なのは訳がある。

    Chrome でもクリップボードへのアクセスは可能だ。まず manifest.json の permission に “clipboardRead” や “clipboardWrite” を追加する。

    クリップボードとの実際のやり取りは、専用の API は提供されておらず、IE 起源のあやふやなメソッド execCommand() を用いる。ドキュメントに適当な textarea 要素を生成し、その内容をバッファにする。

    読み込むときは

    var ta = document.getElementById('clipboard-buffer');
    ta.value = '';
    document.execCommand('paste');
    // ta.value はクリップボードの内容になっている

    書き込むときは

    var ta = document.getElementById('clipboard-buffer');
    ta.value = 'コピーしたい内容';
    ta.select();
    document.execCommand('copy');

    ただ、いくつか制限がある。クリップボードとのアクセスはバックグラウンドスクリプトでしか許されない。コンテントスクリプトではできない。したがって読み書き共にバックグランドへの通信と組み合わせる必要があるのだが、組み合わせること自体は大した問題ではなく……問題は、Chrome の場合、異なるドキュメント同士の通信は必ず非同期で行われることだ。これはクリップボードへの書き込みはともかく、読み込みの際に面倒なことになる。コンテントスクリプト側で、こんな感じでバックグラウンドに対してクリップボードからの読み込みを要求する:

    getClipboard: function () {
    // 1
    var result = '';
    this.postMessage({type:'get-clipboard'}, function (req) {
    // 2
    result = req.data || '';
    });
    // 3
    return result;
    }

    これは 1 -> 3 -> 2 の順で実行されるので、単にこのメソッドを呼び出すだけではクリップボードの内容を得られない。wasavi 側のコマンド実行処理自体を非同期な仕組みにしないといけない。

    うーん。非同期ベースに書き換えるのはやぶさかではないけれど(大量の行に対して :s や :g を実行することを考えると、コマンドの実行を細切れにする、究極的には Web Worker に担当させるのはもっともな話だ)、そうすると将来 wasavi をプラグインによってスクリプタブルにした場合、プラグイン側も非同期を意識して書かないといけなくなる。これはちょっとお手軽感台なしだなーという気が。

    いやそれ以前に自動テストのためのコードも全て非同期式に書き換えないといけないのだけど。自動テストでは

    Wasavi.send('dd');
    assertEquals('1\n2\n3', Wasavi.value);

    みたいな感じなのだ。非同期にすると Wasavi.send() で送ったコマンドの実行が完了する前に assertEquals() に達してしまってテストが失敗する。

うーん! 困ったわぁ。

なんとか同期的な通信ができるか、コンテントスクリプト側でクリップボードの読み書きが行えればとりあえず解決するんだけどなー。

a loadmap

ぼちぼち各ブラウザの公式エクステンションサイトに公開していい頃かと思う。

それはそれとして、これからのロードマップを思い描いてみよう。

  • オンラインストレージとの連携。:read file で dropbox とかからファイルを読み込んだり、:write file で書き込んだりする。ただし、:edit は stand alone form(後述)のときのみ有効。
  • stand alone form。通常 wasavi は、あるページの textarea に寄生というか共生する使い方をする。これを symbiotic form とするならば、対して stand alone form があっていい。たとえば appsweets.net 内の特定の パスにアクセスすると自動的に wasavi がページに最大化する形で起動する。この形態では独立したテキストエディタとして振る舞う。オンラインストレージ上のファイルの編集、保存が可能。
  • syntax hilighting。ただ、symbiotic form の場合にどうやってファイルタイプを決定するか?
  • scripting。バッファを編集するためのインターフェースを公開し、後付の javascript で制御できるようにする。プラグインスクリプトをどこに置いてどうアクセスするか?
  • theming。今は決めうちで背景白の文字黒の…としているが、これもまたテーマを後付できるようにして、実行時に選択できるように。

全部実装できるのかなー。