半自動でNeoBundleLazy autoloadの設定をするプラグイン作りました
この記事は 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']}}
という簡潔な記述で済ませることが出来るのです。
[NeoBundleについての詳細]
以下の記事が詳しいです。
[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:いいえ。技術を磨くことは不可欠です。技術力がないから小細工に走り無駄に時間を浪費するのです。
Gitの更なるまとめ(書きかけ)
関連エントリ
初期化
SN | cmd | 説明/引数例 |
---|---|---|
init | ||
cl | clone {src_repository} [{dst_path}] | https://example.com/path/to/repo.git/ path/to/dir |
addからcommitまで
addの反対がreset
SN | cmd | 説明/引数例 |
---|---|---|
a | add {file/dir_pat} | ., *.txt(このワイルドカードはシェルの機能), file.txt, dir(dir中身全て追加) |
an | add -n {file/dir_pat} | その{file/dir_pat}で何が追加されるか |
au | add -u [{file/dir_pat}] | ciaのciしない版(管理下にあるものの更新) |
ap | add -p [{file/dir_pat}] | 対話的にadd {file/dir_pat}付与でそれ限定 |
aa | add -A [{file/dir_pat}] | 新しく追加・削除・変更された、未管理のファイルを追加 |
r | reset | addしたのを全消し |
r | reset {ver} | HEADとindexを過去{ver}の状態に cimのciしない版 |
rc | rm --cached {filepat} | {filepat}をindexから消す*1 |
rp | reset -p | 対話的にreset |
ci | commit -v | (-vで)変更点を表示しながらコミット |
commit -c ORIG_HEAD | 前の(rsなどで動かされる前のHEADの)コミットメッセージを再利用してci | |
cia | commit -a | auしてci |
cim | commit --amend | rsしてci |
ciam | commit -a --amend | rしてauしてci |
状態の確認
SN | cmd | 説明/引数例 |
---|---|---|
st | status -s -b | -sシンプルに表示 -bブランチも表示 |
dh | diff HEAD | ワークツリーとHEADの差分 |
d | diff | ワークツリーとインデックスの差分 |
dc | diff --cached | インデックスとHEADの差分(次のciする差分) |
lg | log | リポジトリのログ |
ls | ls-files | 管理中のファイル一覧 |
rg | reflog | HEADの遷移 |
リモート
SN | cmd | 説明/引数例 |
---|---|---|
roa | remote add origin {URL} | git@github.com:LeafCage/temp.git |
pu | push -u origin master | 初めてアップロードするときに使う |
ro | remote -v | リモートリポジトリの詳細情報 |
pf | push -f origin {local_blanch:remote_blanch} | pf HEAD~:master masterのコミットを取り消す |
リポジトリをリネーム
SN | cmd | 説明/引数例 |
---|---|---|
mv | mv {src} {dst} | |
rc | rm --cached {src}, add {dst} | すでにリネームしていたとき |
rou | remote set-url origin {URL} | remote originのurlを変更 |
pd | push --delete origin {branch} | リモートブランチを削除 |
pf | push -f origin {:remote_blanch} | リモートブランチを削除 |
まだ考えてない
rs | reset --soft {ver} | ciを取り消し{ver}まで遡る。cimのciしない版に近い。HEAD^ |
---|
統合
SN | cmd | 説明/引数例 |
---|---|---|
m | merge [--no-ff]{branch} | [--no-ff]を付けていないとブランチフラグを動かすだけで統合できるなら新しいリビジョンを作らない |
cp | cherry-pick {ver} | {ver}を取り込む |
rb | rebase {branch} | |
rbc | add ファイル名→ rebase --continue | リベースコンフリクト解消させたことを伝えてrebaseを再会する |
rba | rebase --abort | リベースコンフリクト解消させるのをやめてrebase開始前のworkに戻す |
マージのコンフリクト解消
SN | cmd | 説明/引数例 |
---|---|---|
a | add ファイル名 | コンフリクトを解消したものとして提出 |
com | checkout --merge ファイル名 | コンフリクトを初めからやり直す |
coo | checkout --ours ファイル名 | 自分側を一方的に採用 |
cot | git checkout --theirs ファイル名 | 相手側を一方的に採用 |
gitでブランチをマージした時にコンフリクトを起こしてしまったら | S4U -smile for you-
【派閥別】Gitのコミットを間違えたときの対処法まとめ - 本当は怖い情報科学
*1:r HEAD -- {filepat}でも同じ効果gitでアレを元に戻す108の方法 - TIM Labs
vim処理速度調査(オートロード関数読み込み編2)
※この記事は全面的に誤っていたので修正されました。
そして、修正後の結果は極めて当然の話なので情報的価値がないです。
ふとruntimeを用いてスクリプトを読み込む場合と、autoload関数呼び出し時のスクリプトのautoloadで速度に違いがあるのか気になったので計測してみた。
速度計測にはLeafCage/laptime.vimを使用した。
autoload
let lt = laptime#new() call speed1#nop() call lt.lap() call speed2#nop() call lt.lap() call speed3#nop() call lt.lap() call speed4#nop() call lt.lap() call speed5#nop() call lt.lap() call speed6#nop() call lt.lap() call speed7#nop() call lt.lap() call speed8#nop() call lt.lap() call speed9#nop() call lt.lap() call speed10#nop() call lt.end()
TOTAL LAP 1: 0.003668 0.003668 2: 0.007119 0.003452 3: 0.010381 0.003262 4: 0.013674 0.003293 5: 0.017192 0.003517 6: 0.020612 0.003421 7: 0.025109 0.004497 8: 0.028710 0.003600 9: 0.032110 0.003400 10: 0.037759 0.005649
runtime
let lt = laptime#new() runtime autoload/speed1.vim call lt.lap() runtime autoload/speed2.vim call lt.lap() runtime autoload/speed3.vim call lt.lap() runtime autoload/speed4.vim call lt.lap() runtime autoload/speed5.vim call lt.lap() runtime autoload/speed6.vim call lt.lap() runtime autoload/speed7.vim call lt.lap() runtime autoload/speed8.vim call lt.lap() runtime autoload/speed9.vim call lt.lap() runtime autoload/speed10.vim call lt.end()
TOTAL LAP 1: 0.007248 0.007248 2: 0.013605 0.006357 3: 0.019803 0.006198 4: 0.027628 0.007826 5: 0.034176 0.006548 6: 0.040779 0.006603 7: 0.046969 0.006190 8: 0.053571 0.006602 9: 0.060535 0.006964 10: 0.067262 0.006728
2度読み込みされている分、:runtime のほうが遅かったです
vim処理速度調査(オートロード関数読み込み編)
以下の内容が書かれたファイルをautoload/speed1.vimとして置く。
function! speed1#nop() endfunction
同様に、autoload/speed1.vim、autoload/speed2.vim、...としてautoload/speed10.vimまで同様に置く。
そして、以下のスクリプトを:sourceする。
速度計測にはLeafCage/laptime.vimを使用する。
let lt = laptime#new() call speed1#nop() call lt.lap() call speed2#nop() call lt.lap() call speed3#nop() call lt.lap() call speed4#nop() call lt.lap() call speed5#nop() call lt.lap() call speed6#nop() call lt.lap() call speed7#nop() call lt.lap() call speed8#nop() call lt.lap() call speed9#nop() call lt.lap() call speed10#nop() call lt.end()
結果は以下のとおり
TOTAL LAP 1: 0.003770 0.003770 2: 0.007049 0.003279 3: 0.010308 0.003259 4: 0.013501 0.003194 5: 0.017289 0.003788 6: 0.021382 0.004093 7: 0.024573 0.003191 8: 0.027727 0.003153 9: 0.030940 0.003213 10: 0.034118 0.003179
内容の(ほとんどない)ない10のファイルを読み込むだけで計約0.034秒かかっている。
いったい何が原因なのか?
なぜ(ほとんど)空っぽのファイルをsourceするのにここまで時間がかかっているのか?
Vimはautoload関数が呼ばれるとruntimepathから該当ファイルを探してsourceするが、もしやここで時間がかかっているのかもしれない。
私はneobundle.vimでプラグインを管理しているが、その代償としてruntimepathに複数のパスが設定されている。
私のVim起動直後のruntimepathは以下のとおりであった。
runtimepath=~/vimfiles,~/box/vimfiles/neobundle/vim-fugitive,~/box/vimfiles/neobundle/vimproc,~/box/vimfiles/neobundle/tlib_vim,~/box/vim files/neobundle/vital.vim,~/box/vimfiles/neobundle/vim-openbuf,~/box/vimfiles/neobundle/curses-vim,~/box/vimfiles/neobundle/vim-submode,~/b ox/vimfiles/neobundle/vim-altr,~/box/vimfiles/neobundle/mdv,~/box/vimfiles/neobundle/vimhelpgenerator,~/box/vimfiles/neobundle/laptime.vim, ~/box/vimfiles/neobundle/neocomplcache,~/box/vimfiles/neobundle/neosnippet,~/box/vimfiles/neobundle/vim-operator-user,~/box/vimfiles/neobun dle/vim-textobj-user,~/box/vimfiles/neobundle/textobj-wiw,~/box/vimfiles/neobundle/vim-textobj-indent,~/box/vimfiles/neobundle/vim-textobj- plugins,~/box/vimfiles/neobundle/vim-textobj-xbrackets,~/box/vimfiles/neobundle/vim-textobj-parameter,~/box/vimfiles/neobundle/nerdcommente r,~/box/vimfiles/neobundle/ToggleCase-vim,~/box/vimfiles/neobundle/jasegment.vim,~/box/vimfiles/neobundle/clever-f.vim,~/box/vimfiles/neobu ndle/vim-ambicmd,~/box/vimfiles/neobundle/vim-altercmd,~/box/vim
そこで今度は:set rtp=~/vimfiles
として、runtimepathのパスを一つだけにしてから同じことをやってみた。
TOTAL LAP 1: 0.001546 0.001546 2: 0.002974 0.001428 3: 0.004392 0.001418 4: 0.005837 0.001445 5: 0.007338 0.001501 6: 0.008639 0.001301 7: 0.009950 0.001311 8: 0.011222 0.001271 9: 0.012507 0.001286 10: 0.014164 0.001656
処理時間が目に見えて短くなった(半分以下に)。すなわち時間がかかっている原因は、一つは探索場所が広大だったせいである。
だが、ちょっと待ってほしい。
確かに0.0015秒と短くなったとはいえ、これでもVimの処理の中では時間がかかってるほうである。拙作のプラグインにlaptime.vimというのがあるが、その関数の一つ、s:lt.end()のリストの要素が10個のときと同じくらいの時間がかかっている(これの実行時間は約0.0016秒)。
これはそれなりにリスト処理や文字列処理を含んでいる。つまりruntimepathを1つにして、探索場所を減らした状態でも、それなりの処理を含む1つの関数くらいの時間はかかっているわけである。
ちなみに、call speed1#nop()などの関数はほとんど何の仕事もしていないのであるが、これ自体の処理時間はおよそ0.0001秒である。二度目の関数呼び出しからはsourceの時間がなくなるので、この関数の時間だけしかかからず処理が高速になる。
参考
let s:test = range(101) call filter(s:test, 'v:val != "0"')
↑の処理時間は約0.0019秒前後
let s:test = range(101) call map(s:test, 'v:val. "yen"') call match(s:test, '25yen')
約0.002秒
let s:test = range(101) call map(s:test, 'v:val. "yen"') call filter(s:test, 'v:val != "0yen"')
約0.0039秒前後
vim処理速度調査(関数呼び出しin オートロード変数)
この問題はそれとして、現行の回避策として、以下の4通りの方法がある。
- eval()で評価
- type()で評価
- :silent letで呼び出し
- :silent echoで呼び出し
それぞれの呼び出し速度を比較して、もっとも短く呼び出せる方式を採用することにする。
結果。
eval() 0.003490 0.003302 0.003400 type() 0.003586 0.004332 silent let 0.003543 0.003593 0.003495 silent echo 0.004006 0.004823
明らかにeval()が速かった。見た目的にも一番素直だし、eval()を採用することにする。
Vimプラグインを作るときにはautoload以下のファイルをあまり分割すべきでない
理由はVimの実行速度が遅いから。
おそらく2000行の1つのスクリプトファイルを読み込むよりも、100行の2つのスクリプトを読み込むほうが速度が遅い。なぜなら、該当ファイルを探すのに結構な時間がかかるからである。
(追記:その後、検証して、やはりautoloadスクリプトを1つ探して読み込むのに結構な時間を費やすことが分かった。autoload関数(オートロードスクリプト)の初回読み込みが遅い · Issue #461 · vim-jp/issues)
もちろん、plugin以下の記述は少なくしてその分をautoload以下に移すのは大いに推奨される。
起動時には大量のスクリプトが読まれるので少しでも記述は少なくしておいたほうが起動が早い。
しかしプラグインが実行され、autoload以下が読まれるときに、初回時に必ず読まれる部分は、分割すると、ファイル探索のオーバーヘッドが入る分、遅くなるだけなので出来る限り少数の、出来れば単一のファイルにしておくべきである。
そもそも:sourceするのに時間は余りかからない。さっき言ったとおりファイルを探し出すことのほうがはるかに時間がかかる。
ましてvitalなどのライブラリを使っているプラグインでは、ライブラリの読み込みで余計にファイルを読み込ませているので、そこにさらにスクリプトの分割なんてするとさらなる速度低下を引き起こし、読み込みの遅さが体感レベルに達することになる。
もちろん、一度に読み込むところでないのなら、スクリプトをさらに分割しても問題ない。