Vimで心地良い自動インデント設定を書くためのポイント9個


2011年 04月 11日

問題

Vimではデフォルトで500種類以上の言語をシンタックスハイライトすることができます。
また、シンタックスハイライト以外の設定も充実しており、
デフォルトでは約100種類の言語で専用の自動インデントが行われるようになっています。

この約100種類は普段使用する範囲ならば何の問題もないのですが、
人口比率の少ない言語で何かを書こうとしたら
デフォルトでは専用インデント設定がなかったというケースは案外あります。
文法がC系の言語であれば'smartindent'で誤魔化すことができるのですが、
人口比率が少ない言語は大抵の場合 'smartindent' が使えない言語です。

という訳で独自の自動インデントの設定を書く必要が出てきました。
しかしどう書けばよいのでしょうか。

解決方法

例として Haskell 用のインデント設定を書くことにしましょう。
Haskellはメジャーな言語ではあるものの何故かデフォルトではインデント設定が用意されていません
(探せば既に誰かが書いた設定は見つかるので、普通はそれを流用すれば良いのですが、今回は無視します)。

1. インデント設定用のファイルを作る

ユーザー独自のインデント設定用のファイルは ~/.vim/indent/$filetype.vim に保存することになっています
(~/.vim は適宜読み替えてください)。
ここで $filetype は言語を表すVim固有の名前です。
Haskellの場合は haskell になります。
他の言語の場合は :edit $VIMRUNTIME/syntax/ を眺めて推測しましょう。
$VIMRUNTIME/syntax/ に適当なものがない場合は他と競合しない範囲で適当に決めましょう。

indent/$filetype.vim の中身はひとまず以下の物をコピーしてください。
詳細は後々解説します。

if exists('b:did_indent')
  finish
endif


setlocal autoindent
setlocal indentexpr=GetHaskellIndent()
setlocal indentkeys=!^F,o,O

setlocal expandtab
setlocal tabstop<
setlocal softtabstop=2
setlocal shiftwidth=2

let b:undo_indent = 'setlocal '.join([
\   'autoindent<',
\   'expandtab<',
\   'indentexpr<',
\   'indentkeys<',
\   'shiftwidth<',
\   'softtabstop<',
\   'tabstop<',
\ ])


function! GetHaskellIndent()
  return -1
endfunction


let b:did_indent = 1

2. 他のインデント設定が読み込まれないようガードする

Vimの設定はかなり大雑把に分類すると以下の順序で適用されます:

  1. ユーザー独自の設定
  2. Vimのデフォルトの設定

今回は例としてHaskellのインデント設定を新たに作ることにしましたが、
将来的にHaskell用のインデント設定がデフォルトでVimに同梱される可能性は少なくないでしょう。
となると近い未来にVimのバージョンを上げた際、
Haskellのインデント設定がユーザー独自のものとVimのデフォルトのものの両方が適用されることになります。
設定は後から上書きしたものが優先されるので、
このままではユーザー独自の設定が無視されることになってしまいます。

という訳で意図的にVimデフォルトの設定が適用されないようにガードする必要があります。
これには以下の記述を追加します:

  • 最初に変数 b:did_indent が定義済みかチェックし、定義済みならば何もせずに終了する。 if exists('b:did_indent') finish endif
  • インデント用の設定を一通り終えたら変数 b:did_indent を定義する(値はなんでもいい)。 let b:did_indent = 1

インデント用の設定ファイルでは上記の慣習に沿って記述されているため、
b:did_indent を定義すれば後から読み込まれる設定は無視することができます。
また、ここで作る設定ファイルが他の設定ファイルより後から読み込まれるケースも考慮して、
b:did_indent が定義済みかどうかのチェックも書いておきます。

3. 1レベル分のインデント量を決める

インデント量の設定については以下のオプションで設定することができます。
Haskellの場合は以下のような設定でよいでしょう:

