Compatibility to VimFx #3

色々やりとりしてみた所、何やら思いがけない方向に進んだ。

まず、wasavi 側で VimFx を意識した処理をする必要はなくなった。逆に、VimFx 側で wasavi の iframe を contenteditable な要素として扱ってもらえるようになった。のだが、途中から wasavi が起動中に blur させる方法がないのが問題、という話になり、正直なところへぁ? なんのこと? という気分に。

どうも VimFx の作法として、編集可能な要素がフォーカスを持っている場合は VimFx は自身のキーバインドを使用しないというものがあるようだ(逆に言うと、編集可能な要素の振る舞いを上書くことは VimFx ではできない)。ただし、編集可能な要素上で esc キーを押すとフォーカスを外す、つまり VimFx のキーバインドが再度有効になるようにはなっている。したがって、例えば通常の textarea で編集中に他のタブに切り替え、必要なものをコピー、戻って貼り付けなどという一連の動作をすべてキーボードで行うには、

  • textarea 上で esc
  • gt (次のタブへ)
  • yf 等々で必要なものをコピー
  • gT (前のタブへ)
  • gi (先頭、あるいは最後にフォーカスした編集可能要素に再度フォーカス)
  • wasavi 上で [cci]”*p[/cci]

というようなキーボード操作になる。で、これを可能とするために、wasavi も esc で blur して欲しいということのようだ。

というわけで、そういう風にした。ただ、この常にこの仕様で動作させて良いのか? という懸念はある。

  • vi 使いの中にはノーマルモードに戻るために esc を連打する人もいる
  • そうでなくても、うっかりノーマルモード上で esc を押してしまうことは有り得る
  • あくまで VimFx の作法である。VimFx がインストールされていれば、wasavi がフォーカスを失っても即 [cci]gi[/cci] で戻すことはできるが、インストールされていなければポインティングデバイスで wasavi をクリックし直すしかない

なので、新しいオプション [cci]esctoblur[/cci] を導入し、これがオンの場合のみに blur 処理を行うようにした。

Compatibility to VimFx #2

動作するようにはなったのだが、やはりイベントハンドラの実引数として渡された何かを取っておいてハンドラのスコープの外で使うというやり方はちょっと嫌な感じではある。

VimFx の issue に何か参考になるものがないのかなと見てみたら、丁度よく wasavi が動かない、正確には Pterosaur や CodeMirror や wasavi 等々、自前で色々キー入力を消費するタイプの拡張と競合する、というが上がっていたので乗っかってみよう。

Compatibility to VimFx

Firefox の拡張には、キーボード周りを含めたブラウザのインターフェースを「強力に」書き換える能力があるわけなのだが、それらと wasavi の相性は一般的によくない。wasavi もまたすべてのキーボード入力を自分で消費するからだ。すると、あるキー入力が他の拡張に横取りされてしまうとか、wasavi と同時に処理されてしまうと言った不具合が起きる。

それを避けるために、VimperatorKeysnail 用のプラグインスクリプトを用意してある。これらは主に 2 つの仕事を受け持つ:

  1. タブの切り替えを監視し、アクティブなタブのドキュメント上に実行中の wasavi が存在するかどうかで対象の拡張を一時的にサスペンドさせるかを制御する(ちなみにこの辺の処理は e10s が有効だと完璧に破綻する: プラグインは Chrome 権限上で動いている、そして content を直接触るためだ)
  2. ドキュメントのカスタムイベント [cci]WasaviStarted[/cci] [cci]WasaviTerminated[/cci] を監視し、各イベントに応じて適宜対象の拡張を一時的にサスペンドさせるかを制御する

ところで、こういったインターフェースを再定義する拡張はもちろんこれだけではない。issue では VimFx との相性がよくないというものが上がっているので、とりあえずそれに対応してみることにしよう。

* * *

VimFx というのはなんぞやというと、実はあまり知らないのだが、本質的には Vimperator と同様に Firefox のインターフェースを vim っぽくする系の拡張である。ただし、Vimperator がインターフェースを「ドラスティックに」変えるのに対して VimFx はもうちょっと大人しいようだ。

プラグインの機構はない。ただし、Chrome 権限のスクリプトから使用できる API が提供されている。

