ex コマンド [cci]s[/cci] について、とあるバグを直そうとしたら根本的に書き直さないとダメっぽかったので、根本的に書きなおした。
s コマンドの内部は、javascript の global と multiline フラグを立てた RegExp のインスタンスについて exec() を連続して呼び出すという処理がコアになっている。exec() を呼び出しながら、マッチした箇所について置換を行い、lastIndex プロパティを適宜調整し、次に備えるという 1 パスの処理になっている。
大体の場合はこれで上手く行くのだけど、ゼロ幅にマッチする正規表現の場合に上手く行かない。そして、それを直そうとすると、今まで上手く動いてた部分がこぞっておかしくなるという排他的な状態になってしまうのであった。
これを根本的に直すために、まず exec() のループと置換ループを別に分ける 2 パスの処理にすることにした。exec() で得た各マッチ位置の情報を配列に押し込んで覚えておく必要があるので、若干富豪的ではあるのだが、まあ、だいじょうぶだいじょうぶ。
その他見つけた細々としたバグも直した。
たとえばゼロ幅でマッチする正規表現、たとえば [cci]:s/a\?/!/g[/cci] というコマンドを実行すると、カーソル行の各文字の前後に [cci]![/cci] が挿入されるが、行末には挿入されない。少なくとも vim では挿入されない。これは割と奇妙な動作だ。
つまり、
0123
とある行に前述のコマンドを実行すると、本来ならば
!0!1!2!3!
となっておかしくないはずであるが、少なくとも vim では
!0!1!2!3
となる。行末の [cci]![/cci] がない。
この通りに動作させるには、マッチした位置が改行で、かつマッチした正規表現にメタ文字 [cci]$[/cci] が含まれない場合は置換を行わない、という例外を加えればいい。いいのだけど、後者はとても難しい。javascript ではとても難しい。マッチに使用された正規表現のパスを得る方法が javascript の RegExp にはない。