はじめに
みなさんneovim使ってますか?個人的には年末にようやく乗り換えたところです。折角乗り換えたので何かサポートツールでも作ってみようかと思い(こことか見てると夢は広がるなぁ、と)、neovimのmsgpack-rpcを叩けるC#ライブラリを作ってみました。本当はライブラリはもっとサクッと作ってツールメインになるはずでしたが、リフレクション使ったAPI動的生成とかやってると意外と時間がかかってしまったのでとりあえずライブラリのみ公開します。
動作確認環境
- Windows10
- Visual Studio 2015
- neovim 0.1.1
リンク
使い方
使うべきクラスはNeovimClient<T> where T : INeovimIO
です。INeovimIO
がneovimとの通信I/Fになっていて、現時点では内部でプロセス生成して標準入出力経由で通信するNeovimHost
と、外部のneovimプロセス(環境変数NVIM_LISTEN_ADDRESS指定で起動したもの)にTCP/IP経由で通信するNeovimTcp
の2つの実装があります。(2015/1/3追記:需要はない気もしますがUNIXドメインソケットを実装したNeovimSocketも追加しました。UNIX環境上のMonoで動く予定ですが、一切確認していないので全く動かないかもしれません)
というわけで
var nvimHost = new NeovimClient<NeovimHost>( new NeovimHost( "path to nvim.exe" ) );
var nvimTcp = new NeovimClient<NeovimTcp>( new NeovimTcp( "127.0.0.1", 10000 ) );
nvimHost.Init();
nvimTcp.Init();
のような感じでインスタンス生成します。Init
は例外を吐く可能性がある(nvim.exeが見つからないとかTCP接続失敗とか)ので適宜キャッチするなりしてください。このときneovimからvim_get_api_info経由で使用可能なAPI一覧を取得するので、neovimのバージョンには依存しないはずです。(実際にはui_attachなどUI系のAPIはvim_get_api_info経由では取れないようなので、それだけソース上に直書きしています。このあたりの事情ってどうなってるんでしょうか?)
実際のAPI呼び出しは
nvimHost.Action<string>( "vim_set_current_line", "test" );
var ret = nvimHost.Func( "vim_get_current_line" ); // ret: test
nvimHost.Action<string>( "vim_command", "q" );
という感じで戻り値がある場合はFunc<T...>
、ない場合はAction<T...>
で呼び出します。この時の型引数はAPIごとに異なり
NeovimFuncInfo info = nvimHost.GetFuncInfo( "vim_command" );
List<Type> paramType = info.ParamTypeCs; // paramType : System.String
Type returnType = info.ReturnTypeCs; // returnType: System.Void
string dscr = info.Description; // dscr : void Action<String>( String name, String str )
のようにType
でも取得可能ですが、NeovimFuncInfo.Description
に関数プロトタイプ風の記述を入れてあるのでそれを見るのが簡単です。(呼び出し側コードをParamTypeCs
とか参照しながら動的に生成するのでもなければそれで十分かと。ParamTypeCs
の使い道としてはneovimのバージョンアップ等で引数が変わる場合に備えて型チェックをするくらいでしょうか)
また、neovimからの通知はNotificationReceived
イベントで受け取れます。ui_attach
した時のredraw
イベント発生は確認しました。
とりあえず、いくつかの簡単なAPI呼び出しはテストしましたが、上手く動かないものもあるかもしれません。特にBuffer/Windowなどを引数に取る系は怪しいです…
解説
API動的生成周りは意外と時間がかかったので備忘録を兼ねて簡単な解説を。動的生成している本体はCreateAction<T...>
/CreateFunc<T...>
です。
private object CreateAction<T1>( string name, string returnType, List<string> param, bool async, bool canFail ) {
Expression<Action<T1>> func = ( p => neovimIO.Request( name, new object[] { p }, true ) );
return func.Compile();
}
ここでExpression
を使って最終的に呼び出されるべき関数neovimIO.Request
を与えています。System.Action<T>
等と同様に引数の個数毎に(とりあえず引数6個まで)用意しています。また、Func
は戻り値の型変換があるため
private object CreateFunc<T1>( string name, string returnType, List<string> param, bool async, bool canFail ) {
if( typeof( T1 ) == typeof( long ) ) { Expression<Func<long >> func = ( () => neovimIO.Request( name, new object[] {}, true )[1].AsInt64orExt() ); return func.Compile(); }
if( typeof( T1 ) == typeof( long[] ) ) { Expression<Func<long[] >> func = ( () => neovimIO.Request( name, new object[] {}, true )[1].AsList().Select( i => i.AsInt64orExt() ).ToArray() ); return func.Compile(); }
if( typeof( T1 ) == typeof( string ) ) { Expression<Func<string >> func = ( () => neovimIO.Request( name, new object[] {}, true )[1].AsString() ); return func.Compile(); }
if( typeof( T1 ) == typeof( string[] ) ) { Expression<Func<string[] >> func = ( () => neovimIO.Request( name, new object[] {}, true )[1].AsList().Select( i => i.AsString() ).ToArray() ); return func.Compile(); }
if( typeof( T1 ) == typeof( bool ) ) { Expression<Func<bool >> func = ( () => neovimIO.Request( name, new object[] {}, true )[1].AsBoolean() ); return func.Compile(); }
if( typeof( T1 ) == typeof( object ) ) { Expression<Func<object >> func = ( () => neovimIO.Request( name, new object[] {}, true )[1].ToObject() ); return func.Compile(); }
if( typeof( T1 ) == typeof( MessagePackObject ) ) { Expression<Func<MessagePackObject>> func = ( () => neovimIO.Request( name, new object[] {}, true )[1] ); return func.Compile(); }
throw new NotImplementedException();
}
のように戻り値毎に書き分けています。このあたり暗黙の型変換定義とかですっきりするのでしょうか?(といっても変換元の型は型変数なので簡単にはいきそうにないですが…)
これらの呼び出し元が以下になります。
private object CreateExpression( string name, string returnType, List<string> param, bool async, bool canFail ) {
if( param.Count > 6 ) {
throw new NotImplementedException();
}
var isAction = NeovimUtil.ConvTypeNeovimToCs( returnType ) == typeof( void );
if ( isAction && param.Count == 0 ) {
return CreateAction( name, returnType, param, async, canFail );
} else {
if( !isAction ) {
param.Add( returnType );
}
var creatorName = ( isAction ) ? "CreateAction" : "CreateFunc";
var methods = typeof( NeovimClient<T> ).GetMethods( BindingFlags.NonPublic | BindingFlags.Instance );
var baseMethod = methods.First( m => m.Name == creatorName && m.GetGenericArguments().Count() == param.Count );
var method = baseMethod.MakeGenericMethod( param.Select( i => NeovimUtil.ConvTypeNeovimToCs( i ) ).ToArray() );
return method.Invoke( this, new object[] { name, returnType, param, async, canFail } );
}
}
CreateExpression
の引数がneovimのvim_get_api_infoからくる情報そのものです。param
に引数の型(neovimの型定義なのでstring
です)が入っているので
var creatorName = ( isAction ) ? "CreateAction" : "CreateFunc";
var methods = typeof( NeovimClient<T> ).GetMethods( BindingFlags.NonPublic | BindingFlags.Instance );
var baseMethod = methods.First( m => m.Name == creatorName && m.GetGenericArguments().Count() == param.Count );
で、型変数の数がparam
と一致するCreateAction
/CreateFunc
を探してきて
var method = baseMethod.MakeGenericMethod( param.Select( i => NeovimUtil.ConvTypeNeovimToCs( i ) ).ToArray() );
で、型変数を渡して実際のCreateAction
/CreateFunc
を生成します。(NeovimUtil.ConvTypeNeovimToCs
でneovimの型定義からC#のType
クラスへ変換しています )
return method.Invoke( this, new object[] { name, returnType, param, async, canFail } );
最後にInvoke
でAction
/Func
を生成して返します。ここで戻り値型が何になるかはAPIの引数の個数次第なのでobjectで返し、使用時にキャストすることになります。