Promise and Generator #7

さて、wasavi の根幹の動作ロジックを Promise ベースに大改造した大元の動機のひとつである、migemo 対応について考えてみたい。

とりあえず Chrome extension としての migemo についてまとめてみたい。migemo 自体のホームはここ。そして Chrome 版の migemo というのは、検索文字列に対応する migemo 的な正規表現を返す API を提供することに特化した拡張で、migemo server ということになっている。

まず edvakf さんが制作した ChromeMigemo Extension というものがかつてあった。これ自体はすでにサポートを終了しているが、ソースは公開されている。ただ困ったことに API リファレンスといったドキュメントはないので、このリポジトリ自体はエンドユーザにとってはそれほど有用ではない。API リファレンス的なものは辛うじて彼のはてなダイアリーの古い記事から得られる。

さて、これを現在サポートしているのは mono さんで、blog の記事でfork 版である Migemo Server for Google Chrome™の告知がなされている。wasavi もこれを参照する。

これを wasavi の [cci]/[/cci]、[cci]?[/cci] コマンドに組み込むわけだが。もともとこれらのコマンドは正規表現を入力して検索するものだ。これが migemo に対応すると、入力した文字列が内部で複雑な正規表現に変換された後に検索が行われるという流れになる。つまり何か入力してそれを検索するという手順は同じなのだが、入力するものの性格は全く違うということだ。migemo の場合は入力するのは文字列リテラルなのだ。

そうなると、コマンド自体を別に起こす(例えば、[cci]g/[/cci]、[cci]g?[/cci] とか)ほうが筋がいいのかもしれない。しかし、ここはやはり [cci]/[/cci]、[cci]?[/cci] コマンドにまとめたい。

ところで vim では、正規表現中に [cci]\C[/cci] というものを含めると ignorecase オプションの値にかかわらず、必ず大文字・小文字を区別して検索するようになる。[cci]\C[/cci] には正規表現のメタキャラクタとしての効果は何もなく、それが正規表現中に存在するか否かに意味があるというわけだ。この考え方を流用してみよう。メタキャラクタ [cci]\M[/cci] を導入し、これが検索文字列に含まれている場合は migemo で検索、そうでない場合は従来の検索というように振り分ける。

このやりかたにすると、検索文字列の入力中に「やっぱ migemo で検索しよーっと」とかその逆が簡単に切り替えられるし、また [cci]map g/ /\\M[/cci] などとすることも可能なので、migemo 検索を独立したコマンドにしたいという向きにも対応できる。

というわけで、できた。

Marquee!

wasavi が起動する際、最小の横幅・縦幅というものがあり、横の最小幅は 320px ということになっている。

このくらい幅が狭いと、ステータスラインに何かメッセージを表示する際に、その長さによっては後ろが隠れてしまうことがある。

これを解決するために、いわゆる marquee 的な動作をさせるようにしてみた。的な、というか、実際に marquee 要素で囲むのである。21 世紀も17年過ぎて marquee 要素を使うとは思わなかった…。

この marquee 要素、いつ廃止されてもおかしくない状況なのだが、とりあえず今のところは Chrome でも Firefox でも使える。ただし Chrome においてはかなりスムーズに動くのに対して Firefox ではかなりガックガクだ。

まああんまり気にしないことにしよう。

Promise and Generator #6

一通り修正が終わって、Selenium による Chrome 上の機能テストも一応クリアした。

一応というのは、800 超の機能テストはカテゴリごとに editing.js とか、insert.js とか分かれているのだが。それらを個別に動かすと 100% パスするものが、全てまとめて通すとぽろぽろと失敗するものがランダムに出てくる。これはなんだろうか。

もうひとつ。以前 Firefox でテストしようとしたところ、geckodriver の機能や、Selenium のサポートなどがまだ熟成されていない感じだった。やたらめったら CPU パワーを消費するし、既存のプロファイルを使用できないし、WebExtensions ベースの拡張を認識してくれないし、要素に対して sendKeys() する機能が未実装だし…など、プロダクトレベルの品質に達しているとはとても言えない状態だった。あれから数ヶ月経ったがどうだろうか。

ということで試してみたところ、
Could not convert 'text' to string
なるエラーが sendKeys() で発生する。うーんまだ実装していないのかな? と調べてみたところ、issue が上がっていた。つまるところ、sendKeys() 自体は実装済みなのだが、Selenium は入力されるべきキー情報を文字の配列の形で送出するのに対し、geckodriver 側は文字列の形で受けることを想定しているという型のミスマッチのようだ。issue 後半に掲載されているクイックパッチの通り直してみたらとりあえず動いた。javascript バインディングの Selenium の場合、スクリプト言語で実装されているおかげでこういう小回りがきくのは嬉しい。

その他、CPU パワーを浪費することもないようで、さすがにいろいろと進歩しているようだ。

Promise and Generator #5