setlocal expandtab
setlocal tabstop<
setlocal softtabstop=2
setlocal shiftwidth=2
  • 'expandtab'
    タブ文字を入力した際に自動でホワイトスペースに展開されるかどうかを設定できます。
    タブ文字とその幅は紛争の火種になりかねませんし、
    Haskellの場合はオフサイドルールのためにタブ文字を使うと間違いなく紛争が起きます。
    特に理由がない限りは有効にしておきましょう。
  • 'tabstop'
    タブ文字の幅を設定できます。デフォルトは8です。
    先述のとおり、強い宗教的な理由がない限りは'expandtab' を有効して'tabstop' の値は触らないでおく方がよいでしょう。
    なお、 'tabstop' や大抵のオプションはバッファ別に異なる値を設定でき、
    さらにそのようなオプションではデフォルトの値も設定することができます。
    ここでは'tabstop' の値としてユーザーが設定したかも知れないデフォルト値を使うよう明示的に記述しています。
  • 'softtabstop'
    タブ文字を入力した際にタブ文字の代わりに挿入されるホワイトスペースの量を設定します。
    デフォルトでは 'tabstop' と同じ量です。
    後述する 'shiftwidth' と同じ値に設定しておくと良いでしょう。
  • 'shiftwidth'
    >>等のコマンドや自動インデントの際に使う1レベル分のインデント量を設定します。
    これは2や3や4や8など、ここは好みの量で決めて構いません。

4. 自動インデントを発動させるタイミングを設定する

次に自動インデントが行われるタイミングを決めましょう。
これは'indentkeys'の値を適切なものに調整することでできます。
また、'indentkeys' の値のフォーマットおおよそ以下のようになっています:

  • 値は設定項目をカンマ(,)区切りで並べたものになります。
  • 1項目は [{修飾子}]{入力内容} の形になります。{修飾子} は省略可能です。
  • {入力内容} は1キー分の入力を表す文字列になります。一部の文字は特殊な意味を持ちます。

例えば以下のように設定したとしましょう:

setlocal indentkeys=!^F,o,O,0<Bar>,0=where

各項目の意味は以下の通りです:

  • o / O

    まず改行時に自動インデントがされてほしいのはほとんどの言語で共通でしょう。
    改行そのものは <Enter> で表せられるので
    setlocal indentkeys=...,<Enter>,... などとしてもいいのですが、
    VimではInsert modeで <Enter> を入力する他にも
    Normal modeで o で新しい行を作ることもあり、
    そのタイミングでもやはり自動インデントされて欲しいものです。
    'indentkeys' での o はこの両者を表します。
    また、O はNormal modeの O による改行時を表します。 (一見すると「o そのものが入力された場合に自動インデントを行う」という設定が書けなくなりそうですが、
    <Char-0x6f> のような代替表記ができるので問題ありません)
  • !^F
    伝統的にInsert mode中では <C-f> でカーソル行のインデントができるようになっています。
    !^F はこれを表す設定です。 ^F<C-f> (= Ctrl-F)と同じ意味です。 ! は修飾子で
    「後続するキーに対応する文字はバッファに挿入されない」
    「カーソル行のインデントだけを行う」
    という意味です。 !^F 自体は 'indentkeys' のデフォルト値に含まれているため、
    特に理由がない限り値に含めておいた方が余計な混乱を招かずに済むでしょう。
  • 0<Bar>
    0 は修飾子で
    「現在の行で最初に入力されたキーが後続するキーならばインデントを行う」
    という意味です。
    <Bar>| の代替表記です。
    | はVimのコマンドとして特殊な意味を持つので代替表記を使う必要があります。 例えば以下のようなコードを入力する場合は(特に2個目の) | で自動インデントされてほしいでしょう:
f a b
  | a == b = do foo
                bar
  | otherwise do bar
                  foo

ですが以下のようなコードを入力している最中に自動インデントが発動しても邪魔なだけです:

f a b = a ++ "||" ++ b

