対応する括弧等を入力する生活に疲れた(Vim 編)


2012年 09月 01日

問題

プログラムにせよ何にせよ、

  • ()
  • []
  • {}
  • ''
  • ""

等々、対応する文字を入力することはよくあります。
入力それ自体は難しいことではありませんが、
ペアで入力しなければ意味がないため、
場合によっては入力を忘れてしまうこともあります。

特にプログラムを書いているとこの手の入力漏れで構文エラーになることは多々あります。
例えば以下のような感じです:

ZapZapZap(Convert.ToInt32(e.Item.Cells[0].Text);

まあ何度も
([ を入力していれば一個くらい対応する
]) を入力し損ねるのは仕方がありません。
しかし入力し損ねる度にしょうもない構文エラーでコンパイルが失敗したりテストが失敗するのは士気に響きます。
もう2012年なのですから、どうにかして対応する括弧くらい自動で入力してもらえないものでしょうか。

解答

vim-smartinput を使います。
ぶっちゃけこの手のプラグインは様々な先行実装が存在するのですが、
どれも拡張性に乏しかったりろくすっぽテストが書かれていなかったりと使う気になれなかったので、
自分で満足ができるものを作ろうとしました(つまりまだ不満が残っています; 後述)。

標準機能

vim-smartinput をインストールするとデフォルトで以下のような入力補助が行われます
(以下の例で # はカーソル位置を表すものとします)。

ペアとなる文字の入力補助
初期状態 | 入力 | 結果
============================
#        | (    | (#)
----------------------------
#        | {    | {#}
----------------------------
#        | "    | "#"
現在のブロックからの脱出
初期状態 | 入力 | 結果
============================
(#)      | )    | ()#
----------------------------
[#]      | ]    | []#
----------------------------
"foo#"   | "    | "foo"#
入力補助のキャンセル
初期状態 | 入力 | 結果
============================
(#)     | <BS>| #
----------------------------
()#     | <BS>| #
文字列リテラルや正規表現リテラルを書く際の補助
初期状態 | 入力 | 結果
============================
\#      | "   | \"#
----------------------------
\#      | <   | \<#
英語を入力する際の補助
初期状態 | 入力 | 結果
============================
let#    | 's  | let's#
----------------------------
foo(#   | '   | foo('#'
Lisp/Scheme 系のソースコードを編集する際の補助
初期状態 | 入力 | 結果
============================
#       | 'foo | 'foo#
----------------------------
"#"     | 'hi  | "'hi#'"
C 系の構文のソースコードを編集する際の補助
初期状態     | 入力    | 結果
=====================================
if (...) {#}| <Enter> | if (...) {
            |         |     #
            |         | }
-------------------------------------
if (...)     | <Enter> | if (...)
{#}          |         | {
             |         |     #
             |         | }

拡張性

先述の入力補助は序の口で、
vim-smartinput では必要に応じてどのように入力補助を行うか(まあ割と)自由に拡張できます。
入力補助の拡張や調整は「ルール」単位で行います
(先述の入力補助も単に標準でいくつかルールを定義しているだけです)。

ルール

ルールは
dictionary
(Vim 語; 他言語で言うところの連想配列的なもの)で表現され、
基本的に以下の3要素で構成されます:

  • 入力補助を行う文脈はどこか(at: ただの正規表現)
  • トリガーとなるキー入力は何か(char: キーを表す文字列)
  • トリガーのキー入力の代わりに何を入力するか(input: 代替入力を表す文字列)

例えば
( を入力したら代わりに () を入力してついでにカーソルを括弧の間に入れる」
は以下のルールで実現できます(\%# はカーソル位置を表す Vim 族専用の正規表現です):

{'at': '\%#', 'char': '(', 'input': '()<Left>'}

後は vimrc に

call smartinput#define_rule({'at': '\%#', 'char': '(', 'input': '()<Left>'})

等を追加してルールの定義を行えば新しいルールが有効になります。
やりましたね。

ルールが競合した場合の措置

先程の
( を入力したら代わりに () を入力してついでにカーソルを括弧の間に入れる」
というルール単体だけでは Perl 等で正規表現を書く際に困ります。
( にマッチする正規表現を書こうと \( を入力すると代わりに \() が入力されてしまうからです。
なので正規表現の入力用に
「カーソルが \ の直後ならば ( 単品を入力する」
というルールを追加しましょう:

{'at': '\\\%#', 'char': '(', 'input': '('}

こうすると「( を入力した時に一体どちらのルールを適用すべきか」という問題が発生しますが、
実のところルール毎に
優先度
が定義されており、
優先度の高い順で
「ルールに合致する状況であればそのルールを適用する」
ことになっています。

基本的には「 at がやたら長いものはそれだけ複雑な文脈の指定になっている」という前提で
at の長いルールの優先度が高くなるように設定されています。

上記の例の場合は \%# より \\\%# の方が明らかに長いので、
まず正規表現用のルールが試され、
その後に一般的なルールが試されることになります。

その他

at の他に filetype だの syntax だのと追加で細かい文脈の指定ができるので、
それなりに愉快なルールが記述できるようになっています。
ルールの詳細は
:help smartinput-rules
を、
ルールの具体例は
標準のルールの定義
を参照してください。

課題

とまあ色々と持ち上げてみたものの、
実際のところ汎用的なルールを書くのは難しいものがあります。

なので我こそはと思う方はどうぞ改善してください。

次回は Emacs 編です。