さて、対象の拡張が変わったとしても、やるべきことは上記の 2 点なのは変わらない。ただし 1 は上記のとおり、e10s を見越して content 内で処理する必要がある。普通の DOM のイベントでタブがアクティブ・非アクティブになったというのを判定するには Page Visibility という割と新し目の API で定義される visibilitychange イベントを使えば良いだろう。一方で、wasavi の状態によって他の拡張にアクセスするのは content ではなくバックエンドの chrome スクリプトでなければならないので、つまり visibilitychange イベントでやることはアクティブなタブだったらバックエンドにメッセージを投げる、ということになる。そんなわけで:


window.addEventListener('visibilitychange', function () {
if (document.hidden) return;
extension.postMessage({
type: 'visibility-change',
wasaviRunning: !!wasaviFrame
})
}, false);

というような感じ。

このメッセージをバックエンドで受け取り、wasavi が起動しているようなら VimFx をサスペンドさせればいいのである。

* * *

というわけで色々作って、VimFx の API を使ってどうこうするところまで来たのだが。

ドキュメントによれば、API はそれを提供するオブジェクトのインスタンスを通して使用する。インスタンスの取得は

let {classes: Cc, interfaces: Ci, utils: Cu} = Components
Cu.import('resource://gre/modules/Services.jsm')
let apiPref = 'extensions.VimFx.api_url'
let apiUrl = Services.prefs.getComplexValue(apiPref, Ci.nsISupportsString).data
Cu.import(apiUrl, {}).getAPI(vimfx => {

// Do things with the `vimfx` object here.

})

こんな感じで、ここで [cci]getAPI[/cci] に渡しているコールバックに渡される仮引数 vimfx がそのインスタンスなのだが。困ったことにその中に VimFx をサスペンドさせるようなそのものズバリの機能はないのである。

というより VimFx 自体にサスペンドという概念がない。その代わり、すべての入力を素通りさせて VimFx は関与しない ignore モードというのがあって、normal モードから ignore モードへ遷移することで結果的にサスペンドが行える。で、このモードを遷移するための [cci]enterMode()[/cci] メソッドはどこに属しているのかというと VimFx のコマンド群のそのハンドラに渡される vim パラメータが持っているらしい。しかしこれを API から直接的に得る手段はない。うーんどうすれば……。

しかし、間接的に得る手段はある。[cci]vimfx.on()[/cci] でいくつかのイベントハンドラを登録することができて、その時も vim パラメータが渡される。これを取っておいて、必要なときに使えばいけるかもしれない。

* * *

というわけで、期待した動きをするようになった。

The Traditional vi

issue #113

これはいわゆる vim の compatible mode を実装してくれという要望なのだが、あれを実装するのは大変そうだ。具体的にはテストが大変だ。様々なオプションのオン・オフの組み合わせを網羅するとテスト項目が半端ないものになってしまう。

件の issue の方は最低でも traditional な vi の uu だけでもいいということなのでそういう方向で行ってみたい。

vi の uu というのは、u で一旦 undo した後、続けて u を押すと今度は redo 動作になる、という不思議な動作のことだ。この動作のことを一般に何と言うのかわからないが、とりあえず flipping undo と呼んでおこう。

これをどうやって実装するかは、素直に boolean なフラグを保持しておいて(以下 flipped)、u が押された時にそれを元に動作を振り分けということになるだろう。

いくつか気になる点:

  • flipping undo とふつーの undo をどう切り替えるか? vim では

    :set compatible
    :set cpoptions+=u

    とすることで flipping undo モードになる。しかし前述の通り [cci]compatible[/cci] や [cci]cpoptions[/cci] の類はあんまり実装したくない。ちなみに vim では、flipping undo モードにするための方法はもう一つあり、それはつまり [cci]undolevels[/cci] を 0 にするというものだ。じゃあこれでいいかな。
  • wasavi では vim と同様、[cci]^R[/cci] に redo を割り当てている。flipping undo の場合これは何をするべきなのか? vim の場合は、どうも flipped 変数を変更しない undo として振る舞うようだ。つまり flipping undo 時の u コマンドは

    if (flipped)
    redo();
    else
    undo();
    flipped = !flipped;

    という動作になる。[cci]^R[/cci] もほぼ同じなのだけど、最後の flipped 変数の変更だけは行わない。

