Vim/Neovim両対応とテストで試行錯誤している話
Clojure での開発を支援する vim プラグイン vim-iced を作っているのですが、その中の課題で以下2点が未だに納得のいく解決策が見いだせていません。 機能毎にコンポーネント化
テスト時にはモック化したい
これらを現状どう解決しようとしているかについて簡単にまとめたいと思います。
なお後述しますが未だに試行錯誤している段階なので、こんな方法もあるんだなぁくらいの軽い気持ちでお読みください。
同じインターフェイスの提供
vim プラグインを書いている方であればおなじみかと思いますが辞書を使っています。
例えば以下のような感じです。
code:vim
let s:vim = {}
function! s:vim.say(s) abort
echo printf('Hello vim %s', a:s)
endfunction
let s:nvim = {}
function! s:nvim.say(s) abort
echo printf('Hello neovim %s', a:s)
endfunction
let s:common = has('nvim')
\ ? s:nvim
\ : s:vim
call s:common.say('hello')
また機能毎のコンポーネント化についてはこの辞書を細分化するのみです。
vim の channel と neovim の job に関する処理のみ
vim の popup と neovim の floatingwindow に関する処理のみ
/icons/point.icon
これで同じインターフェイスを vim/neovim 向けに提供はできるのですが、それぞれの辞書で用意する関数を自力で合わせる必要があるのが無駄に脳の領域を使っている感があり改善したいポイントです。
java の interface や clojure の protorol のように間違いを自動で検知できたら嬉しいです。
テスト時のモック化
上記の辞書をグローバルスコープにおいて書き換えれば可能といえば可能ですが、グローバルスコープの変数はユーザー設定につかうものだけに限りたいです。
またコンポーネントを複数に分割するとコンポーネント内で別のコンポーネントを利用したい(依存したい)ケースも出てきます。
それらを単なる辞書が格納された変数だけで管理するのは限界があります。
そこで以下のようにコンポーネントを管理する関数を用意しました。
なお以下は 簡略化したバージョンです。フルバージョンは こちら を参照してください。 code:system.vim
let s:nvim = has('nvim')
let s:system_map = {
\ 'foo': {'start': (s:nvim ? {_ -> {'who': 'nvim foo'}}
\ : {_ -> {'who': 'vim foo'}})},
\ 'bar': {'start': {x -> {'who': 'bar', 'foo': x'foo'}}, \ }
" 依存するコンポーネントを返す
function! s:requires(name) abort
let requires = get(s:system_mapa:name, 'requires', []) return copy(requires)
endfunction
" コンポーネントを開始して辞書を返す
function! s:get(name) abort
let params = {}
for required_component_name in s:requires(a:name)
endfor
if type(StartFn) == v:t_string
let StartFn = function(StartFn)
endif
return StartFn(params)
endfunction
" => {'foo': {'who': 'vim foo'}, 'who': 'bar'}
echo s:get('bar')
clojure ライブラリである stuartsierra/component をリスペクトした構成にしたつもりですが、先頭にある s:system_map にてそれぞれのコンポーネント(辞書)をどう受け取るのか(start)、また他のどのコンポーネントに依存しているか(requires)を定義しています。 vim/neovimによる辞書の切り替えはシンプルにstartする関数を分岐させているだけです。
こうすることで使う側としては例えば s:get('something').do('args') のような形で vim/neovim の違いを意識することなく処理を呼び出せます。
/icons/注意.icon なお依存関係についてはトポロジカルソートを使っているわけではないので、現状、順番等は自分で気をつける必要があります。
この s:system_map を使うとモック化も簡単で、以下のような書き換え用の関数を用意すればテスト時にだけ挙動を変えることができます。
なおこれも簡略版です。フルバージョンは変わらず こちら です。 code:vim
function! s:set(name, component_map) abort
let s:system_mapa:name = copy(a:component_map) endfunction
" foo コンポーネントを書き換える
call s:set('foo', {'start': {_ -> {'who': 'nisemono'}}})
" => {'foo': {'who': 'nisemono'}, 'who': 'bar'}
echo s:get('bar')
/icons/point.icon
これでテスト時のモック化はできはしているのですが、なにか処理をする際に必ずコンポーネントの get を経由しないといけず冗長で、また呼び出し頻度が高い処理はパフォーマンスへの影響も懸念があります。(そのためフルバージョンでは get 時にキャッシュがあれば使うといった処理を加えたりしています)
vim/neovim の違いは起動時にわかり途中で変わるものでないので、多少乱暴ですが autoload な関数を直接書き換えて以降それを使うだけのようなことができたらより良いのかなぁなどと考えていたります。
最後に
拙作のプラグインでの vim/neovim 両対応ならびにテストにおける現対応をまとめたわけですが、前述の通りまだ納得がいっているわけではないのでこれで確定版ではありません。ちなみに試行錯誤は結構前からやっていて、今の形になるまで2回くらいはコンポーネントまわりをゼロから書き直していたりします。
なのでこのやり方が良いと言うつもりは全くなく、むしろ他に良い方法があれば逆に教えてください /icons/bow.icon