✍️blog

技術系のこととか

vim9 scriptに対応するためにしたこと

vimプラグイン管理用にpathogen.vimとかvim-plugとか使っている方が多い?のかなと思いますが、
私は自作の簡易プラグイン管理スクリプトを書いて使っています。

動きは単純で設定ファイル通りにプラグインディレクトリへgit clone したりするだけの簡単なものなのですが、
当然遅延ロードなどのリッチな仕組みは用意していないので、起動までの時間が若干かかっていました。

vim9 scriptは公式ドキュメントによると
10倍から100倍の実行速度の向上が期待できます。とのことなのでvim9 scriptに書き直してみました。

その際にやったことをまとめます。

TL;DR

公式ドキュメントにVim scriptからの変更点がまとまっています。
正直これ読めば十分です。

vim-jp.org

vim-startuptime

vimの起動時間を計測するためには--startuptimeオプションを使えばよいのですが、複数回計測したりとか、時間のかかっている順にソートしたりとか
面倒なのですがそのあたりを一括でやってくれるツールとしてvim-startuptimeがあります。

github.com

Go製のツールで実行すると以下のように結果が出力されます。

このキャプチャだと、10回vimを起動させた際の 平均、最大、最小の起動時間と各スクリプトに読み込みにかかった時間が表示されています。

Before

vim scriptのままの状態で計測すると以下のような結果でした。

Total Average: 693.800000 msec
Total Max:     837.000000 msec
Total Min:     573.000000 msec

   AVERAGE        MAX        MIN
---------------------------------
602.000000 757.000000 475.000000: $HOME\_vimrc
588.400000 746.000000 463.000000: C:\Users\sabiz\vimfiles\conf\loader.vim
 21.000000  61.000000  15.000000: loading packages
 15.133333  25.000000  13.000000: C:\Users\sabiz\vimfiles\conf\plugin/molokai.vim
 12.933333  33.000000   8.000000: BufEnter autocommands
 11.666666  31.000000   9.000000: C:\Users\sabiz\vimfiles\pack\plugin\start\vim-lsp-settings\plugin\lsp_settings.vim
 10.200000  29.000000   7.000000: loading plugins
  5.533333  12.000000   5.000000: C:\Users\sabiz\vimfiles\pack\plugin\start\vim-gitgutter\plugin\gitgutter.vim
  5.066666   7.000000   4.000000: parsing arguments
  4.466666   8.000000   4.000000: C:\Program Files\Vim\vim90\syntax\syntax.vim

Top10までの抜粋しましたが、2番目に時間のかかっているloader.vimが今回vim9 scriptに書き換える対象です。
3番目との差は明らかですねw
作りが良くないという説もありますが、vim9 scriptに書き換えて速度アップをはかります。

vim9 scriptへ書き換え

vim scriptとvim9 scriptは同一のファイル内に共存することができるようです。
そのため今回は以下のようにして、vimのバージョンを確認し9以降であればvim9 scriptを呼び出すようにしました。
(vim9 scriptが有効かどうかだけを判定する方法はあるんだろうか?)

  if v:version >= 900
    call {vim9 script}
  else
    call {vim script}
  endif

書き換えの対象を以下の関数にします。
以下関数をvim9 script化します。

