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

半自動でNeoBundleLazy autoloadの設定をするプラグイン作りました

vim

この記事は Vim Advent Calendar 2012 286日目の記事です。

:NeoBundleLazy に autoload機能が搭載されたのは、このAdventCalendarが始まって間もなくのことでした。

これはVimの鈍重な立ち上がりに喘いでいたプラグイン重層派が、Vim本来の軽量な立ち上がりに立ち返り、スパルタンに対する反攻の狼煙を上げるかのように見えました。
しかしLazy autoloadを手に沸き上がる重装派に、恐るべき困難が待ち構えていたのです。

[設定がすごく面倒くさい]

NeoBundleLazy設定の書式

NeoBundleLazy <リポジトリ名>, {'autoload':
  \ {'commands': ['Cmd1', 'Cmd2', {'name': 'Cmd3', 'complete': <利用する補完>}, ...]}
  \ {'mappings': ['<Plug>mapping1', ['i', '<Plug>mapping2'], ...]}
  \ }

neobundleのヘルプに載っていた例

NeoBundleLazy 'Shougo/vimshell',{
      \ 'depends' : 'Shougo/vimproc',
      \ 'autoload' : {
      \   'commands' : [{ 'name' : 'VimShell',
      \                   'complete' : 'customlist,vimshell#complete'},
      \                 'VimShellExecute', 'VimShellInteractive',
      \                 'VimShellTerminal', 'VimShellPop'],
      \   'mappings' : ['<Plug>(vimshell_']
      \ }})

こんなのをいくつも書かないといけないなんて死んでしまいます。
そもそもVimの達人でしたならまだこういったJSONを苦痛なく記述するすごい技術をお持ちかも知れませんが、
私のようなしょっぱい編集能力では{ }括弧と[ ]括弧の入力の連続で指が疲れてしまいます。
そしてまた、こういう設定をするためには、当該プラグインのドキュメントを参照したり、ソースを直接読みに行って、
どういったインターフェイスが用意されているのかを確かめなければいけません。
やはり重装派は、プラグインの設定のためだけに無駄な時間と労力を費やして、スパルタンにせせら笑われる運命なのでしょうか?
そうして設定が面倒になって、lazyでない:NeoBundleを使い始めて、元の重たいVimへと戻っていくのでしょうか?

[重装派は解決もプラグインでやる]

そうです。重装派はいつだって最新鋭の装備でもって技術の未熟さを補うものです。*1
そんなわけで自動でNeoBundleLazy autoloadの設定をするプラグインを作りました。

開発のきっかけ

設定例

  nnoremap <silent>,bl    :<C-u>NebulaPutLazy<CR>
  nnoremap <silent>,bc    :<C-u>NebulaPutConfig<CR>
  nnoremap <silent>,by    :<C-u>NebulaYankOptions<CR>
  nnoremap <silent>,bp    :<C-u>NebulaPutFromClipboard<CR>

使い方
カーソルをvimrcなどに書いた、NeoBundleの以下のような設定行へ持っていきます。

NeoBundle 'kana/vim-smartword'

:NebulaPutLazy を呼びます。するとこの次の行に以下の行が出力されます。

NeoBundleLazy 'kana/vim-smartword', {'autoload': {'mappings': [['sxno', '<Plug>(smartword-']]}}

後はこの出力された行をお好みで手直しした後、元の行を消して完成。
lazyした場合マッピングなどの設定は自分でやるのを忘れないようにしましょう。

  map w <Plug>(smartword-w)
  map b <Plug>(smartword-b)
  map e <Plug>(smartword-e)
  map ge <Plug>(smartword-ge)

他のコマンドですが、:NebulaPutCongigは、NeoBundleLazyではなくneobundle#config()を出力します。
:NebulaYankOptionsはオプションをレジスタに入れます。
:NebulaPutFromClipboardは、OSのクリップボードに入っている文字列を

NeoBundle 'クリップボード文字列'

の形にして出力します。新しいプラグインのインストールに便利です。

(注意)

複雑な構造を持つプラグインや、動的にコマンドやマッピングを生成するようなプラグインには対応していません。
不具合が出ると分かっているもの

  • 'bkad/CamelCaseMotion' のマッピングは取得できない。
  • 'Shougo/unite.vim' のsourceが一部取得できない。unite-source取得が不完全という例は他にもあると思います。
  • 'scrooloose/nerdcommenter' のマッピングのモードに'x'が含まれていない。正しくは、以下のようにする。
{'mappings': [['inx', '<Plug>NERDCommenter']]}
  • 'tpope/vim-fugitive'のコマンドが取得できない。

