Vim の矩形選択の痒いところに手を届かせる


2012年 10月 03日

問題

Vim の矩形選択
は便利です。痒いところに手が届く感じの便利さです。

例えば以下のようなコードを編集していて、
一時的に真ん中のブロックをコメントアウトして無効化したくなったとしましょう。

foo1()
foo2()
foo3()

bar1()
bar2()
bar3()

baz1()
baz2()
baz3()

やり方は色々あります。

  • 対象が1行だけなら I# <Esc> で十分です。
  • 複数行あるなら最初の行を I# <Esc> でコメントアウトした後に j. を繰り返していけば良いですね。
  • 数が多くて j. の連打が難しい場合は vip で選択して :s/^/# /<Enter> という手もあるでしょう。

これ以外に矩形選択を使う方法もあります。

  1. <C-v> で矩形選択を開始し、
  2. j 等を連打して適当な範囲を選択し、
  3. I# <Esc>

こうすれば選択範囲の各行の頭にまとめてコメント文字を入力することが出来ます。

しかし、現実には「 j 等を連打する」といった優雅さに欠ける操作はしません。
ブロック単位でテキストを選択したければ十中八九 vip で事足ります。
ところが vip を使うと操作手順が以下のようになります:

  1. vip で選択し、
  2. <C-v> で矩形選択状態に変更し、
  3. I# <Esc>

というのも、ビジュアルモード中の IA は矩形選択のみで使えるもので、
文字単位や行単位で選択しているときには使えないからです。
そして ip を使うと選択単位は自動的に行単位になります。
だから <C-v> で矩形選択状態に変更する必要があります。

しかしこれは不便です。 j 等を連打することと同じくらい優雅さに欠けます。
もっと便利にできないものでしょうか。

回答その1(失敗例)

問題はビジュアルモード中の IA が矩形選択状態でしか使えないということです。
ならば文字単位や行単位で選択中でも IA を使用できるようにすれば良いということです。
ということで以下のコードを vimrc に追加して IA の挙動を調整しましょう:

vnoremap I  <C-v>I
vnoremap A  <C-v>A

これで vipI# <Esc> 等としてまとめてコメントアウトできるようになりました。
やりましたね。

……と思いきやこれには致命的な欠点があります。
矩形選択状態で IA を押下した際にも <C-v> が実行されます。
矩形選択の開始は <C-v> ですが、矩形選択の終了も <C-v> なので、
これでは
「矩形選択を終了してから IA を実行する」
ことになって本来の機能が使えなくなります。

回答その2(失敗例)

問題はビジュアルモード中の種別を無視していたことです。
種別に応じて矩形選択に切り替えるかどうかを決めれば良いのです。

vnoremap <expr> I  mode() == '<C-v>' ? 'I' : '<C-v>I'
vnoremap <expr> A  mode() == '<C-v>' ? 'A' : '<C-v>A'
  • :map 等でのキーバインド変更は本来のキー入力の代わりに入力されるキー入力を指定するものなのですが、
    <expr> を指定すればキー入力の代わりに式を書くことができ、
    その式の評価結果が本来のキー入力に代わって入力されます。
  • 現在のモードの詳細は mode() で取得できます。

これを組み合わせれば
「矩形選択ならそのまま」
「矩形選択でないなら矩形選択に切り替えておく」
という目的が実現できますね。

……と思いきやこれにも欠点があります。
行単位で選択している場合に IA での挿入位置が直観に反するのです。
例えばバッファ中のテキストが以下の通りだとしましょう:

abcXdef
abc_defghi
abc_defghijkl

ggfXVjj<C-v> とすると、選択範囲は以下のようになります(選択箇所は # で塗り潰しています):

abc#def
abc#defghi
abc#defghijkl

これでは I で挿入されるテキストの位置は # の左側になってしまいます。
行単位で選択しているということは I で挿入する位置は行の先頭(a の左側)であるべきです。
同様に A で挿入されるテキストの位置も # の右側になってしまいますが、
これも挿入位置は各行の末尾であるべきです。

回答その3(完成)

つまり単純に矩形選択状態にすれば良い訳ではなく、
現在の状態に応じて適切な矩形範囲を選択する必要があるということです。

vnoremap <expr> I  <SID>force_blockwise_visual('I')
vnoremap <expr> A  <SID>force_blockwise_visual('A')

function! s:force_blockwise_visual(next_key)
  if mode() ==# 'v'
  return "\<C-v>" . a:next_key
  elseif mode() ==# 'V'
  return "\<C-v>0o$" . a:next_key
  else  " mode() ==# "\<C-v>"
  return a:next_key
  endif
endfunction

さすがに場合分けが多くなってきたので関数に分離しましたが、
やっていることは回答その2と一緒で、
行単位の場合のみ選択範囲を調整したというだけです。

これで

  1. <C-v> で矩形選択を開始し、
  2. j 等を連打して適当な範囲を選択し、
  3. I# <Esc>

  1. vip で選択し、
  2. <C-v> で矩形選択状態に変更し、
  3. I# <Esc>

等と言った手順が

  1. vip で選択し、
  2. I# <Esc>

という華麗な手順で済むようになります。
たかだか1ステップが省略されただけですが、
この1ステップに魚の小骨が喉に引っかかったようなもどかしさに近い思いをするので、
これが省略できたことにはとても価値があります。

予告

次回は Emacs 編です。

追記(2012-10-09T18:52:24)

上記の設定をプラグイン化しました。

追記(2012-10-09T18:53:43)

http://b.hatena.ne.jp/tmatsuu/20121007#bookmark-113886930 より:

矩形選択は結構使うんだけど、 :s/// による置換ができない(ビジュアル選択範囲で置換してしまう)のが残念です。なんとかならないでしょうか

条件付きですがなんとかなります。

Ex コマンドの範囲指定は行単位です。
矩形範囲を対象にしようにも範囲指定は行位置のみで桁位置の指定ができません。
これはどうしようもありません。

しかし、 :s に限って言えば矩形範囲を対象にすることは可能です。
「Visual mode で選択された範囲内」を表す特殊な正規表現 \%Vがあるので、
これを利用すれば

  1. <C-v>jjjlll 等で矩形範囲を選択する。
  2. :s/\%Vfoo\%V/bar/g 等として検索パターンに \%V を付け加えて置換を行う。

という手順で矩形範囲を対象に置換を実行することができます。
少々面倒ですが、矩形範囲を対象にしたい機会というのはそうそうないので、特に問題はないと思います。
どうしても多用するようであれば本記事のように工夫してみてはいかがでしょうか。

あと、

ビジュアル選択範囲で置換してしまう

は「行単位で置換してしまう」の間違いですね。