特定の列まで指定した文字を連続入力するVim pluginを作った

この記事はVim Advent Calendar 2012の172日目の記事です。
171日目はmanga_osyoさんによるunite-source を作成する流れをまとめてみたでした。

整形のために、スペースをたくさん入力するのが億劫だったので題記のプラグインを作りました。 本記事はこのプラグインについての紹介記事です。

目的

コーディング規約にもよると思いますが、上の行とxxの文字と揃えるために insert modeでスペースをたくさん入力するようなケースがたまにあります。

例えば、以下の様なケース

  // ケース1
  int hogehogehoge = 0;
  int hoge         = 0;
  //      ^^^^^^^^^
  //      ここの部分のスペース

  // ケース2
  callfuncion("hoge"
             ,"hige");
//^^^^^^^^^^^
//ここの部分のスペース

ケース1は、aligntaというVim pluginで整形可能で、 ケース2は、言語によってはset indentexprが設定されていて自動で入力されたりします。

でもたまに、言語が対応してなかったり、思うようにスペース入れてくれなくて <Space><Space><Space>...とか、<Tab><Tab><Tab>...とかしてて悲しくなることがあります。 Vim使ってて、繰り返し同じキー打ってる時ほど悲しい気分になることはないので 改善することにしました。

使い方

目的の例のケース2について使い方を載せます。 なお、本プラグインはinsert modeで使うことを意図したプラグインです。

以下のアニメーションGIFをご覧ください。

f:id:deris:20130522003802g:plain

ちょっとわかりづらいかもしれませんが、insert modeで、callfunctionの下の行に カーソルがある状態で、<C-k>(デフォルトキーマッピングの場合)をタイプすると Insert above column:というメッセージが表示されるので、(とタイプし、 Insert insert column:というメッセージが表示されるので、スペースをタイプすると、 callfunctionの行の(の列までスペースが入力されます。
((が複数ある場合は、直近の(まで入力されます。)

Insert above column:で入力する文字、Insert insert column:で入力する文字は あらかじめキーマッピングで指定することができるので、例えば必ずスペースを 入力するのであれば、Insert insert column:を出さないことも可能です。

また、上の行ではなく、下の行を見たい場合は<C-j>(デフォルトキーマッピング) を使えますし、入力する位置の終端が気に食わなければオプションで調整出来ます。

インストール方法

プラグイン名はfit-columnという名前で、githubにあげてます。
(helpは、間に合わなかったので明日くらいには上げる予定です。)

なので、NeoBundleを入れていれば.vimrcに以下を書いておけばインストール出来ます。

NeoBundle 'deris/vim-fitcolumn'

もちろん、.vim配下に突っ込んでも構いません。

設定方法

デフォルトキーマッピングが以下のように割り当てているため、これで良ければそのまま使えます。

" デフォルトキーマッピング
imap <C-k>  <Plug>(fitcolumn-abovecolumn)
imap <C-j>  <Plug>(fitcolumn-belowcolumn)

デフォルトキーマッピングが気に入らなければ以下のように.vimrcに書けば変更できます。

" デフォルトキーマッピングを変更
let g:fitcolumn_no_default_key_mappings = 1
imap <C-h>  <Plug>(fitcolumn-abovecolumn)
imap <C-l>  <Plug>(fitcolumn-belowcolumn)

さらにデフォルト動作が気に入らなければ、関数にマッピングすることで設定出来ます。 例えば、入力する文字を手動入力でなく、固定でスペースに割り当ててしまいたい場合、 以下のように設定することで割り当てられます。

" 入力文字をスペースにする
let g:fitcolumn_no_default_key_mappings = 1
inoremap <expr> <C-k>  fitcolumn#fitabovecolumn({
  \ 'insertchar': ' ',
  \ })
inoremap <expr> <C-j>  fitcolumn#fitbelowcolumn({
  \ 'insertchar': ' ',
  \ })

他にも、searchcharrightwardというオプションがあります。
searchcharは、使い方の章で紹介したInsert above char:で 入力する文字を指定することができます。
rightwardは、スペースを入力する終端を数値で微調整できます。 例えば、1を指定すると1文字多めに文字を入力します。 マイナスも指定出来ますが、入力する文字がなくなった場合は 何も入力されません。

例えば、以下のように設定出来ます。

" 上(下)の行の、'('と同じ位置まで、スペースを入力する
let g:fitcolumn_no_default_key_mappings = 1
inoremap <expr> <C-k>  fitcolumn#fitabovecolumn({
  \ 'insertchar': ' ',
  \ 'searchchar': '(',
  \ 'rightward': 1,
  \ })
inoremap <expr> <C-j>  fitcolumn#fitbelowcolumn({
  \ 'insertchar': ' ',
  \ 'insertchar': '(',
  \ 'rightward': 1,
  \ })

おまけ(考察)

使い方のケース2で挙げたケースの場合は、set indentexpr用の関数書いて設定するのが正解な 気もするんですが(lingrvim部屋でご指摘頂きました。)、 別解を思いついて作ってしまったのでVACに書いて公開することにしました。

作ってみたものの、使い道は限られるかもしれません。

まとめ

以上が、自作Vim pluginのご紹介でした。

あと、本題とは関係ありませんが、Vimレベルが上がるのでvimrc読書会に参加することをおすすめします。毎週土曜日23時からやってます。

