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に,コマンド実行や関数定義,オートコマンドの定義など,一通りのことを行わせることができることもわかった.
重い処理をサーバーに行わせ,結果をクライアントで受け取るという使い方もできるだろう.
おまけ
Vimのサーバー機能を活かしたプラグインとして,
等がある.
これらのプラグインについては,以下の記事あたりを参考にするとよいだろう.
このように,サーバー機能には様々な活用方法があるので,その可能性を模索していきたい.
追記
2015 08/05
tyruさんからサーバ機能を用いたプラグインを紹介していただいた.
koron/minimap-vim や osyo-manga/vim-sugarpot は,ややホビー用途のプラグインのように思えたが,このプラグインは実用的だと感じた.