'kana/vim-textobj-user' を使って作られたtextobjectは g:textobj_{textobjname}_no_default_key_mappings が設定されているかのように振る舞います。つまり設定されるはずのキーマッピングが設定されません。
これに限らずlazy化されたプラグインはデフォルトマッピングが設定されません。vimrcなどで各自キー割り当てを設定してください。
'kana/vim-textobj-user' を使って作られたプラグインなど、依存関係があるものには、'depends'属性を設定して、先に親に当たるプラグインを読み込ませないと、lazy atoload で読み込めません。

NeoBundleLazy 'kana/vim-textobj-user'
NeoBundleLazy 'osyo-manga/vim-textobj-multiblock',
  \ {'depends': 'kana/vim-textobj-user',
  \ 'autoload': {'mappings': ['<Plug>(textobj-multiblock']}}

['autoload'に autoload-functions を設定する必要はない]

foo.vimというプラグインの foo#bar()とかfoo#baz#qux()のような関数は、呼ばれたとき勝手に foo.vim を source してくれるので、'functions'に設定する必要はありません。また、制作者の努力により、'mappings'の設定は先頭マッチで見てくれるようになっています。
プラグインのマッピングは大抵はプレフィックスがつくのでプレフィクスだけを指定することで、多くの場合、'mappings'に指定する要素は1つで済みます。
例えば、NeoBundle 'deton/jasegment.vim' の設定を

NeoBundleLazy 'deton/jasegment.vim', {'autoload': {'mappings': [
  \ ['sx', '<Plug>JaSegmentTextObjVA'], '<Plug>JaSegmentTextObjA', '<Plug>JaSegmentMoveOB', '<Plug>JaSegmentMoveOE',
  \ '<Plug>JaSegmentTextObjI', ['sx', '<Plug>JaSegmentMoveVE'], '<Plug>JaSegmentMoveOW', ['sx', '<Plug>JaSegmentMoveVW'],
  \ '<Plug>JaSegmentMoveNB', '<Plug>JaSegmentMoveNE', ['sx', '<Plug>JaSegmentMoveVB'], ['sx', '<Plug>JaSegmentTextObjVI'],
  \ '<Plug>JaSegmentMoveNW'
  \ ], 'commands': ['JaSegmentSplit']}}

のようなヤバいことをしなくても、

NeoBundleLazy 'deton/jasegment.vim', {'autoload': {'mappings': [['sxno', '<Plug>JaSegment']],
  \ 'commands': ['JaSegmentSplit']}}

という簡潔な記述で済ませることが出来るのです。

[NeoBundleLazyしたプラグインがうまく動かないときの対処法]

大抵のプラグインはうまく動きますが、中にはうまく動かないものもあります。
この原因の一つがプラグインがlazy状態にある間は:autocmdが働かないことにあります。
そんなときは bundle.hooks.on_post_source(bundle)を定義して、中で:doautocmdしてやるとうまくいきます。
例えば、'scrooloose/nerdcommenter' は、新しくバッファに入るとBufEnterイベントでそのバッファのファイル情報を取得します。
しかしlazyで無効になっている間はBufEnterイベントも働いていないので、lazy autoloadでsourceされたときに、そのときのバッファには本来ならある取得しているはずの情報がなくてエラーが発生します。
そこで、bundle.hooks.on_post_source(bundle):doautocmdを設定して、sourceされたときにBufEnterイベントを起こしてやります。

  let s:bundle = neobundle#get('nerdcommenter')
  function! s:bundle.hooks.on_post_source(bundle)
    doautocmd NERDCommenter BufEnter
  endfunction

また、'tpope/vim-fugitive' は、新しく編集を始めるときにBufNewFileイベントで、そのバッファがGitの管理下かどうかを調べているのですが、これもlazyで無効になっている間は働いていないので、呼び出されたとき本来Git管理下のファイルであってもGitの管理下にないかのように振る舞います。この場合は:doautoallで全てのバッファにautocmdを発生させなければいけません。

  let s:bundle = neobundle#get('vim-fugitive')
  function! s:bundle.hooks.on_post_source(bundle)
    doautoall fugitive BufNewFile
  endfunction

というわけで、基本はautocmdを疑って、bundle.hooks.on_post_source(bundle)の中で:doautocmd, :doautoallを呼びます。
これで解決しないならlazyとは相性が悪いプラグインということになります。諦めましょう。

[NeoBundleLocalにはあまりプラグインを置くべきでない]

NeoBundleLocal(pathogenみたいな管理)で設定しているディレクトリにスクリプトを置くと遅くなります。定期的に掃除して、NeoBundleで管理できそうなものはNeoBundleで管理するディレクトリに移して、lazy化するといいでしょう。

*1:いいえ。技術を磨くことは不可欠です。技術力がないから小細工に走り無駄に時間を浪費するのです。