function! s:installAndUpdatePlugin() abort
  let job_list = []
  let idx = 0
  for k in s:plugin_names
    let clonePath = s:plugin_path . k
    if !isdirectory(clonePath)
      let cmd = 'git ' . 'clone https://github.com/' . g:plugin_list[idx] . ' ' . clonePath
      call echoraw("\x1b[33mNew:\x1b[0m\t".k."\n")
    elseif s:update_plugin
      let cmd = 'git --git-dir='.clonePath.'/.git pull origin'
      call echoraw("\x1b[33mUpdate:\x1b[0m\t".k."\n")
    else
      let cmd = 'git'
    endif
    let job = job_start(cmd, #{in_io: 'null', out_io: 'null', err_io: 'null'})
    call add(job_list, #{name: k, job: job, pos:idx})
    let idx += 1
  endfor
  call echoraw("\x1b[s") " Save cursor pos

  " Wait jobs
  while empty(job_list) == 0
    function! s:checkJobStatus(idx, val)
      let st = job_status(a:val.job)
      if st == 'dead'
        let info = job_info(a:val.job)
        if info.cmd[0] == 'git' " ignore
          return 0
        endif
        let exitval = info.exitval
        call echoraw("\x1b[u\x1b[".a:val.pos."A") " Restore cursor pos & Move cursor
        if exitval == 0
          call echoraw("\x1b[34mSuccess:\x1b[0m\t".a:val.name."\n")
        else
          call echoraw("\x1b[31mFailed:\x1b[0m\t".a:val.name."\n")
          echon " "
        endif
      endif
      return st !=# 'dead'
    endfunction
    call filter(job_list, function("s:checkJobStatus"))
  endwhile

endfunction

この関数はプラグインのリストを見て、
プラグインプラグインディレクトリになければgit cloneし既に存在し
アップデート設定が有効であればgit pullをjobで非同期に並列で行うようになっています。

関数定義

vim9 scriptでは関数はdefで定義します。

+ def! InstallAndUpdatePlugin()
- function! s:installAndUpdatePlugin() abort
...
+ enddef
- endfunction

※)diff形式のシンタックスにしています。

変数宣言

let ではなく var で宣言します。
また、代入する際にletを書く必要はありません。

+ var idx = 0
- let idx = 0
for k in s:plugin_names
...
+   idx += 1
-   let idx += 1
endfor

文字列の結合

.ではなく..を使用します。
vim scriptでも..使えたような気がするので、
変更点としては.での結合ができなくなったということでしょうか。

- 'git ' . 'clone https://github.com/' . g:plugin_list[idx] . ' ' . clonePath
+ 'git ' .. 'clone https://github.com/' .. g:plugin_list[idx] .. ' ' .. clonePath

コメント

"ではなく#を使います。

-   " Wait jobs
+  # Wait jobs

ラムダ式

元のvim scriptの方では使ってないのですが、
vim9 scriptにするにあたりインナー関数になっているところをラムダ式に変更しました。
vim scriptでは () -> {}というような書き方をするようです。

# vim9 scriptでの書き方
filter(job_list, (i, value) => {
...
})

終結

以上がvim9 scriptにするためにしたことです。
最終的に先ほどのvim scriptの関数は以下のようになりました。
変換+若干の処理変更が入っていますがほぼ同じ処理内容です。

def! InstallAndUpdatePlugin()
  var job_list = []
  var idx = 0
  for k in s:plugin_names
    var clonePath = s:plugin_path .. k
    var cmd = ''
    if !isdirectory(clonePath)
      cmd = 'git ' .. 'clone https://github.com/' .. g:plugin_list[idx] .. ' ' .. clonePath
      echoraw("\x1b[33mNew: " .. k .. "\x1b[0m")
      echoraw("\n")
    elseif s:update_plugin
      cmd = 'git --git-dir=' .. clonePath .. '/.git pull origin'
      echoraw("\x1b[33mUpdate: " .. k ..  "\x1b[0m")
      echoraw("\n")
    else
      continue
    endif
    var job = job_start(cmd, {'in_io': 'null', 'out_io': 'null', 'err_io': 'null'})
    add(job_list, {'name': k, 'job': job, 'pos': idx})
    idx += 1
  endfor
  echoraw("\x1b[s") # Save cursor pos

  # Wait jobs
  while empty(job_list) == 0
    filter(job_list, (i, value) => {
      var st = job_status(value.job)
      if st == 'dead'
        var info = job_info(value.job)
        if info.cmd[0] == 'git' # ignore
          return false
        endif
        var exitval = info.exitval
        echoraw("\x1b[u\x1b[" .. value.pos .. "A") # Restore cursor pos & Move cursor
        if exitval == 0
          echoraw("\x1b[34mSuccess: " .. value.name .. "\x1b[0m")
          echoraw("\n")
        else
          echoraw("\x1b[31mFailed: " .. value.name .. "\x1b[0m")
          echoraw("\n")
          echon " "
        endif
      endif
      return st !=# 'dead'
    })
  endwhile
enddef

メインのロジックはほぼそのままで周辺の書き方を少しづつ変えた形ですね
めっちゃ楽でしたw

After

今回は高速化を狙ってvim9 scriptに書き換えていました。
vim9 script版で計測してみました。

Total Average: 113.066666 msec
Total Max:     119.000000 msec
Total Min:     110.000000 msec

  AVERAGE       MAX       MIN
------------------------------
35.466666 40.000000 34.000000: $HOME\_vimrc
23.333333 24.000000 22.000000: C:\Users\sabiz\vimfiles\conf\loader.vim
17.200000 19.000000 13.000000: loading packages
12.666666 13.000000 12.000000: C:\Users\sabiz\vimfiles\conf\plugin/molokai.vim
10.866666 13.000000  9.000000: BufEnter autocommands
 9.200000 10.000000  9.000000: C:\Users\sabiz\vimfiles\pack\plugin\start\vim-lsp-settings\plugin\lsp_settings.vim
 8.266666 10.000000  6.000000: loading plugins
 5.000000  5.000000  5.000000: C:\Users\sabiz\vimfiles\pack\plugin\start\vim-gitgutter\plugin\gitgutter.vim
 4.733333  6.000000  4.000000: parsing arguments
 3.533333  4.000000  3.000000: C:\Program Files\Vim\vim90\syntax\syntax.vim

以上のような結果でした。
対応前後で表にまとめると以下のような感じです。

項目 Before After
Total Average 693.8 ms 113.1 ms
Total Max 837.0ms 119.0 ms
Total Min 573.0ms 110.0 ms
loader.vim AVERAGE 588.4 ms 23.3 ms
loader.vim MAX 746.0 ms 24.0 ms
loader.vim MIN 463.0 ms 22.0 ms

うん、比較するのがバカバカしいほどに高速化されましたねw

※ちなみに、計測時は上記のスクリプトでいうところのgit clonepullも実行されないケースで試しています。
外部コマンド入れたらそれに依存した時間になりますからね。

今回vim9 scriptに書き換えたスクリプトは以下に置いています。
物好きな方はどうぞ参考にしてください。

github.com

まとめ

控えめに言ってvim9 script速えぇ
(今までが遅すぎたという見方はしちゃダメだ)

via GIPHY