expandtab #4

言うまでもなく vi はモード志向の強いテキストエディタだ。たとえば vi で文字を入力するにはまず input モードに移行する必要があるのだけど、これは総称であって、実際には i、a、o とかを押して入る insert モードと、それから R を押して入る overwrite モードがある。

この overwrite モード、かなり奇妙である。overwrite モードのカーソルが何かの文字の上にあるときに文字を入力すると、それはカーソルの下の文字を上書きする。ただし、改行の上にあるときは改行の前に挿入される。一方 enter を押した場合は常に改行が挿入される。その他にもちょっとした制限があったりなかったりする。

はっきり言ってこのモード、普通にテキストを編集するのにあたっては使うことはまったくないと言っていい。しかし作ってしまったので、expandtab 絡みの動きも overwrite を考慮しないといけないのだった。これが実に面倒くさい。また、あろうことかどうも vim では expandtab+overwrite でタブを入力した時のタブ展開の動きが、expandtab+insert の場合のそれとなんか微妙に違うのである。これ、考えぬいた仕様というよりは、overwrite なんて誰も使ってないのでテストも比較もいまいちされていないのではないかという思いが脳裏をかすめてならない。

それにしても今考えると overwrite モードは作らなくても良かったなーと思うことしきりである。もし wasavi がそれを備えてなかったとしても「なんで overwrite モードねーんだよこのハゲ!」と怒る vi guy は地球上にいないだろう。たぶん Bill Joy さんも許してくれると思う。

expandtab #3

vim が持つ expandtab 絡みのさまざまなオプションであるが、とりあえず wasavi には expandtab オプション以外は実装しない。つまり smarttab と softtabstop は実装しない(tabstop、shiftwidth はすでに実装してある)。

で、実際に input モードで tab キーを押した際になにが起こるのかをまとめると:

  1. タブ(U+0009)をカーソル位置に挿入する
  2. 行頭のインデント中のタブを一旦すべて空白に置き換える
  3. 行頭のインデント中のマークをすべて退避する
  4. expandtab がオンなら、何もしない。オフなら、行頭のインデントを先頭から走査して、タブに置き換えられる箇所を置き換える。つまり tabstop の字数ぶん連続した U+0020 を U+0009 に置き換える。その過程で、退避したマークのオフセットのつじつま合わせも行う
  5. 退避したマークを再設定する

という感じになる。実はこの処理の大部分は、シフトコマンドのそれである。なのでまずその辺を詰めることになる。例えば tab キーを押した際の処理は左や右へシフトを行うわけではない。いわば 0 カウントだけシフトすることになる。という訳でそんな感じのいろいろな修正を施した。

expandtab #2

もう少し整理してみる。

vim で input モードで tab キーを押した時に、まず以下のオプションが参照される:

  • tabstop
  • expandtab

tab とスペースの変換という点で見ると、この 2 つがコアになる。tabstop は tab が最大何文字幅分に伸びるかを指定する。expandtab は、tab キーを押した際に対応する文字数分の空白に置き換えるか、および行頭インデントの最小化(つまり、空白を可能ならタブに置き換える処理)をスキップするかを指定する。

一方で、本質的ではない以下のオプションも参照される:

  • softtabstop
  • shiftwidth
  • smarttab

softtabstop は、「タブ幅は標準の 8 桁から変えたくないけど、インデントの単位は 8 じゃない数値にしたい!!」という向きに応えるためにある。タブ幅自体を変えても気にしないなら、このオプションを弄る必要はない。

shiftwidth は、本来は、< と > コマンドによってシフトする際のシフト量を指定するためのものだが、smarttab の状態によっては tab キー押下時に参照されるときがある。

smarttab は、インデント量をカーソルの位置に応じた文脈で選択させるようにする。smarttab がオンの状態で、カーソルが行頭のインデント領域の中にある時、shiftwidth の分だけインデントされる。smtarttab がオフであるか、あるいはオンであってもカーソルが行頭インデントではない箇所にある場合は、softtabstop、tabstop の順に評価して 0 ではない最初の値を取り出し、それをインデント量とする。

とこのように、expandtab 自体は単純明快なのだけど。字下げスタイルという個々人の好みにものすごく左右されるものにできるだけ対応するための、付加的なオプションのおかげで全体的になんだかわよくわからないものになってしまっている。

さらに cpoptions オプションの設定によって微妙に部分的な動作が変わったり、あるいは前の記事の通り backspace オプションが絡んできたり、:retab コマンドの動作にも影響したりする。カオスすぎる。