ex の executor はかなり書き換えたわけだが、一方で vi コマンドの executor というのも一応ある。これは ex ほど大規模ではなくて、単に vi コマンドの文字列をキー入力キューに挿入するだけだ。この機能は [cci].[/cci] コマンド及び [cci]@[/cci] コマンドが利用している。つまりいわゆるマクロ機能に相当する。

今回コマンドの実行が非同期になったことで難しくなったのは、マクロの実行が完了したタイミングが取れないということで


executeViCommand('2w');
doSomethingAfterExecute();

ということができない。そこで、キー入力の最後に特別な擬似キー [cci]*macro-end*[/cci] を付加するようにした。なぜマクロ完了のタイミングが重要なのかといえば、カウントというものがあるからだ。[cci].[/cci] の場合は実行する vi コマンド列がカウントを内包しているのであんまり関係ないのだが、[cci]@[/cci] の場合はそうではない。

そういうわけでこのマクロ実行完了を表す擬似キーを使う。対応するコマンドハンドラ内でカウントを見て、必要なら再度コマンド列をキー入力キューに充填したりすればいい。ところでそのハンドラは normal モードにしか存在しないので、マクロ実行完了時のモードもまた normal モードじゃいけない。つまり他のモードの場合は [cci]escape[/cci] キーを再充填するなどの処置も必要。

それから、[cci]@[/cci] コマンドというのは指定したレジスタの内容を vi コマンドとして実行する機能なのだが。そうすると今回施した修正の元、たとえば [cci]@a[/cci] という文字列がレジスタ [cci]a[/cci] に格納されている状態で [cci]@a[/cci] を実行すると実質的な再帰が起こってしまう。ちなみに vim 7.4 (2013 Aug 10, compiled Nov 24 2016 16:44:48) で試したところ、固まってしまった(固まらない時もある。よくわからない)。

というわけで、[cci]@[/cci] コマンドのもとで vi コマンドを実行している際は同じレジスタを使った [cci]@[/cci] コマンドを入れ子で使えないようにした。入れ子でなければいいので、@a が @b を呼び、それが @c を呼び…ということはできる。

Promise and Generator #4

ぼちぼち Selenium でテストし始める。

ところで Selenium でテストする際、ブラウザの console 出力を得たい時がある。…のだけど、いまいちそのへんのドキュメントが整備されていない気がする。javascript バインディングの API はドキュメントがあるが、これはあくまでリファレンスであってハウツー的なものはほとんど書いてない。

いろいろ調べてみたところ、

const webdriver = require('selenium-webdriver');
const chrome = require('selenium-webdriver/chrome');
:
:
var loggingPrefs = new webdriver.logging.Preferences()
loggingPrefs.setLevel(
webdriver.logging.Type.BROWSER,
webdriver.logging.Level.ALL);

var options = new chrome.Options();
options.setLoggingPrefs(loggingPrefs);

var driver = new webdriver.Builder()
.withCapabilities(webdriver.Capabilities.chrome())
.setChromeOptions(options)
.build()
:
:
driver.manage().logs().get(webdriver.logging.Type.BROWSER).then(log => {
// log は Entry インスタンスの配列で、
// 各要素の message プロパティに console 出力の各行が格納されている
});

という感じでいいようだ。message プロパティは前後にソースファイル名や行番号などが付加されていたりするので、そういうものを削ぎ落とす加工は必要。

Promise and Generator #3

長い時間をかけてちくちくいじっていたものが終結しつつある。キーボード入力から vi コマンド / ex コマンドの実行、その実行の完了を保証するシーケンスポイントまでの一連の関数呼び出しが Promise の連鎖によって行われるように修正した。とても疲れた。

これにより、従来あんまり綺麗な形で実装していなかった非同期処理を、それを必要とする関数内に局所的に閉じ込める形にできて見通しが良くなった。例えばクラウドに対する読み書きの場合は、それを担当するのは ex コマンドのハンドラなのだが。応答が複数回来るので、一旦途中で ex 仮想マシンの実行を中断し、wasavi 側のグローバルなメッセージハンドラで受けて、適宜再開する…のようなことをしていた。バックエンドに対するメッセージは基本的に送信と応答の 1 回のやり取りで完結するので複数のやり取りに対応できないからだ。とても不自然。

これを、ex コマンドハンドラ内で Promise を生成し、新規に開いたメッセージポートを通して複数回のやり取りをし、すべてが完了したら resolve() するようになった。とても見通しがいい。

ex コマンドのエグゼキュータも Promise ベースで動作するように書き直した。疲れた。

マップマネージャもまた Promise を使用するように完全に書き直した。これもまた疲れた。

疲れすぎてもう海に還りたい。

Promise and Generator #2

そういうわけでちまちまといじっている。

とりあえず vi コマンドを回す部分に関しては修正した。これで非同期の処理だろうが同期の処理だろうが不自然なく呼べるようになった。それはいいのだが、vi コマンド群は本丸ではない。qeema が発生するキーボードイベントから、各 vi コマンドのハンドラに達するまで、せいぜい 4〜5 の関数がネストするだけであり、ただそれを Promise を使うようにすれば済む。

