読者です 読者をやめる 読者になる 読者になる

折り畳み嫌いの男が一夜でFolding freakにまでなった話2

この記事はVim Advent Calendar 2012の144日目の記事です。前日はujihisaさんによる動画Gitのログをいい感じに読むGitLogViewerを用いてneosnippetやneocomplcacheの更新を追うなどする (vim's podcast)でした。

ごぶさたしておりますVimmerのみなさまいかがお過ごしでしょうか。

さて、1年と4カ月ほど前に、折り畳み嫌いの男が一夜でFolding freakにまでなった話と言う記事があったのを覚えているでしょうか。

その当時は私はVimの折りたたみを使い始めたばかりでした。
その後、設定は洗練され、その時作ったプラグインもアップデートされましたので、再びご紹介しようと思います。



foldCC

一昨年、Vim Advent Carendarのために作ったプラグインです。
折りたたみの見た目を変更します。(こんなふうに)
before
f:id:leafcage:20130424061355j:plain
after
f:id:leafcage:20130424061408j:plain
そのほかにいくつかの機能と変数が追加されました。
LeafCage/foldCC · GitHub*1

セットアップ
set foldtext=foldCCtext()

これで折り畳みの表示ににFoldCCtext()が使われるようになります。*2

表示される内容の変更

{g:foldCCtext_headで定義した内容} + {折り畳みの内容*3} + {g:foldCCtext_tailで定義した内容}が表示されます。
これらの変数をいじることで表示内容をある程度操作できます。

let g:foldCCtext_tail = 'v:foldend-v:foldstart+1'

f:id:leafcage:20130424062310j:plain
ただし、g:foldCCtext_headg:foldCCtext_tailに指定する文字列は式として評価されるので文字列を表示させるにはさらにクォートで括る必要があります。
数値を導く式と文字列を連結させる時には先に数値が評価されるように括弧を忘れないでください。あと全角文字使うと切り詰め計算が狂って余計に切り詰められる不具合がありそうです。

let g:foldCCtext_head = '"(  ゚ェ゚)"'
let g:foldCCtext_tail = '"(゚ェ゚  )". (v:foldend-v:foldstart+1)'

f:id:leafcage:20130424063655j:plain

なお、変数はg:foldCCtext_headが 'v:folddashes. " "' で、
g:foldCCtext_tailが 'printf(" %s[%4d lines Lv%-2d]%s", v:folddashes, v:foldend-v:foldstart+1, v:foldlevel, v:folddashes)'
で初期化されています。

新機能
let g:foldCCtext_enable_autofdc_adjuster = 1

g:foldCCtext_enable_autofdc_adjusterを非0にすると折り畳みを表示するついでに、その折り畳みが深くてfoldcolumnをはみ出る場合は、自動でfoldcolumnの値を増加させてくれます。
普段はfoldcolumnを

set foldcolumn=3

位の設定で控えめに表示させ、深い折り畳みがあるファイルに限ってfoldcolumnを増やすということができます。
before
f:id:leafcage:20130424065106j:plain
after (foldcolumnが増えた!)
f:id:leafcage:20130424065116j:plain
foldcolumnが足りなくて変な数字が表示されてしまう症状から解放されます。
(この機能が発動しない時には一度折り畳みを閉じてから開き直してください。)





折り畳みを活用するためのキーバインド

Folding使い始めのときには勝手がわからず、用意されているバインドを手当たりしだいに使ったものでした。
今現在は、zfとzd、[zと]z、zi、後は自分で用意したバインドを複数個程度使っています。
zfとzdはFoldingの作成と削除。ziはFolding有効無効切り替え。[zと]zについては後述します。
先に自分で用意したバインドについて利用頻度の高いものから順に説明します。

l(開く)
nnoremap <expr>l  foldclosed('.') != -1 ? 'zo' : 'l'

折り畳みを「開く」です。カーソル下に折り畳みがあれば一段階開きます。通常のl(右移動)と兼用になっているので経済的・直感的です。

<C-_>(閉じる)
nnoremap <silent><C-_> :<C-u>call <SID>smart_foldcloser()<CR>
function! s:smart_foldcloser() "{{{
  if foldlevel('.') == 0
    norm! zM
    return
  endif

  let foldc_lnum = foldclosed('.')
  norm! zc
  if foldc_lnum == -1
    return
  endif

  if foldclosed('.') != foldc_lnum
    return
  endif
  norm! zM
endfunction
"}}}

Ctrl押しながら-(マイナス)*4で、折り畳みを「閉じる」です。NetBeansの「折り畳みを閉じる」バインドと同じです。
連打すると次々と親Foldingを閉じていきます。閉じられる折り畳みがない時には全ての折り畳みを閉じます。
折り畳みをすべて閉じて初期状態に戻りたいときにも使います。

z[,z] (FoldingMarkerを挿入する)
nnoremap  z[     :<C-u>call <SID>put_foldmarker(0)<CR>
nnoremap  z]     :<C-u>call <SID>put_foldmarker(1)<CR>
function! s:put_foldmarker(foldclose_p) "{{{
  let crrstr = getline('.')
  let padding = crrstr=='' ? '' : crrstr=~'\s$' ? '' : ' '
  let [cms_start, cms_end] = ['', '']
  let outside_a_comment_p = synIDattr(synID(line('.'), col('$')-1, 1), 'name') !~? 'comment'
  if outside_a_comment_p
    let cms_start = matchstr(&cms,'\V\s\*\zs\.\+\ze%s')
    let cms_end = matchstr(&cms,'\V%s\zs\.\+')
  endif
  let fmr = split(&fmr, ',')[a:foldclose_p]. (v:count ? v:count : '')
  exe 'norm! A'. padding. cms_start. fmr. cms_end
endfunction
"}}}

FoldingMarkerを現在行の末尾に挿入します。z[で'{{{'が、z]で'}}}'が挿入されます。
カウントを指定すると、数字付きのFoldingMarkerが挿入されます。5z[だと'{{{5'が、1z]だと'}}}1'が挿入されます。
また、自動的にマーカーをコメント化したり、間に空白文字を挟むなどしてくれるので便利です。

z<C-_> (現在折り畳みを閉じて、他の折り畳みをすべて閉じる)
nnoremap <silent>z<C-_>    zMzvzc

複数の折り畳み階層があるファイルで使います。現在自分がいる階層の一つ上までを閉じます。また、自分がいない折り畳みはすべて閉じます。(先ほどの<C-_>の強化版?なのでz<C-_>というバインドを与えています。)

z0 (現在いる折り畳みと同じ階層までの全ての折り畳みを開く)
nnoremap <silent>z0    :<C-u>set foldlevel=<C-r>=foldlevel('.')<CR><CR>

複数の折り畳み階層があるファイルで使います。ファイル内の折り畳みの、現在自分がいる階層と同じ階層までを開きます。
(zo(折り畳みを開く)の強化版?なのでz0というバインドを与えています。)

[zと]z

現在自分がいる折り畳みの先頭や末尾にジャンプします。特に大見出しなどの役割を果たしている巨大な折り畳み内で威力を発揮します。
逆に小さな折り畳みではそれほど役に立ちません。

zjとzk

いらない子です。折り畳みを閉じた後は通常のページスクロールでほとんど足ります。過去に何とか活躍させようと頑張った人もいましたが、やっぱりいらない子でした。







折り畳み活用編

いくら折り畳みを作れるからって、むやみに階層化させるべきではない

無駄に階層化させると、ある地点を開いたり閉じたりするまでに何度も折り畳みを開いたり閉じたりしなければならなくなる羽目になります。そもそもVimの折り畳み機能は自分が今どの階層にいるのかが把握しづらいUIなので*5、自分のいる場所を見失うことが多くなります。
折り畳みの入れ子を作るのは割とコストが高いことだと私は認識しているので、コメントで見出しをつけたらそれで済ませられるなら、折り畳みを作らないようにしています。
人それぞれの使い方があるでしょうけれど、私の感覚では使いやすさを考えると2階層までが許容範囲ですね*6

見出しとして使う

構造的に大きな区切りに使います。
大きくなりがちな設定ファイルや、構造がある程度決まっているファイルに威力を発揮します。
内容を論理的に整理できて便利ですが、中見出し、小見出しと細分化するにつれてアクセスが煩雑になったり分け方が恣意的になるので注意が必要です。
私は大見出しのみを、大きな折り畳みで作ることを推奨します。また、私はファイルが大きくならないうちにはこの使い方はほとんどしません。*7
この使い方をする人は、番号付きの折り畳みを使うことが多いようです。番号付き折り畳みは指定された深さの折り畳みを作成し、自分のところにまで続いている閉じられていない折り畳みを、それが自分より深いか同じならば、自分の手前で終了させます。
たとえば{{{1の折り畳みの中に新しく{{{1が登場したとき、古い{{{1は手前で閉じられます。
これを利用して終了括弧}}}1を使わず、見出し部分に{{{1を付けているだけの折り畳みの作り方は、終わりを気にしなくてよい分、管理が楽です。*8

モジュールに折り畳みを適用する

私や某neoの人はVim関数の開始点、終了点に折り畳みを設定しています。
私なんかは関数には必ず折り畳みを作ると決めているので、neosnippetに折り畳み付きの関数作成スニペットを定義しています。

snippet     function
abbr        func endfunc
alias       fu fun "fu
  function! ${1}() "{{{
  endfunction
  "}}}

折り畳みへの操作はその折り畳みの中の全ての行に適用されます*9
つまり折りたたまれた行は仮想的に一行として扱われるので、折り畳みにddしたりyyしたりしてからカーソルを移動させてpすれば、モジュールの移動や複製が簡単に、直感的にできるのです。
あと、見た目にわかりやすいという理由で、私はVim scriptでは関数に折り畳みを必ず適用しています。*10

foldexprでそのファイルに適した折り畳みを自動生成する

構造が決まっている折り畳みであれば、折り畳み生成関数を'foldexpr'に指定することで、常に一定のルールに従って折り畳みが作成されます。手動で折り畳みを作る煩わしさや手間や迷いから解放されます。すでに様々なファイルタイプ用の折り畳みが用意されています。ただ、折り畳み関数は工夫して作らないと重くなってしまうのが難点でしょうか。



折り畳みを上手に使えば、整理されたファイルを作れます。
これを使わないのは勿体ないので、まだ使っていない人はこれを機会に使ってみてはいかがでしょう。
また、使っている方も設定を見直すことでさらに使いやすくなります。工夫して自分にあった折り畳みの使い方を見つけてみましょう。

次の人はmanga_oshoさんです。

*1:公開して大分経ってから名前に.vimを付けていないことに気付いたが、後の祭り

*2:foldCCtext()の代わりのFoldCCtext()は古い関数です。

*3:長すぎるときにはg:foldCCtext_maxcharsで指定された値(既定78)で切り詰められる

*4:<C-_>は実際には<C-->で働きます。なお<C-->は機能しません(たぶん)

*5:折り畳みの階層によってシンタックスハイライトを変更するということができたりすればいいのですが。ちなみにfoldcolumnは普段は意外と目に入らず、当てにできません。

*6:それ以上になると関数を分割するとかコメントで区切り線を入れるなどで対応します

*7:過去にこの種の折り畳みで失敗しているからです。

*8:この運用はファイルの末尾がわかりづらくなる欠点がありますが、ファイル末に'END {{{1'などを置いてファイル末を明らかにしている人もいます。

*9:全ての操作がそういうわけではありません。

*10:他の言語における折り畳みは目下試行錯誤中です。