Two of three

issue をちまちま片付けていたのだがなぜが次々と新しい issue が上がってくる。なにこれ…と思ったら、3 日に Hacker News で wasavi が取り上げられていたようだ。

前にも書いたが、書いたプログラムが Hacker News と Reddit と Slashdot で取り上げられたらまあある程度浸透したのかもしれないと勝手に思っているのである。Reddit にはすでに取り上げられていて、今回は Hacker News なので 2/3 のスコアになった。

残りは Slashdot なのだが。しかし最近の Slashdot はなんか知らないがとても gdgd な状況になっているようなので、まあいいかなという気分になりつつある。

Access local files from wasavi #4

Chromeでローカルファイルの読み書きの目処が立ったということで、Firefox にも同じ機能を実装してみる。Firefox の拡張からローカルファイルを操作するための API はいくつかあるが、新規に実装するということで最もナウい OS.File を使う。どれくらいナウいのか、なんと言っても完全に Promise と融合したインターフェースになっていると言うことだろう。これは今風だ。ナウいぜイマいぜ超マブいぜ。

ちなみに Presto Opera に対しては同じ機能を実装することはできない。Presto Opera 上の wasavi で file: スキームのファイルの読み書きを試そうとしても必ずエラーになる。前の記事の通り、Opera Unite がポシャらなければそれを経由して何とかなったんだけど……。

もう一つちなむと、かねてから記事にしている通りビューとモデルの分離という大きな作業も並行して行っている。そして、それが完了したバージョンから、wasavi はもはや Presto Opera 向けにはリリースしない。

* * *

実装した。

Chrome と Firefox でローカルファイルの読み書きで最も違うのは、後者はローカルファイルシステムそのものを取り扱うことができるのに対し、前者は LFO アプリケーションというラッパーを通す際にマウント位置が自由であることで実質的にローカルファイルシステムが仮想的になってしまうということだ。これはファイルシステムのルート以外をマウントした場合に違いがはっきり現れる。

実際のファイルシステム上の /home/akahuku を LFO にマウントする。このとき、wasavi を起動する。wasavi 上のカレントディレクトリは [cci]/[/cci] なので、Chrome の場合は wasavi 上で [cci]:r [/cci] したとき補完されるファイル群はホームディレクトリ直下のファイルだが、Firefox の場合はルート直下のファイルになってしまう。

LFO でどこでもマウントできるという仕様自体に何か問題があるわけではないので、これは運用でカバーすべき問題かもしれない。つまり Chrome 版 wasavi でローカルファイルを扱うための現実的な作業手順というのは、

  • LFO でドライブのルートをマウントする
  • wasavi の exrc で [cci]cd /path/to/home[/cci] する

ということになる。

* * *

ローカルファイルアクセスが可能になることでもう一つ影響を受けるかもしれないものがあって、それは exrc だ。ホームディレクトリに .wasavirc を置いておいたほうが楽な場合は多々あることだろう。ただし、ここでも若干問題があり、Chrome の filesystem では現在のユーザのホームディレクトリがどこかを得る手段がない(そもそも現在のユーザというものが、OS にログインしているアカウントなのか、ブラウザの同期サービスにログインしているアカウントなのか?)

これは、LFO でホームディレクトリがどこかも登録させるようにすべきだろうか?

Access local files from wasavi #3

実際に wasavi 上のファイルシステムの1つとして組み込む。

のだが、その前に、ファイルの読み書きの他にもう一つコマンドが必要だ。それは任意のディレクトリ内のファイル群のリストで、いわゆる ls だ。これは wasavi では tab キーによるファイル名の補完時に呼ばれる。ls 的な処理を書いていて、いくつか奇妙なことに気がついた。

Chrome の filesystem API でそれを行うには、

  1. ルートとなる directoryEntry から、任意のパスへの directoryEntry を得る
  2. 得た directoryEntry の createReader() を呼び出す
  3. 得た DirectoryReader インスタンスの readEntries() を複数回呼び出す。

という処理を行う必要がある。

このとき、1. がなんだか奇妙だ。

rootEntry.getDirectory(
'', // path
{}, // options
function (dir) {
// success callback
},
function (err) {
// error callback
}
);