expandtab

issue #16 の、vim の expandtab を実装してよというリクエスト。

expandtab がオンの状態だと、input モードで tab を押した際、タブそのものではなくタブ幅に応じた適当な個数の空白が挿入される。また、> や < でシフトした際、通常はタブで置き換えられるなら置き換える(つまり ts=8 で sw=4 の状態で 2 回シフトすると、インデントは 1 個のタブになる)のだが、その処理を迂回して常に空白でインデントするようになる。

他にも影響があるところがあるかも知れない。実は個人的にこのオプション使ったことがないので、よくわかっていないのだった。インデントを常に空白で行うというのはなんというか富豪的である。貧乏人は普通にタブを使うのだ。

また、間接的に backspace オプション、retab コマンドとも関連する。これらとセットじゃないとやはり実用上は辛いかもしれない。というわけでフルに実装するとけっこうかなり面倒な代物なのだった。

ということを issues やりとりしたら、とりあえず単にインデントを空白で行う処理だけでもいいんじゃね? みたいな話でまとまったので、そういう方向でやってみよう。

the place of extensions

wasavi の crx/nex/oex/xpi の置き場所として、各ブラウザの公式のストアには安定版、ここには開発版、github にはソース、という分け方をしていたのだが、開発版のパッケージもソースの一部として github に置くようにした。

あわせて、各パッケージが内包している最新バージョンの問い合わせ先も github 上のファイルとなるようにした。また、特に xpi を https で配布するためには McCoy を用いて七面倒臭い署名のプロセスを経なければならなかったのだが、github 上での配布ということで https になったので、McCoy は使わなくても良くなった。

McCoy はよくわからないツールで、要するに xpi と、xpi のアップデート情報を含む rdf ファイルに対して署名を施すのに使うのだが、XUL な GUI アプリケーションなのである。コマンドラインから使うことは考えられていない…というよりも、わざと不便なように作ってあるように思える。mozilla にしてみれば、AMO 以外の場所からの配布を手助けするツールをまじめに作る理由がないわけであろうから、まあそういうことなんだろうと思う。

coexistence with CodeMirror #2

CodeMirror 上で wasavi を起動できるようにした。また、ace でも同じように特別な対応を施すようにした。

ただ、若干もったいない話ではある。CodeMirror も ace も、それ自体が高機能なエディタなのだ。また、両者とも vi っぽいキーバインドにすることができる(ex コマンドっぽいものはない)。

coexistence with CodeMirror

github 上の issue についていくつか。

#18
sqlfiddle というサイトで wasavi がうまく起動しないというもの。このサイトでは、ページ上に CodeMirror を置いている。

CodeMirror 自体が javascript と DOM と css からなる複雑な要素で構築されたテキストエディタである。一方で wasavi はシンプルな textarea に対して起動する。したがって両者の相性はそもそも良くない。CodeMirror の実行中に ctrl+enter を押すと一応 wasavi が起動するが、これはたまたま現在の CodeMirror がキーボード入力を受け付けるために隠された textarea 要素を抱えているだけの話だ。起動しても、編集中のテキストが正しく wasavi に転送されるわけでもあるいは逆に書き出せるわけでもない。

なので、単にサポート外ですと言ってもいいのだけど、なんとかならないものなのだろうか?

CodeMirror を構成するオブジェクトとしては、

  • 置き換え先の textarea 要素: CodeMirror がアクティブな間は display:none されている
  • CodeMirror の DOM 上の表現: “CodeMirror” をクラス名に含んだ div 要素
  • javascript 上の CodeMirror インスタンス

がある。wasavi の content script/injected script から、最後の CodeMirror インスタンスにアクセスできればなんとかなる。

実はこのインスタンスは div.CodeMirror 要素から参照できる。なのでページ上の javascript からは、

document.querySelector("div.CodeMirror").CodeMirror.getValue()

などとすれば CodeMirror が保持するテキスト全体を得ることはできる。しかし content script からは、参照している window が別だったりラップされていたりするのでこの方法は使えない(Opera ではできるが、inject script のコンテキストからページ上のスクリプトが生成したオブジェクトを直接呼び出すのは怖いのでやりたくない)。

wasavi が置き換える対象となる要素に対して内容を読み出したり書き込んだりする機構を汎用化してページのスクリプトのコンテキストを通すようにすればいいかもしれない。

when and who

