Vimのあまり知られていない(?)機能として,サーバー機能がある.
この機能を用いると,他に立ち上げているVimとやりとりすることができる.
ただし, +clientserver
でコンパイルされていないと,利用することはできない.
このサーバー機能を活かして,サーバーとして立ち上げたVimに時間を計測してもらい,時間がきたらクライアントに通知するという機能を実装してみる. (Windows以外の環境ではテストしていないので注意)
let s:VimServer = {} function! s:VimServer.new(name) abort dict let self.name = a:name return copy(s:VimServer) endfunction function! s:VimServer.start(...) abort dict if has('win95') || has('win16') || has('win32') || has('win64') execute 'silent !start /min vim -u NONE -i NONE -n -N --servername' self.name else silent call system('vim -u NONE -i NONE -n -N --servername ' . self.name . ' &') endif execute 'sleep' (a:0 > 0 ? a:1 : '1000') . 'm' call remote_send(self.name, '', 'id') let self.id = id endfunction function! s:VimServer.execute(cmd) abort dict call remote_send(self.name, '<Esc>:<C-u>' . a:cmd . '<CR>') endfunction function! s:VimServer.define_function(name, args, attr, body) abort dict let head = '<Esc>:<C-u>function! ' . a:name . a:args . ' ' . a:attr call remote_send(self.name, join(add(insert(a:body, head), 'endfunction'), '<CR>') . '<CR>') endfunction function! s:check_reply(replied_id, server_id) if a:replied_id == a:server_id if remote_peek(a:replied_id) > 0 echomsg remote_read(a:replied_id) endif endif endfunction augroup VimServer autocmd! autocmd RemoteReply * call s:check_reply(expand('<amatch>'), s:vs.id) augroup END let s:vs = s:VimServer.new('VIMSERVER') call s:vs.start(500) call s:vs.execute('set updatetime=1000') call s:vs.execute('let g:time_to_stop = 3000') call s:vs.execute('let g:clock = &updatetime') call s:vs.define_function('Update', '()', 'abort', [ \ ' echo (g:clock / 1000.0)', \ ' if g:clock < g:time_to_stop', \ ' call feedkeys(mode() ==# "i" ? "\<lt>C-g>\<lt>ESC>" : "g\<lt>ESC>", "n")', \ ' let g:clock += &updatetime', \ ' else', \ ' call server2client(expand("<client>"), "3 seconds elapsed")', \ ' quitall!', \ ' endif' \]) call s:vs.execute('augroup Server') call s:vs.execute(' autocmd!') call s:vs.execute(' autocmd CursorHold,CursorHoldI * call Update()') call s:vs.execute('augroup END')
このスクリプトを実行すると,サーバーが立ち上がってから3秒後に 3 seconds elapsed
と表示されるはずだ.
ポイントはいくつかある.
まず,サーバーの起動部分であるが,Windowsならば, !start
を利用して,新しいウィンドウでサーバーとなるVimを起動,Windows以外ならば, system()
関数を利用して,バックグラウンドプロセスでサーバーとなるVimを起動している.
Windowsでは start
コマンドのオプション /min
を指定することで,サーバーの起動が目立たないようにした.
次に, remote_send()
関数は,サーバーのVimにキー操作を送信する関数である.
この関数を用いて,サーバーにコマンドを実行させたり,関数を定義させたりする.
remote_send()
関数は,サーバーが起動していないとエラーを吐くので,サーバー起動後,数百ミリ秒程度おいてから呼び出す必要がある.
上記コードでは,500ms 待機している.
しかし, sleep
を用いて待機しているので,どうしてもVimがブロックされてしまう.
ブロックさせない手法については後述する.
500ms 経過後に,
call remote_send(self.name, '', 'id')
とすることで,サーバーに空のキー操作,すなわち,何もしないことをするように要求しているが,これはサーバーのIDを取得するために実行している.
remote_send()
は第三引数(文字列)を指定した場合,その変数名の変数にサーバーのIDが格納されるためだ.
上記コードでは,サーバーに以下のスクリプトと同等のことを実行させている.
set updatetime=1000 let g:time_to_stop = 3000 let g:clock = &updatetime function! Update() abort echo (g:clock / 1000.0) if g:clock < g:time_to_stop call feedkeys(mode() ==# "i" ? "\<C-g>\<ESC>" : "g\<ESC>", "n") let g:clock += &updatetime else call server2client(expand("<client>"), "3 seconds elapsed") quitall! endif endfunction augroup VimServer autocmd! autocmd CursorHold,CursorHoldI * call Update() augroup END
このスクリプトを別ファイルに書き出し,サーバーに :source
させてもよかったが,それでは芸が無いので,クライアントからのキー操作でスクリプトの実行を実現する形にした.
サーバーは,1秒毎に経過秒数を表示し,3秒経過したときに,クライアントに "3 seconds elapsed"
という文字列を送信し,終了する.
<C-g><ESC>
という文字列は,サーバー側で CTRL-G + Esc
と解釈されてしまうので,それを防いで,そのまま送信するために, \<lt>C-g>\<lt>ESC>
と変換している.
"\<lt>"
はクライアントで "<"
と解釈される.
サーバーからクライアントに返信する場合は, server2client()
関数を用いる.
この関数を用いた返信は,クライアントのオートコマンド RemoteReply
を発火させる.
オートコマンドで実行するコマンド定義部分では,expand('<amatch>')
とすることで, RemoteReply
の発火の原因となったサーバーのIDを取得することができる.
server2client()
からの返信は remote_read()
で読み取ることができる.
remote_read()
はFIFOであり,最も古い返信を1つ取り出す.
この remote_read()
はサーバーからの返信キューが空ならば,メッセージが来るまでブロックするので,以下のように, remote_peek()
を用いて,メッセージがあるかどうかを判別した方がよい.
remote_peek()
は引数に指定したIDのサーバーから返信を受け取ることができるならば,正の整数を返す関数である.
if remote_peek(id) > 0 echomsg remote_read(a:replied_id) endif
大まかな解説は以上となる. もし,サーバーを起動する部分で,ブロッキングが行われないようにしたい場合は,次に紹介する2つの方法のいずれかをとるとよい.
let s:VimServer = {} let s:server_list = [] function! s:VimServer.new(name) abort dict if !has('clientserver') echoerr 'This vim cannot use serverclient fucntions' return {} endif let server = copy(s:VimServer) let server.name = a:name let server._instance_id = len(s:server_list) call add(s:server_list, server) return server endfunction function! s:VimServer.lazy_start(...) abort dict if has('win95') || has('win16') || has('win32') || has('win64') execute 'silent !start /min vim -u NONE -i NONE -n -N --servername' self.name else silent call system('vim -u NONE -i NONE -n -N --servername ' . self.name . ' &') endif let self._wait = a:0 > 0 ? a:1 : 1000 let self._clock = &updatetime if a:0 > 1 let self.lazy_start_callback = a:2 endif let group = 'VimServer' . self._instance_id execute 'augroup' group execute ' autocmd!' execute ' autocmd' group 'CursorHold,CursorHoldI * call s:server_list[' . self._instance_id . ']._update()' execute 'augroup END' endfunction function! s:VimServer._update() abort dict if self._clock < self._wait call feedkeys(mode() ==# 'i' ? "\<C-g>\<ESC>" : "g\<ESC>", 'n') let self._clock += &updatetime else execute 'autocmd! VimServer' . self._instance_id 'CursorHold,CursorHoldI *' call remote_send(self.name, '', 'id') let self.id = id if !exists('self.lazy_start_callback') return endif if type(self.lazy_start_callback) == type(function('function')) call self.lazy_start_callback() elseif type(self.lazy_start_callback) == type('') execute self.lazy_start_callback elseif type(self.lazy_start_callback) == type([]) for callback in self.lazy_start_callback execute callback endfor endif endif endfunction function! s:VimServer.execute(cmd) abort dict call remote_send(self.name, '<Esc>:<C-u>' . a:cmd . '<CR>') endfunction function! s:VimServer.define_function(name, args, attr, body) abort dict let head = '<Esc>:<C-u>function! ' . a:name . a:args . ' ' . a:attr call remote_send(self.name, join(add(insert(a:body, head), 'endfunction'), '<CR>') . '<CR>') endfunction function! s:check_reply(replied_id, server_id) if a:replied_id == a:server_id if remote_peek(a:replied_id) > 0 echomsg remote_read(a:replied_id) endif endif endfunction augroup VimServer autocmd! autocmd RemoteReply * call s:check_reply(expand('<amatch>'), s:vs.id) augroup END let s:vs = s:VimServer.new('VIMSERVER') function! s:vs.lazy_start_callback() abort dict call self.execute('set updatetime=1000') call self.execute('let g:time_to_stop = 3000') call self.execute('let g:clock = &updatetime') call self.define_function('Update', '()', 'abort', [ \ ' echo (g:clock / 1000.0)', \ ' if g:clock < g:time_to_stop', \ ' call feedkeys(mode() ==# "i" ? "\<lt>C-g>\<lt>ESC>" : "g\<lt>ESC>", "n")', \ ' let g:clock += &updatetime', \ ' else', \ ' call server2client(expand("<client>"), "3 seconds elapsed")', \ ' quitall!', \ ' endif' \]) call self.execute('augroup Server') call self.execute(' autocmd!') call self.execute(' autocmd CursorHold,CursorHoldI * call Update()') call self.execute('augroup END') endfunction call s:vs.lazy_start(500)
例によって, CursorHold[I]
を定期的に発火させることで,時間計測をしている.
指定時間(上記コードでは500ms)の満了後,コールバック関数 s:vs.lazy_start_callback()
が呼び出され,サーバーに実行させるコマンドが送信される仕組みになっている.
この方法だと,オプション updatetime
に設定した値によっては,サーバーが立ち上がるまでにクライアントの負荷が大きくなったり,サーバーの立ち上がりまでに時間がかかる可能性がある.
ベストと思われる方法は次の方法だ.
let s:VimServer = {} let s:server_list = [] function! s:SID() abort return matchstr(expand('<sfile>'), '<SNR>\zs\d\+\ze_SID$') endfun function! s:VimServer.new(name) abort dict if !has('clientserver') echoerr 'This vim cannot use serverclient fucntions' return {} endif let server = copy(s:VimServer) let server.name = a:name let server._instance_id = len(s:server_list) call add(s:server_list, server) return server endfunction function! s:VimServer.lazy_start(...) abort dict if has('win95') || has('win16') || has('win32') || has('win64') execute 'silent !start /min vim -u NONE -i NONE -n -N --servername' self.name \ printf('-c "call remote_expr(%s, %s)"', string(v:servername), string('<SNR>' . s:SID() . '_on_vim_server_start(' . self._instance_id . ')')) else silent call system('vim -u NONE -i NONE -n -N --servername ' . self.name \ . printf('-c "call remote_expr(%s, %s)"', string(v:servername), string('<SNR>' . s:SID() . '_on_vim_server_start(' . self._instance_id . ')')) \ . ' &') endif if a:0 > 0 let self.lazy_start_callback = a:1 endif endfunction function! s:on_vim_server_start(instance_id) abort let server = s:server_list[a:instance_id] call remote_send(server.name, '', 'id') let server.id = id if !exists('server.lazy_start_callback') return endif if type(server.lazy_start_callback) == type(function('function')) call server.lazy_start_callback() elseif type(server.lazy_start_callback) == type('') execute server.lazy_start_callback elseif type(server.lazy_start_callback) == type([]) for callback in server.lazy_start_callback execute callback endfor endif endfunction function! s:VimServer.execute(cmd) abort dict call remote_send(self.name, '<Esc>:<C-u>' . a:cmd . '<CR>') endfunction function! s:VimServer.define_function(name, args, attr, body) abort dict let head = '<Esc>:<C-u>function! ' . a:name . a:args . ' ' . a:attr call remote_send(self.name, join(add(insert(a:body, head), 'endfunction'), '<CR>') . '<CR>') endfunction function! s:check_reply(replied_id, server_id) if a:replied_id == a:server_id if remote_peek(a:replied_id) > 0 echomsg remote_read(a:replied_id) endif endif endfunction augroup VimServer autocmd! autocmd RemoteReply * call s:check_reply(expand('<amatch>'), s:vs.id) augroup END let s:vs = s:VimServer.new('VIMSERVER') function! s:vs.lazy_start_callback() abort dict call self.execute('set updatetime=1000') call self.execute('let g:time_to_stop = 3000') call self.execute('let g:clock = &updatetime') call self.define_function('Update', '()', 'abort', [ \ ' echo (g:clock / 1000.0)', \ ' if g:clock < g:time_to_stop', \ ' call feedkeys(mode() ==# "i" ? "\<lt>C-g>\<lt>ESC>" : "g\<lt>ESC>", "n")', \ ' let g:clock += &updatetime', \ ' else', \ ' call server2client(expand("<client>"), "3 seconds elapsed")', \ ' quitall!', \ ' endif' \]) call self.execute('augroup Server') call self.execute(' autocmd!') call self.execute(' autocmd CursorHold,CursorHoldI * call Update()') call self.execute('augroup END') endfunction call s:vs.lazy_start()
Vimの起動オプション -c <command>
を付加すると,Vimが起動し,最初のファイルをロードした後に,指定したコマンドを実行する.
このオプションを利用することで,サーバーのが立ち上がった後に,サーバーが remote_expr()
をコールすることを実現している.
サーバーが remote_expr()
を通じて,クライアントにスクリプトローカル関数 s:on_vim_server_start()
を呼び出すことを要求する(正確にはクライアントにおいて,式 s:on_vim_server_start()
を評価した結果を返却するように要求している).
remote_expr()
は,
call remote_expr('VIM', 's:on_vim_server_start()')
のように,単純な方法ではスクリプトローカル関数の呼び出しができない(スクリプト内ではないので,スクリプトローカル関数が見えない)が,
call remote_expr('VIM', '<SNR>42_on_vim_server_start()')
のように,スクリプト番号を渡すことで,スクリプトローカル関数の呼び出しが可能となる.
スクリプト番号の取得方法については, :help <SID>
を参照するとよい.
この待機方法は,サーバー自身に立ち上がったことを知らせてもらうので,クライアント側の負荷や無駄な待機時間も少なく,また,確実に立ち上がりを検知できる.
まとめ
今回はVimのサーバクライアント機能を用いて,タイマーを実装した.
これにより,定期的に CursorHold[I]
を発火させることによって実現するタイマーを,クライアントのVimで実行しなくてもよくなり,クライアントのVimの負荷が無くなる.
サーバーのVimに,コマンド実行や関数定義,オートコマンドの定義など,一通りのことを行わせることができることもわかった.
重い処理をサーバーに行わせ,結果をクライアントで受け取るという使い方もできるだろう.
おまけ
等がある. これらのプラグインについては,以下の記事あたりを参考にするとよいだろう.
このように,サーバー機能には様々な活用方法があるので,その可能性を模索していきたい.
追記
2015 08/05
tyruさんからサーバ機能を用いたプラグインを紹介していただいた. koron/minimap-vim や osyo-manga/vim-sugarpot は,ややホビー用途のプラグインのように思えたが,このプラグインは実用的だと感じた.