のうち、path。これは絶対パスか、相対パスを指定すると定義されている。rootEntry の実体が [cci]/home/akahuku[/cci] だったとして、例えばその中の [cci]bin[/cci] ディレクトリの下のファイルリストを取りたい場合、

  • bin
  • ./bin
  • /bin

いずれでも同じ結果が返るはずである。しかし、Chrome の実装では絶対パスを与えるとエラーになる。確かに、そもそもこの文脈での絶対パスというものの定義がよく分からない。結局のところ rootEntry からの相対パスとして扱えばいいのか、実際のファイルシステム上の絶対パスとして扱うべきものなのか W3C の仕様定義には書いてない。うーん?

もうひとつ、fileEntry や directoryEntry の親である fullPath プロパティもなんか変。上記の通り [cci]/home/akahuku[/cci] に対応する directoryEntry の fullPath を参照すると、[cci]/akahuku[/cci] なのである。…なんで? 仕様定義では、このプロパティは

The full absolute path from the root to the entry.

である。つまり [cci]/[/cci] じゃないとおかしい。

このへんの辻褄を何とか合わせる必要がある。

Access local files from wasavi

issue を少し消化する。その中に、ローカルファイルシステムのサポートというものがあり、ちょっと考えている。ちなみに、この issue を書いた人は Chrome ユーザーだ。

いうまでもなく、Chrome の extension はローカルファイルシステムにアクセスすることはできない。issue の中でこれは使えるのではないか? という API がいくつか挙げられているのだが:

  • https://developer.chrome.com/apps/fileSystem: これは、いわゆる Chrome apps 向けの API だ。Chrome apps は extension と違い、よりローカルのリソースに近い層にアクセスできる API を使用できる。しかし extension からこの API を使うことはできない
  • http://www.html5rocks.com/en/features/file_access: これはローカルではあるが、ただし「サンドボックス」なファイルシステムを操作する API だ。従って既存のファイルを直接いじったりはできない

というわけで、残念ながらすぐに使えるというものはない。

現実的な妥協案としては、wasavi のうち http://wasavi.appsweets.net/ 上での動作モードを分離する、つまり extension としての wasavi、apps としての wasavi を独立してリリースするというものが考えられる。そうすれば apps モードはローカルファイルシステムにアクセスできて、よりローカルなアプリケーションっぽくなる。

この案の弱点としては、

  • 2 つのリリースを考えなければならないのが開発側として面倒
  • 各ページの wasavi はバックグラウンドを通して各種ヒストリやレジスタの内容や exrc その他を共有している。リリースが別になると、その繋がりから抜けることになる。特にレジスタと exrc が別になるのはすごく使い勝手が悪そうだ

もう少し別の角度から何か出てこないだろうか。もしも apps をアプリケーションではなく、ライブラリ的に使うことができればいいのだが。つまりエクステンションから接続を受け、ローカルファイルシステムの読み書きを代行する中間層として動作してくれればいいのである。そんなことが可能だろうか。

まあたぶん、こんな感じで誰でも思いつきそーな使い方は当然できないんだろうけど、一応それを確かめるためにちょっと試してみよう。

support the webapp

Pull Request が来ているのでサクッと片付けようとしたら、片付かなかったという話。

まず webapp 向けのマニフェストを追加して欲しい、というリクエスト

webapp とはなんぞやというのは、google によるドキュメントやそのサンプルW3C によるドキュメントを参照のこと。

で、そのマニフェストというのはつまり、webapp、いわゆる web アプリケーションがあったとして、それをまさにアプリケーションとして扱う場合にそのメタデータ(アイコンとか、エントリポイントとなるパスとか、画面の方向とか、色々)を記述しておくための統一書式に従って記述された json ファイルのことらしい。

Chrome 38+ では、バーガーメニューから例えば「デスクトップに追加…」みたいな項目を選ぶと、現在のページヘのショートカットをデスクトップに保存する。このとき、マニフェストを読み込み、アプリケーション名やタイトルや何やらが自動的に設定される。Android 版の Chrome にも似たようなメニュー項目があり、ホーム画面へ追加することができる。というか、最初 google のドキュメント読んでてタイトルが “for Android” なんてなってるものだから、思わずリクエストをくれた方に「これって Android 向けなの?」とか聞いてしまった。PC 版の Chrome にもあります。

