cafegale(LeafCage備忘録)

LeafCage備忘録(はてなダイアリー)と統一しました。

半自動で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']}}

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

[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の遷移

私の使うGitコマンドまとめ 見る編 - Log for Backup - Naoki_Rinの学習

リモート

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などのライブラリを使っているプラグインでは、ライブラリの読み込みで余計にファイルを読み込ませているので、そこにさらにスクリプトの分割なんてするとさらなる速度低下を引き起こし、読み込みの遅さが体感レベルに達することになる。

もちろん、一度に読み込むところでないのなら、スクリプトをさらに分割しても問題ない。