難しいのは ex コマンドの方なのだった。

ex コマンドは、ex コマンド列をコンパイルして中間コードの配列に落とし、それを逐一実行するという構造になっている。なので、修正するとしたら ex コマンドごとに Promise をつなげていく、ex コマンド自体はそれぞれ resolve() でコマンドの終了を通知するといった形になる。

ただいくつか面倒な点がある。

  • write、read、edit などバックグラウンドに処理を投げるファイルシステム系のコマンドは、読み書きの進捗イベントをバックグラウンドから複数回返される。従来は、この複数回返されるメッセージというのは ex コマンドのハンドラの外で処理していた。というのも非同期的な ex コマンドを実行する際は一旦そのコマンドの同期的な部分のみを実行したあと executor を一時停止し、バックグラウンドから処理完了メッセージが到着したら executor.resume() で再開するようになっていたからだ。これは処理が分散しているといえばそうなので、若干不自然ではある。

    個々の ex コマンドを Promise によって駆動するのであれば、複数回返されるメッセージの扱いというものも ex コマンドのハンドラ内で完結できるわけで、そうすべきだと思う。具体的にはハンドラ内で long-lived connection をその都度行い、コマンド完了時に閉じればいい

  • シグナルの扱い。シグナルというと若干語弊があるのだけど、つまりやはりファイルシステム系の時間がかかるコマンドの実行中、[cci]^C[/cci] が押されたらキャンセル扱いにする処理のことだ。これも Promise 化によって中身がかなり変わる。

ちなみに非同期で実行される ex コマンドとしては

  • cd
  • edit
  • read
  • c オプションが付加された s
  • 非同期とマークされたオプションの値の変更を含んだ set
  • write

となっている。また、レジスタを指定できる ex コマンドで [cci]*[/cci] レジスタが指定された場合も非同期になる。レジスタを指定できる ex コマンドは、

  • delete
  • put
  • yank
  • @
  • *

となっている。非同期とマークされたオプションは以下のとおり。

  • fullscreen
  • syncsize
  • columns
  • lines

Promise and Generator

Migemo Server for Google Chrome™ というものがある。これ自体は migemo のライブラリのように動作し、外部の拡張からのリクエストを受け入れ、適切な結果を返す。

これに対応してみようかなとコードを頭の中で組み立ててみたのだけど、すぐに行き詰まってしまった。というのも拡張から拡張への通信というものは非同期に行われるからだ。一方で wasavi がキー入力を処理する構造は同期ベースで、非同期な動作を組み入れるのはとっても大変なのだ。

この辺を、Generator と Promise を組み合わせて使うようにすればとてもすっきり書けるのだが。なぜ今までそうしなかったかと言えばすなわち Presto Opera でも動かすためだ。Opera 12 は Promise の Polyfill は動くが、Generator は無理なのだ。

しかし、もう wasavi/0.6.657 で Presto Opera のサポートは終了した。従っていよいよそのへんを書き直す頃合いだ。これは割と大掛かりな仕事になるかもしれない。

Unicode data updated

wasavi は内部(unicode_utils.js)にいろいろと Unicode のプロパティの情報を持っている。それらの情報は、例えば f/F/t/T コマンドにおける検索対象の判別とか、textwidth オプションを介した自動的な折り返しの際に適切な折り返し位置を見つける処理とか、さまざまなところで役に立っている、なくてはならないものだ。

今までは、それらのプロパティは Unicode 6.2.0 ベースだった。6.2.0 といえば 2012 年の話であり、さすがにちょっと古くなり始めた感が否めない。そういうことで今回 9.0.0 に引き上げた。同時に、プロパティを javascript のコードに変換するスクリプトを今までは PHP で書いていたが、javascript で書き直した。これで、wasavi 本体は当然として、ビルドスクリプトも、テストスクリプトも、データ生成スクリプトもほぼ全てが javascript になった(なったからどうというものでもないが)。

ところで Unicode の各種プロパティのバージョンを引き上げて、それでおしまいというわけではない。データを使用する javascript のコードも Unicode 9.0.0 に合わせる必要がある。ということで合わせた。とても疲れた。

Unicode 絡みで言うと、unicode_utils.js 以外に Unistring というライブラリも使っている。こちらもとても重要なものだ。Unistring が文字列を grapheme cluster 単位に分解することで grapheme cluster 単位のカーソルの移動を実現している。これがなくては、テキスト中に絵文字が 1 つあっただけでカーソル移動がめちゃくちゃになってしまうのだ。

ということで Unistring が持つ内部のテーブルも 9.0.0 に合わせた。4351 件のテストにもパスする。

Unit testing

wasavi の動作の保証として Selenium を用いた機能テストを通してからリリースしているわけだが、まとまった、自動化された形でのユニットテストは行っていなかった。これはけっこうイケてないので、ユニットテストの仕組みを構築したい。

そんなわけでいくつかのユニットテストコードを追加した。特に難しいことはなく
$ mocha src/unit-tests/strftime.js
などとする。