ニュースというものは、「いつ」「誰が何をした」ということが、伝えられるべき最重要な事柄だということは言うまでもない。

というのも、IT にまつわる何かをググって IT 系ニュースサイトの記事が引っかかった時、それがいつ書かれたものか、記事の末尾にちんまりとしか載せられていないものがあって困るのだ。読み進めて「ふーん…」「うん…?」「これって、いつの記事なんだ?」……「2005 年じゃねーか!」とかなる。時間の無駄である。大見出しの直下にいつの記事なのか書かれてしかるべきものだと思う。それが統一されていないサイトが少なくない。

また、「誰が書いた記事か」というのも重要だ。かつて ZDNet Japan というサイトがあったのだけど、その中の単発のコラムかアンカーデスク的なものか忘れたが(憶えておく価値もないのだが)Dan なんとかという奴(憶えておく価値もないのだが)が「おまえは何を言っているんだ?」という全く的はずれな文章をよく載せていた。このサイトも、記名は文章の最後尾だったので、「ふーん…」「はぁ…?」「なんでそうなるの…?」……「Dan、またお前かよ!」とよくなったものなのだ。ページの頭に誰が書いたのかが書いてあれば、このような無駄なことをする必要もなかったのだ。

このような不誠実なことが起こらないために、ニュースサイトには「いつ」「誰が書いた」記事なのかをページの最初にはっきり書いて欲しいのである。

impress.co.jp、あなたのことを言っています。

When I overwrite a directory as a file…

wasavi を起動し、dropbox や gdrive にすでにディレクトリとして保存されているパスに対して :write した場合、エラーになってもらわないと困る。もしも保存したパスがディレクトリで、なおかつそのディレクトリの下に多数のファイルが保持されている時、有無を言わさずそれらがすべて消去され指定のパスが同名の単なるファイルにすり替えられるとしたら、これは絶対に避けなければならないケースだ。

google drive の場合、ファイルを上書きするには先んじてそのファイルの ID を得なければならない。これはこれで面倒な作業ではあるけれど、しかしその過程でファイルの mime タイプが application/vnd.google-apps.folder かどうかを判断するチャンスがアプリケーション側に与えられるので問題ない(それを一つのトランザクションとして見なした場合、そんなゆるゆるなチェック体制でいいのか? とは思うけど)。

dropbox の場合、ファイルを更新するにはそのパスと内容を同時にアップロードするだけだ。dropbox で上記のクリティカルなケースがどうなるか試してみたところ、どうもサブディレクトリの下にファイルがある場合はエラーになり、完全に空のディレクトリの場合は単なるファイルへの上書きが成功するようだ。

それはそれで賢い振る舞いなのかもしれない。また、この辺の振る舞いは別に wasavi 側で規定できるわけでもない。

ということで、そういう動きを受け入れることにする。

picking up the locale

Presto Opera のエクステンションにおいて、エクステンション内のファイルを xhr なりなんなりで読み出した場合、先んじて /locales/[現在のロケール]/[指定されたファイル] というパスが検査され、そこにファイルがあれば優先して使用されるようになっている。この仕組みをもって、i18n に対応してます! と Opera は嘯いている。

このとき、[現在のロケール] とは具体的には何なのか。たとえば Opera の設定で UI のロケールを変更しても、エクステンションの上記の仕組みには影響しないようだ。

コンピュータにログインした状態では、だいたい

  • システムのロケール
  • ログインしたユーザのロケール
  • 実行したアプリケーションのロケール
  • Opera の設定: UI のロケール、accept-language に送出するロケール

みたいな階層構造でロケールが定義されると思う。Presto Opera のエクステンションでは、どうも最後の UI ロケールは見ていないような気がする。このように、エクステンションのロケールを気軽に切り替えられないのは特にテストの時に不便だ。というのは、テストは en-us ロケールで実行するのが前提だからだ。Chrome はコマンドラインのオプションでロケールを指定できる、あるいは Firefox はプロファイルに設定したロケールが Add on SDK のロケールモジュールにも正しく影響するので問題ないのだけど、Opera だけがポンコツだ。

というわけで、Opera ネイティブの i18n システムをそのまま利用するのではなく、navigator の browserLanguage/language/userLanguage あたりを見て自前でロケールを決定するようにした。

つまり結局のところ Opera ネイティブの i18n システムは利用しないことになった。用意されてる機能がポンコツ過ぎて役に立たないというのは、まあ Opera においてはよくある話なのであります。