後述する関数の方で調整してもいいのですが、
余分なケースについてまで考えるのは手間ですので、
このようにして発動タイミングを調整した方が良いでしょう。

  • 0=where
    = は修飾子で
    「後続する単語が入力されたらインデントを行う」
    という意味です。
    ここでは where を例に挙げましたが、
    Haskellの場合は他にもインデント調整が必要なキーワードとして
    letinthenelse 等がありますから、
    必要に応じて適宜追加するとよいでしょう。

後はこれを応用すれば自動インデントの発動タイミングを自由自在に決定することができます。

5. どのように自動インデントさせるか設定する

コンテキストに応じたインデント量の算出は'indentexpr'オプションでカスタマイズできます。
'indentexpr' の値はVim scriptの任意の式を表す文字列で、
自動インデントが行われるたびに評価されます。
評価結果によって自動インデントによるインデント量が決められます。
評価結果は数値として解釈され、例えば結果が3ならスペース3個分のインデントが行われます。

ワンライナーで済ませられるほどインデント量の算出は甘くないため、
大抵は 'indentexpr' の値は以下のような関数呼び出しにしておき:

setlocal indentexpr=GetHaskellIndent()

実際の処理は関数の方で書くことになります。

function! GetHaskellIndent()
  return -1
endfunction

取り敢えずは -1 を返すだけにしておいて、具体的な処理は後から書くことにしましょう。
なお、 -1 は「直前の行のインデント量をそのまま使う」という意味です
(正確には'autoindent'を有効にしておく必要があります。
同じ動作は 'indentexpr' 側でカバーできるのですが、
いちいち実装するのも手間なので 'autoindent' を利用する方が楽です)。

(この例ではインデント量算出用の関数をグローバルな名前空間に定義しています。
本当は関数をスクリプトローカルな名前空間に定義しておく方が
他の設定と干渉する可能性がなくなって良いのですが、
話を単純にするためにここではグローバルな名前空間に定義しています。)

6. インデント量の算出に必要な道具を押さえる

'indentexpr' でインデント量を決められます」と言われても
適切なインデント量を求めるためにはカーソル付近のテキストをあれこれ調べる必要があります。
一先ずは以下のAPIを押さえておけば困らないでしょう:

  • v:lnum
    自動インデントが発動した時点でのカーソル位置の行番号です。 なお indentkeys では特に明示されていない限り
    「指定されたキーをバッファへ挿入した後に自動インデントを行う」
    ことになります。
    特に改行時にインデントを行う場合、 v:lnum は新しく作成された行を指します。
  • indent({lnum})
    指定した行のインデント量を返します。
    例えば戻り値が3ならインデント量はスペース3個分です。
    タブ文字がインデントに使われている場合はよしなにスペースに換算した値になります。
  • prevnonblank({lnum})
    指定した行かそれより上にある行のうち、空行でない最初の行の行番号を返します。 例えば 'autoindent' 相当のことは indent(prevnonblank(v:lnum)) で表現できます。
  • nextnonblank({lnum})
    prevnonblank() と同様ですが、指定した行かそれより下の行を探す点が異なります。
  • getline({lnum})
    指定した行の内容を文字列で返します。
    例えば getline(prevnonblank(v:lnum)) =~# '^\s*\<if\>.*:' とすれば
    カーソルがPythonの if 文の後の行にあるかどうかが判定できます。
  • &l:shiftwidth
    現在のバッファの 'shiftwidth' の値です。
    インデント量を増減させるときは基本的にこの値を1レベル分として値を決めます。

7. インデント量の算出でよくあるパターンを押さえる

改行による自動インデントかどうか判定する
(col('.') - 1) == matchend(getline('.'), '^\s*')
インデント量を1レベル分増減する
indent(lnum) + &l:shiftwidth

もしくは

indent(lnum) - &l:shiftwidth

ここで lnum は基準となる行の行番号で、大抵の場合は prevnonblank(v:lnum - 1) となるでしょう。

現在の行が特定の行に後続するかどうか判定する

例えば classinstancewhere などのキーワードのある行で改行したならば
後続する行は1レベル分インデントを増やすことになるでしょう。
これは以下のコードで実現できます:

let plnum = prevnonblank(v:lnum - 1)
if getline(plnum) =~# '\v^\s*<class|instance|where>'
  return indent(plnum) + &l:shiftwidth
endif
カーソルが文字列リテラルやコメント中にあるかどうか判定する

シンタックスハイライトの結果を再利用すると簡単に判定できます。
例えば以下のコードでカーソルが文字列リテラル中にあるかどうか判定できます:

has('syntax_items') && synIDattr(synID(line('.'), col('.'), 1), 'name') =~? 'String$'
他にも○○かどうか判定したいんですが……

追記するのでコメントをください。

8. カスタマイズしたインデント設定を元に戻すことができるようにする

Vimでは編集中のテキストの種類('filetype')に応じてシンタックスハイライトやインデント等の設定を行うのですが、同一バッファでも 'filetype' を切り替えることは可能です。

この時、インデント設定も適宜切り替わってくれればいいのですが、
残念ながらVim本体側ではインデント用の設定として何がどう変更されたかは把握できません。
例えば 'filetype' をAからBへ切り替えた場合、
自動でA用のインデント設定が行われる前の状態に戻すことができません。
結果としてA用のインデント設定とB用のインデント設定が中途半端に混合した状態になってしまいます。

そのため、各々の設定ファイルで
「この設定ファイルで変更した項目を元に戻すにはどうすればいいか」
を記述してあげる必要があります。
これには変数 'b:undo_indent' へ「元に戻す」コマンドを文字列の形で設定します。

let b:undo_indent = 'setlocal '.join([
\   'autoindent<',
\   'expandtab<',
\   'indentexpr<',
\   'indentkeys<',
\   'shiftwidth<',
\   'softtabstop<',
\ ])

今回は上記のオプションの値を変更しているので、
各オプションの値を元に戻すためのコマンドを設定しています。

'b:undo_indent' の記述を省略しても動くには動くのですが、
後々「なんかいつのまにかインデントがおかしくなることがある」
現象に遭遇して気分が悪くなります。
できるだけ書いておきましょう。

9. 既存のインデント設定を利用しつつ細かいところを調整する

今回は独自のインデント設定を新たに作ることを主眼に解説しましたが、
実際には一からインデント設定を作る機会よりも、
既存のインデント設定を利用しつつ細かいところを調整する機会の方が多いでしょう。
最もよくあるパターンは
「既存のインデント設定だと1レベル分のインデント量が2になってるけど4に変えたい」
のようなものです。

この場合、これまでに紹介したポイントは利用できますが、
以下の点を変更する必要があります:

  • 作成するファイルは ~/.vim/after/indent/$filetype.vim にします。
    ~/.vim/after ディレクトリの内容はデフォルトの設定よりも後で読み込まれるため、
    上書きしたい設定がある場合はここに記述しておきます。
  • b:did_indent が定義済みかどうかのチェックは削除します。
    このファイルでは意図的に他の設定内容を後から上書きするため、
    b:did_indent のチェックをしてしまうと意味がありません。
  • b:undo_indent へ「変更した設定項目を元に戻す」コマンドを追記します。
    既存の設定ファイルで変更された項目を上書きした場合はいいのですが、
    既存の設定ファイルで変更されていない項目を新たに設定する可能性もあります。
    b:undo_indent へは逐次追記してあげる必要があります。

    また、
    「メインとなる設定ファイルが存在しなかった」
    「既存の設定ファイルが行儀の悪いもので b:undo_indent が定義されていない」
    というケースがあるため、
    b:undo_indent が定義されていない場合にも備えておきます。

例えばRubyのデフォルトのインデント設定では1レベル分のインデント量が8になっています。
これを3に変える場合、 ~/.vim/after/indent/ruby.vim に以下のような内容を書くことになるでしょう:

setlocal softtabstop=3
setlocal shiftwidth=3

if !exists('b:undo_indent')
  let b:undo_indent = ''
endif
let b:undo_indent .= '| setlocal '.join([
\   'shiftwidth<',
\   'softtabstop<',
\ ])

補足