それではVim Advent Calendar 2012の172日目を終わります。

明日のVim Advent Calendar 2012はthincaさんです。

追記(2013/5/22)

記載を忘れていたので追記します。

  • 今のところタブは対応していません。要望があれば対応するかも

Vimでパターン検索するなら知っておいたほうがいいこと

この記事はVim Advent Calendar 2012の166日目の記事です。
165日目はaueweさんによる.vimrcに書くべきでないVimの設定項目でした。

Vimに限りませんが、テキストエディタを使う際にテキスト検索はよく使うと思います。 ある程度慣れた人だと正規表現を用いたパターン検索を使うようになります。 もちろん、Vimにも正規表現を用いたパターン検索を行う機能は備わっています。 しかし、お世辞にもVimのデフォルト設定ではパターン検索は書きやすいとは言えません。 特に、他の正規表現方言に慣れている人は、Vim正規表現の書き方の違いに戸惑うと思います。 本記事では、Vimでパターン検索するなら知っておいたほうがいいこと+ Vimのパターン検索に使えるメタ文字をご紹介します。

パターンって何?という方は:help usr_27.txtを一読することをおすすめします。 一般的な正規表現について詳しく知りたい方は、ウェブや書籍などで一度しっかり勉強することをおすすめします。 ちょっと難しいですが、おすすめの書籍はサイドバーに載せてますのでご参考までに。

Vimでのパターン検索

Vim正規表現を用いたパターンは、検索コマンド/での検索、:subsutituteコマンド のパターン部分、:global:vglobalコマンドのパターン部分などで使うことができます。

他にもVim scriptの正規表現マッチ(=~,!~)や、いくつかの関数で使用出来ます。

ただ、オプションによってパターンで扱うメタ文字(正規表現として扱う特殊な文字、文字列)の指定の仕方に違いがあります。 オプション名はmagicで、現在のオプション値を確認するためには以下のようにコマンドを実行します。

:set magic?

デフォルトはmagicなので、おそらくmagicと表示されるはずです。

試しに検索コマンドでパターン検索をしてみましょう

以下は、0文字以上の任意の文字列にマッチするパターンです。

/.*

普段は.*だけでパターンを書くことはないと思いますが、一般的な正規表現ですね。

次は、fooかbarにマッチするパターンを入力してみましょう。

/\(foo\|bar\)

検索は出来ましたが、何か\でたくさん前置してますね。 そうなんです。Vimのデフォルトのmagicオプションでは、一部の特殊文字は、 \で前置しないとメタ文字として扱われません。

厄介なのが、メタ文字として扱うために前置する必要がある文字と、 前置が不要な文字が混在しているのです。

例えば、magicの場合、.,*,^,$\で前置しなくてもメタ文字として扱われますが、 (,),|,+,?\で前置しないとメタ文字として扱われません。

正直慣れていても、誤爆する事が多々あります。

要は慣れてないってことですねorz
特に+は鬼門。。。

very magicを使う

上記問題は、very magicを使うことで解消出来ます。 vim Hacksでも紹介があったのでご存じの方も多いと思います。

Hack #55: 正規表現のメタ文字の扱いを制御する

この機能を使うと、正規表現に使うすべての特殊文字\で前置せずに使えるようになります。 ただ残念ながらこの機能、set verymagicのようにオプションとして設定出来ません。 very magicを有効にするには、パターンを入力するところで\vと入力します。 すると\v以降がvery magicとして扱われます。 例えば検索コマンドでは以下の様に入力します。

/\v

very magicを有効にした状態でfooかbarにマッチする文字列を検索してみましょう。

/\v(foo|bar)

これで、パターン検索する際に(|)\で前置する必要はなくなりました。