wasavi の場合、textarea に乗せて起動する場合は webapp とは言えないが、http://wasavi.appsweets.net/ をブラウザで開いた場合は自動的に wasavi が起動し、ある程度 standalone なテキストエディタとして使える(ローカルファイルシステムへの読み書きはできず、web 上のストレージが対象になる)。このモードは確かに webapp と見なせて、件のリクエストもこのサイトに置くべきマニフェストを追加するというものだ。

そんなわけで、リクエストを単にマージし、試してみたのだがどういうわけか manifest.json を読んでくれない。何やらエラーになる。え…なんで…というわけで原因を突き止めるのに 1 日かかってしまった。

wasavi が起動する際、上記の通り 2 つのモードがある。textarea が対象となる場合は iframe のドキュメントを操作する。standalone で使用する場合は http://wasavi.appsweets.net/ 上のドキュメントを操作する。さらに iframe の場合はブラウザごとに微妙に分かれるので、計 6 パターン、実質的に 3 パターンがある。

  1. Chrome / textarea: エクステンション内の wasavi.html(web_accessible_resources にリストしてある)をソースとする。内容はそのまま使う
  2. Chrome / standalone: http://wasavi.appsweets.net/ をソースとする。エクステンション内の mock.html の内容で上書きする
  3. Opera / textarea: http://wasavi.appsweets.net/ をソースとする。エクステンション内の mock.html の内容で上書きする
  4. Opera / standalone: http://wasavi.appsweets.net/ をソースとする。エクステンション内の mock.html の内容で上書きする
  5. Firefox / textarea: about:blank をソースとする。エクステンション内の mock.html の内容で上書きする
  6. Firefox / standalone: http://wasavi.appsweets.net/ をソースとする。エクステンション内の mock.html の内容で上書きする

この内ソースを上書きするか否かだが、ソースの内容が元々空だったり(about:blank)、信用できない外部リソース(http://wasavi.appsweets.net/)の場合はエクステンションが用意する DOM の構造で上書きする。Chrome / textarea の場合は自分のリソースをそのまま使うので上書きする必要はない。

この時、上書きする head 要素のマークアップにマニフェストを指定する link 要素を含めているはずなのだが、実際にそれを「デスクトップに保存」すると動的に追加した link 要素ではエラーになってしまうようなのだ。これを突き止めるのに 1 日かかった(自分を擁護すると早い段階で原因の1つにはリストアップしてたのだけど、他の可能性を確認して最後の最後に試してみたのがこれだった)。これはバグなのか仕様なのかよく分からない。

とりあえずのワークアラウンドとしては、http://wasavi.appsweets.net/ が返す内容の head 要素にもマニフェストへのリンクを含めるようにし(これ自体は全くおかしくない)、standalone のときは head 要素の内容については上書きをせずそのまま使う、ということになる。

ここで1つ問題がある。そもそもなぜ上書きするのかといえば、上にも書いた通り信用できない外部リソースだからだ。http://wasavi.appsweets.net/ は単にプレースホルダドメインとしての役割以上の何物でもない。これを翻して、head 要素の内容をそのまま使うとすると、せめて http ではなく https にしないとまったく割が合わないのである。

でもねえ https ってけっこう維持費かかるのよねぇ〜と思っていたのだが、ぐぐってみると最近はかなり低コストで、というか無料で https 化できたりできなかったりするようだ。そのへんも含めて色々変える必要がある。

とこういうわけで 1 つの Pull Request にまったく四苦八苦しているのであった。

Separation #6

DOMの構造について。

1
a text


こんな感じにして、floaterは[cci]position:relative[/cci]にする。これは、カーソルが位置する基準がビューの上端か下端かのふた通りある仕様への対応。

各行はfloater内に生成し、それは行番号とテキスト本体を含む。従来は行番号はCSSによるナンバリングとbefore擬似要素で実装していたのだが、単純な要素にする。行要素自体(lineクラスのdiv)は単純に[cci]position:static[/cci]。行番号とテキストとを横並びにするのはCSS flex。

そろそろ、実際にwasaviのコードをいじり始めたい。まずビューとモデルを分離し、その完了後モデルをバックエンドへ移動させる。