Vimではデフォルトで500種類以上の言語をシンタックスハイライトすることができます。
また、シンタックスハイライト以外の設定も充実しており、
デフォルトでは約100種類の言語で専用の自動インデントが行われるようになっています。
この約100種類は普段使用する範囲ならば何の問題もないのですが、
人口比率の少ない言語で何かを書こうとしたら
デフォルトでは専用インデント設定がなかったというケースは案外あります。
文法がC系の言語であれば'smartindent'
で誤魔化すことができるのですが、
人口比率が少ない言語は大抵の場合 'smartindent'
が使えない言語です。
という訳で独自の自動インデントの設定を書く必要が出てきました。
しかしどう書けばよいのでしょうか。
例として Haskell 用のインデント設定を書くことにしましょう。
Haskellはメジャーな言語ではあるものの何故かデフォルトではインデント設定が用意されていません
(探せば既に誰かが書いた設定は見つかるので、普通はそれを流用すれば良いのですが、今回は無視します)。
ユーザー独自のインデント設定用のファイルは ~/.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
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
が定義済みかどうかのチェックも書いておきます。
インデント量の設定については以下のオプションで設定することができます。
Haskellの場合は以下のような設定でよいでしょう:
setlocal expandtab
setlocal tabstop<
setlocal softtabstop=2
setlocal shiftwidth=2
'expandtab'
'tabstop'
'expandtab'
を有効して'tabstop'
の値は触らないでおく方がよいでしょう。'tabstop'
や大抵のオプションはバッファ別に異なる値を設定でき、'tabstop'
の値としてユーザーが設定したかも知れないデフォルト値を使うよう明示的に記述しています。'softtabstop'
'tabstop'
と同じ量です。'shiftwidth'
と同じ値に設定しておくと良いでしょう。'shiftwidth'
>>
等のコマンドや自動インデントの際に使う1レベル分のインデント量を設定します。次に自動インデントが行われるタイミングを決めましょう。
これは'indentkeys'
の値を適切なものに調整することでできます。
また、'indentkeys'
の値のフォーマットおおよそ以下のようになっています:
,
)区切りで並べたものになります。[{修飾子}]{入力内容}
の形になります。{修飾子}
は省略可能です。{入力内容}
は1キー分の入力を表す文字列になります。一部の文字は特殊な意味を持ちます。例えば以下のように設定したとしましょう:
setlocal indentkeys=!^F,o,O,0<Bar>,0=where
各項目の意味は以下の通りです:
o
/ O
<Enter>
で表せられるのでsetlocal indentkeys=...,<Enter>,...
などとしてもいいのですが、<Enter>
を入力する他にもo
で新しい行を作ることもあり、'indentkeys'
での o
はこの両者を表します。O
はNormal modeの O
による改行時を表します。 (一見すると「o
そのものが入力された場合に自動インデントを行う」という設定が書けなくなりそうですが、<Char-0x6f>
のような代替表記ができるので問題ありません)!^F
<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
を例に挙げましたが、let
や in
や then
や else
等がありますから、後はこれを応用すれば自動インデントの発動タイミングを自由自在に決定することができます。
コンテキストに応じたインデント量の算出は'indentexpr'
オプションでカスタマイズできます。'indentexpr'
の値はVim scriptの任意の式を表す文字列で、
自動インデントが行われるたびに評価されます。
評価結果によって自動インデントによるインデント量が決められます。
評価結果は数値として解釈され、例えば結果が3ならスペース3個分のインデントが行われます。
ワンライナーで済ませられるほどインデント量の算出は甘くないため、
大抵は 'indentexpr'
の値は以下のような関数呼び出しにしておき:
setlocal indentexpr=GetHaskellIndent()
実際の処理は関数の方で書くことになります。
function! GetHaskellIndent()
return -1
endfunction
取り敢えずは -1
を返すだけにしておいて、具体的な処理は後から書くことにしましょう。
なお、 -1
は「直前の行のインデント量をそのまま使う」という意味です
(正確には'autoindent'
を有効にしておく必要があります。
同じ動作は 'indentexpr'
側でカバーできるのですが、
いちいち実装するのも手間なので 'autoindent'
を利用する方が楽です)。
(この例ではインデント量算出用の関数をグローバルな名前空間に定義しています。
本当は関数をスクリプトローカルな名前空間に定義しておく方が
他の設定と干渉する可能性がなくなって良いのですが、
話を単純にするためにここではグローバルな名前空間に定義しています。)
「'indentexpr'
でインデント量を決められます」と言われても
適切なインデント量を求めるためにはカーソル付近のテキストをあれこれ調べる必要があります。
一先ずは以下のAPIを押さえておけば困らないでしょう:
v:lnum
indentkeys
では特に明示されていない限りv:lnum
は新しく作成された行を指します。indent({lnum})
prevnonblank({lnum})
'autoindent'
相当のことは indent(prevnonblank(v:lnum))
で表現できます。nextnonblank({lnum})
prevnonblank()
と同様ですが、指定した行かそれより下の行を探す点が異なります。getline({lnum})
getline(prevnonblank(v:lnum)) =~# '^\s*\<if\>.*:'
とすればif
文の後の行にあるかどうかが判定できます。&l:shiftwidth
'shiftwidth'
の値です。(col('.') - 1) == matchend(getline('.'), '^\s*')
indent(lnum) + &l:shiftwidth
もしくは
indent(lnum) - &l:shiftwidth
ここで lnum
は基準となる行の行番号で、大抵の場合は prevnonblank(v:lnum - 1)
となるでしょう。
例えば class
や instance
や where
などのキーワードのある行で改行したならば
後続する行は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$'
追記するのでコメントをください。
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'
の記述を省略しても動くには動くのですが、
後々「なんかいつのまにかインデントがおかしくなることがある」
現象に遭遇して気分が悪くなります。
できるだけ書いておきましょう。
今回は独自のインデント設定を新たに作ることを主眼に解説しましたが、
実際には一からインデント設定を作る機会よりも、
既存のインデント設定を利用しつつ細かいところを調整する機会の方が多いでしょう。
最もよくあるパターンは
「既存のインデント設定だと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<',
\ ])
b:did_indent
だの b:undo_indent
の定型文を書くのは結構面倒臭いです。