他にも、+,?,{など、よく使う正規表現のメタ文字が\での前置なしで使えるようになります。

私はパターン検索を常用しており、毎回\vと入力するのが嫌なので、以下のようにキーマップを設定しています。

nnoremap /  /\v

これで大分Vimでのパターン検索がしやすくなりました。

Vimのパターン検索に使えるメタ文字

very magicで、パターン検索でのメタ文字の入力が楽になりました。 ただ、Vimのパターンはちょっとした方言があるので、よく使うメタ文字をまとめておきたいと思います。

わかりやすさのため使っている人が多いと思われるPerl正規表現方言との比較を載せます。 簡単な比較は:help perl-patternsに載っています。

ここでまとめるVimのメタ文字はすべてvery magicの指定を前提としており\で前置していませんのでご注意ください。 また、全てを網羅しているわけではないので、詳細が知りたい方は:help pattern-overviewで調べるといいです。 ただし、helpにはmagicnomagicしか載ってません。といってもvery magicは基本的にはmagicで 前置されている\をとるだけなのでhelpを見て困ることはないと思います。

Perlのメタ文字 Vimのメタ文字 説明 備考
. 同じ 任意の1文字
* 同じ 直前のアトムの繰り返し(0回以上) 最長マッチ
+ 同じ 直前のアトムの繰り返し(1回以上) 最長マッチ
? 同じ(=も同じ) 直前のアトム(0回、または1回) 最長マッチ
{n,m} 同じ 直前のアトムの繰り返し(n回以上m回以下) 最長マッチ
^ 同じ 先頭にマッチ
$ 同じ 末尾にマッチ
(...) 同じ グループ化してアトムにする
| 同じ 選択の区切り
[...] 同じ [...]内の任意の1文字にマッチ
\w 同じ 単語を構成する文字([0-9A-Za-z_])
\W 同じ 単語を構成する文字以外([\^0-9A-Za-z_])
\d 同じ 数字([0-9])
\D 同じ 数字以外([\^0-9])
\s 同じ 空白文字(微妙に違う。後述)
\S 同じ 空白文字以外(微妙に違う。後述)
*? {-} 直前のアトムの繰り返し(0回以上) 最短マッチ
+? {-1,} 直前のアトムの繰り返し(1回以上) 最短マッチ
?? {-0,1} 直前のアトム(0回、または1回) 最短マッチ
{n,m}? {-n,m} 直前のアトムの繰り返し(n回以上m回以下) 最短マッチ
\b < or > 単語の境界にマッチ(<は単語先頭、>は単語末尾)
(?=atom) atom@= 幅ゼロの肯定先読み
(?!atom) atom@! 幅ゼロの否定先読み
(?<=atom) atom@<= 幅ゼロの肯定後読み
(?<!atom) atom@<! 幅ゼロの否定後読み
(?>atom) atom@> 幅ゼロの否定後読み
(?:...) %(...) グループ化して部分正規表現としてカウントしない

その他Vim特有の便利なメタ文字

Vim特有で便利なメタ文字を紹介します。

メタ文字 説明 備考
\zs どこにでもマッチしてマッチの開始地点を設定する foo\zsbarとすればfoobarのbarの部分にマッチします
\ze どこにでもマッチしてマッチの終了地点を設定する foo\zebarとすればfoobarのfooの部分にマッチします
%[...] 任意にマッチするアトム列 r%[ead]とすれば'r','re','rea','read'にマッチします

PerlVim正規表現の意味が異なる主なメタ文字

PerlVimで意味が異なる主なメタ文字を紹介します。 他にもあるので、使用する際は、:help \xなどで調べてから 使用することをおすすめします。

メタ文字 Perl Vim
\s スペース、タブ(\t)、改行文字(\n)、復帰文字(\r)、フォームフィード(\f) スペース、タブ(\t)
\S 上記以外 上記以外
\b 単語境界 <BS>にマッチ
\B 単語境界以外 なし
\A 文字列の先頭 英字以外
\Z 文字列の最後 Unicodeの合成文字は無視

very nomagicを使う

very magicでメタ文字を入力しやすくなりましたが、\で前置が必要な文字がいっぱいあって、 メタ文字じゃなくてその文字自体を検索したい場合に困るという方は、 very nomagicを使うと便利です。

very nomagicはパターンを指定するところで、\Vと入力するとそれ以降の文字で すべての特殊文字\で前置が必須になります。 つまり\を検索したい場合だけ\\とすればよく、他の文字は\で前置せずに 検索することが出来るようになります。

例えば、very magicで入力していて、ここからは普通にテキスト検索したいと思ったら途中で \Vvery nomagicに切り替えることができます。 以下の様に入力します。

/\v(foo|bar)\V(foo|bar)

これは、fooかbarにマッチする文字列の後に(foo|bar)という文字列を検索という意味になります。

ちなみにデフォルトのmagic\m、他にもnomagicの指定は\Mと指定することで切り替えられます。 \v,\V,\m,\Mはやろうと思えば、パターン内で何度も切り替えられるので便利です。

エスケープが必要な文字

very magicだとすべての特殊文字が前置不要になるためその特殊文字自体に マッチさせたい場合は\でエスケープが必要になります。 また、very magicmagicnomagicvery nomagicと、それぞれエスケープが 必要な文字が異なります。

ここでは、very magicmagicnomagicvery nomagic のそれぞれでエスケープが必要な特殊文字(つまりその文字単体でメタ文字として扱われる文字)をまとめます。

very magicの場合

  • \
  • .
  • *
  • +
  • ?
  • =
  • {
  • ^
  • $
  • (
  • )
  • |
  • [
  • &
  • @
  • ~
  • /(検索コマンド/で検索する場合、/でパターンを囲う場合)

特に、=,&,@,~は、Perl正規表現方言ではメタ文字ではないのでハマりポイントです。

magicの場合

  • \
  • .
  • *
  • ^
  • $
  • [
  • ~
  • /(検索コマンド/で検索する場合、/でパターンを囲う場合)

nomagicの場合

  • \
  • ^
  • $
  • /(検索コマンド/で検索する場合、/でパターンを囲う場合)

very nomagicの場合

  • \
  • /(検索コマンド/で検索する場合、/でパターンを囲う場合)

おまけ

関連オプション

パターン検索に関連するオプションを紹介します。

" 大文字と小文字を無視する
set ignorecase
" 大文字と小文字を無視しない
set noignorecase

" 検索パターンが大文字を含んでいたら'ignorecase'を上書きする。('ignorecase'オンのときのみ使われる)
set smartcase
" 'ignorecase'を上書きしない
set nosmartcase

" メタ文字として扱う文字を`magic`で指定(`.`,`*`,`$`,`^`,`[`などを`\`で前置なしでメタ文字として扱う)
set magic
" メタ文字として扱う文字を`nomagic`で指定(`$`,`^`などを`\ `で前置なしでメタ文字として扱う)
set nomagic

Perlのplugin使う

Perl/Ruby方言の正規表現で検索、置換などができる eregex.vimというplugin があります。

ただ、eregex.vimインクリメンタルサーチができません。 何より、突然素のVimを使う必要に迫られた際や、 急に思い立ってVim scriptを書きたいと思った時にも、 Vim正規表現が使えなくて困るかもしれません。

なので、本記事ではインクリメントサーチが不要な方で、 どうしてもPerl/Ruby方言の正規表現じゃなきゃやだ! という方以外にはおすすめしません。

ちなみに私はPerl使い(というほどたいしたものでもありませんが。。。)なので、 しばらく、eregex.vimを使っていた時期がありました。 ただ、very magicを知ってからは、Vim正規表現もそんなに不便なく使えることがわかったので、 今はVimのパターン検索を使っています。

Vimのパターン検索で参考になるhelp

Vimのパターン検索は以下のhelpを参照するといいです。 本記事で書いたことはほとんどhelpに書かれていることです。

:help usr_27.txt
:help pattern.txt

まとめ

以上が、Vimでパターン検索する際に知っておくと便利なことでした。 正直ほとんどhelpに載っている話だったり、他の方が書かれた記事と変わらなかったりしますが、自分の整理のために書いてみました。
ちょっととっつきにくいだけでVim正規表現は普通に使えるのでちょこちょこ使ってVim正規表現に慣れていくことをおすすめします。

もしパターン検索について、こんなことも載せて欲しいとか、これも載せたほうがいいとかあれば、コメントやTwitterなどでご指摘いただけるとうれしいです。

あと、Vimのパターン検索に限りませんが、Vimレベルが上がるのでvimrc読書会に参加することをおすすめします。毎週土曜日23時からやってます。 次回(5/18)は大御所kanaさんのvimrcを読む予定です。すごいことになりそう。。。

それではVim Advent Calendar 2012の166日目を終わります。

明日のVim Advent Calendar 2012はyonchuさんです。

Vimでgitのログをきれいに表示する

この記事はVim Advent Calendar 2012の161日目の記事です。
160日目はrbtnnさんによるEffective NeoBundle -- autoload関数を理解しNeoBundleを使いこなすための8の方法 --でした。

Inspired by ujihisa's VAC

本記事は、Vim Advent Calendar 2012 143日目にujihisaさんが書いた記事 撮った動画にインスパイアされて書いた記事です。ujihisa++

ujihisaさんの動画はこちら

ujihisaさんの動画を見て、GitLogViewer便利と思って早速設定してみました。 たしかに便利なんですが、コミットメッセージが表示されないので いちいち折畳を開かないと何のコミットなのかわからなくてちょっとめんどうだと思いました。 tigのメイン画面みたいに表示できたらもっと便利なのになーと思って ちょこちょこいじってたらそれっぽいものができました。 なので、本来今日上げようと思っていた記事を放り投げてこのネタで VACを書くことにしました。 本来今日あげようと思っていた記事は別の機会に載せます。

ちなみにtigって何?という方のために一応説明しておくと、 CUI上でGitのログとかdiffとかを見れるGitブラウザってやつですね。 紹介記事はWebに色々上がっているので詳しく知りたい方はググっていただきたいですが、 例えば以下の記事を見たらなんとなくイメージが掴めるかと思います。
CUI な Git ブラウザ tig を入れてみた

設定を載せる前に、実際の表示される画面の画像を載せておきます。

f:id:deris:20130510001655p:plain

画面の上部ウィンドウがgitのログを折りたたんで表示したものです。 流石にマージのグラフ表示とか色付けとかできてませんが、tigのメイン画面そっくりですね。 Date,Author,Commit Titleの順で並んでます。

折畳なのでzoなどで開けば以下の画像のように各コミットの中身が見れます。diffが見れて便利。 折畳を閉じる時はzcですね。

f:id:deris:20130510001703p:plain

以下の設定をすることで表示できます。元ネタのujihisaさんの設定もそうですが、動作にはvimprocが必要です。

" Inspired by ujihisa's vimrc
function! s:GitLogViewer()
  " vnewだとコミットメッセージが切れてしまうのでnew
  new
  VimProcRead git log -u 'ORIG_HEAD..HEAD'
  set filetype=git-log.git-diff
  setlocal foldmethod=expr
  setlocal foldexpr=getline(v:lnum)=~'^commit'?'>1':getline(v:lnum+1)=~'^commit'?'<1':'='
  setlocal foldtext=FoldTextOfGitLog()
endfunction
command! GitLogViewer call s:GitLogViewer()

" git log表示時の折りたたみ用
function! FoldTextOfGitLog()
  let month_map = {
    \ 'Jan' : '01',
    \ 'Feb' : '02',
    \ 'Mar' : '03',
    \ 'Apr' : '04',
    \ 'May' : '05',
    \ 'Jun' : '06',
    \ 'Jul' : '07',
    \ 'Aug' : '08',
    \ 'Sep' : '09',
    \ 'Oct' : '10',
    \ 'Nov' : '11',
    \ 'Dec' : '12',
    \ }

  if getline(v:foldstart) !~ '^commit'
    return getline(v:foldstart)
  endif

  if getline(v:foldstart + 1) =~ '^Author:'
    let author_lnum = v:foldstart + 1
  elseif getline(v:foldstart + 2) =~ '^Author:'
    " commitの次の行がMerge:の場合があるので
    let author_lnum = v:foldstart + 2
  else
    " commitの下2行がどちらもAuthor:で始まらなければ諦めて終了
    return getline(v:foldstart)
  endif

  let date_lnum = author_lnum + 1
  let message_lnum = date_lnum + 2

  let author = matchstr(getline(author_lnum), '^Author: \zs.*\ze <.\{-}>')
  let date = matchlist(getline(date_lnum), ' \(\a\{3}\) \(\d\{1,2}\) \(\d\{2}:\d\{2}:\d\{2}\) \(\d\{4}\)')
  let message = getline(message_lnum)

  let month = date[1]
  let day = printf('%02s', date[2])
  let time = date[3]
  let year = date[4]

  let datestr = join([year, month_map[month], day], '-')

  return join([datestr, time, author, message], ' ')
endfunction

一応gistにもあげておきました。

この設定を書いて、おもむろにGitLogViewerコマンドを実行すると上の画像のように 別ウィンドウでgitのログが表示されます。
ちなみにvimのカレントディレクトリ(:pwdで表示されるディレクトリ)でgit logコマンドを 実行するので、当たり前ですが、カレントディレクトリはgitで管理している ルートディレクトリ(.gitがあるディレクトリ)以下である必要があります。 必要に応じて:lcd,:cdなどで移動しておきましょう。
自動でプロジェクトルートディレクトリに移動してくれるvim-rooterなんて pluginもあるので必要に応じて入れておくといいと思います。

あと、ujihisaさんの動画でご紹介の通り、motemenさんのgit-vimという pluginを入れておくとGitLogViewerで表示されたウィンドウでの差分などの表示が 良い感じに色付けして表示されるのでこちらも必要に応じて入れておきましょう。 (というか元々は、git-vimで設定している FileTypegit-logをGitLogViewerで設定しているだけなんですけどね)

私は上記設定の他にautocmd FileType git-logでmotemenさんのgit-vimGitLogコマンドの出力も折畳で表示するようにしていますが、オプションによっては うまく表示されない可能性があるので、その設定は載せてません。

他にやりたいこと

大分きれいに表示されて自分的には満足なんですが、出来れば以下も設定したいなぁ。

  • tigみたいにブランチの位置も表示
  • 色付け(折畳表示なのでたぶん無理)

やるかどうかは未定。

おわり

折畳は普段あまり便利だと思ったことはないんですが、今回のように表示専用で 使う分にはスッキリ表示できて便利ですね。

折畳の設定などの普段使わない設定や、今回紹介したpluginなど新たな発見が あるのでvimrc読書会に参加することをおすすめします。
毎週土曜日23時からやってます。
ちなみに、5/11(土)は私のvimrc(後半)です。gkbr

それではVim Advent Calendar 2012の161日目を終わります。

明日のVim Advent Calendar 2012はtyruさんです。

追記(2013/5/10)

  • 今回載せた設定でGitLogViewerを実行した場合、本文中の画像と異なり、 ログが表示されるウィンドウの先頭行に空行が出ると思います。 本文中に載せた画像は自分が別で設定したmotemenさんのGitLogコマンドで 出力したもの折畳表示したものだったため微妙な差異が出てしまいました。すいません。
  • 本文中に補足するのを忘れていましたが、記事に載せた設定はgit log -u 'ORIG_HEAD..HEAD'としてます。 つまり、ORIG_HEADとHEADの間に何もなければ何も表示されません。必要に応じて書きかえてください。

vimでキーマッピングする際に考えたほうがいいこと

この記事はVim Advent Calendar 2012の153日目の記事です。
152日目はmanga_osyoさんによるVim で現在の検索位置を表示するでした。

vimをある程度使うようになると、ある操作(機能)を素早く呼び出すためなどでキーマップを設定するようになります。 ですが、数あるキーの中でどのキーに機能を割り当てるか結構頭を悩ませることが多いかと思います。
ちゃんと既存のキーの機能を意識して割り当てられれば良いですが、たまに「えっ、そのキー潰しちゃうの?」と思うような キーを潰しているのを見かけます(出典:vimrc読書会にて)。
意図してやっているならいいですが、わからぬまま重要なキーを潰してその機能を全く使わないという結構もったいないものです。
そこで、キーマッピングする際に考えたほうがいいことを記述したいと思います。

いつキーマッピングするか?

まずはどういう時にキーマッピングするかについて簡単にまとめたいと思います。

pluginの呼び出しに割り当てる

便利なplugin入れたら素早く呼び出すためにキーマップを設定したいと思うのがvimmerのさがというものです。 特にコマンドの入力が必要なpluginでは毎回コマンドを入力するのは面倒なのでキーマップを設定することが多いです。 例えば、pluginのUniteのコマンドを定義する場合以下の様に設定します。

nnoremap [unite]    <Nop>
nmap     <Space>u [unite]

nnoremap <silent> [unite]c   :<C-u>UniteWithCurrentDir -buffer-name=files buffer file_mru bookmark file<CR>
nnoremap <silent> [unite]b   :<C-u>Unite buffer<CR>

この設定により、<Space>uc<Space>ubでUniteのコマンドを実行することができます。 ちなみに、初めの二行は<Space>uをprefixキーとして設定して、[unite]でキーマッピングした箇所は<Space>uで置き換えるというhackです。 このように定義しておくメリットは、vimrc上見やすくなるという効果もありますが、<Space>uと打った時に[unite]と表示されるため、prefixキーを打ったということが把握できるという効果もあります。

vim hacksにも説明があるので知らなかった方はこちらもどうぞ
Hack #59: 分かりやすいKey-mappingsを定義する

よく行う操作に割り当てる

上の「plugin呼び出しに割り当てる」と近いですが、こちらは自分で作った関数やちょっとした操作をキーマップに割り当てます。

例えば以下のようにhelpを引くキーマップを設定するなどです。

nnoremap <C-h>      :<C-u>help<Space>
nnoremap <C-h><C-h> :<C-u>help<Space><C-r><C-w><CR>

使いづらいキーを使いやすいキーに割り当てる

Vimではたくさんのキーに色々な機能が割り当てられていますが、人によって使うキー、使わないキーが結構まちまちだと思います。 マクロバリバリ使うぜ!という人や、マクロなにそれ美味しいのという人ではよく使うキーが異なるわけです。(あーでも皆さんマクロ使いましょうねマクロ。こんな便利な機能使わないともったいないですよ。) このキーよく使うのになんか押しづらいんだよなぁと思ったらキーマッピングを考える時かもしれません。 例えば、自分は以下のように置き換えてます。

コマンドラインモードに入るための:と、f,t,F,Tを繰り返す;を入れ替えてます。 ;のデフォルトの機能も使いますが、コマンドラインモードに入る方が圧倒的に多いので押しやすい;にコマンドラインモードに入る機能を割り当てています。

nnoremap ; :
nnoremap : ;

表示上の行移動(エディタで表示されている行)であるgj,gkと、実際の行移動(エディタの表示行ではなく改行コードを意識した実際の行)であるj,kを入れ替えてます。 表示上の行移動の方が直感的に便利だという判断から押しやすいj,kに割り当てています。

nnoremap j gj
nnoremap k gk
nnoremap gj j
nnoremap gk k

誤操作すると困るキーを無効化する

例えば、ZZ(保存して閉じる)とZQ(保存せず閉じる)はとても便利な機能なんですが、常用するとたまに「あ、保存したかったのに間違ってZQで閉じちゃったorz」なんてことがあり、誤タイプによるリスクが利便性を上回ります。 なのでこんな感じで無効化してしまうといいでしょう。

nnoremap ZZ <Nop>
nnoremap ZQ <Nop>

<Nop>は空マップで何もしないという意味になります(:help <nop>)。

他に無効にするといいのはQです。Qでexモードに入れるのですが、意図せずexモードに入ってしまい、特に初心者はどうしようもなくなって:qしてしまう方もいると思います(自分もその一人)。 今の時代exモードを使う事なんてほとんどないので、誤タイプ防止で無効化します。

nnoremap Q <Nop>

:help gqを参考にgqに割り当ててもいいですね。

nnoremap Q gq

どのキーにキーマッピングするか?

ここまでで、どういう時にキーマップを設定するかについてお話しました。 ではどのキーに割り当てるのがいいのかについてお話します。

といっても、感覚的にここらへんのキーが良さそうというのはあるんですが、実際本当にそうなのか整理したことがなかったので、この機会に整理してみました。 整理のポイントは2点、

  1. そのキーにデフォルトで割り当てられている機能を使うか?
  2. そのキーは押しやすいか?

1.は割り当てられている機能を使わないキーのほうがキーマッピングに適しており、2.は押しやすいほどキーマッピングに適しているということです。

上記2つのポイントで整理してまとめた資料をgistにあげました。 今回整理したのはnormalモードだけです。

vimで使うキーの機能使用頻度と打ちやすさの整理

上記を整理した結果以下のことがわかりました。

キーマッピングに適しているキー(今回はnormalモードだけ)

詳細はgistの資料を見ていただくとして、 normalモードでは特に以下のキーがキーマッピングに適していそうです。

  1. <Space>
  2. ,
  3. s
  4. t
  5. m(prefixに)
<Space>キー

<Space>のデフォルトの機能はlと同じであり、<Space>としてほぼ使う機会がない割にとても押しやすいキーです(:help <Space>)。

<Space>は他のキーと合わせてprefixキーとして使うことをおすすめします。

上でも例としてあげましたが、以下の様な感じです。 <Space>uで、plugin uniteの専用のprefixキーとして使っています。

nnoremap [unite]    <Nop>
nmap     <Space>u [unite]

nnoremap <silent> [unite]c   :<C-u>UniteWithCurrentDir -buffer-name=files buffer file_mru bookmark file<CR>
nnoremap <silent> [unite]b   :<C-u>Unite buffer<CR>
,キー

f,t,F,Tでジャンプした後に、反対方向に繰り返すのに使うキーです。 f,t,F,Tを多用する人であれば使う機会もあるかと思いますが、;の進行方向に繰り返す機能は使えど、行きすぎて戻りたいと思うことはほとんどありません。

なので自分は潰してしまっています。

<Space>ほどではありませんが、程々に押しやすいキーなので他のキーと合わせてprefixキーとして使うのがいいかもしれません。 以下のようにしてmapleaderに設定するのもお勧めです。

let mapleader = ","

" ,のデフォルトの機能は、\で使えるように退避
noremap \  ,

このように設定しておくと、mappingの際<Leader>といれた箇所が,で置き換わります。 以下の様な感じです。

nnoremap <Leader>vv  :<C-u>VimShell<CR>

" 上記は実際にはこのようにキーマッピングされる
nnoremap ,vv  :<C-u>VimShell<CR>

<Leader>は自分でキーマッピングする際にも使えますが、helpにも書かれている通り、pluginが使うことが意図されているため自分でキーマッピングする際はちょっと注意が必要です(:help mapleader)。

sキー

sのデフォルトの機能は、clで代用可能であり、この機能のためだけにsキーを使うのはちょっともったいないです(:help s)。
なので使えるキーがなくて困っている場合は思い切って潰す事をおすすめします。

<Space>より押しやすいとはいえないですが(キーの位置的に)、指一つで使える貴重なキーなので多用する機能に割り当てるのがいいです。 もちろんprefixとして使うのもいいと思います。

tキー

tのデフォルトの機能は、カーソル左方向に向かって、tの後に入力した文字までジャンプ出来る機能です(:help t)。 fと似ていますが、fと違うのはfは入力した文字までジャンプするのに対して、tは入力した文字の左側の文字までジャンプすることです。

tの機能はオペレーターモードで使う機会が多い機能ですが、normalモードで使いたいと思うことはほとんどありません。 最悪fで飛んだ後にhで同じ事が出来るので、潰してしまっても構わないと思います。

tsと同じような考えで割り当てるといいと思います。

mキー

mのデフォルトの機能は、マーカを設定することですが(:help m)、最大52個のキー(a-z,A-Z)に割り当てることができ、これらすべてのキーを使う人はめったにないと思います。

なので、例えばマーカに使うのはabcdeなどと決めておき、それ以外のキーをキーマッピング用として使ってもいいと思います。

マーカをあまり使わない人であれば、mm,mn,mk,mjなど片手で入力しやすいキーをキーマッピング対象として考えることをおすすめします。

キーマッピングの覚え方

上では、特に押しやすさに注目して話をしましたが、それだけだと困ることがあります。 マッピングするキーが増えていくごとに、忘れてしまうのです。

そこで、押しやすさ以外にも忘れにくさ(連想のしやすさ)も考慮してキーマッピングすることをおすすめします。

例えば、pluginの機能を割り当てるキーマッピングでは、そのpluginの頭文字を使うなどです。

上でも例に上げましたが(3回目)、uniteのキーマッピングは覚えやすさも考慮して、<Space>uをprefixとして設定しています。 さらに機能ごとにもなるべく連想できそうなキーを割り当てていきます。 例えば、Unite bufferなら、Uniteのu、bufferのbを取って、<Space>ubなどとします。

nnoremap [unite]    <Nop>
nmap     <Space>u [unite]

nnoremap <silent> [unite]c   :<C-u>UniteWithCurrentDir -buffer-name=files buffer file_mru bookmark file<CR>
nnoremap <silent> [unite]b   :<C-u>Unite buffer<CR>

<Space>は関係なくない?と思われるかもしれませんが、そこは、pluginに割り当てるキーは<Space>と決めておけば覚えられます。

置き換えた方が良いキー

整理した表を見ると他にも気になる点が見えてきます。 例えば、よく使うキーなのに押しづらいキーです。

最初の方でもお話しましたが、よく使うキーで押しづらいキーは、別のキーにキーマッピングする候補になります。

例えば、^,$,*などはよく使う割に、全てShiftを伴うキーであり(小指を疲弊する)、さらにホームポジションから若干遠く押しづらいキーです。 なので、私は以下のように割り当てています。

noremap <Space>h  ^
noremap <Space>l  $
nnoremap <Space>/  *

ちなみに今回整理したことで以下のキーマップを設定することにしました。%も押しづらいですよね。

noremap <Space>m  %

ちなみに、上で説明した「pluginに割り当てるキーを<Space>と決めておけば」、の説明と矛盾するのでは?と思った方はするどいです。 自分もちゃんと頭で整理してキーマッピングを行なってきたわけではないので、こういうキーマッピングもあります。 まだあまりキーマッピングしていない方は、なるべく自分の中で納得できるように整理するといいです。

まとめ

以上が、私がキーマッピングでキーを割り当てる際に考えることです。
大層な考え方でもないですが、皆さんがキーマッピングを考える際の一助となれば幸いです。
時間があれば、自分の整理表を作ってまとめて見ることをおすすめします。(きっと新たなキーマッピングの可能性が出てくると思います。) こんな考え方もあるよなどのご意見があればコメントいただけるとありがたいです。

あと、キーマッピングに限りませんが、vimレベルが上がるのでvimrc読書会に参加することをおすすめします。毎週土曜日23時からやってます。

それではVim Advent Calendar 2012の153日目を終わります。

明日のVim Advent Calendar 2012はtyruさんです。

空行を挿入する+α

Vim Advent Calendar 2012の125日目の記事です。
124日目はcohamaさんによるはじめてプラグインを作ってみた。それとhelpの書き方など - 反省はしても後悔はしない。でした。
もう、1年の1/3越えちゃったんですね。一人で10記事くらい書いている方もいらっしゃるし、いったいVimmerはどこへ向かっているのか、僕も最後まで見守りたいと思います。

前置き

今日は、空行を挿入する方法をご紹介したいと思います。空行を挿入する方法と聞いて、あれ?どこかで聞いたことがあると思われた方も多いと思います。
そうですね。Vim HacksのVim-users.jp - Hack #57: 空行を挿入するで紹介されていますね。ujihisa++

上記記事の問題を要約すると、ノーマルモードで挿入モードに入らず副作用なく(コメントやインデント等の自動挿入なしに)空行を挿入したい。ということでした。
上記問題は記事で解決していますが、今回はちょっと気になった点があったのでその解決策をご紹介したいと思います。

気になったのは以下の2点です。

  • カウント指定(何行挿入する指定)したい
  • .で繰り返したい

本記事では上記を解決する話をします。時間がない方はまとめだけ見ればコードが載っているのでそちらをご覧ください。

カウント指定したい

行挿入はノーマルモードでのoappend()関数で可能です。
oを使えば、5oなどと入力すれば5行挿入することができますが、自動インデント挿入やコメント挿入があるので今回は使いません。

じゃあappend()関数を使って以下のようにすればどうでしょうか?

nnoremap <silent> <Space>o   :call append(line('.'),   '')<CR>

これだと、5oとした時に以下のようになります。

:.,.+4call append(line('.'),   '')<CR>

範囲指定でのコマンド実行になるので、現在行から下4行目まで順にappend()がコールされていくことになり、一応カウント対応可能です。
ただ、この場合、カーソルが挿入した行の最終行から1つ上の行に移動してしまいます。
現在行の次行以降に5行挿入したいだけで、移動はして欲しくありません。

そこで、v:count1を使います。

カウント系の変数はv:countv:count1があります。違いはカウントを指定しなかった時にv:countは0、v:count1は1になります。以下の様なイメージです。

指定したカウント 1 2 3 4 5
v:count 0 2 3 4 5
v:count1 1 2 3 4 5

次行にカウント指定で挿入したい場合、v:count1を使って以下の様に書けます。

for i in range(1, v:count1)
  call append(line('.'),   '')
endfor

ループのインデックスのiは捨ててます。
同じように前行の場合は以下のように書けますね。

for i in range(1, v:count1)
  call append(line('.') - 1,   '')
endfor

それぞれ一行で書いてマッピング書くと以下になります。

nnoremap <silent> <Space>o   :<C-u>for i in range(1, v:count1) \| call append(line('.'),   '') \| endfor<CR>
nnoremap <silent> <Space>O   :<C-u>for i in range(1, v:count1) \| call append(line('.')-1, '') \| endfor<CR>

.で繰り返したい

最初は上記で満足していたんですが、そのうち上記操作後の.の動作が気に入らなくなりました。上記のマッピング後に.を入力すると、以前カウントが指定されていても、行が1行だけしか入力されません。.は最後の変更を繰り返す。ということなので、この最後の変更が、ループの最後のappend()を繰り返すと解釈されます。
やりたいのは、5行挿入したなら、.で5行挿入したいのです。
繰り返し対応のvim pluginもあることだし(repeat.vim)、何かしら繰り返しの動作を設定・変更する方法があるのだろうなぁと思って調べてみましたが、全く見当たりませんでした。はて、じゃあrepeat.vimはどうやって繰り返しを実現しているのだろうと思って中身を見てみたところ、以下のマッピングをしていました。

nnoremap <silent> .     :<C-U>call repeat#run(v:count)<CR>
nnoremap <silent> u     :<C-U>call repeat#wrap('u',v:count)<CR>

おおう、key mappingで対応していたんですね。。。しかもご丁寧にuまでマッピングしている。。。で、繰り返し操作を変更したい場合はどうするかというとrepeat.vimの関数をコールして直前の操作を教えてあげてると、repeat.vimプラグイン側で管理してくれます。以下はsurround.vimのソースから抜粋したものです。。

silent! call repeat#set("\<Plug>Dsurround".char,scount)

第一引数に繰り返し時(.入力時)にタイプしたい文字列、第二引数にカウント数を指定します。

で、空行挿入の話に戻ると、先ほどの空行挿入はoにマッピングしているので以下のように関数をコールしてあげれば、次に.を入力した時にこれを入力する動きになります。

call repeat#set("<Space>o", v:count1)

で、最終的にどうなったかというと以下のkey mappingです。

nnoremap <silent> <Space>o   :<C-u>for i in range(1, v:count1) \| call append(line('.'),   '') \| endfor \| silent! call repeat#set("<Space>o", v:count1)<CR>
nnoremap <silent> <Space>O   :<C-u>for i in range(1, v:count1) \| call append(line('.')-1, '') \| endfor \| silent! call repeat#set("<Space>O", v:count1)<CR>

まとめ

以下コードをvimrcに貼り付け、repeat.vim プラグインを入れれば、oで現在行の次行に挿入、Oで現在行の前行に挿入でき、カウント指定、.での繰り返しができるようになりました。

nnoremap <silent> <Space>o   :<C-u>for i in range(1, v:count1) \| call append(line('.'),   '') \| endfor \| silent! call repeat#set("<Space>o", v:count1)<CR>
nnoremap <silent> <Space>O   :<C-u>for i in range(1, v:count1) \| call append(line('.')-1, '') \| endfor \| silent! call repeat#set("<Space>O", v:count1)<CR>

できれば、vim pluginなしで.の繰り返しの操作を変更したかったので、どなたかご存知のかたいらっしゃいましたらコメントなどいただけるとありがたいです。Vim側で対応できないものなんですかね?

明日のVim Advent Calendar 2012はShougoさんです。